llama : report compute graph reallocations during inference (#30329)

Add an optional ggml_backend_sched_realloc_callback that user code registers with
ggml_backend_sched_set_realloc_callback to be notified when the buffers reserved for a
graph are not sufficient and have to be reserved again. The callback reports whether the
backend assignments and the graph size changed since the previous scheduling.

llama_context registers the callback when creating the scheduler (wrapped into the new
llama_context::sched_new helper) and treats the reallocations that follow a change of the
backend assignments as expected, counting the rest by graph size - unchanged or changed
from the previously scheduled graph. Both counters are reported on one line in the perf
stats.

Assisted-by: pi:llama.cpp/Qwen3.8-Flash-Next
This commit is contained in:
Georgi Gerganov
2026-10-12 11:53:58 +03:00
committed by GitHub
parent 2f160ba59a
commit 972b5fcc19
7 changed files with 92 additions and 14 deletions
+1
View File
@@ -589,6 +589,7 @@ void common_perf_print(const struct llama_context * ctx, const struct common_sam
LOG_INF("%s: total time = %10.2f ms / %5d tokens\n", __func__, (t_end_ms - data.t_start_ms), (data.n_p_eval + data.n_eval));
LOG_INF("%s: unaccounted time = %10.2f ms / %5.1f %% (total - sampling - prompt eval - eval) / (total)\n", __func__, t_unacc_ms, t_unacc_pc);
LOG_INF("%s: graphs reused = %10d\n", __func__, data.n_reused);
LOG_INF("%s: graph reallocs = %10d / %5d (same size) / (changed size)\n", __func__, data.n_reallocs_same, data.n_reallocs_diff);
common_memory_breakdown_print(ctx);
}
+11
View File
@@ -326,6 +326,14 @@ extern "C" {
// `graph` is the compute graph of the split
typedef bool (*ggml_backend_sched_copy_callback)(ggml_backend_t backend, const struct ggml_tensor * src, struct ggml_tensor * dst, struct ggml_cgraph * graph, void * user_data);
// Callback when the buffers reserved for a graph are not sufficient and have to be reserved again
// it happens on the graph allocation path and costs a synchronization of the backends
// `sched` is the scheduler
// `graph` is the compute graph being allocated
// `backend_ids_changed` is true if the backend assignment of the graph nodes changed since the last allocation
// `graph_size_changed` is true if the graph size changed since the last allocation
typedef void (*ggml_backend_sched_realloc_callback)(ggml_backend_sched_t sched, struct ggml_cgraph * graph, bool backend_ids_changed, bool graph_size_changed, void * user_data);
// Initialize a backend scheduler, backends with low index are given priority over backends with high index
GGML_API ggml_backend_sched_t ggml_backend_sched_new(ggml_backend_t * backends, ggml_backend_buffer_type_t * bufts, int n_backends, size_t graph_size, bool parallel, bool op_offload);
GGML_API void ggml_backend_sched_free(ggml_backend_sched_t sched);
@@ -367,6 +375,9 @@ extern "C" {
// Set a callback to be called when the inputs weights of a split are being copied
GGML_API void ggml_backend_sched_set_copy_callback(ggml_backend_sched_t sched, ggml_backend_sched_copy_callback callback, void * user_data);
// Set a callback to be called when the compute buffers of a graph are being reallocated
GGML_API void ggml_backend_sched_set_realloc_callback(ggml_backend_sched_t sched, ggml_backend_sched_realloc_callback callback, void * user_data);
//
// Meta backend
//
+28 -10
View File
@@ -969,6 +969,9 @@ struct ggml_backend_sched {
ggml_backend_sched_copy_callback callback_copy;
void * callback_copy_user_data;
ggml_backend_sched_realloc_callback callback_realloc;
void * callback_realloc_user_data;
char * context_buffer;
size_t context_buffer_size;
@@ -976,11 +979,13 @@ struct ggml_backend_sched {
int debug;
// used for debugging graph reallocations [GGML_SCHED_DEBUG_REALLOC]
// graph size of the current and the previous scheduling, used by the realloc callback and for debugging
// graph reallocations [GGML_SCHED_DEBUG_REALLOC]
// ref: https://github.com/ggml-org/llama.cpp/pull/17617
int graph_size;
int prev_graph_size;
int debug_realloc;
int debug_graph_size;
int debug_prev_graph_size;
};
#define hash_id(tensor) ggml_hash_find_or_insert(&sched->hash_set, tensor)
@@ -1633,9 +1638,9 @@ void ggml_backend_sched_split_graph(ggml_backend_sched_t sched, struct ggml_cgra
}
int graph_size = std::max(graph->n_nodes, graph->n_leafs) + total_inputs * 2 * sched->n_copies + n_dep_nodes;
// remember the actual graph_size for performing reallocation checks later [GGML_SCHED_DEBUG_REALLOC]
sched->debug_prev_graph_size = sched->debug_graph_size;
sched->debug_graph_size = graph_size;
// remember the actual graph_size for performing reallocation checks later
sched->prev_graph_size = sched->graph_size;
sched->graph_size = graph_size;
if (sched->graph.size < graph_size) {
sched->graph.size = graph_size;
@@ -1772,14 +1777,21 @@ static bool ggml_backend_sched_alloc_splits(ggml_backend_sched_t sched) {
GGML_LOG_DEBUG("%s: failed to allocate graph, reserving (backend_ids_changed = %d)\n", __func__, backend_ids_changed);
#endif
// a reallocation with an unchanged graph size means that the previous reservation was not sufficient
const bool graph_size_changed = sched->prev_graph_size != sched->graph_size;
if (sched->callback_realloc != NULL) {
sched->callback_realloc(sched, &sched->graph, backend_ids_changed, graph_size_changed, sched->callback_realloc_user_data);
}
if (sched->debug_realloc > 0) {
// we are interested only in situations where the graph was reallocated even though its size remained the same [GGML_SCHED_DEBUG_REALLOC]
// example: https://github.com/ggml-org/llama.cpp/pull/17143
const bool unexpected = !backend_ids_changed && sched->debug_prev_graph_size == sched->debug_graph_size;
const bool unexpected = !backend_ids_changed && !graph_size_changed;
if (unexpected || sched->debug_realloc > 1) {
GGML_ABORT("%s: unexpected graph reallocation (graph size = %d, nodes = %d, leafs = %d), debug_realloc = %d\n", __func__,
sched->debug_graph_size, sched->graph.n_nodes, sched->graph.n_leafs, sched->debug_realloc);
sched->graph_size, sched->graph.n_nodes, sched->graph.n_leafs, sched->debug_realloc);
}
}
@@ -1973,8 +1985,8 @@ ggml_backend_sched_t ggml_backend_sched_new(
sched->prev_node_backend_ids = (int *) calloc(nodes_size, sizeof(sched->prev_node_backend_ids[0]));
sched->prev_leaf_backend_ids = (int *) calloc(nodes_size, sizeof(sched->prev_leaf_backend_ids[0]));
sched->debug_graph_size = 0;
sched->debug_prev_graph_size = 0;
sched->graph_size = 0;
sched->prev_graph_size = 0;
sched->context_buffer_size = ggml_sched_max_splits*GGML_SCHED_MAX_SPLIT_INPUTS*2*sizeof(struct ggml_tensor) + ggml_graph_overhead_custom(graph_size, false);
sched->context_buffer = (char *) malloc(sched->context_buffer_size);
@@ -2143,6 +2155,12 @@ void ggml_backend_sched_set_copy_callback(ggml_backend_sched_t sched, ggml_backe
sched->callback_copy_user_data = user_data;
}
void ggml_backend_sched_set_realloc_callback(ggml_backend_sched_t sched, ggml_backend_sched_realloc_callback callback, void * user_data) {
GGML_ASSERT(sched);
sched->callback_realloc = callback;
sched->callback_realloc_user_data = user_data;
}
int ggml_backend_sched_get_n_splits(ggml_backend_sched_t sched) {
GGML_ASSERT(sched);
return sched->n_splits;
+2
View File
@@ -1686,6 +1686,8 @@ extern "C" {
int32_t n_p_eval; // number of prompt tokens
int32_t n_eval; // number of generated tokens
int32_t n_reused; // number of times a ggml compute graph had been reused
int32_t n_reallocs_same; // number of graph reallocations during inference with an unchanged graph size
int32_t n_reallocs_diff; // number of graph reallocations during inference with a changed graph size
};
struct llama_perf_sampler_data {
+35 -4
View File
@@ -704,6 +704,14 @@ static int llama_graph_n_input_tensors(ggml_cgraph * gf) {
return (int) users.size();
}
void llama_context::sched_new(size_t max_nodes, bool parallel) {
sched.reset(ggml_backend_sched_new(
backend_ptrs.data(), backend_buft.data(), backend_ptrs.size(), max_nodes, parallel, cparams.op_offload));
ggml_backend_sched_set_copy_callback(sched.get(), sched_copy_experts, this);
ggml_backend_sched_set_realloc_callback(sched.get(), sched_graph_realloc, this);
}
void llama_context::sched_reserve() {
if (!sched_need_reserve) {
return;
@@ -730,8 +738,7 @@ void llama_context::sched_reserve() {
gf_res_reserve.reset(new llm_graph_result(max_nodes));
gf_res_prev_active = nullptr;
sched.reset(ggml_backend_sched_new(backend_ptrs.data(), backend_buft.data(), backend_ptrs.size(), max_nodes, cparams.pipeline_parallel, cparams.op_offload));
ggml_backend_sched_set_copy_callback(sched.get(), sched_copy_experts, this);
sched_new(max_nodes, cparams.pipeline_parallel);
llama_memory_context_ptr mctx;
if (memory) {
@@ -770,8 +777,7 @@ void llama_context::sched_reserve() {
if (cparams.pipeline_parallel) {
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));
ggml_backend_sched_set_copy_callback(sched.get(), sched_copy_experts, this);
sched_new(max_nodes, false);
gf = graph_reserve(n_tokens, n_seqs, n_outputs_pp, mctx.get());
}
if (!gf) {
@@ -2697,6 +2703,25 @@ llm_graph_params llama_context::graph_params(
};
}
void llama_context::sched_graph_realloc(ggml_backend_sched_t sched, ggml_cgraph * gf, bool backend_ids_changed, bool graph_size_changed, void * user_data) {
// reallocating after the backend assignments changed is expected, trace only the unexpected ones
if (backend_ids_changed) {
return;
}
auto * lctx = static_cast<llama_context *>(user_data);
if (graph_size_changed) {
lctx->n_reallocs_diff++;
} else {
lctx->n_reallocs_same++;
}
LLAMA_LOG_INFO("%s: graph reallocation: nodes = %d, splits = %d, n_same = %d, n_diff = %d\n",
__func__, ggml_graph_n_nodes(gf), ggml_backend_sched_get_n_splits(sched),
lctx->n_reallocs_same, lctx->n_reallocs_diff);
}
ggml_status llama_context::graph_compute(
ggml_cgraph * gf,
bool batched) {
@@ -3651,6 +3676,9 @@ llama_perf_context_data llama_context::perf_get_data() const {
data.n_eval = std::max(1, n_eval);
data.n_reused = std::max(0, n_reused);
data.n_reallocs_same = n_reallocs_same;
data.n_reallocs_diff = n_reallocs_diff;
return data;
}
@@ -3659,6 +3687,8 @@ void llama_context::perf_reset() {
t_eval_us = n_eval = 0;
t_p_eval_us = n_p_eval = 0;
n_reused = 0;
// note: the reallocation counters are not reset - they track the lifetime totals for this context
}
llama_memory_breakdown llama_context::memory_breakdown() const {
@@ -4624,6 +4654,7 @@ void llama_perf_context_print(const llama_context * ctx) {
__func__, data.t_eval_ms, data.n_eval, data.t_eval_ms / data.n_eval, 1e3 / data.t_eval_ms * data.n_eval);
LLAMA_LOG_INFO("%s: total time = %10.2f ms / %5d tokens\n", __func__, (t_end_ms - data.t_start_ms), (data.n_p_eval + data.n_eval));
LLAMA_LOG_INFO("%s: graphs reused = %10d\n", __func__, data.n_reused);
LLAMA_LOG_INFO("%s: graph reallocs = %10d / %5d (same size) / (changed size)\n", __func__, data.n_reallocs_same, data.n_reallocs_diff);
}
void llama_perf_context_reset(llama_context * ctx) {
+11
View File
@@ -273,9 +273,15 @@ private:
llm_graph_cb graph_get_cb() const;
// create a new backend scheduler for a graph of the specified size and register the callbacks below
void sched_new(size_t max_nodes, bool parallel);
// ggml_backend_sched copy callback, copies only the experts used by MUL_MAT_ID and updates the MoE cache
static bool sched_copy_experts(ggml_backend_t backend, const ggml_tensor * src, ggml_tensor * dst, ggml_cgraph * graph, void * user_data);
// ggml_backend_sched realloc callback, counts and traces the graph reallocations of this context
static void sched_graph_realloc(ggml_backend_sched_t sched, ggml_cgraph * graph, bool backend_ids_changed, bool graph_size_changed, void * user_data);
// disable auto fused ops (Flash Attention, Gated Delta Net) whose op lands on a device
// that differs from the layer it belongs to (usually due to missing backend support)
void resolve_fused_ops(const llama_memory_context_i * mctx, uint32_t n_seqs);
@@ -377,6 +383,11 @@ private:
copy_experts_info copy_experts;
// graph reallocations during inference where the backend assignments did not change, split by whether the
// graph size is the same as the previously scheduled one (unexpected) or different
int32_t n_reallocs_same = 0;
int32_t n_reallocs_diff = 0;
ggml_backend_t backend_cpu = nullptr;
std::vector<ggml_backend_ptr> backends;
+4
View File
@@ -693,6 +693,10 @@ struct server_slot {
" graphs reused = %10d\n",
llama_perf_context(ctx_tgt).n_reused);
SLT_INF(*this,
" graph reallocs = %10d / %5d (same size) / (changed size)\n",
llama_perf_context(ctx_tgt).n_reallocs_same, llama_perf_context(ctx_tgt).n_reallocs_diff);
const int32_t n_draft_total = stats.n_draft_tokens;
const int32_t n_draft_accepted = stats.n_draft_accepted;
const int32_t n_draft_verif_steps = stats.n_draft_verif_steps;