Skip to content

Commit d819603

Browse files
committed
feat(wp): add resident-device routing for paging
Assisted-by: Codex
1 parent 042ef45 commit d819603

10 files changed

Lines changed: 196 additions & 31 deletions

File tree

common/arg.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2731,6 +2731,16 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
27312731
params.weight_paging_prefetch = true;
27322732
}
27332733
).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI, LLAMA_EXAMPLE_PERPLEXITY}).set_env("LLAMA_ARG_WEIGHT_PAGING_PREFETCH"));
2734+
add_opt(common_arg(
2735+
{"--weight-paging-resident-device"}, "<dev|auto>",
2736+
"device for resident dense weights under WP_RESIDENT_DENSE=1 (default: auto)",
2737+
[](common_params & params, const std::string & value) {
2738+
if (value.empty()) {
2739+
throw std::invalid_argument("invalid value");
2740+
}
2741+
params.weight_paging_resident_device = value;
2742+
}
2743+
).set_examples({LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI, LLAMA_EXAMPLE_PERPLEXITY}).set_env("LLAMA_ARG_WEIGHT_PAGING_RESIDENT_DEVICE"));
27342744
add_opt(common_arg(
27352745
{"-sm", "--split-mode"}, "{none,layer,row,tensor}",
27362746
"how to split the model across multiple GPUs, one of:\n"

common/common.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,9 +1570,10 @@ struct llama_model_params common_model_params_to_llama(common_params & params) {
15701570
mparams.no_alloc = params.no_alloc;
15711571

15721572
// Weight paging parameters
1573-
mparams.weight_paging_enabled = params.weight_paging_enabled;
1574-
mparams.weight_paging_slots = params.weight_paging_slots;
1575-
mparams.weight_paging_prefetch = params.weight_paging_prefetch;
1573+
mparams.weight_paging_enabled = params.weight_paging_enabled;
1574+
mparams.weight_paging_slots = params.weight_paging_slots;
1575+
mparams.weight_paging_prefetch = params.weight_paging_prefetch;
1576+
mparams.weight_paging_resident_device = params.weight_paging_resident_device.c_str();
15761577

15771578
return mparams;
15781579
}

common/common.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ struct common_params {
587587
bool weight_paging_enabled = false; // enable NVMe→VRAM demand paging for model weights
588588
int32_t weight_paging_slots = -1; // number of VRAM slots for weight paging (-1 = auto)
589589
bool weight_paging_prefetch = false; // enable async prefetch of next layer
590+
std::string weight_paging_resident_device = "auto"; // dense-resident device name or auto
590591

591592
bool single_turn = false; // single turn chat conversation
592593

include/llama.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ extern "C" {
337337
bool weight_paging_enabled; // enable NVMe→VRAM demand paging for model weights
338338
int32_t weight_paging_slots; // number of VRAM slots for weight paging (-1 = auto)
339339
bool weight_paging_prefetch; // enable async prefetch of next layer
340+
const char * weight_paging_resident_device; // dense-resident device name or "auto"
340341
};
341342

342343
struct llama_sampler_seq_config {

src/llama-model.cpp

Lines changed: 101 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,54 @@ static bool wp_is_routed_expert(const char * name) {
12731273
std::strstr(p, "ffn_down_exps.") != nullptr;
12741274
}
12751275

1276+
static int wp_find_device_index_by_name(const std::vector<llama_device> & devices, const char * name) {
1277+
if (name == nullptr || name[0] == '\0') {
1278+
return -1;
1279+
}
1280+
for (size_t i = 0; i < devices.size(); ++i) {
1281+
if (std::strcmp(ggml_backend_dev_name(devices[i].dev), name) == 0) {
1282+
return (int) i;
1283+
}
1284+
}
1285+
return -1;
1286+
}
1287+
1288+
static int wp_select_paging_device_index(const llama_model_params & params, const std::vector<llama_device> & devices) {
1289+
if (devices.empty()) {
1290+
return -1;
1291+
}
1292+
if (params.main_gpu >= 0 && params.main_gpu < (int) devices.size()) {
1293+
return params.main_gpu;
1294+
}
1295+
return 0;
1296+
}
1297+
1298+
static int wp_select_resident_device_index(const llama_model_params & params,
1299+
const std::vector<llama_device> & devices,
1300+
int paging_idx) {
1301+
if (devices.empty()) {
1302+
return -1;
1303+
}
1304+
const char * requested = params.weight_paging_resident_device
1305+
? params.weight_paging_resident_device
1306+
: "auto";
1307+
if (std::strcmp(requested, "auto") != 0) {
1308+
int idx = wp_find_device_index_by_name(devices, requested);
1309+
if (idx >= 0) {
1310+
return idx;
1311+
}
1312+
LLAMA_LOG_WARN("%s: resident device '%s' not found; falling back to paging device\n",
1313+
__func__, requested);
1314+
return paging_idx;
1315+
}
1316+
for (size_t i = 0; i < devices.size(); ++i) {
1317+
if ((int) i != paging_idx) {
1318+
return (int) i;
1319+
}
1320+
}
1321+
return paging_idx;
1322+
}
1323+
12761324
bool llama_model_base::load_tensors(llama_model_loader & ml) {
12771325
const auto & split_mode = params.split_mode;
12781326
const auto & use_mlock = params.use_mlock;
@@ -1308,6 +1356,43 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) {
13081356
pimpl->gpu_buft_list.emplace(dev.dev, std::move(buft_list));
13091357
}
13101358

1359+
std::string wp_expert_override_pattern;
1360+
std::string wp_dense_override_pattern;
1361+
std::vector<llama_model_tensor_buft_override> wp_tensor_buft_overrides;
1362+
ggml_backend_buffer_type_t wp_paging_buft = nullptr;
1363+
ggml_backend_buffer_type_t wp_resident_buft = nullptr;
1364+
bool wp_device_router_enabled = false;
1365+
1366+
if (params.weight_paging_enabled && wp_resident_dense_enabled() && !devices.empty()) {
1367+
const int paging_idx = wp_select_paging_device_index(params, devices);
1368+
const int resident_idx = wp_select_resident_device_index(params, devices, paging_idx);
1369+
if (paging_idx >= 0 && resident_idx >= 0) {
1370+
wp_paging_buft = ggml_backend_dev_buffer_type(devices[paging_idx].dev);
1371+
wp_resident_buft = ggml_backend_dev_buffer_type(devices[resident_idx].dev);
1372+
}
1373+
if (wp_paging_buft != nullptr && wp_resident_buft != nullptr) {
1374+
wp_expert_override_pattern = "ffn_(up|gate|down)_exps\\.";
1375+
wp_dense_override_pattern = ".*";
1376+
wp_tensor_buft_overrides.push_back({ wp_expert_override_pattern.c_str(), wp_paging_buft });
1377+
wp_tensor_buft_overrides.push_back({ wp_dense_override_pattern.c_str(), wp_resident_buft });
1378+
if (params.tensor_buft_overrides != nullptr) {
1379+
for (const auto * o = params.tensor_buft_overrides; o->pattern != nullptr; ++o) {
1380+
wp_tensor_buft_overrides.push_back(*o);
1381+
}
1382+
}
1383+
wp_tensor_buft_overrides.push_back({ nullptr, nullptr });
1384+
ml.tensor_buft_overrides = wp_tensor_buft_overrides.data();
1385+
wp_device_router_enabled = true;
1386+
LLAMA_LOG_INFO("%s: WP_RESIDENT_DENSE router: paging=%s (%s), resident=%s (%s)\n",
1387+
__func__,
1388+
ggml_backend_dev_name(devices[paging_idx].dev), ggml_backend_buft_name(wp_paging_buft),
1389+
ggml_backend_dev_name(devices[resident_idx].dev), ggml_backend_buft_name(wp_resident_buft));
1390+
} else {
1391+
LLAMA_LOG_WARN("%s: WP_RESIDENT_DENSE router disabled: could not resolve paging/resident bufts\n",
1392+
__func__);
1393+
}
1394+
}
1395+
13111396
ggml_backend_dev_t cpu_dev = ggml_backend_dev_by_type(GGML_BACKEND_DEVICE_TYPE_CPU);
13121397
if (cpu_dev == nullptr) {
13131398
throw std::runtime_error(format("%s: no CPU backend found", __func__));
@@ -1760,10 +1845,21 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) {
17601845
}
17611846
}
17621847

1763-
bool ctx_has_weights = false;
1848+
bool ctx_has_paged_weights = false;
17641849
for (ggml_tensor * t = ggml_get_first_tensor(ctx); t != nullptr; t = ggml_get_next_tensor(ctx, t)) {
17651850
const auto * weight = ml.get_weight(ggml_get_name(t));
17661851
if (!weight) { continue; }
1852+
if (wp_device_router_enabled && ctx_buft != nullptr) {
1853+
const char * n = ggml_get_name(t);
1854+
ggml_backend_buffer_type_t expected_buft =
1855+
wp_is_routed_expert(n) ? wp_paging_buft : wp_resident_buft;
1856+
if (expected_buft != nullptr && ctx_buft != expected_buft) {
1857+
throw std::runtime_error(format(
1858+
"weight-paging: tensor %s routed to %s, expected %s",
1859+
n, ggml_backend_buft_name(ctx_buft),
1860+
ggml_backend_buft_name(expected_buft)));
1861+
}
1862+
}
17671863
// MAD-88: skip huge non-block tensors from paging. token_embd
17681864
// and output are needed every forward pass (vocab embed +
17691865
// logits), so paging them just adds SSD churn for no win.
@@ -1835,13 +1931,13 @@ bool llama_model_base::load_tensors(llama_model_loader & ml) {
18351931
if (weight_pager) {
18361932
weight_pager->weight_tensor_ptrs.push_back(t);
18371933
}
1934+
ctx_has_paged_weights = true;
18381935
}
1839-
ctx_has_weights = true;
18401936
}
18411937

1842-
// Record the buft once per ctx that owns weight tensors.
1938+
// Record the buft once per ctx that owns paged weight tensors.
18431939
// De-dupe so the multi-device guard reads a clean set.
1844-
if (ctx_has_weights && ctx_buft != nullptr && weight_pager) {
1940+
if (ctx_has_paged_weights && ctx_buft != nullptr && weight_pager) {
18451941
bool seen = false;
18461942
for (auto * existing : weight_pager->weight_bufts) {
18471943
if (existing == ctx_buft) { seen = true; break; }
@@ -2778,6 +2874,7 @@ llama_model_params llama_model_default_params() {
27782874
/*.weight_paging_enabled =*/ false,
27792875
/*.weight_paging_slots =*/ -1,
27802876
/*.weight_paging_prefetch =*/ false,
2877+
/*.weight_paging_resident_device =*/ "auto",
27812878
};
27822879

27832880
return result;

src/llama.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,14 @@ static bool init_weight_pager(llama_model & model, llama_model_loader & ml, cons
174174
// 3. Build the new pager: catalog + init.
175175
model.wp_pager = std::make_unique<wp::WeightPager>();
176176
for (const auto & info : ml.weight_page_infos) {
177-
model.wp_pager->add_page(info.name, info.file_idx, info.offset, info.size, info.n_experts);
177+
ggml_backend_buffer_type_t page_buft = buft;
178+
for (ggml_tensor * t : model.weight_pager->weight_tensor_ptrs) {
179+
if (t != nullptr && info.name == ggml_get_name(t) && t->buffer != nullptr) {
180+
page_buft = ggml_backend_buffer_get_type(t->buffer);
181+
break;
182+
}
183+
}
184+
model.wp_pager->add_page(info.name, info.file_idx, info.offset, info.size, info.n_experts, page_buft);
178185
}
179186
LLAMA_LOG_INFO("%s: catalog populated: %d page entries (source had %zu)\n",
180187
__func__, model.wp_pager->n_pages(), ml.weight_page_infos.size());

src/weight-pager/wp-eval-cb.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,7 @@ bool weight_pager_eval_cb(struct ggml_tensor * t, bool ask, void * user_data) {
784784
// src0->buffer = nullptr for paged tensors, so without
785785
// this patch we NULL-deref before reaching the
786786
// routing-aware dispatcher gate.
787-
ggml_backend_buffer_t pool_buf = pager->pool_buf();
787+
ggml_backend_buffer_t pool_buf = pager->pool_buf(weight_page);
788788
if (t->src[0]->buffer == nullptr && pool_buf != nullptr) {
789789
t->src[0]->buffer = pool_buf;
790790
}
@@ -1093,7 +1093,6 @@ bool weight_pager_eval_cb(struct ggml_tensor * t, bool ask, void * user_data) {
10931093

10941094
// Step 2: page each one in (waiting on prefetch if in flight, sync
10951095
// fallback otherwise) and patch the matching src tensors.
1096-
ggml_backend_buffer_t pool_buf = pager->pool_buf();
10971096
int patches_this_op = 0;
10981097
int views_this_op = 0;
10991098

@@ -1123,6 +1122,7 @@ bool weight_pager_eval_cb(struct ggml_tensor * t, bool ask, void * user_data) {
11231122
#endif
11241123

11251124
const std::string & page_name = pager->page_meta(page_idx).tensor_name;
1125+
ggml_backend_buffer_t pool_buf = pager->pool_buf(page_idx);
11261126

11271127
// Patch every src whose direct name OR view_src's name matches
11281128
// this page.

src/weight-pager/wp-pager.cpp

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,28 @@ int WeightPager::register_pinned(const std::string & name, void * device_ptr, si
150150
}
151151

152152
int WeightPager::add_page(const std::string & name, uint16_t file_idx,
153-
uint64_t file_offset, size_t size, int n_experts) {
153+
uint64_t file_offset, size_t size, int n_experts,
154+
ggml_backend_buffer_type_t buft) {
155+
const int n_before = catalog_.size();
156+
int first = -1;
154157
// Non-MoE / per-expert tensor: add as-is.
155158
if (n_experts <= 1) {
156-
return catalog_.add(name, file_idx, file_offset, size);
159+
first = catalog_.add(name, file_idx, file_offset, size);
160+
} else {
161+
// Consolidated MoE tensor: register N sub-pages, one per expert.
162+
// Returns the index of the FIRST sub-page (subsequent experts are at
163+
// sequential indices). Per-expert size is the consolidated size
164+
// divided by n_experts; per-expert offset is base_offset + e * size_e.
165+
first = catalog_.add_consolidated_experts(name, file_idx, file_offset, size, n_experts);
166+
}
167+
const int n_after = catalog_.size();
168+
if ((int) page_buft_.size() < n_after) {
169+
page_buft_.resize((size_t) n_after, nullptr);
157170
}
158-
// Consolidated MoE tensor: register N sub-pages, one per expert.
159-
// Returns the index of the FIRST sub-page (subsequent experts are at
160-
// sequential indices). Per-expert size is the consolidated size
161-
// divided by n_experts; per-expert offset is base_offset + e * size_e.
162-
return catalog_.add_consolidated_experts(name, file_idx, file_offset, size, n_experts);
171+
for (int i = n_before; i < n_after; ++i) {
172+
page_buft_[(size_t) i] = buft;
173+
}
174+
return first;
163175
}
164176

165177
bool WeightPager::init(const Config & cfg,
@@ -176,20 +188,11 @@ bool WeightPager::init(const Config & cfg,
176188
return false;
177189
}
178190

179-
// Multi-GPU guard (B-P7). Phase 1 is single-device by explicit design.
180-
// This is the *only* defence — the rest of the pager assumes a single
181-
// pool on a single device.
182191
if (devices_used.size() > 1) {
183-
LLAMA_LOG_ERROR(
184-
"wp::WeightPager::init: multi-device configurations are not supported by the "
185-
"weight pager (got %zu devices). Use --device with a single ROCm/CUDA index "
186-
"for paging, or run without --weight-paging.\n",
187-
devices_used.size());
188-
// Caller passed fds; close them so they don't leak.
189-
for (int fd : fds) {
190-
if (fd >= 0) close(fd);
191-
}
192-
return false;
192+
LLAMA_LOG_WARN(
193+
"wp::WeightPager::init: model uses %zu devices; pager pool remains on configured "
194+
"paging device %d and per-page buffer selection is enabled\n",
195+
devices_used.size(), device_idx);
193196
}
194197
if (!devices_used.empty() && devices_used.front() != device_idx) {
195198
LLAMA_LOG_WARN("wp::WeightPager::init: device mismatch (used=%d, configured=%d)\n",
@@ -379,6 +382,7 @@ void WeightPager::shutdown() {
379382
}
380383
}
381384
page_async_event_.clear();
385+
page_buft_.clear();
382386

383387
// Tear down in reverse construction order.
384388
prefetch_.shutdown();
@@ -419,6 +423,11 @@ void WeightPager::shutdown() {
419423
initialized_ = false;
420424
}
421425

426+
ggml_backend_buffer_t WeightPager::pool_buf(int page_idx) const {
427+
(void) page_idx;
428+
return pool_.vram_buf();
429+
}
430+
422431
void WeightPager::restore_disable_graphs_env_() {
423432
if (!env_disable_graphs_forced_) {
424433
return;

src/weight-pager/wp-pager.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,8 @@ class WeightPager {
104104
uint16_t file_idx,
105105
uint64_t file_offset,
106106
size_t size,
107-
int n_experts = 1);
107+
int n_experts = 1,
108+
ggml_backend_buffer_type_t buft = nullptr);
108109

109110
// Initialise the pool, transport, file-io layer, and prefetch scheduler.
110111
//
@@ -248,6 +249,7 @@ class WeightPager {
248249
// Backing buffer for the pool — used by the eval-cb adapter when
249250
// patching tensor->buffer (B-P4 requires a valid ggml backend buffer).
250251
ggml_backend_buffer_t pool_buf() const { return pool_.vram_buf(); }
252+
ggml_backend_buffer_t pool_buf(int page_idx) const;
251253

252254
// Slot-and-page metadata (read-only public view).
253255
const PageMeta & page_meta(int page_idx) const { return catalog_.at(page_idx); }
@@ -317,6 +319,7 @@ class WeightPager {
317319
std::vector<bool> cross_layer_prefetch_candidate_;
318320
std::vector<std::chrono::steady_clock::time_point> prefetch_started_at_;
319321
std::vector<int> page_async_event_;
322+
std::vector<ggml_backend_buffer_type_t> page_buft_;
320323
// Reverse map: slot_idx -> page_idx (or -1 if free). Used by the
321324
// eviction callback to clear page_to_slot_ / page_loaded_ correctly.
322325
std::vector<int> slot_to_page_;

tests/test-weight-pager.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,41 @@ static int test_pool_alloc_skips_pinned_in_eviction() {
10171017
return fails;
10181018
}
10191019

1020+
static int test_pool_allocator_two_pools_independent() {
1021+
int fails = 0;
1022+
ggml_backend_buffer_type_t buft = ggml_backend_cpu_buffer_type();
1023+
EXPECT(buft != nullptr, "cpu buffer_type available");
1024+
if (!buft) return fails;
1025+
1026+
wp::PoolAllocator pool_a;
1027+
wp::PoolAllocator pool_b;
1028+
EXPECT(pool_a.init(buft, /*n_slots=*/2, /*slot_size=*/128), "pool A init");
1029+
EXPECT(pool_b.init(buft, /*n_slots=*/3, /*slot_size=*/256), "pool B init");
1030+
1031+
int evict_a = -1;
1032+
int evict_b = -1;
1033+
pool_a.set_eviction_callback([&](int slot) { evict_a = slot; });
1034+
pool_b.set_eviction_callback([&](int slot) { evict_b = slot; });
1035+
1036+
EXPECT_EQ_INT(pool_a.alloc_slot(), 0, "pool A first slot");
1037+
EXPECT_EQ_INT(pool_a.alloc_slot(), 1, "pool A second slot");
1038+
EXPECT_EQ_INT(pool_b.alloc_slot(), 0, "pool B first slot");
1039+
EXPECT_EQ_INT(pool_b.alloc_slot(), 1, "pool B second slot");
1040+
EXPECT_EQ_INT(pool_b.alloc_slot(), 2, "pool B third slot");
1041+
1042+
pool_a.pin_slot(0);
1043+
EXPECT_EQ_INT(pool_a.alloc_slot(), 1, "pool A evicts only its unpinned slot");
1044+
EXPECT_EQ_INT(evict_a, 1, "pool A eviction callback");
1045+
EXPECT_EQ_INT(evict_b, -1, "pool B not evicted by pool A pressure");
1046+
EXPECT(pool_a.is_pinned(0), "pool A pin remains local");
1047+
EXPECT(!pool_b.is_pinned(0), "pool B pin state remains independent");
1048+
1049+
EXPECT_EQ_INT(pool_b.alloc_slot(), 0, "pool B evicts its own LRU slot");
1050+
EXPECT_EQ_INT(evict_b, 0, "pool B eviction callback");
1051+
pool_a.unpin_slot(0);
1052+
return fails;
1053+
}
1054+
10201055
static int test_pool_alloc_returns_neg1_when_all_pinned() {
10211056
int fails = 0;
10221057
ggml_backend_buffer_type_t buft = ggml_backend_cpu_buffer_type();
@@ -1676,6 +1711,7 @@ int main() {
16761711
{ "read_mem_available_bytes", test_read_mem_available_bytes },
16771712
{ "is_uma_device_smoke", test_is_uma_device_smoke },
16781713
{ "pool_allocator", test_pool_allocator },
1714+
{ "pool_allocator_two_pools_independent", test_pool_allocator_two_pools_independent },
16791715
{ "pool_size_class_packs_small_pages", test_pool_size_class_packs_small_pages },
16801716
{ "pool_size_class_pin_skip", test_pool_size_class_pin_skip },
16811717
{ "pool_pin_basic", test_pool_pin_basic },

0 commit comments

Comments
 (0)