@@ -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