Skip to content
Closed
8 changes: 8 additions & 0 deletions common/arg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2328,6 +2328,14 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
}
}
).set_examples({LLAMA_EXAMPLE_SPECULATIVE, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_CLI}).set_env("LLAMA_ARG_N_CPU_MOE_DRAFT"));
add_opt(common_arg(
{"--expert-cache-slots"}, "N",
"N-slot LRU expert cache: N GPU slots per MoE layer (saves VRAM vs full GPU copy); use with --cpu-moe. "
"N=0 disables (default), N=-1 enables dedup-only (no VRAM saving, same as old --expert-cache)",
[](common_params & params, const std::string & value) {
params.expert_cache_n_slots = std::stoi(value);
}
).set_env("LLAMA_ARG_EXPERT_CACHE_SLOTS"));
GGML_ASSERT(params.n_gpu_layers < 0); // string_format would need to be extended for a default >= 0
add_opt(common_arg(
{"-ngl", "--gpu-layers", "--n-gpu-layers"}, "N",
Expand Down
1 change: 1 addition & 0 deletions common/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,7 @@ struct llama_context_params common_context_params_to_llama(const common_params &
cparams.offload_kqv = !params.no_kv_offload;
cparams.no_perf = params.no_perf;
cparams.op_offload = !params.no_op_offload;
cparams.expert_cache_n_slots = params.expert_cache_n_slots;
cparams.swa_full = params.swa_full;
cparams.kv_unified = params.kv_unified;

Expand Down
1 change: 1 addition & 0 deletions common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ struct common_params {
bool no_op_offload = false; // globally disable offload host tensor operations to device
bool no_extra_bufts = false; // disable extra buffer types (used for weight repacking)
bool no_host = false; // bypass host buffer allowing extra buffers to be used
int32_t expert_cache_n_slots = 0; // N-slot LRU expert cache: 0=off, -1=dedup-only, N>0=N GPU slots per layer (--expert-cache-slots)

bool single_turn = false; // single turn chat conversation

Expand Down
14 changes: 14 additions & 0 deletions ggml/include/ggml-backend.h
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,20 @@ extern "C" {
// Set a callback to be called for each resulting node during graph compute
GGML_API void ggml_backend_sched_set_eval_callback(ggml_backend_sched_t sched, ggml_backend_sched_eval_callback callback, void * user_data);

// Expert cache for MoE weight offloading: enable/disable and query stats
GGML_API void ggml_backend_sched_set_expert_cache(
ggml_backend_sched_t sched, int32_t n_slots);

GGML_API void ggml_backend_sched_get_expert_cache_stats(
ggml_backend_sched_t sched,
int64_t * n_hits,
int64_t * n_misses,
int64_t * n_fate_hits,
int64_t * bytes_saved,
int64_t * bytes_copied);

GGML_API void ggml_backend_sched_reset_expert_cache_stats(ggml_backend_sched_t sched);

//
// Utils
//
Expand Down
498 changes: 480 additions & 18 deletions ggml/src/ggml-backend.cpp

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions include/llama.h
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ extern "C" {
bool offload_kqv; // offload the KQV ops (including the KV cache) to GPU
bool no_perf; // measure performance timings
bool op_offload; // offload host tensor operations to device
int32_t expert_cache_n_slots; // N-slot LRU expert cache: 0=off, -1=dedup-only (no VRAM saving), N>0=N GPU slots per layer
bool swa_full; // use full-size SWA cache (https://github.com/ggml-org/llama.cpp/pull/13194#issuecomment-2868343055)
// NOTE: setting to false when n_seq_max > 1 can cause bad performance in some cases
// ref: https://github.com/ggml-org/llama.cpp/pull/13845#issuecomment-2924800573
Expand Down Expand Up @@ -991,6 +992,18 @@ extern "C" {
// and is not necessary to call it explicitly in most cases
LLAMA_API void llama_synchronize(struct llama_context * ctx);

// Expert cache statistics for MoE weight offloading (--expert-cache-slots / --n-cpu-moe).
// All output pointers are optional (may be NULL). Reset counters with llama_expert_cache_stats_reset().
LLAMA_API void llama_expert_cache_stats(
const struct llama_context * ctx,
int64_t * n_hits,
int64_t * n_misses,
int64_t * n_fate_hits,
int64_t * bytes_saved,
int64_t * bytes_copied);

LLAMA_API void llama_expert_cache_stats_reset(struct llama_context * ctx);

// Token logits obtained from the last call to llama_decode()
// The logits for which llama_batch.logits[i] != 0 are stored contiguously
// in the order they have appeared in the batch.
Expand Down
58 changes: 56 additions & 2 deletions src/llama-context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ llama_context::llama_context(

cparams.n_ubatch = std::min(cparams.n_batch, params.n_ubatch == 0 ? params.n_batch : params.n_ubatch);

cparams.op_offload = params.op_offload;
cparams.kv_unified = params.kv_unified;
cparams.op_offload = params.op_offload;
cparams.expert_cache_n_slots = params.expert_cache_n_slots;
cparams.kv_unified = params.kv_unified;

// initialized later
cparams.pipeline_parallel = false;
Expand Down Expand Up @@ -365,6 +366,15 @@ llama_context::llama_context(
}

llama_context::~llama_context() {
if (cparams.expert_cache_n_slots != 0 && sched) {
int64_t hits, misses, fate, saved, copied;
ggml_backend_sched_get_expert_cache_stats(sched.get(), &hits, &misses, &fate, &saved, &copied);
if (hits + misses > 0) {
LLAMA_LOG_INFO("%s: expert-cache: hits=%" PRId64 " (%.1f%%) misses=%" PRId64 " fate=%" PRId64 " saved=%.1fMB copied=%.1fMB\n",
__func__, hits, 100.0 * hits / (hits + misses), misses, fate,
saved / 1048576.0, copied / 1048576.0);
}
}
if (!model.hparams.no_alloc) {
for (size_t i = 0; i < backend_ptrs.size(); ++i) {
ggml_backend_t backend = backend_ptrs[i];
Expand Down Expand Up @@ -409,6 +419,10 @@ void llama_context::sched_reserve() {

sched.reset(ggml_backend_sched_new(backend_ptrs.data(), backend_buft.data(), backend_ptrs.size(), max_nodes, cparams.pipeline_parallel, cparams.op_offload));

if (cparams.expert_cache_n_slots != 0) {
ggml_backend_sched_set_expert_cache(sched.get(), cparams.expert_cache_n_slots);
}

llama_memory_context_ptr mctx;
if (memory) {
LLAMA_LOG_DEBUG("%s: reserving full memory module\n", __func__);
Expand Down Expand Up @@ -562,6 +576,9 @@ void llama_context::sched_reserve() {
LLAMA_LOG_WARN("%s: compute buffer allocation failed, retrying without pipeline parallelism\n", __func__);
cparams.pipeline_parallel = false;
sched.reset(ggml_backend_sched_new(backend_ptrs.data(), backend_buft.data(), backend_ptrs.size(), max_nodes, false, cparams.op_offload));
if (cparams.expert_cache_n_slots != 0) {
ggml_backend_sched_set_expert_cache(sched.get(), cparams.expert_cache_n_slots);
}
gf = graph_reserve(n_tokens, n_seqs, n_tokens, mctx.get());
}
if (!gf) {
Expand Down Expand Up @@ -2217,6 +2234,27 @@ llm_graph_cb llama_context::graph_get_cb() const {
}
}
}

// Expert cache: when expert weights are on CPU (via --n-cpu-moe / tensor_buft_overrides)
// the scheduler normally assigns MUL_MAT_ID to CPU, preventing cross-backend copies
// and leaving the expert weight cache inactive. Force MUL_MAT_ID to the layer's GPU
// backend so the scheduler creates a CPU→GPU copy that the cache can intercept.
if (cparams.expert_cache_n_slots != 0 &&
cur->op == GGML_OP_MUL_MAT_ID &&
il >= 0 &&
cur->src[0] != nullptr &&
cur->src[0]->buffer != nullptr &&
ggml_backend_buffer_is_host(cur->src[0]->buffer) &&
ggml_backend_buffer_get_usage(cur->src[0]->buffer) == GGML_BACKEND_BUFFER_USAGE_WEIGHTS) {
const auto & dev_layer = model.dev_layer(il);
for (const auto & backend : backends) {
if (ggml_backend_get_device(backend.get()) == dev_layer &&
ggml_backend_supports_op(backend.get(), cur)) {
ggml_backend_sched_set_tensor_backend(sched.get(), cur, backend.get());
break;
}
}
}
};
}

Expand Down Expand Up @@ -2910,6 +2948,7 @@ llama_context_params llama_context_default_params() {
/*.offload_kqv =*/ true,
/*.no_perf =*/ true,
/*.op_offload =*/ true,
/*.expert_cache_n_slots =*/ 0,
/*.swa_full =*/ true,
/*.kv_unified =*/ false,
/*.sampler =*/ nullptr,
Expand Down Expand Up @@ -3068,6 +3107,21 @@ void llama_synchronize(llama_context * ctx) {
ctx->synchronize();
}

void llama_expert_cache_stats(
const llama_context * ctx,
int64_t * n_hits,
int64_t * n_misses,
int64_t * n_fate_hits,
int64_t * bytes_saved,
int64_t * bytes_copied) {
ggml_backend_sched_get_expert_cache_stats(
ctx->get_sched(), n_hits, n_misses, n_fate_hits, bytes_saved, bytes_copied);
}

void llama_expert_cache_stats_reset(llama_context * ctx) {
ggml_backend_sched_reset_expert_cache_stats(ctx->get_sched());
}

float * llama_get_logits(llama_context * ctx) {
ctx->synchronize();

Expand Down
1 change: 1 addition & 0 deletions src/llama-cparams.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct llama_cparams {
bool no_perf;
bool warmup;
bool op_offload;
int32_t expert_cache_n_slots;
bool kv_unified;
bool pipeline_parallel;

Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ llama_build_and_test(
peg-parser/tests.h
)
llama_build_and_test(test-regex-partial.cpp)
llama_build_and_test(test-expert-cache.cpp)

if (NOT ${CMAKE_SYSTEM_PROCESSOR} MATCHES "s390x")
set(MODEL_NAME "tinyllamas/stories15M-q4_0.gguf")
Expand Down
Loading