Skip to content

perf(qwen35): use in-place GDN state write in pure AR#473

Open
dusterbloom wants to merge 9 commits into
Luce-Org:mainfrom
dusterbloom:perf/qwen35-gdn-inplace-consumer
Open

perf(qwen35): use in-place GDN state write in pure AR#473
dusterbloom wants to merge 9 commits into
Luce-Org:mainfrom
dusterbloom:perf/qwen35-gdn-inplace-consumer

Conversation

@dusterbloom

@dusterbloom dusterbloom commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

Dense (27B) qwen35 AR-decode performance stack: in-place GDN state write, 256-token FA-bucket CUDA-graph reuse (with a props-check skip), FWHT K-rotation gate for q4_0 KV, swiglu fuse, and concat fast-paths. Rebased clean onto current main.

Supersedes #476, whose micro-opts are a subset of this stack.

Dependency (resolved)

The in-place GDN op itself (ggml_gated_delta_net_inplace, originally Lucebox ggml #24/#27) is now vendored into main via #499 (5e302cbb, "port(ggml): in-place GDN state write + qwen dense decode recovery"). main ships the op but still calls the non-inplace variant at the qwen call site (qwen35_target_graph.cpp:941); this PR wires the qwen35 dense pure-AR consumer to use it (ce2325aa). After the ggml-only vendorize (#486), there is no external submodule bump left — the earlier "draft until the ggml submodule commit is available" blocker no longer applies. This branch was rebased onto current main, dropping the obsolete submodule-gitlink bumps.

Commits

  • ce2325aa use in-place GDN state write in pure AR
  • e1ce9c73 fuse silu+mul → swiglu_split in DeltaNet gated output
  • 96fd3ea9 gate FWHT rotation for q4_0 KV + skip repeat_back in pure AR
  • 5fd7c3fb cont concat inputs for fast concat_cont path
  • a5915e29 graph reuse in AR decode — skip rebuild within 256-token FA bucket
  • a8253730 reduce concat cont overhead — materialize qkv_T once
  • 9d450624 recover dense AR replay fast path

Correctness (verified)

  • Adversarial static review (Codex): clean — no silent-corruption, race, or aliasing across all 7 commits. Checked with file:line: 256-bucket boundary condition, in-place GDN aliasing, FWHT K/Q rotation symmetry, swiglu numerical equivalence.
  • AR-replay bit-identity: DFLASH_AR_NO_REUSE ON vs OFF → byte-identical greedy output at 4K and 16K (16K crosses two bucket boundaries). The 256-bucket rebuild guard re-captures exactly when the FA window advances; the props-check skip is a launch-overhead optimization only, not a numerics change.
  • In-place GDN bit-identity: revert-ce2325aa A/B with WHT held constant → byte-identical. Output-inert when live.
  • FWHT-skip fidelity: teacher-forced over 32K of real corpus, bucketed vs an f16 reference — top-1 agreement gap ≤ 0.6pp (flat with depth) and PPL flips direction bucket-to-bucket → within noise. Skipping the K-rotation on q4_0 does not measurably degrade quality on real text.

Performance

27B dense, q4_0 KV, pure AR, temp 0, HTTP serving path, median of N post-warmup, same binary both arms (accept_rate=0, prefix_len=0 verified — no caching asymmetry). AR graph-reuse, replay ON vs OFF:

context replay ON (tok/s) replay OFF (tok/s) Δ decode
3.1K 41.25 40.70 +1.35%
14.1K 38.45 37.75 +1.85%
30.6K 34.90 34.35 +1.60%
71K 29.15 28.70 +1.57%
114K 24.55 24.10 +1.87%

The graph-reuse decode gap is a flat ~1.6% band across a 37× context range (3K→114K) and never clears each arm's own run-to-run spread (e.g. at 71K, ON's own min–max is 2.7% vs a 1.57% gap). It does not grow with depth, contradicting the "launch overhead scales with KV length → win grows with context" rationale. prefill throughput and wall are statistically identical between arms (the reuse only touches per-token decode launch).

The optimization's originally reported ~+6% (106→113 tok/s) was measured on Q3_K_XL at 16K; it does not reproduce on Q4_K_M at any depth tested here (3K/16K/32K/71K/114K). Read this as: correctness-neutral and safe on Q4_K_M, but no HTTP-measurable decode win on this quant — the speedup appears quant-specific.

Notes

  • Rebase resolved the stale llama.cpp submodule-gitlink vs the ggml vendorize on main (dropped the obsolete gitlink bumps; rerere combined main's src[7] persist-wiring with the pure_ar in-place op call — verified bit-identical).
  • Kept as draft pending review.

In build_delta_net_block, replace the two-op silu(z_4d) + mul(output, z_silu)
pattern with ggml_swiglu_split(z_4d, output_n). swiglu_split(a,b)=silu(a)*b,
mathematically identical. Removes 30 kernel launches per AR decode step
(UNARY-30, MUL-30, GLU+30; graph 2744->2714). Token-identical at temp=0
confirmed. Part 1 of launch-count gap closure (30/162 = 18.5% of gap).
…re AR

FWHT gate: DFLASH_NO_WHT=1 disables the K/Q rotation that costs ~16 extra
kernels/token (~3% decode). q4_0 KV doesn't need the outlier spreading on
Ampere. Default off for TQ3_0 (WHT baked into quant).

Repeat_back skip: in pure AR (no tree/capture), the SSM kernel's broadcast
handles the head mismatch natively — skip the repeat_back copy per DeltaNet
layer per step (~1% decode).
The DeltaNet conv_input concat (conv_states_r, qkv_T) hit the slow
concat_f32_dim0 kernel (15us) because both inputs were non-contiguous
(reshape + transpose). Wrapping in ggml_cont routes to the fast
concat_cont path (3.3us) — ~0.5ms/token saved in the 30-layer serial
DeltaNet recurrence.
…n FA bucket

build_target_step rebuilds the 2744-node cgraph every decode step, costing
~0.38ms/token and resetting ggml-cuda's CUDA-graph warmup counter. The only
per-step topology change is win_len_padded = round_up(committed+1, 256),
which only advances every 256 steps. Gate the rebuild on committed/256;
within a bucket reuse the cached graph and update only mutable inputs.

DFLASH_AR_NO_REUSE=1 restores the per-step rebuild.

Measured: +6% decode tok/s (106→113) on Q3_K_XL, q4_0 KV, 16K ctx.
Move ggml_cont from the concat call to the qkv_T transpose definition.
This makes qkv_T contiguous once (1 cont per DeltaNet layer) instead of
wrapping both concat inputs (2 conts per layer). conv_states_r is already
contiguous (reshape of contiguous cache tensor). Net: -30 graph nodes
(2704→2674), compute -24µs/step.
@dusterbloom dusterbloom force-pushed the perf/qwen35-gdn-inplace-consumer branch from 6309f28 to 9d45062 Compare July 9, 2026 12:58
@dusterbloom dusterbloom marked this pull request as ready for review July 9, 2026 13:27
@dusterbloom dusterbloom marked this pull request as draft July 9, 2026 13:30

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread server/src/qwen35/qwen35_backend.h Outdated
…se; K-basis in disk-cache salt; CPU GDN inplace guard
@dusterbloom dusterbloom marked this pull request as ready for review July 9, 2026 13:55
@dusterbloom dusterbloom marked this pull request as draft July 9, 2026 13:56

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 8 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread server/src/qwen35/qwen35_backend.cpp Outdated
Comment thread server/deps/llama.cpp/ggml/src/ggml-cpu/ops.cpp Outdated
@dusterbloom dusterbloom marked this pull request as ready for review July 10, 2026 08:21

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 9 files

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant