Skip to content

Commit 22277e3

Browse files
committed
ported residual changes about grad_checkpointing
1 parent 70730e8 commit 22277e3

5 files changed

Lines changed: 171 additions & 17 deletions

File tree

examples/training/finetune.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ int main(int argc, char ** argv) {
7373
/*get_opt_pars =*/common_opt_lr_pars,
7474
/*get_opt_pars_ud =*/&params.lr,
7575
/*optimizer_type =*/params.optimizer,
76+
/*grad_checkpoint_interval =*/params.grad_checkpoint_interval,
7677
};
7778
llama_opt_init(ctx, model, lopt_params);
7879

ggml/include/ggml-opt.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,13 @@ extern "C" {
126126
ggml_opt_get_optimizer_params get_opt_pars; // callback for calculating optimizer parameters
127127
void * get_opt_pars_ud; // userdata for calculating optimizer parameters
128128

129+
// Gradient checkpointing: keep the output of every Nth forward node alive through
130+
// the backward pass so the allocator cannot reuse its memory for other tensors.
131+
// This trades compute for VRAM — intermediate activations between checkpoints are
132+
// freed and recomputed during the backward pass by the existing graph structure.
133+
// Set to 0 (default) to disable. A value of ~32–64 cuts activation VRAM by ~50%.
134+
int32_t grad_checkpoint_interval;
135+
129136
// only GGML_OPT_OPTIMIZER_TYPE_ADAMW needs m, v momenta per parameter tensor
130137
enum ggml_opt_optimizer_type optimizer;
131138
};

ggml/src/ggml-opt.cpp

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,13 @@ struct ggml_opt_context {
5858
std::vector<struct ggml_tensor *> grad_accs;
5959
std::vector<struct ggml_tensor *> grad_m;
6060
std::vector<struct ggml_tensor *> grad_v;
61+
std::vector<ggml_backend_buffer_t> bufs_momenta; // per-param moment buffers (one per param node)
62+
std::vector<struct ggml_context *> ctxs_momenta; // corresponding ggml contexts (keep alive for tensor metadata)
6163

6264
int64_t iter = 1;
6365
int32_t opt_period = 1;
6466
int32_t opt_i = 0;
67+
int32_t grad_checkpoint_interval = 0;
6568
bool loss_per_datapoint = false;
6669

6770
ggml_opt_get_optimizer_params get_opt_pars = nullptr;
@@ -254,9 +257,10 @@ struct ggml_opt_params ggml_opt_default_params(
254257
/*loss_type =*/ loss_type,
255258
/*build_type =*/ GGML_OPT_BUILD_TYPE_OPT,
256259
/*opt_period =*/ 1,
257-
/*get_opt_pars =*/ ggml_opt_get_default_optimizer_params,
258-
/*get_opt_pars_ud =*/ nullptr,
259-
/*optimizer =*/ GGML_OPT_OPTIMIZER_TYPE_ADAMW,
260+
/*get_opt_pars =*/ ggml_opt_get_default_optimizer_params,
261+
/*get_opt_pars_ud =*/ nullptr,
262+
/*grad_checkpoint_interval =*/ 0,
263+
/*optimizer =*/ GGML_OPT_OPTIMIZER_TYPE_ADAMW,
260264
};
261265
}
262266

@@ -476,8 +480,23 @@ static void ggml_opt_build(ggml_opt_context_t opt_ctx) {
476480
for (int i = 0; i < n_nodes; ++i) {
477481
ggml_tensor * node = opt_ctx->gf->nodes[i];
478482
if (node->flags & GGML_TENSOR_FLAG_PARAM) {
479-
opt_ctx->grad_m[i] = ggml_new_tensor(opt_ctx->ctx_static, GGML_TYPE_F32, GGML_MAX_DIMS, node->ne);
480-
opt_ctx->grad_v[i] = ggml_new_tensor(opt_ctx->ctx_static, GGML_TYPE_F32, GGML_MAX_DIMS, node->ne);
483+
// Allocate moments on the same buffer type as the param tensor so
484+
// the ADAMW op runs on the correct backend (avoids cross-device mismatch
485+
// when some LoRA tensors are on CPU and others on GPU with partial offload).
486+
ggml_backend_buffer_type_t param_buft = node->buffer
487+
? ggml_backend_buffer_get_type(node->buffer)
488+
: ggml_backend_cpu_buffer_type();
489+
490+
// Allocate a tiny context + buffer for this pair of moment tensors.
491+
const size_t sz = 2 * ggml_tensor_overhead();
492+
struct ggml_init_params mip = { sz, nullptr, true };
493+
struct ggml_context * mctx = ggml_init(mip);
494+
opt_ctx->grad_m[i] = ggml_new_tensor(mctx, GGML_TYPE_F32, GGML_MAX_DIMS, node->ne);
495+
opt_ctx->grad_v[i] = ggml_new_tensor(mctx, GGML_TYPE_F32, GGML_MAX_DIMS, node->ne);
496+
ggml_backend_buffer_t mbuf = ggml_backend_alloc_ctx_tensors_from_buft(mctx, param_buft);
497+
ggml_backend_buffer_clear(mbuf, 0);
498+
opt_ctx->bufs_momenta.push_back(mbuf);
499+
opt_ctx->ctxs_momenta.push_back(mctx); // keep alive for tensor metadata
481500
} else {
482501
opt_ctx->grad_m[i] = nullptr;
483502
opt_ctx->grad_v[i] = nullptr;
@@ -486,6 +505,31 @@ static void ggml_opt_build(ggml_opt_context_t opt_ctx) {
486505
}
487506
}
488507

508+
// Gradient checkpointing: mark every Nth forward node as OUTPUT so the allocator
509+
// keeps its memory alive through the backward pass. The backward graph already
510+
// contains the forward ops (gb_grad is a superset of gf), so the checkpointed
511+
// activations are naturally available for backward matmuls without recomputation.
512+
// This prevents the allocator from aliasing those buffers to later ops, cutting
513+
// peak activation VRAM at the cost of slightly larger static allocation.
514+
if (opt_ctx->grad_checkpoint_interval > 0) {
515+
const int interval = opt_ctx->grad_checkpoint_interval;
516+
const int n_fwd = opt_ctx->gf->n_nodes;
517+
int ckpt_count = 0;
518+
for (int i = interval - 1; i < n_fwd; i += interval) {
519+
struct ggml_tensor * node = opt_ctx->gf->nodes[i];
520+
// Only checkpoint F32 compute nodes — skip I32 index tensors and already-output nodes.
521+
if (node->type != GGML_TYPE_F32) continue;
522+
if (node->flags & GGML_TENSOR_FLAG_OUTPUT) continue;
523+
if (node->flags & GGML_TENSOR_FLAG_INPUT) continue;
524+
node->flags |= GGML_TENSOR_FLAG_OUTPUT;
525+
ckpt_count++;
526+
}
527+
if (ckpt_count > 0) {
528+
GGML_LOG_DEBUG("%s: gradient checkpointing: marked %d/%d nodes as persistent (interval=%d)\n",
529+
__func__, ckpt_count, n_fwd, interval);
530+
}
531+
}
532+
489533
// gb_grad == graph backward gradients, forward pass, then backward pass to calculate gradients.
490534
opt_ctx->gb_grad = ggml_graph_dup(opt_ctx->ctx_compute, opt_ctx->gf, /*force_grads =*/ true);
491535
ggml_build_backward_expand(opt_ctx->ctx_compute, opt_ctx->gb_grad, opt_ctx->grad_accs.data());
@@ -556,10 +600,11 @@ ggml_opt_context_t ggml_opt_init(struct ggml_opt_params params) {
556600
result->build_type_alloc = params.build_type;
557601
result->inputs = params.inputs;
558602
result->outputs = params.outputs;
559-
result->opt_period = params.opt_period;
560-
result->get_opt_pars = params.get_opt_pars;
561-
result->get_opt_pars_ud = params.get_opt_pars_ud;
562-
result->optimizer = params.optimizer;
603+
result->opt_period = params.opt_period;
604+
result->grad_checkpoint_interval = params.grad_checkpoint_interval;
605+
result->get_opt_pars = params.get_opt_pars;
606+
result->get_opt_pars_ud = params.get_opt_pars_ud;
607+
result->optimizer = params.optimizer;
563608

564609
GGML_ASSERT(result->opt_period >= 1);
565610

@@ -588,6 +633,12 @@ void ggml_opt_free(ggml_opt_context_t opt_ctx) {
588633
}
589634
ggml_backend_buffer_free(opt_ctx->buf_static);
590635
ggml_backend_buffer_free(opt_ctx->buf_cpu);
636+
for (ggml_backend_buffer_t buf : opt_ctx->bufs_momenta) {
637+
ggml_backend_buffer_free(buf);
638+
}
639+
for (struct ggml_context * ctx : opt_ctx->ctxs_momenta) {
640+
ggml_free(ctx);
641+
}
591642
ggml_free(opt_ctx->ctx_static);
592643
ggml_free(opt_ctx->ctx_cpu);
593644
delete opt_ctx;

include/llama.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,12 @@ extern "C" {
15521552
void * get_opt_pars_ud; // userdata for calculating optimizer parameters
15531553

15541554
enum ggml_opt_optimizer_type optimizer_type;
1555+
1556+
// Gradient checkpointing: mark every Nth forward graph node as persistent so the
1557+
// allocator cannot reuse its memory during backward. Reduces peak activation VRAM
1558+
// at the cost of ~0 extra compute (activations are kept, not recomputed).
1559+
// Set to 0 (default) to disable. Good values: 32–64 nodes ≈ every 1–2 transformer layers.
1560+
int32_t grad_checkpoint_interval;
15551561
};
15561562

15571563
LLAMA_API void llama_opt_init(struct llama_context * lctx, struct llama_model * model, struct llama_opt_params lopt_params);

src/llama-context.cpp

Lines changed: 97 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2618,11 +2618,71 @@ void llama_context::opt_init(struct llama_model * model, struct llama_opt_params
26182618
GGML_ASSERT(model->hparams.n_ctx_train % n_batch == 0);
26192619
GGML_ASSERT(n_batch % n_ubatch == 0);
26202620

2621+
// Recreate the scheduler and gf_res_prev with a training-inflated graph size before
2622+
// creating opt_ctx, so opt_ctx captures the new (larger) scheduler pointer.
2623+
// The backward graph (gb_grad) duplicates gf and adds ~2-3x more nodes+leafs;
2624+
// gb_opt adds optimizer step nodes on top.
2625+
//
2626+
// We measure the actual training forward graph node count at n_ubatch here,
2627+
// then multiply by 4 to cover gf + gb_grad + gb_opt. This is exact for any
2628+
// model size — no magic constant needed.
2629+
{
2630+
uint32_t train_fwd_nodes = 0;
2631+
2632+
// Build a real training-ubatch forward graph in split-only mode (no buffer realloc)
2633+
// so we can count its actual nodes. Fall back to n_tensors formula if it fails.
2634+
if (memory) {
2635+
auto mctx_tmp = memory->init_full();
2636+
if (mctx_tmp) {
2637+
// graph_reserve() uses gf_res_reserve to build the graph, so both
2638+
// must be large enough to hold the training forward graph.
2639+
// Use 16x n_tensors as a generous temporary cap for the measurement pass.
2640+
const uint32_t tmp_cap = std::max<uint32_t>(4096u, 16u * model->n_tensors());
2641+
gf_res_prev.reset(new llm_graph_result(tmp_cap));
2642+
gf_res_reserve.reset(new llm_graph_result(tmp_cap));
2643+
// split_only=true: only splits the graph, doesn't reallocate compute buffers
2644+
auto * gf_train = graph_reserve(n_ubatch, 1, n_ubatch, mctx_tmp.get(), /*split_only=*/true);
2645+
if (gf_train) {
2646+
train_fwd_nodes = (uint32_t)ggml_graph_n_nodes(gf_train);
2647+
LLAMA_LOG_INFO("%s: measured training graph nodes = %u (n_ubatch=%u)\n",
2648+
__func__, train_fwd_nodes, n_ubatch);
2649+
}
2650+
}
2651+
}
2652+
2653+
if (train_fwd_nodes == 0) {
2654+
// Fallback: use n_tensors formula
2655+
train_fwd_nodes = std::max<uint32_t>(1024u, 8u * model->n_tensors());
2656+
LLAMA_LOG_WARN("%s: could not measure training graph, using fallback nodes=%u\n",
2657+
__func__, train_fwd_nodes);
2658+
}
2659+
2660+
// gf + gb_grad + gb_opt each need ~train_fwd_nodes; multiply by 4 for safety headroom.
2661+
// Multiply by 2 again for the scheduler's n_nodes + n_leafs check.
2662+
const int64_t inflated = (int64_t)std::max<uint32_t>(train_fwd_nodes, 1024u) * 4;
2663+
const int64_t sched_size = inflated * 2;
2664+
// Both gf_res_prev and gf_res_reserve are used to build forward graphs
2665+
// (graph_reserve uses gf_res_reserve; opt_epoch_iter uses gf_res_prev).
2666+
// Both must have capacity for the full backward graph.
2667+
gf_res_prev.reset(new llm_graph_result(inflated));
2668+
gf_res_reserve.reset(new llm_graph_result(inflated));
2669+
sched.reset(ggml_backend_sched_new(backend_ptrs.data(), backend_buft.data(), backend_ptrs.size(),
2670+
sched_size, cparams.pipeline_parallel, cparams.op_offload));
2671+
// Suppress the next sched_reserve() call so that llama_decode() during GRPO inference
2672+
// steps does NOT replace the training sched with a smaller inference sched.
2673+
// opt_ctx->backend_sched stores a raw pointer to sched.get(); replacing sched while
2674+
// opt_ctx is alive would leave that pointer dangling and crash on the next opt_epoch.
2675+
sched_need_reserve = false;
2676+
LLAMA_LOG_INFO("%s: training graph capacity = %lld (train_fwd_nodes=%u x4)\n",
2677+
__func__, (long long)inflated, train_fwd_nodes);
2678+
}
2679+
26212680
ggml_opt_params opt_params = ggml_opt_default_params(sched.get(), GGML_OPT_LOSS_TYPE_CROSS_ENTROPY);
2622-
opt_params.opt_period = n_batch / n_ubatch;
2623-
opt_params.get_opt_pars = lopt_params.get_opt_pars;
2624-
opt_params.get_opt_pars_ud = lopt_params.get_opt_pars_ud;
2625-
opt_params.optimizer = lopt_params.optimizer_type;
2681+
opt_params.opt_period = n_batch / n_ubatch;
2682+
opt_params.get_opt_pars = lopt_params.get_opt_pars;
2683+
opt_params.get_opt_pars_ud = lopt_params.get_opt_pars_ud;
2684+
opt_params.optimizer = lopt_params.optimizer_type;
2685+
opt_params.grad_checkpoint_interval = lopt_params.grad_checkpoint_interval;
26262686
opt_ctx = ggml_opt_init(opt_params);
26272687

26282688
llama_opt_param_filter param_filter = lopt_params.param_filter;
@@ -2706,6 +2766,8 @@ void llama_context::opt_epoch_iter(
27062766
};
27072767

27082768
uint32_t pos_batch = 0;
2769+
static bool timings_printed = false; // print per-ubatch timings only for the first window
2770+
struct ggml_context * ctx_compute_opt = nullptr;
27092771
do {
27102772
const auto & ubatch = mctx->get_ubatch();
27112773

@@ -2718,26 +2780,38 @@ void llama_context::opt_epoch_iter(
27182780

27192781
auto * res = gf_res_prev.get();
27202782

2783+
const int64_t t0_build = ggml_time_ms();
27212784
const auto gparams = graph_params(res, ubatch, mctx.get(), LLM_GRAPH_TYPE_DEFAULT);
27222785

27232786
res->reset();
27242787

27252788
auto * gf = model.build_graph(gparams);
27262789

2727-
struct ggml_context * ctx_compute_opt;
2728-
{
2790+
// Allocate the tensor metadata context once, then reset it each iteration.
2791+
// ggml_reset() is much cheaper than ggml_free()+ggml_init() — it just resets the
2792+
// allocation pointer without freeing/reallocating the backing memory buffer.
2793+
if (!ctx_compute_opt) {
27292794
const size_t size_gf = ggml_graph_size(gf);
2730-
const size_t size_meta = 4*size_gf*ggml_tensor_overhead() + 2*ggml_graph_overhead_custom(size_gf, /*grads = */ true);
2795+
const size_t size_meta = 4*size_gf*ggml_tensor_overhead() + 3*ggml_graph_overhead_custom(size_gf, /*grads = */ true);
27312796
struct ggml_init_params params = {
27322797
/*.mem_size =*/ size_meta,
27332798
/*.mem_buffer =*/ nullptr,
27342799
/*.no_alloc =*/ true,
27352800
};
27362801
ctx_compute_opt = ggml_init(params);
2802+
if (!timings_printed) {
2803+
LLAMA_LOG_INFO("%s: [timing] graph capacity=%zu n_nodes=%d size_meta=%.1fMB\n", __func__,
2804+
size_gf, ggml_graph_n_nodes(gf), (double)size_meta / (1024*1024));
2805+
}
2806+
} else {
2807+
ggml_reset(ctx_compute_opt);
27372808
}
2809+
2810+
const int64_t t1_alloc = ggml_time_ms();
27382811
ggml_opt_prepare_alloc(opt_ctx, ctx_compute_opt, gf, res->get_inp_tokens(), res->get_logits());
27392812
ggml_opt_alloc(opt_ctx, train);
27402813

2814+
const int64_t t2_inputs = ggml_time_ms();
27412815
res->set_inputs(&ubatch);
27422816
{
27432817
struct ggml_tensor * labels = ggml_opt_labels(opt_ctx);
@@ -2753,14 +2827,29 @@ void llama_context::opt_epoch_iter(
27532827
ggml_backend_tensor_set(labels, &reward_scale, (pos_ubatch*labels->ne[0] + labels_sparse[ilabel])*sizeof(float), sizeof(float));
27542828
}
27552829
}
2830+
2831+
const int64_t t3_eval = ggml_time_ms();
27562832
ggml_opt_eval(opt_ctx, result);
2833+
2834+
const int64_t t4_done = ggml_time_ms();
2835+
if (!timings_printed) {
2836+
LLAMA_LOG_INFO("%s: [timing] build=%" PRId64 "ms alloc=%" PRId64 "ms inputs=%" PRId64 "ms eval=%" PRId64 "ms total=%" PRId64 "ms\n",
2837+
__func__,
2838+
t1_alloc - t0_build,
2839+
t2_inputs - t1_alloc,
2840+
t3_eval - t2_inputs,
2841+
t4_done - t3_eval,
2842+
t4_done - t0_build);
2843+
timings_printed = true;
2844+
}
2845+
27572846
if (callback) {
27582847
callback(train, opt_ctx, dataset, result, idata_in_loop + (pos_ctx + pos_batch)/n_ubatch + 1, ndata_in_loop, t_loop_start);
27592848
}
2760-
ggml_free(ctx_compute_opt);
27612849

27622850
pos_batch += ubatch.n_tokens;
27632851
} while (mctx->next());
2852+
ggml_free(ctx_compute_opt);
27642853
}
27652854
}
27662855

0 commit comments

Comments
 (0)