Skip to content

graph: six fusions for the hybrid-attention decode graph (-25% ops/token; Metal 2B +7.7%, 27B +2.6% decode; CUDA 9B +2.5%) - #165

Merged
khosravipasha merged 9 commits into
prismfrom
perf/metal-fwht-signed
Sep 8, 2026
Merged

khosravipasha merged 9 commits into
prismfrom
perf/metal-fwht-signed

Conversation

@bri-prism

Copy link
Copy Markdown
Collaborator

What

Six graph-level changes that cut the encoded ops per decoded token on the hybrid-attention models by 25% (2,359 to 1,766 on the largest folded band) without changing any result bit:

  1. Metal: Hadamard sign flip fused into the FWHT kernel. Every folded weight's activation went MUL(signs) -> RESHAPE -> FWHT matmul; the transform now applies the +-1 factors on load (CUDA already did this). The pattern is matched on the graph's own indices because the encode list drops empty ops.
  2. GDN raw gates (ggml op extension, CPU + Metal + CUDA). ggml_gated_delta_net_set_raw_gates() lets the fused op take beta and alpha pre-activation with dt_bias and A as extra sources, applying sigmoid and A * softplus(alpha + dt_bias) in its prologue with the unary kernels' formulas. The qwen35 builder uses it on the fused-op paths only; the chunked path keeps the activated tensors. Backends that compute GDN but do not implement the flag (SYCL, OpenCL, WebGPU, Hexagon) decline it in supports_op, and the builder only emits it when every device is CPU/Metal/CUDA/ROCm/MUSA. GGML_GDN_RAW_GATES_DISABLE=1 restores the separate ops.
  3. qwen35: gated norm as one ggml_swiglu_split instead of silu + mul.
  4. qwen35: one l2_norm over the adjacent q and k head groups, q and k as views of the result.
  5. Metal: trailing silu fused into the ssm_conv kernels (function constant), as on CUDA.
  6. qwen35: device scans hoisted to the graph constructor (the two per-layer loops that cuda: query the device type with cudaDeviceGetAttribute, not cudaGetDeviceProperties (+8% decode) #163 made cheap now run once per graph).

Verification (M5 Pro, prism 8c0170d base, separate worktrees, r=3 x2 interleaved)

Correctness: test-backend-ops -b MTL0 -o GATED_DELTA_NET 39/39 (4 new raw-gate cases against the CPU reference), -o MUL_MAT_HADAMARD 24/24, -o SSM_CONV 45/45. Greedy output byte-identical to base on the largest folded band for 3 prompts x 64 tokens and a 230-token prompt on both the smallest and largest bands (exercises the batched conv, chunked GDN and prefill FWHT paths).

band tg128 base tg128 branch pp512 base pp512 branch
2B PQ2_0 207.0 223.0 (+7.7%) 5,131 5,257 (+2.5%)
largest band (folded PQ2_0) 27.15 27.85 (+2.6%) 391.0 399.5 (+2.2%)

Per change on the 2B decode: sign+FWHT +2.8%, raw gates +1.3%, swiglu gating +3%, l2 pair ~0, conv+silu +1.6%.

CUDA (H100 PCIe, CUDA 12.x, base 8c0170d vs branch, separate trees, sheet flags, 2 passes)

test-backend-ops -b CUDA0: -o GATED_DELTA_NET 39/39 with the 3 applicable raw-gate cases OK (the rows-mode one is not supported on CUDA, as before), -o MUL_MAT_HADAMARD 27/27, -o SSM_CONV 45/45, -o L2_NORM 20/20. Greedy output identical to base on the 9B and 2B (48 tokens).

band tg128 base tg128 branch tg128 branch, GGML_GDN_RAW_GATES_DISABLE=1 pp512
9B PQ2_0 166.6 170.8 (+2.5%) 167.5 unchanged (6.1k, noisy)
2B PQ2_0 355-373 367-369 360 unchanged (noisy)

CUDA already fused the sign flip and the conv silu, so the CUDA gain is the raw gates plus the two graph-level ops; the 2B numbers sit inside that host's run-to-run spread.

The two device-scan loops in the qwen35 builder are also hoisted to the graph constructor (one scan per graph build), as suggested in the #163 review.

Every folded weight's activation went through three encoded ops: an elementwise
MUL by the +-1 sign vector, a RESHAPE and the FWHT-hint MUL_MAT. The sign factors
are exact, so the transform now applies them while loading its row and the MUL
is elided, matching what the CUDA backend already does. The encode list drops
empty ops, so the MUL / RESHAPE / MUL_MAT pattern is matched on the graph's own
indices and checked with ggml_can_fuse_subgraph_ext; the fused kernel writes the
matmul's output, so its ranges are checked for concurrency explicitly.

Measured on an M5 Pro, largest band PQ2_0 folded (block 1024), r=3 x2 interleaved:
encoded nodes per decoded token 2359 -> 2103, tg128 27.05 -> 27.45 (+1.5%),
pp512 390.5 -> 397.2 (+1.7%). Greedy output byte-identical over 3 prompts;
test-backend-ops MUL_MAT_HADAMARD 24/24.
The hybrid-attention layers spent four elementwise launches per layer per
token on the gate scalars: sigmoid(beta), and softplus(alpha + dt_bias) * A
for the decay. ggml_gated_delta_net_set_raw_gates() marks a gated_delta_net
op as taking beta and alpha pre-activation, with dt_bias and A as two extra
sources, and the kernel applies the same formulas as the unary kernels while
reading the scalars it already read. The qwen35 builder hands the fused op the
raw projections (GGML_GDN_RAW_GATES_DISABLE=1 restores the separate ops); the
non-fused chunked path keeps the activated tensors. CPU reference and Metal
kernel implemented here; CUDA follows in the next commit.

test-backend-ops -o GATED_DELTA_NET on MTL0: 39/39 incl. 4 raw-gate cases
(decode, 2 seqs, prefill 64, rows mode). Encoded nodes per decoded token on the
largest band folded PQ2_0: 2103 -> 1910. Greedy output byte-identical to base.
M5 Pro, r=3 x2 interleaved, both fusions vs base: 2B tg128 206.4 -> 214.8
(+4.1%), pp512 +1.0%; largest band tg128 27.08 -> 27.55 (+1.7%), pp512 +1.3%.
Same folding as the Metal and CPU implementations: with the op's raw-gate flag
set, beta goes through sigmoid and the decay is raw_a[h] * softplus(g +
raw_dt_bias[h]) inside the kernel, using the formulas of the unary kernels.
The template gains a RAW parameter (scalar gate only; KDA asserts it off) and
the two tables are passed as extra pointers.

Compile-checked only by inspection here; needs a CUDA build plus
test-backend-ops -o GATED_DELTA_NET before this branch is pushed.
…he builder on device backend

SYCL, OpenCL, WebGPU and Hexagon compute gated_delta_net but do not implement the
raw-gate flag, so their supports_op now declines such ops (the CPU fallback is
correct). The qwen35 builder only emits raw gates when every device is CPU,
Metal, CUDA, ROCm or MUSA, so those backends keep their native op instead of a
CPU fallback for the whole recurrence.
silu(z) * rms_norm(o) was a unary and a mul; ggml_swiglu_split computes the same
expression in one kernel. Encoded nodes per decoded token on the largest folded band
PQ2_0: 1910 -> 1862. Greedy output byte-identical. M5 Pro decode with the three
graph changes so far vs base: 2B 205.3 -> 219.9 tok/s (+7.1%), largest band 27.12 ->
27.72 (+2.2%).
q and k occupy adjacent, equally shaped head groups in the conv output, so a
single l2_norm over the joint view replaces two launches per recurrent layer;
q and k become views into its result. Encoded nodes per decoded token on the
largest band: 1862 -> 1814. Greedy output byte-identical.
When the conv output is consumed only by a silu, the conv kernels apply it on
store (function constant) and write the silu's tensor directly, matching what
the CUDA backend already does. Encoded nodes per decoded token on the largest band:
1814 -> 1766. test-backend-ops -o SSM_CONV 45/45; greedy output byte-identical.
Cumulative decode vs base on an M5 Pro: 2B 207.0 -> 223.0 tok/s (+7.7%),
largest band 27.15 -> 27.85 (+2.6%).
The fused-GDN state-rows check and the raw-gates check each looped over the
model's devices on every recurrent layer, asking the backend for the device
type each time. Both flags are now computed once in the graph constructor.
They had been appended to the perf-mode list, so test mode never ran them.
@khosravipasha

Copy link
Copy Markdown
Collaborator

Tested on both backends, PR head 6e92bc0 vs its merge-base 9294043 (prism with #160), separate worktrees, llama-bench -p 512 -n 128 -r 3 -fa 1.

Correctness

  • Metal (M5 Max): test-backend-ops -b MTL0 GATED_DELTA_NET 43/43, MUL_MAT_HADAMARD 24/24, SSM_CONV 45/45, L2_NORM 20/20.
  • CUDA (L40S, 12.8, sm_89): -b CUDA0 GATED_DELTA_NET 39/39, MUL_MAT_HADAMARD 27/27, SSM_CONV 45/45, L2_NORM 20/20.
  • Greedy 64 tokens on 9B PQ2_0, 2B PTQ1_0 and 27B PQ2_0: token-for-token identical to base on CUDA.
  • KLD vs F16 golden on 9B PQ2_0 and 2B PTQ1_0 (CUDA): identical to base to every printed digit.

CUDA L40S, tg128, mean of two interleaved passes

Band base PR decode
2B Q2_0 / PQ2_0 / PTQ1_0 429.2 / 435.6 / 415.5 445.3 / 453.6 / 430.6 +3.6 to +4.1%
4B Q2_0 / PQ2_0 / PTQ1_0 268.6 / 275.7 / 276.7 276.3 / 283.6 / 283.5 +2.5 to +2.9%
9B Q2_0 / PQ2_0 / PTQ1_0 174.8 / 180.2 / 186.0 178.1 / 183.4 / 189.8 +1.8 to +2.0%
27B Q2_0 / PQ2_0 / PTQ1_0 69.2 / 72.0 / 79.1 70.3 / 73.2 / 80.2 +1.5 to +1.6%

Prefill flat within noise. Same PR binary with GGML_GDN_RAW_GATES_DISABLE=1: PQ2_0 decode 439.3 / 181.0 / 72.2 on 2B / 9B / 27B, so on CUDA about three quarters of the gain is the raw gates and the rest the graph-level ops, consistent with the +2.5% on the 9B in the description.

Metal M5 Max, tg128, first pass (runs spaced 100 s to avoid throttling; second pass and KLD still running, will append if anything moves)

Band base PR decode pp512 base pp512 PR
2B PQ2_0 / PTQ1_0 267.7 / 281.2 286.7 / 298.0 +7.1% / +6.0% 9507 / 9016 9860 / 9296
4B PQ2_0 / PTQ1_0 159.8 / 163.2 175.6 / 179.6 +9.8% / +10.0% 4115 / 3902 4232 / 3987
9B PQ2_0 / PTQ1_0 116.4 / 114.4 120.2 / 119.6 +3.2% / +4.5% 2429 / 2267 2451 / 2304
27B PQ2_0 45.04 46.72 +3.7% 771 777

The 4B (block 512) gains most on Metal, ahead of the 2B figure in the description; Metal error bars here are 0.1 to 2 t/s.

Review notes: op_params[1] on GATED_DELTA_NET was unused before, the fused sigmoid / softplus use the unary kernels' exact formulas, the Metal sign-flip fusion checks the MUL, RESHAPE and hinted MUL_MAT form a closed subgraph and re-runs the concurrency check on the real output, and the non-implementing backends decline the raw-gate op in supports_op so nothing silently drops to CPU. Merging.

@khosravipasha
khosravipasha merged commit 00e8187 into prism Sep 8, 2026
8 checks passed
khosravipasha added a commit to pooyakhosravi/llama.cpp that referenced this pull request Sep 8, 2026
Resolves the conflicts with PrismML-Eng#160 (PTQ1_0 mmvq multi-column path uses the
compile-time gate flag) and PrismML-Eng#165 (gated_delta_net kernel carries both the
raw-gates flag and the GB10 exp(g) precompute flag; the precompute only
engages when raw gates are off).
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Oct 13, 2026
Pass raw beta and alpha projections to GDN on supported devices. Normalize
the adjacent Q and K head groups together and express gated RMS norm as
one SwiGLU operation. Keep activated gates for the chunked path.

Based on bri-prism's model-graph fusions in PrismML-Eng#165.

Source: PrismML-Eng@00e8187
Assisted-by: GitHub Copilot
Assisted-by: Codex (porting and rebasing)

Ported and rebased by Codex.
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Oct 13, 2026
Apply sigmoid and softplus inside the recurrent shader. Read the selected
cache row directly while retaining snapshot output and rollback layout.
Validate row indices, strides, types and buffer bounds before dispatch.

Implement the Vulkan path for Pasha Khosravi's indexed-state API and
bri-prism's raw-gate fusion, using the existing Vulkan GDN kernel.

Inspired-by: PrismML-Eng#165
Inspired-by: PrismML-Eng@bed6e9a
Assisted-by: Codex
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Oct 13, 2026
Fuse residual normalization, activation quantization and shared-Q8 projections. Add transpose concat, recurrent gate activation and scale combinations, with the original GB10 dispatch controls. Preserve portable device queries and PTX guards, and adapt fused RMS launches to the current channel and sample dimensions.

Source: PrismML-Eng#135
Source: PrismML-Eng#163
Source: PrismML-Eng#165
Source: PrismML-Eng#168

Source commits (PrismML-Eng/llama.cpp):
3dd7675
00e8187
d8d96cf
d8f26ee

Co-authored-by: bri-prism <288398250+bri-prism@users.noreply.github.com>
Co-authored-by: Pasha Khosravi <khosravipasha@users.noreply.github.com>
Co-authored-by: Pasha Khosravi <pasha@prismml.com>
Assisted-by: GitHub Copilot
Assisted-by: OpenAI Codex

Ported and rebased by Codex.
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Oct 13, 2026
Enable indexed state on Metal and raw gates on Metal/CUDA/ROCm/MUSA.
Use Brian (bri-prism)'s separate capability checks and preserve the CPU
and Vulkan paths. The Vulkan extensions were authored by Codex.

Source: PrismML-Eng#165
Original-commit: 00e8187
Source: c1f65ed
Source: 631528b
Assisted-by: GitHub Copilot
Assisted-by: OpenAI Codex

Ported and rebased by Codex.
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Oct 13, 2026
Pass raw beta and alpha projections to GDN on supported devices. Normalize
the adjacent Q and K head groups together and express gated RMS norm as
one SwiGLU operation. Keep activated gates for the chunked path.

Based on bri-prism's model-graph fusions in PrismML-Eng#165.

Source: PrismML-Eng@00e8187
Assisted-by: GitHub Copilot
Assisted-by: Codex (porting and rebasing)

Ported and rebased by Codex.
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Oct 13, 2026
Apply sigmoid and softplus inside the recurrent shader. Read the selected
cache row directly while retaining snapshot output and rollback layout.
Validate row indices, strides, types and buffer bounds before dispatch.

Implement the Vulkan path for Pasha Khosravi's indexed-state API and
bri-prism's raw-gate fusion, using the existing Vulkan GDN kernel.

Inspired-by: PrismML-Eng#165
Inspired-by: PrismML-Eng@bed6e9a
Assisted-by: Codex
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Oct 13, 2026
Fuse residual normalization, activation quantization and shared-Q8 projections. Add transpose concat, recurrent gate activation and scale combinations, with the original GB10 dispatch controls. Preserve portable device queries and PTX guards, and adapt fused RMS launches to the current channel and sample dimensions.

Source: PrismML-Eng#135
Source: PrismML-Eng#163
Source: PrismML-Eng#165
Source: PrismML-Eng#168

Source commits (PrismML-Eng/llama.cpp):
3dd7675
00e8187
d8d96cf
d8f26ee

Co-authored-by: bri-prism <288398250+bri-prism@users.noreply.github.com>
Co-authored-by: Pasha Khosravi <khosravipasha@users.noreply.github.com>
Co-authored-by: Pasha Khosravi <pasha@prismml.com>
Assisted-by: GitHub Copilot
Assisted-by: OpenAI Codex

Ported and rebased by Codex.
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Oct 13, 2026
Enable indexed state on Metal and raw gates on Metal/CUDA/ROCm/MUSA.
Use Brian (bri-prism)'s separate capability checks and preserve the CPU
and Vulkan paths. The Vulkan extensions were authored by Codex.

Source: PrismML-Eng#165
Original-commit: 00e8187
Source: c1f65ed
Source: 631528b
Assisted-by: GitHub Copilot
Assisted-by: OpenAI Codex

Ported and rebased by Codex.
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Nov 2, 2026
Pass raw beta and alpha projections to GDN on supported devices. Normalize
the adjacent Q and K head groups together and express gated RMS norm as
one SwiGLU operation. Keep activated gates for the chunked path.

Based on bri-prism's model-graph fusions in PrismML-Eng#165.

Source: PrismML-Eng@00e8187
Assisted-by: GitHub Copilot
Assisted-by: Codex (porting and rebasing)

Ported and rebased by Codex.
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Nov 2, 2026
Apply sigmoid and softplus inside the recurrent shader. Read the selected
cache row directly while retaining snapshot output and rollback layout.
Validate row indices, strides, types and buffer bounds before dispatch.

Implement the Vulkan path for Pasha Khosravi's indexed-state API and
bri-prism's raw-gate fusion, using the existing Vulkan GDN kernel.

Inspired-by: PrismML-Eng#165
Inspired-by: PrismML-Eng@bed6e9a
Assisted-by: Codex
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Nov 2, 2026
Fuse residual normalization, activation quantization and shared-Q8 projections. Add transpose concat, recurrent gate activation and scale combinations, with the original GB10 dispatch controls. Preserve portable device queries and PTX guards, and adapt fused RMS launches to the current channel and sample dimensions.

Source: PrismML-Eng#135
Source: PrismML-Eng#163
Source: PrismML-Eng#165
Source: PrismML-Eng#168

Source commits (PrismML-Eng/llama.cpp):
3dd7675
00e8187
d8d96cf
d8f26ee

Co-authored-by: bri-prism <288398250+bri-prism@users.noreply.github.com>
Co-authored-by: Pasha Khosravi <khosravipasha@users.noreply.github.com>
Co-authored-by: Pasha Khosravi <pasha@prismml.com>
Assisted-by: GitHub Copilot
Assisted-by: OpenAI Codex
Assisted-by: Claude (Opus 5.5), rebase onto ggml-org master with conflict resolution

Ported and rebased by Codex.
imaami pushed a commit to imaami/llama.cpp that referenced this pull request Nov 2, 2026
Enable indexed state on Metal and raw gates on Metal/CUDA/ROCm/MUSA.
Use Brian (bri-prism)'s separate capability checks and preserve the CPU
and Vulkan paths. The Vulkan extensions were authored by Codex.

Source: PrismML-Eng#165
Original-commit: 00e8187
Source: c1f65ed
Source: 631528b
Assisted-by: GitHub Copilot
Assisted-by: OpenAI Codex

Ported and rebased by Codex.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants