Repository navigation
ggml: a broadcast divisor gets its summed gradient; a backward that breaks its shape contract refuses the training graph instead of aborting the server - #99
Merged
joelteply merged 1 commit intoNov 7, 2026
Conversation
…at breaks its shape contract refuses the training graph instead of aborting the server The M5's engine aborted mid-act on 2026-11-07 16:13Z during a /train on Qwen3.6-35B-A3B (continuum card 6be38d73): ggml.c:7797 GGML_ASSERT(!src1_needs_grads || kv_sunk || ggml_are_same_shape(src1, cgraph->grads[isrc1])), from ggml_opt_build in server_trainer's thread, killing five serving slots. Cause: build_moe_ffn normalizes the router weights as weights / clamp(sum_rows(weights), eps, inf) (llama-graph.cpp:2134-2144): dividend [n_expert_used, n_tokens], divisor [1, n_tokens], broadcast. GGML_OP_DIV's backward gave the divisor grad * tensor / src1 with the dividend's shape and never summed it over the broadcast (ADD and MUL do, via ggml_repeat_back). #84 opened the router path (CLAMP's backward) and was proved on the toy qwen35moe of test-llama-archs, which uses EXPERT_USED_COUNT 1, where the two shapes agree; the 35B-A3B uses 8. SUB had the same gap (delta-net-base.cpp's g_cs - g_last on the chunked path). - DIV and SUB: a broadcast src1's gradient is ggml_repeat_back'd onto it. - ggml_compute_backward returns its contract break (a source needing a gradient given none, or one of another shape) instead of asserting. ggml_build_backward_expand_or_refuse returns the refused node and why (the mark walk's refusals included); ggml_build_backward_expand is it plus the abort, now naming the node. - ggml_opt_build builds through it: a refused build sets opt_ctx->refusal, drops the half-built graphs and returns false before any allocation, so opt_ubatch's opt_fail carries it to /train's status as state "error" and the process keeps serving. - test-opt-dynamic-accum: the router's normalization (2 experts, 1 token) trains one SGD step to the analytic update; a mis-shaped gradient (a wrong-shape accumulator) is refused by name, not asserted. - test-backend-ops grad mode: SUB/MUL/DIV (and broadcast ADD) are checked against numeric gradients with broadcast src1; a refused build is a FAIL, not "not supported". Measured on the M5 CPU only (GGML_METAL=OFF, nice 19, ggml libs only): test-opt-dynamic-accum OK; with the DIV repeat_back removed it fails with "cannot be built through DIV (node "node_3"): its backward gave src[1] no gradient or one of another shape" where the deployed build asserts at ggml.c:7797. Metal/CUDA unbuilt here: the fix adds REPEAT_BACK, which both backends already run (MUL_MAT_ID's backward uses it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
joelteply
merged commit Nov 7, 2026
54939d6
into
889580fc/seq-state-across-stream-counts
11 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this serves
Learning (plan path step 3) and one live engine (line 3b): the in-engine trainer (the dream stage) must never take the serving process down. On 2026-11-07 16:13Z the M5's engine aborted mid-act during a
/trainon Qwen3.6-35B-A3B and took five serving slots with it (continuum card 6be38d73; excerpt at~/.continuum/evidence/live-read-20261107/engine-abort-1613Z.txton the M5).Cause (receipts)
ggml/src/ggml.c:7797GGML_ASSERT(!src1_needs_grads || kv_sunk || ggml_are_same_shape(src1, cgraph->grads[isrc1])). That is the shape check that runs after every op's backward inggml_compute_backward. Call path:server_trainer::run->opt_epoch->opt_walk::train_chunk->decode->opt_ubatch->ggml_opt_alloc->ggml_opt_build->ggml_build_backward_expand.build_moe_ffnnormalizes the router weights asweights / clamp(sum_rows(weights), eps, inf)(src/llama-graph.cpp:2134-2144). The dividend is[n_expert_used, n_tokens]and the divisor is[1, n_tokens], so the divisor is broadcast.GGML_OP_DIV's backward gave the divisorgrad * tensor / src1in the dividend's shape and never summed it over the broadcast. ADD and MUL do that sum withggml_repeat_back. Any LoRA upstream of a router (the defaultattn_q,attn_vtargets included) makes that divisor need a gradient.tests/test-llama-archs.cpp:291, which setsEXPERT_USED_COUNT 1; with one expert the two shapes agree. The 35B-A3B uses 8.repeat_backremoved, the new test is refused at exactly this node:cannot be built through DIV (node "node_3"): its backward gave src[1] no gradient or one of another shape. The deployed build asserts there instead.training on the served modelline, the run that aborted. I found no successful 35B-A3B training run to compare against.src/models/delta-net-base.cpp:216(g_cs - g_last) on the chunked delta-net path, used when the fused GDN op is not available. This one is latent.Fix
ggml_repeat_back.Isolation (the architectural part)
ggml_compute_backwardnow returns its contract break instead of asserting: a source that needs a gradient was given none, or one of a different shape. The contract is checked in one place, so this covers that whole class of defect in every op, not just DIV.ggml_build_backward_expand_or_refuse(new,ggml.h) returns the refused node and the reason. That includes the existing mark-walk refusals (no backward, in-place write, non-float source).ggml_build_backward_expandis that call plus the abort, and the abort message now names the node.ggml_opt_buildbuilds throughggml_build_backward_expand_or_refuse. On a refusal it setsopt_ctx->refusal, drops the half-builtgb_grad/gb_opt, and returns false before anything is allocated. From there the existingopt_ubatch->opt_failpath carries it to/train's status asstate: "error"with the node named, and the process keeps serving. The same holds for/learnon the serving context.GGML_ASSERTs inside individual backward cases (for example MUL_MAT's ne checks) still abort. The mark walk already refuses the structural ones (ggml_backward_reaches_src). Converting the rest is follow-up work. No asserts are globally disabled.Tests
test-opt-dynamic-accum:-lr * (w1, -w0) / (w0 + w1)^2;test-backend-opsgrad mode: SUB, MUL, DIV and broadcast ADD are checked against numeric gradients with a broadcast src1, which covers the router'snr {k,1,..}shape. A refused build now counts as a FAIL, not "not supported".Measured
These ran on the M5, CPU only (GGML_METAL=OFF,
nice -n 19, ggml libraries only, own build dir, beside live serving):test-opt-dynamic-accum: OK. With the mutant (DIVrepeat_backremoved) it fails with the refusal above.test-backend-ops grad -o ADD,SUB,MUL,DIV(CPU): every broadcast ADD/SUB/MUL/DIV grad case passes against finite differences (19839/19840 cases matched by the filter). The one failure istest_flash_attn_ext_grad(hs=16,nh=4,nhkv=2,kv=24,nb=8,native=0, MAA 1.7e-4 > 1e-4). It also fails on the unmodified base 46c6644 (native=0 MAA 3.8e-4, native=1 MAA 1.2e-4), so its tolerance is flaky and the failure predates this changeNot built: Metal and CUDA. The fix adds REPEAT_BACK, which both backends already run (MUL_MAT_ID's backward uses it). Not run: a live
/trainon the 35B-A3B. That needs a deploy and is continuum's call.Stack order with #85-#89
No file overlap. #85, #86, #88 and #89 touch
include/llama.h,src/llama-context*.{h,cpp}(opt_init/opt_select/opt_release, not train_chunk/opt_ubatch) andtools/server/*. This PR touchesggml/include/ggml.h,ggml/src/ggml.c,ggml/src/ggml-opt.cppand two tests. None of them changes the crash path (the backward build) or fixes this assert. #85 puts learning on the serving context (per-mind shadows), so without this PR a/learnon the 35B-A3B would hit the same DIV assert inside the serving context itself. Suggested order: this PR first, or in either order, since neither rebases onto the other.🤖 Generated with Claude Code
https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo