cuda: fusion hit counters, test guards and the #343 review follow-ups - #349
Conversation
…ed() The cached GGML_TQ_MMQ lookup initialized its static from a call to itself, so the first call on the AMD MMQ path (a TQ mul_mat with n >= 8, i.e. any prefill) re-entered the static's guard from the same thread and waited on it forever. NVIDIA builds short-circuit the getenv away and never call the function, which is why the CUDA gates stayed green. Read the environment variable directly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxP6x5bmUDYFvmouceN2mR (cherry picked from commit f92652a)
…eTom#343 review - Count elementwise chains, tuned multi-ADD/MUL runs and mmvq shared-quantize cache hits on the CUDA context, readable through ggml_backend_cuda_fusion_count() and the backend's get_proc_address table, so a test can tell whether a fusion fired instead of only checking the fused result. - test-backend-ops: test_case::required_fusion() names a counter that must advance while the graph runs on the tested backend; a case fails with "fusion did not fire" when it does not. ELEM_CHAIN_FUSION and the Q8_0/Q4_0 cases of MUL_MAT_SHARED_SRC1 use it. Backends without the counter are unaffected. - New chain cases: SCALE with a nonzero bias, and a pure run of four same-shape ADDs. - The chain detector now hands a pure same-layout ADD or MUL run, chained through src0, back to the tuned multi-ADD/MUL kernels, which are a little faster for it (MI210, four ADDs over 4096x64 f32: 3.75-3.96 us per run tuned vs 4.0 us chain). Comments at both sites say which patterns go where. - MOE_REDUCE_FUSION renamed to MUL_MAT_ID_REDUCE: it covers the MoE reduce tail, not a fusion. - GGML_TQ_Q8CACHE renamed to GGML_CUDA_Q8CACHE (the cache is not TQ-specific); it and GGML_CUDA_FUSE_CHAIN are documented in the AGENTS.md environment table. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxP6x5bmUDYFvmouceN2mR
|
The counters and the proc-address plumbing are right: each increments once per launch inside the branch that actually launches, never on a declined fusion; test-backend-ops stays free of CUDA headers and gets a null proc address on other backends; both guarded cases run whole-graph so Three things before I merge, all in the test file:
Follow-up rather than blocker: Nits: I'll run the four suites on a GB10 on the current head in the meantime; happy to push 1 to 3 myself if you'd rather rest the hands. |
- MUL_MAT_SHARED_SRC1 required a q8 cache hit for every n <= 8, but ggml_cuda_should_use_mmvq picks the mmvq path from a per-architecture table whose lowest bound among the types tested here is ne11 <= 6 (Q8_0 on CDNA1), so n = 8 routes to MMQ there and the guard failed on a correct build. Guard only n <= 4, which every architecture routes to mmvq. - Skip the guard entirely under GGML_CUDA_DISABLE_FUSION, matching the backend's own predicate, so the global kill switch stays usable while bisecting a numeric problem. - Treat an unknown counter name as a failure instead of a silent pass, so a typo cannot disable the check, and print the failure reason on the console: it was only reaching the CSV output, which left every failing case as a bare FAIL. - Report every failure reason, so a numeric failure stays visible when the guard trips too, and evaluate required_fusion() once. - Split fused_binary into fused_add and fused_mul, counted in their own branches, so the multi-ADD case cannot be satisfied by a stray fused MUL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NxP6x5bmUDYFvmouceN2mR
|
All three are fixed in af1b860, thank you. The CDNA1 one was a real hole rather than a style point. 1. 2. 3. An unknown counter name is now a failure, not a silent pass. Chasing that third one turned up something you did not ask for, and I would like your read on it: the console printer was discarding Also from your nits:
On MI210 (gfx90a),
The guard itself, checked in both directions:
Thank you for the offer to push 1 to 3 yourself. The hands are slow but they work, and this was small enough to be worth doing here. I am glad you caught the CDNA1 table; that is not a case this machine can reach. |
|
All three land the way I hoped, and the CDNA1 bound being a literal with the source named in the comment is the right call given the header constraint. The console printer change is welcome here rather than as its own PR: a guard that cannot say why it tripped is exactly the thing this PR exists to fix, and |
6d7faf6
into
TheTom:feature/turboquant-kv-cache
Follow-up to #343, the non-blocking items from the review there.
What changed
ggml_backend_cuda_fusion_count(backend, name), also reachable viaggml_backend_reg_get_proc_address(reg, "ggml_backend_cuda_fusion_count")so test-backend-ops needs no CUDA header.test_casegets arequired_fusion()hook; when the tested backend exposes the counter and it does not advance while the graph runs, the case fails withfusion did not fire.ELEM_CHAIN_FUSIONrequires the chain (the multi-ADD case requires the tuned kernel instead, see below),MUL_MAT_SHARED_SRC1requires a cache hit for the n <= 8 Q8_0/Q4_0 cases (the mmvq path; native TQ weights quantize their activation on their own path, so those stay numeric-only). Backends without the counter are unaffected. Sanity check below: withGGML_CUDA_FUSE_CHAIN=0the chain cases fail, withGGML_CUDA_Q8CACHE=0the eight Q8_0/Q4_0 cache cases fail.SCALEwith a nonzero bias (ggml_scale_bias), and a pure run of four same-shape f32ADDs, which is the pattern the tuned multi-ADD kernel also takes.ggml_cuda_try_fuse()'s tuned path and keeps everything else. Both call sites carry a comment saying so.MOE_REDUCE_FUSIONrenamed toMUL_MAT_ID_REDUCE, since it no longer tests a fusion.GGML_TQ_Q8CACHEbecomesGGML_CUDA_Q8CACHE(the cache is not TQ-specific), and both it andGGML_CUDA_FUSE_CHAINare documented in the AGENTS.md table. This is a breaking rename with no alias for the old name. It is safe here only because the variable landed in cuda: shared-quantize cache, residual and elementwise-chain fusions (generic half of #338) #343 and has not been in a release; anyone who picked it up from that branch has to change the name.Second review round
MUL_MAT_SHARED_SRC1's guard is narrowed fromn <= 8ton <= 4.ggml_cuda_should_use_mmvqpicks the mmvq path from a per-architecture table whose lowest bound among the types registered here isne11 <= 6(Q8_0 on CDNA1), son = 8routes to MMQ there and the guard would have failed on a correct build.GGML_CUDA_DISABLE_FUSIONis set and non-zero, matching the backend's own predicate, so the global kill switch stays usable while bisecting a numeric problem.print_test_consoleshows the failure reason: it was only reaching the CSV output, which left every failing case as a bareFAIL.fused_binaryis split intofused_addandfused_mul, counted in their own branches; the multi-ADD case requiresfused_add.required_fusion()is evaluated once, and every failure reason is reported so a numeric failure stays visible when the guard trips too.Test
MI210 (gfx90a),
GGML_TQ_MMQ=1 GGML_TQ_NATIVE=1:-o ELEM_CHAIN_FUSION(5 cases: base, gelu+softplus, self_mul, scale bias, add run)-o MUL_MAT_SHARED_SRC1-o MUL_MAT_RESIDUAL_FUSION-o MUL_MAT_ID_REDUCE-o MUL_MAT -p type_a=tq4_1s/tq3_1sGGML_CUDA_FUSE_CHAIN=0 -o ELEM_CHAIN_FUSIONGGML_CUDA_Q8CACHE=0 -o MUL_MAT_SHARED_SRC1The branch sits on top of #348, since the tq4 suites hang on the base without it.
Timing, chain kernel vs tuned multi-ADD
test-backend-ops perf -o ELEM_CHAIN_FUSION, theadd_run=1case ({4096, 64} f32, four ADDs, 3 MB moved per run), three runs each, before the yield was added:GGML_CUDA_FUSE_CHAIN=1, before the yield)ggml_cuda_op_fused_add(GGML_CUDA_FUSE_CHAIN=0)For a mixed chain (SILU, MUL, ADD, ADD, SCALE with bias, CLAMP over the same shape) the chain kernel is the faster one: 2.50 us/run vs 2.52-2.54 us/run with it disabled.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NxP6x5bmUDYFvmouceN2mR