Repository navigation
[Qwen3.5] gfx1250 GDN prefill kernels (conv + norm, WY, recurrence, output + norm), no runtime autotune; DP points on unified attention - #2830
Merged
azaidy merged 10 commits intoOct 29, 2026
Conversation
…m in its epilogue `causal_conv1d_fn` runs `_causal_conv1d_fwd_rows_kernel` for a width-4 conv on channel-last input without prefix-cache block states (the GDN prefill), on the same (sequence, 8-token chunk) launch tables as before. The chunk's tokens are an unrolled loop, so every row is one independent load and the window shifts in registers, and one wave covers 256 channels (16 B a lane) where the reference kernel spread them over four waves with a dynamic token loop. `qk_l2norm_head_dim` L2-normalizes each head of q and k after the bf16 rounding of the conv output, which is what `l2norm_fwd` computes on the stored q and k; the reference path applies `l2norm_fwd` after its kernel. The GDN eager and planned prefill pass it and call the chunk kernels with use_qk_l2norm_in_kernel=False, unless the FlyDSL prefill may take q and k. The arithmetic per element is the reference kernel's, so q, k, v and the conv states are bit-identical to conv -> l2norm_fwd (checked at Hk/Hv 16/32 and 4/8, T up to 16384, forked and in-place slots, cold and warm states, sequences shorter than the state). Cold-cache CUDA-graph timing, T=16384: conv + both l2norms 140-143 us -> 87-90 us (DEP4 heads), 44 us -> 27 us (TP4).
The chunked Gated DeltaNet state recurrence walks one (sequence, value head, V block) over its chunks: two dependent GEMMs a chunk. On gfx1250 it now runs as a Gluon kernel: the state stays fp32 in WMMA accumulator registers, the tensor data mover loads the next chunks' w, k, u and gate tiles into LDS while the current one computes, and stores h and v_new from LDS. The launch configuration (V block, operand buffers) is a function of the host-known sequence and head counts. The Triton kernel keeps one fixed configuration for the other architectures. Its autotuner timed 12 configurations on the first launch of each process, in the serving loop and on the launch's own in-place arguments, which is why the slot-indexed launch needed a scratch-copy pre-tune; both are gone. v_new is bitwise the same; h and the final state differ by fp32 summation order.
As Triton folds `h += dot(k, v)` into the WMMA accumulator. h, v_new and the final state are now bitwise those of the Triton kernel.
…n its epilogue, no autotuning The chunked GDN prefill wrote its output, and a second kernel read it back for the layer's gated RMSNorm norm(o) * weight * silu(z). On gfx1250 the output kernel is now a Gluon kernel: the tensor data mover brings q, k, h, v_new, the gates and z into LDS, three WMMA GEMMs form one chunk of one head, and with `output_gate` the norm runs in the epilogue, its sum of squares in layer_norm_fwd's register layout. The eager and planned prefill paths of the GDN layer pass it whenever the kernel writes the layer's output in place, and skip the separate norm. The Triton kernel keeps one fixed configuration for the other architectures, where `output_gate` runs layer_norm_fwd after it; its autotuner timed up to 36 configurations on the first launch of each process. Output and final states are bitwise those of chunk_o + layer_norm_fwd.
… a test against the Triton kernels A fixed-length batch (no cu_seqlens) has B * NT chunks of h; the store descriptor was bounded at NT, so the stores of rows past the first were dropped. tests/test_gdn_chunk_gfx1250.py compares the gfx1250 kernels with the Triton ones they replace: varlen by slot and by plan, a fixed-length batch, the output with and without the gate, and the gate against chunk_o + layer_norm_fwd bit for bit.
…o kernels, no runtime autotune The chunked Gated DeltaNet prefill (AMD T >= 64 path) ran fused_cumsum_kkt -> solve_tril_16x16 -> fused_merge_recompute, the configs of the first and last "swept-best on MI308" and solve_tril_16x16 autotuned at run time over 16 configs. fused_cumsum_kkt: one program per (chunk, key head), so k and k.k^T once for the key head's value heads; k.k^T on bf16 WMMA (k is bf16: its products are exact in fp32) where k widened to fp32 ran fp32 WMMA at 8x the instructions; the gates load as [64, H] tiles instead of one word per token and head, and key head 0 stores g_cumsum the same way; A stores from the transpose of the (symmetric) k.k^T, whose WMMA result holds each lane's values along a row, so the stores are contiguous instead of one word per 8 KB row. It also stores beta and beta * exp(g_cumsum) head-major for the next kernel. fused_inverse_recompute (new, replaces solve_tril_16x16 + fused_merge_recompute): the 16 x 16 diagonal inverses as (I + N)(I + N^2)(I + N^4)(I + N^8) on fp32 WMMA in-kernel instead of a second kernel through HBM, the block merge as before, and w / u with the per-token scale folded into Ai's columns so k and v enter bf16 WMMA exactly, the scaled Ai as a bf16 high + low pair (~16 bits). w and u stay at the bf16 rounding of the fp64 result; 99.8-99.9% of w and 100% of u are bitwise equal to the old kernels. Short-prefill (T < 64) path: chunk_local_cumsum, chunk_scaled_dot_kkt, solve_tril and recompute_w_u launch at fixed configs (the autotuner's gfx1250 picks) instead of autotuning on first use. gfx1250, cold caches, CUDA-graph replay, w/u stage (kkt + inverse + w/u): T=16384, 32 value heads (DEP4): 241-246 -> 147-154 us T=16384, 8 value heads (TP4): 71-79 -> 57-59 us T=4096: 74-78 -> 55 us (DEP4), 27 -> 23 us (TP4) chunk_gated_delta_rule output / final state against an fp64 token-by-token recurrence: 3.47e-3 / 2.52e-3 relative, as before (3.46e-3 / 2.52e-3).
… / reciprocal SiLU The reference kernel rounds each tap product to bf16 before the fp32 sum. Against an fp64 conv of the same bf16 inputs (Qwen3.5-35B-A3B conv weights, layers 0/1/12/28) that puts its outputs at 1.43-1.48x the bf16 rounding error, with 66-70% of them correctly rounded and a p99 of 4-8 bf16 steps. fp32 FMA taps give the correctly rounded value for every output, as the fused GDN verify kernel's conv already does in decode. The IEEE divide and the range-reduced exp in SiLU were about half of the kernel's 27 VALU instructions per element; the hardware reciprocal and exp move about 1 output in 10^5 by one bf16 step. conv + q/k L2 norm, 16384 tokens, cold caches: 16/32 heads 87-90 -> 62-66 us (base tree conv + 2x l2norm_fwd: 140-143 us); 4/8 heads 27-28 -> 20-22 us. The test compares q, k, v with an fp64 conv instead of the reference kernel bit for bit: at most one bf16 step (two for a normalized q / k head), on fewer than 1e-3 of the values (measured 4e-6 to 2e-5); the conv-state pool still matches the reference kernel bit for bit.
…h 64-token KV blocks; the c2048 GDN results serve_ep.sh sets ATOM_USE_UNIFIED_ATTN=1 and --block-size 64 (BLOCK_SIZE overrides) on DP points: no KV gather before the prefill FMHA and the 64-token decode / verify attention configs. It needs aiter alizaidy/gfx1250-qwen-support from 3d3228c1, whose 2D kernel takes the block-table stride at runtime. The recipe gains the c2048 DP4 + EP4 points of this branch's GDN kernels and the unified attention (27.4k -> 33.1k generation tok/s, GSM8K 0.8635 / 0.8499) with the exact server command they ran.
…for ruff (SIM102)
zufayu
added a commit
that referenced
this pull request
Oct 30, 2026
near_duplicates() only catches a duplicate that arrives as a whole new file. Most of "we already have it" / "duplicated with the one in aiter" / "use new util func" in the lead reviewer's comments is a function or class added into a file that already exists -- a shape the file-level check cannot see, since it never looks inside a diff hunk for a modified file. Adds symbol_duplicates(): for each newly added top-level def/class in a modified file's diff (whole new files stay out of scope -- near_duplicates already covers them, and double-reporting the same evidence under two headings was not the goal), compare its body (>=15 substantial lines) against every top-level def/class already in the tree (same kind, whole tree -- measured at 0.16s / ~14k symbols over ATOM's 1504 .py files, cheap enough that a name-similarity prefilter would suppress exactly the "unrelated names, near-identical bodies" case this exists to find). Same threshold as the file-level check, 60%, and same posture: a pair and a ratio, not a verdict. Measured over 197 recently merged ATOM PRs (#2388-#2830) at threshold 0.60: 2 fired (1.0%). Both spot-checked as real hits, not coincidental shape: PR#2560 modifies atom/plugin/vllm/paged_state_image.py and its tests in place, 10 pairs against the pre-existing paged_state_checkpoint.py sibling it mirrors (the "image vs checkpoint" variant pair near_duplicates' own docstring describes, but file-level could not see here because neither file is new). PR#2553 adds a test 95% identical to an existing one, down to the mocked runner setup, with one of three asserted values changed. No threshold raise needed -- 1.0% sits well inside the file-level check's own ~3% territory, with no false alarms in the spot check. Wired into the same `twins` CLI mode as a second section, after the file-level pairs; fetch.sh's existing `triage.py twins ... | tee twins.txt` call is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Qwen3.5 GDN layers on gfx1250: new prefill kernels for the conv, the chunk-local WY step, the recurrence and the output (no runtime autotune left in the GDN prefill path), and the recipe's DP points on unified attention with 64-token KV blocks.
Needs aiter
alizaidy/gfx1250-qwen-supportfrom 3d3228c1 (pushed): the chunk-formfused_gdn_verifydecode kernel, ROCm/aiter#7100's current configs and routing, and ROCm/aiter#6999 (the 2D unified-attention kernel takes the block-table stride at runtime; before it, unified-attention prefill recompiled for every prefill batch).Commits
causal_conv1d.py). The GDN prefill conv used 8-token x 256-channel tiles; the new kernel unrolls the chunk's tokens so each row is one independent load.qk_l2norm_head_dimnormalizes each head after the bf16 rounding of the conv output, sochunk_gated_delta_ruleskips its twol2norm_fwdlaunches (use_qk_l2norm_in_kernel=not qk_normed_by_conv).chunk_delta_h_gfx1250.py). The state stays fp32 in registers for a sequence's whole chunk walk, and the next chunks' w / k / u / gate tiles are prefetched into LDS. The launch config is a fixed rule on sequences x heads. The@triton.autotune(12 configs, timed on the first launch in the serving loop, and not even the same pick across processes) and the scratch-copy pre-tune are gone; the Triton kernel stays for other archs with one fixed config.chunk_o_gfx1250.py).chunk_gated_delta_rule(..., output_gate=(z, weight, eps))writesnorm(o) * weight * silu(z)straight into the out_proj input; the pure-prefill call sites inattention_gdn.pypass it and skip the separate norm.fused_cumsum_kkt.py, newfused_inverse_recompute.py). k kᵀ once a key head on bf16 WMMA, coalesced gate / beta loads, and the 16x16 inverses, their merge and w / u in one kernel. This replacessolve_tril_16x16+fused_merge_recompute, whose single configs were tuned on MI308. The short-prefill (< 64 tokens) kernels (cumsum,chunk_scaled_dot_kkt,solve_tril,wy_fast) take fixed configs instead of runtime autotune.ATOM_USE_UNIFIED_ATTN=1and--block-size 64; the recipe doc gains the c2048 results and the exact server command.Results
c2048 DP4 + EP4, MTP3, 512 sequences an engine. Generation tok/s is the mean over the 2 minutes before a 15 s profiler window at minute 3; step times are DP rank 0's:
Per GDN layer at 16k prefill tokens, 32 value heads (cold-cache microbenchmarks; the trace agrees):
TP4 heads (8 value heads) gain about the same factors.
GSM8K, DEP4, 3-shot, 256 concurrent, on the last column's stack: 0.8635 / 0.8499 (0.861 / 0.851 before).
Testing
tests/test_causal_conv1d_rows_gpu.pyandtests/test_gdn_chunk_gfx1250.py.test_gdn_midstep_state_gpu,test_gdn_midstep_checkpoints,test_gdn_page_checkpoint,test_state_checkpoint,test_gdn_fused_decode,test_gdn_fused_verify,test_fused_sigmoid_gate) give 401 passed on gfx1250.test_aiter_flydsl_gdn.pyis MI308-only and fails identically on the base tree.ruff/blackare clean for new code; existing files have no more findings than before.