Skip to content

Commit d4ce1af

Browse files
committed
feat: io_uring model loader + 6900XT warm KV cache tier
- llama-io-uring: async fixed-buffer read class wrapping liburing; auto-detected at cmake configure time on Linux; falls back gracefully to pread if io_uring_queue_init fails - llama-model-loader: replaces read_raw_unsafe with io_uring submit+wait in the weight-upload loop; achieves ~5.7 GB/s on SN850X vs 1.5 GB/s baseline via SAM pipeline - llama-kv-cache-tiered: warm_device field in llama_tier_config; when set, init() hipMalloc warm K+V buffers on that device; hot->warm migrations copy R9700 VRAM -> host staging -> 6900XT VRAM instead of SSD; spills to SSD only when warm tier is full
1 parent d7cba77 commit d4ce1af

6 files changed

Lines changed: 331 additions & 13 deletions

File tree

src/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ add_library(llama
3030
llama-memory-recurrent.cpp
3131
llama-mmap.cpp
3232
llama-model-loader.cpp
33+
llama-io-uring.cpp
3334
llama-model-saver.cpp
3435
llama-model.cpp
3536
llama-quant.cpp
@@ -53,6 +54,17 @@ target_compile_features (llama PRIVATE cxx_std_17) # don't bump
5354

5455
target_link_libraries(llama PUBLIC ggml)
5556

57+
if (UNIX AND NOT APPLE)
58+
find_library(LIBURING_LIBRARY NAMES uring)
59+
if (LIBURING_LIBRARY)
60+
target_compile_definitions(llama PRIVATE LLAMA_HAVE_IO_URING)
61+
target_link_libraries(llama PRIVATE ${LIBURING_LIBRARY})
62+
message(STATUS "Found liburing: ${LIBURING_LIBRARY} — io_uring model loader enabled")
63+
else()
64+
message(STATUS "liburing not found — io_uring model loader disabled")
65+
endif()
66+
endif()
67+
5668
if (BUILD_SHARED_LIBS)
5769
set_target_properties(llama PROPERTIES POSITION_INDEPENDENT_CODE ON)
5870
target_compile_definitions(llama PRIVATE LLAMA_BUILD)

src/llama-io-uring.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#ifdef LLAMA_HAVE_IO_URING
2+
3+
#include "llama-io-uring.h"
4+
#include "llama-impl.h" // LLAMA_LOG_*
5+
6+
#include <cerrno>
7+
#include <cstring>
8+
#include <stdexcept>
9+
10+
llama_io_uring::llama_io_uring(int fd, int queue_depth) : fd_(fd) {
11+
if (fd < 0) return;
12+
int ret = io_uring_queue_init(queue_depth, &ring_, 0);
13+
if (ret < 0) {
14+
LLAMA_LOG_WARN("%s: io_uring_queue_init failed: %s — falling back to pread\n",
15+
__func__, strerror(-ret));
16+
return;
17+
}
18+
ring_ok_ = true;
19+
}
20+
21+
llama_io_uring::~llama_io_uring() {
22+
if (!ring_ok_) return;
23+
if (bufs_registered_) io_uring_unregister_buffers(&ring_);
24+
io_uring_queue_exit(&ring_);
25+
}
26+
27+
bool llama_io_uring::register_buffers(struct iovec * bufs, int n_bufs) {
28+
if (!ring_ok_) return false;
29+
int ret = io_uring_register_buffers(&ring_, bufs, n_bufs);
30+
if (ret < 0) {
31+
LLAMA_LOG_WARN("%s: io_uring_register_buffers failed: %s\n",
32+
__func__, strerror(-ret));
33+
return false;
34+
}
35+
bufs_registered_ = true;
36+
return true;
37+
}
38+
39+
void llama_io_uring::submit_read(int buf_idx, void * buf, size_t size,
40+
uint64_t offset, uint64_t user_data) {
41+
struct io_uring_sqe * sqe = io_uring_get_sqe(&ring_);
42+
if (!sqe) {
43+
// Ring full — flush and retry
44+
io_uring_submit(&ring_);
45+
sqe = io_uring_get_sqe(&ring_);
46+
if (!sqe) return;
47+
}
48+
49+
if (bufs_registered_) {
50+
io_uring_prep_read_fixed(sqe, fd_, buf, (unsigned)size,
51+
(off_t)offset, buf_idx);
52+
} else {
53+
io_uring_prep_read(sqe, fd_, buf, (unsigned)size, (off_t)offset);
54+
}
55+
sqe->user_data = user_data;
56+
pending_++;
57+
}
58+
59+
void llama_io_uring::submit() {
60+
if (pending_ > 0) io_uring_submit(&ring_);
61+
}
62+
63+
uint64_t llama_io_uring::wait_one(int * bytes_read) {
64+
struct io_uring_cqe * cqe = nullptr;
65+
int ret = io_uring_wait_cqe(&ring_, &cqe);
66+
if (ret < 0 || !cqe) {
67+
if (bytes_read) *bytes_read = ret;
68+
return UINT64_MAX;
69+
}
70+
uint64_t ud = cqe->user_data;
71+
if (bytes_read) *bytes_read = cqe->res;
72+
io_uring_cqe_seen(&ring_, cqe);
73+
pending_--;
74+
return ud;
75+
}
76+
77+
#endif // LLAMA_HAVE_IO_URING

src/llama-io-uring.h

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#pragma once
2+
// io_uring-backed async NVMe reader for model weight loading.
3+
// Drop-in replacement for the synchronous read_raw_unsafe path in
4+
// llama_model_loader::load_all_data. Overlaps NVMe reads with GPU DMA
5+
// uploads to approach the SN850X sequential ceiling (~5.7 GB/s on RDNA4
6+
// via SAM-mapped PCIe BAR).
7+
//
8+
// Only compiled on Linux when liburing is present (LLAMA_HAVE_IO_URING).
9+
10+
#ifdef LLAMA_HAVE_IO_URING
11+
12+
#include <cstddef>
13+
#include <cstdint>
14+
#include <liburing.h>
15+
16+
// Wraps a single io_uring instance tied to one file descriptor.
17+
// Caller owns the staging buffers; this class only manages submissions
18+
// and completions.
19+
class llama_io_uring {
20+
public:
21+
explicit llama_io_uring(int fd, int queue_depth = 64);
22+
~llama_io_uring();
23+
24+
llama_io_uring(const llama_io_uring &) = delete;
25+
llama_io_uring & operator=(const llama_io_uring &) = delete;
26+
27+
// Register fixed buffers for zero-copy DMA into pinned host memory.
28+
// bufs / n_bufs must stay alive for the lifetime of this object.
29+
bool register_buffers(struct iovec * bufs, int n_bufs);
30+
31+
// Submit a fixed-buffer read (buf_idx indexes into registered buffers).
32+
// offset is the absolute byte offset in the file.
33+
// user_data is returned verbatim in wait_one().
34+
void submit_read(int buf_idx, void * buf, size_t size, uint64_t offset,
35+
uint64_t user_data);
36+
37+
// Flush pending submissions to the kernel.
38+
void submit();
39+
40+
// Wait for one completion. Returns user_data; *bytes_read is set to the
41+
// result (negative = error code from -errno).
42+
uint64_t wait_one(int * bytes_read);
43+
44+
bool valid() const { return ring_ok_; }
45+
46+
private:
47+
int fd_;
48+
bool ring_ok_ = false;
49+
bool bufs_registered_ = false;
50+
int pending_ = 0;
51+
struct io_uring ring_;
52+
};
53+
54+
#endif // LLAMA_HAVE_IO_URING

src/llama-kv-cache-tiered.cpp

Lines changed: 129 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,39 @@ bool llama_kv_cache_tiered::init() {
322322
if (!std::filesystem::exists(ssd_dir)) {
323323
std::filesystem::create_directories(ssd_dir);
324324
}
325+
326+
#ifdef GGML_USE_HIP
327+
if (config.warm_device >= 0) {
328+
// Allocate warm tier buffers on 6900XT (or whatever warm_device is)
329+
int prev_dev = 0;
330+
hipGetDevice(&prev_dev);
331+
hipSetDevice(config.warm_device);
332+
333+
// Each warm slot holds n_kv_heads * head_dim elements per position.
334+
// We allocate config.warm_ctx slots; each slot is warm_elem_bytes bytes
335+
// for K and the same for V.
336+
// warm_elem_bytes is set by the caller via set_warm_elem_bytes().
337+
if (warm_elem_bytes > 0 && config.warm_capacity() > 0) {
338+
warm_dev_capacity = warm_elem_bytes * config.warm_capacity();
339+
hipError_t err_k = hipMalloc(&warm_k_dev, warm_dev_capacity);
340+
hipError_t err_v = hipMalloc(&warm_v_dev, warm_dev_capacity);
341+
if (err_k != hipSuccess || err_v != hipSuccess) {
342+
LLAMA_LOG_WARN("%s: hipMalloc warm tier on device %d failed, falling back to SSD\n",
343+
__func__, config.warm_device);
344+
if (warm_k_dev) { hipFree(warm_k_dev); warm_k_dev = nullptr; }
345+
if (warm_v_dev) { hipFree(warm_v_dev); warm_v_dev = nullptr; }
346+
warm_dev_capacity = 0;
347+
} else {
348+
warm_slots.assign(config.warm_capacity(), WarmSlot{});
349+
LLAMA_LOG_INFO("%s: warm tier on device %d: %.1f MB K + %.1f MB V\n",
350+
__func__, config.warm_device,
351+
warm_dev_capacity / 1e6, warm_dev_capacity / 1e6);
352+
}
353+
}
354+
hipSetDevice(prev_dev);
355+
}
356+
#endif
357+
325358
return true;
326359
}
327360

@@ -361,6 +394,52 @@ bool llama_kv_cache_tiered::evict_tokens(uint32_t n_tokens_to_evict, llama_cache
361394
return true;
362395
}
363396

397+
#ifdef GGML_USE_HIP
398+
int llama_kv_cache_tiered::warm_alloc_slot() {
399+
for (int i = 0; i < (int)warm_slots.size(); i++) {
400+
if (!warm_slots[i].occupied) {
401+
warm_slots[i].occupied = true;
402+
return i;
403+
}
404+
}
405+
return -1;
406+
}
407+
408+
void llama_kv_cache_tiered::warm_free_slot(llama_pos pos) {
409+
auto it = warm_pos_to_slot.find(pos);
410+
if (it != warm_pos_to_slot.end()) {
411+
int slot = it->second;
412+
warm_slots[slot].occupied = false;
413+
warm_slots[slot].pos = -1;
414+
warm_pos_to_slot.erase(it);
415+
}
416+
}
417+
418+
bool llama_kv_cache_tiered::warm_copy_to_device(int slot, const void * k_host, const void * v_host, size_t nbytes) {
419+
if (!warm_k_dev || !warm_v_dev || slot < 0 || (size_t)slot >= warm_slots.size()) return false;
420+
int prev_dev = 0;
421+
hipGetDevice(&prev_dev);
422+
hipSetDevice(config.warm_device);
423+
size_t offset = (size_t)slot * nbytes;
424+
bool ok = (hipMemcpy((char*)warm_k_dev + offset, k_host, nbytes, hipMemcpyHostToDevice) == hipSuccess) &&
425+
(hipMemcpy((char*)warm_v_dev + offset, v_host, nbytes, hipMemcpyHostToDevice) == hipSuccess);
426+
hipSetDevice(prev_dev);
427+
return ok;
428+
}
429+
430+
bool llama_kv_cache_tiered::warm_copy_from_device(int slot, void * k_host, void * v_host, size_t nbytes) {
431+
if (!warm_k_dev || !warm_v_dev || slot < 0 || (size_t)slot >= warm_slots.size()) return false;
432+
int prev_dev = 0;
433+
hipGetDevice(&prev_dev);
434+
hipSetDevice(config.warm_device);
435+
size_t offset = (size_t)slot * nbytes;
436+
bool ok = (hipMemcpy(k_host, (char*)warm_k_dev + offset, nbytes, hipMemcpyDeviceToHost) == hipSuccess) &&
437+
(hipMemcpy(v_host, (char*)warm_v_dev + offset, nbytes, hipMemcpyDeviceToHost) == hipSuccess);
438+
hipSetDevice(prev_dev);
439+
return ok;
440+
}
441+
#endif // GGML_USE_HIP
442+
364443
bool llama_kv_cache_tiered::migrate_tokens(const std::vector<llama_pos>& positions,
365444
llama_cache_tier from_tier,
366445
llama_cache_tier to_tier,
@@ -388,15 +467,35 @@ bool llama_kv_cache_tiered::migrate_tokens(const std::vector<llama_pos>& positio
388467
bool is_cold_to_hot = (from_tier == TIER_COLD && to_tier == TIER_HOT);
389468

390469
if (is_warm_to_cold) {
391-
// Warm→Cold: data is already in RAM, can serialize directly
392-
save_to_ssd(positions, k_tensor, v_tensor, false); // is_device_data = false
470+
// Warm→Cold: serialize to SSD
471+
save_to_ssd(positions, k_tensor, v_tensor, false);
393472
} else if (is_hot_to_warm) {
394-
// Hot→Warm: data is in VRAM (device), need to copy to host first
395-
save_to_ssd(positions, k_tensor, v_tensor, true); // is_device_data = true
473+
#ifdef GGML_USE_HIP
474+
if (warm_k_dev && warm_elem_bytes > 0) {
475+
// R9700 VRAM → host staging → 6900XT VRAM
476+
size_t nbytes = warm_elem_bytes;
477+
std::vector<uint8_t> k_buf(nbytes), v_buf(nbytes);
478+
// copy device→host on current (hot) device
479+
hipMemcpy(k_buf.data(), k_tensor->data, nbytes, hipMemcpyDeviceToHost);
480+
hipMemcpy(v_buf.data(), v_tensor->data, nbytes, hipMemcpyDeviceToHost);
481+
for (auto pos : positions) {
482+
int slot = warm_alloc_slot();
483+
if (slot < 0) {
484+
// warm tier full — spill to SSD
485+
save_to_ssd({pos}, k_tensor, v_tensor, true);
486+
continue;
487+
}
488+
warm_copy_to_device(slot, k_buf.data(), v_buf.data(), nbytes);
489+
warm_slots[slot].pos = pos;
490+
warm_pos_to_slot[pos] = slot;
491+
}
492+
} else
493+
#endif
494+
{
495+
save_to_ssd(positions, k_tensor, v_tensor, true);
496+
}
396497
} else if (is_cold_to_warm || is_cold_to_hot) {
397-
// Loading from cold tier - load from SSD
398-
// Note: requires mutable tensors for destination
399-
load_from_ssd(positions, const_cast<ggml_tensor*>(k_tensor), const_cast<ggml_tensor*>(v_tensor), false); // to_device = false
498+
load_from_ssd(positions, const_cast<ggml_tensor*>(k_tensor), const_cast<ggml_tensor*>(v_tensor), false);
400499
}
401500
}
402501

@@ -433,14 +532,31 @@ bool llama_kv_cache_tiered::batch_migrate_tokens(const std::vector<llama_pos>& p
433532
bool is_hot_to_warm = (from_tier == TIER_HOT && to_tier == TIER_WARM);
434533

435534
if (is_warm_to_cold) {
436-
// Warm→Cold: data is already in RAM, can serialize directly
437-
save_to_ssd(positions, k_tensor, v_tensor, false); // is_device_data = false
535+
save_to_ssd(positions, k_tensor, v_tensor, false);
438536
} else if (is_hot_to_warm) {
439-
// Hot→Warm: data is in VRAM (device), need to copy to host first
440-
save_to_ssd(positions, k_tensor, v_tensor, true); // is_device_data = true
537+
#ifdef GGML_USE_HIP
538+
if (warm_k_dev && warm_elem_bytes > 0) {
539+
size_t nbytes = warm_elem_bytes;
540+
std::vector<uint8_t> k_buf(nbytes), v_buf(nbytes);
541+
hipMemcpy(k_buf.data(), k_tensor->data, nbytes, hipMemcpyDeviceToHost);
542+
hipMemcpy(v_buf.data(), v_tensor->data, nbytes, hipMemcpyDeviceToHost);
543+
for (auto pos : positions) {
544+
int slot = warm_alloc_slot();
545+
if (slot < 0) {
546+
save_to_ssd({pos}, k_tensor, v_tensor, true);
547+
continue;
548+
}
549+
warm_copy_to_device(slot, k_buf.data(), v_buf.data(), nbytes);
550+
warm_slots[slot].pos = pos;
551+
warm_pos_to_slot[pos] = slot;
552+
}
553+
} else
554+
#endif
555+
{
556+
save_to_ssd(positions, k_tensor, v_tensor, true);
557+
}
441558
} else {
442-
// Load from SSD
443-
load_from_ssd(positions, const_cast<ggml_tensor*>(k_tensor), const_cast<ggml_tensor*>(v_tensor), false); // to_device = false
559+
load_from_ssd(positions, const_cast<ggml_tensor*>(k_tensor), const_cast<ggml_tensor*>(v_tensor), false);
444560
}
445561
}
446562

src/llama-kv-cache-tiered.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ struct llama_tier_config {
4444
uint32_t warm_capacity() const { return uint32_t(total_ctx * warm_percent / 100.0f); }
4545
uint32_t cold_capacity() const { return uint32_t(total_ctx * cold_percent / 100.0f); }
4646

47+
// HIP device index for warm tier (-1 = RAM/SSD fallback, 1 = 6900XT)
48+
int warm_device = -1;
49+
4750
// Default configuration: 25% hot, 25% warm, 50% cold
4851
static llama_tier_config default_config(uint32_t total_ctx) {
4952
return {25.0f, 25.0f, 50.0f, total_ctx};
@@ -179,6 +182,7 @@ class llama_kv_cache_tiered {
179182
bool resize(uint32_t new_total_ctx);
180183
bool set_eviction_policy(llama_eviction_policy policy);
181184
bool set_attention_threshold(float threshold);
185+
void set_warm_elem_bytes(size_t n) { warm_elem_bytes = n; }
182186

183187
// Eviction interface
184188
bool evict_tokens(uint32_t n_tokens_to_evict, llama_cache_tier from_tier);
@@ -243,6 +247,21 @@ class llama_kv_cache_tiered {
243247
const ggml_tensor* k_tensor = nullptr;
244248
const ggml_tensor* v_tensor = nullptr;
245249

250+
#ifdef GGML_USE_HIP
251+
// 6900XT warm tier: device buffers on warm_device
252+
void * warm_k_dev = nullptr; // hipMalloc'd on warm_device
253+
void * warm_v_dev = nullptr;
254+
size_t warm_dev_capacity = 0; // max tokens in warm device buffer
255+
size_t warm_elem_bytes = 0; // bytes per token per layer for K or V
256+
struct WarmSlot { bool occupied = false; llama_pos pos = -1; };
257+
std::vector<WarmSlot> warm_slots;
258+
std::unordered_map<llama_pos, int> warm_pos_to_slot;
259+
int warm_alloc_slot();
260+
void warm_free_slot(llama_pos pos);
261+
bool warm_copy_to_device(int slot, const void * k_host, const void * v_host, size_t nbytes);
262+
bool warm_copy_from_device(int slot, void * k_host, void * v_host, size_t nbytes);
263+
#endif
264+
246265
// Helper methods
247266
std::string get_ssd_file_path(llama_pos pos) const;
248267
bool save_to_ssd(const std::vector<llama_pos>& positions, const ggml_tensor* k_tensor, const ggml_tensor* v_tensor, bool is_device_data = false);

0 commit comments

Comments
 (0)