From ee86a150bd183aa3e8bb4f641da3262731a9a73c Mon Sep 17 00:00:00 2001 From: Georgi Gerganov Date: Mon, 28 Sep 2026 16:36:38 +0300 Subject: [PATCH] tests : refactor test-recurrent-state-rollback (llama/29426) * tests : use llama_context_ptr in test-recurrent-state-rollback Replace raw llama_context pointers with llama_context_ptr and drop the manual llama_free calls and cleanup lambda. Assisted-by: pi:llama.cpp/MiMo-V2.6-Flash-RL * tests : run test-recurrent-state-rollback over all dummy models Add a --models DIR mode that mirrors test-save-load-state: iterate every dummy model, report PASS/FAIL/SKIP in a table and fail only when a model fails. Register a single ctest entry with ARGS --models instead of the four per-model registrations. Assisted-by: pi:llama.cpp/MiMo-V2.6-Flash-RL * cont : fix typo * metal : allow fusing 0-element nodes to keep graph packing shape-independent The fusion packing in ggml_metal_fusion_max excluded 0-element tensors and the topk_moe/moe_reduce checks rejected n_tokens == 0, so graphs decoding batches with no outputs packed differently from the worst-case reserved graph. The Metal optimizer then reordered the nodes differently and ggml_gallocr_needs_realloc failed on the layout mismatch, forcing an unexpected graph re-reserve (caught by GGML_SCHED_DEBUG_REALLOC). Treat empty tensors like their non-empty counterparts: match them in the pattern sequence and only reject genuinely malformed shapes. Fused kernels dispatch zero threadgroups for empty graphs, which is a legal no-op. Assisted-by: pi:llama.cpp/MiMo-V2.6-Flash-RL * tests : run test_multi_seq_split_replay as a separate test test_multi_seq_split_replay was invoked at the end of test_rollback, so its result was folded into the rollback status and it only ran when the rollback part passed. Give it its own test_status return, run both tests independently over both cache fills via a shared run_tests helper, and report them as separate rollback / split replay columns in the --models table with per-test summaries. The exit code fails when either test fails. Assisted-by: pi:llama.cpp/MiMo-V2.6-Flash-RL * tests : loosen the split replay nmse bound to 1e-4 test-generate-models seeds its weights from std::random_device, and some generated lfm2 models drift up to ~1.7e-5 nmse on the split replay due to rounding noise, tripping the previous 1e-5 bound. Raise the bound to 1e-4 so the random generations stop flaking. Assisted-by: pi:llama.cpp/MiMo-V2.6-Flash-RL * tests : reuse run_tests_for_model in single-model mode The single-model path duplicated the model init and the non-recurrent check from run_tests_for_model; route it through the shared helper instead. Model load failures now return FAIL rather than SKIP so that --model with a broken file still exits non-zero, and the helper loads with model_only like the --models loop does since the tests create their own contexts. Assisted-by: pi:llama.cpp/MiMo-V2.6-Flash-RL --- ggml/src/ggml-metal/ggml-metal-fusion.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ggml/src/ggml-metal/ggml-metal-fusion.cpp b/ggml/src/ggml-metal/ggml-metal-fusion.cpp index eac3bd6fe..e55b01503 100644 --- a/ggml/src/ggml-metal/ggml-metal-fusion.cpp +++ b/ggml/src/ggml-metal/ggml-metal-fusion.cpp @@ -418,7 +418,8 @@ static bool ggml_metal_fusion_check_topk_moe( const int64_t n_tokens = logits->ne[1]; const int64_t n_expert_used = ids->ne[0]; - if (n_expert <= 0 || n_tokens <= 0 || n_expert_used <= 0 || n_expert_used > n_expert || + // note: n_tokens == 0 (no-output batch) must match so that the packing stays shape-independent + if (n_expert <= 0 || n_expert_used <= 0 || n_expert_used > n_expert || n_expert > GGML_METAL_TOPK_MOE_MAX_EXPERTS || n_expert_used > GGML_METAL_TOPK_MOE_MAX_EXPERTS) { return false; } @@ -545,7 +546,8 @@ static bool ggml_metal_fusion_match_moe_reduce( const int64_t n_embd = experts->ne[0]; const int64_t n_tokens = experts->ne[2]; - if (n_embd <= 0 || n_tokens <= 0 || experts->ne[1] != n_expert_used || experts->ne[3] != 1 || + // note: n_tokens == 0 (no-output batch) must match so that the packing stays shape-independent + if (n_embd <= 0 || experts->ne[1] != n_expert_used || experts->ne[3] != 1 || weights->ne[0] != 1 || weights->ne[1] != n_expert_used || weights->ne[2] != n_tokens || weights->ne[3] != 1 || dst->ne[0] != n_embd || dst->ne[1] != n_tokens || dst->ne[2] != 1 || dst->ne[3] != 1) { return false; @@ -1079,16 +1081,17 @@ const ggml_metal_fusion * ggml_metal_fusion_next( // transparent) node sequence that the compute phase uses, so the returned count is the raw index // span from idx to the last matched node (intermediate views are packed along). int ggml_metal_fusion_max(const ggml_cgraph * gf, int idx) { - // an empty/view node cannot start a pattern - pack it alone - if (ggml_op_is_empty(gf->nodes[idx]->op) || ggml_is_empty(gf->nodes[idx])) { + // a view node cannot start a pattern - pack it alone + if (ggml_op_is_empty(gf->nodes[idx]->op)) { return 1; } - // collect the non-empty node indices starting at idx + // collect the non-view node indices starting at idx; 0-element tensors are included so + // that empty graphs pack like their non-empty counterparts (see ggml_metal_fusion_filter_ops) int idxs[GGML_METAL_FUSION_MAX]; int n_idxs = 0; for (int i = idx; i < gf->n_nodes && n_idxs < GGML_METAL_FUSION_MAX; i++) { - if (!ggml_op_is_empty(gf->nodes[i]->op) && !ggml_is_empty(gf->nodes[i])) { + if (!ggml_op_is_empty(gf->nodes[i]->op)) { idxs[n_idxs++] = i; } }