Skip to content

hip: Qwen3.8-Flash-Next on gfx1151, a reference branch for review (to be split) - #1667

Draft
davidtai wants to merge 34 commits into
ashhart:mainfrom
davidtai:upstream/qwen-hip
Draft

davidtai wants to merge 34 commits into
ashhart:mainfrom
davidtai:upstream/qwen-hip

Conversation

@davidtai

@davidtai davidtai commented Oct 30, 2026 •

Copy link
Copy Markdown

Refs #1661.

Draft. This pull request is a series of 34 self-contained commits, one change each, on main 94b90c9db.
Each commit has its own tests, its kernel-contract registration (gates 1-4 test type and gate-5 record) and its
exactness statement. We can split the series into separate pull requests in any grouping the maintainers prefer.

Scope

The gfx11 HIP path for Qwen3.8-Flash-Next, measured on gfx1151. Every commit keeps the output bytes of the base path:
the greedy logits hash is 243ecea88fe50d9f at main and after each of the 33 code commits.
The rounding-class kernels and the numerics=exact|inexact switch of the earlier squashed head are not in this pull
request. #1661 records why (FP64 error and row-grouping dependence) and keeps their results, labelled.

Commits

# Commit Change
1 4ee2e85be hip: move code out of files near the 600-line limit, unchanged
2 f7ac354c5 contract: five 1x2 stream-tile gate-5 records as hipcc builds them now
3 88aa18df9 flashnext hip test tool: --margins N
4 9411438ec flashnext hip: n-gram tables as a resident copy at load
5 daf7ec74f flashnext hip: a 4,096-row default prompt chunk
6 12a6370ed hip decode: router logits, top-k pick and route plan in one launch
7 4115368ff hip draw: a wide row's top-k in parts, then a join
8 eb50c0b43 hip stream tile: one row loads x before the next round's codes
9 bff7fc1a8 hip stream tile: column sums in one transposing butterfly
10 c0a9e6270 hip stream tile: one column a lane for narrow plain launches
11 fd4797d56 hip stream tile: row blocks on the grid's x
12 461d8e001 hip stream tile: a SwiGLU pair lane carries at most 8 code words
13 fc3d43bc7 hip stream tile: routed plain at most 4 columns a lane past one row
14 e432e36fc hip stream tile: one row under 20,000 columns takes one column a lane
15 61088491a hip stream tile: a pass with one live row reads that row only
16 7821dc4c4 hip stream tile: routed 5 to 8 rows as a work list
17 9bcd8a63b hip stream tile: the held-codes body at 3 to 8 rows
18 72f868fa8 hip: prompt expert gate+up and down on one WMMA tile
19 adb89effe hip: dense prompt products on a K-specialized WMMA tile
20 42ccbbbfc hip recurrence: prompt DeltaNet at half a value head a block
21 1b6c01697 hip DeltaNet: prompt conv, split, norms and gates in one launch
22 9701c7ca4 flashnext hip: a verify window holds its DeltaNet state; keep replays
23 dd7dbafae hip flashnext: a prompt slice's indexer scores at 4 lanes a score
24 e2750fb5c hip flashnext: a QSA layer's decode prep in one launch
25 2a1260918 flashnext hip: the MoE combine inside the next HC norm
26 61529bc14 hip: a prompt's picks a wave a row, its plan in three launches
27 f132d54ae flashnext hip: MTP head caches only for streams that draft
28 c99b0d0bd flashnext hip: sampled streams draft the target's keyed choice
29 8d9e3346a flashnext hip: the depth rule prices drafts from forwards timed at load
30 4ee89c6cb flashnext hip: verify windows of up to 8 rows
31 6e4727c66 flashnext hip: mtp_depth=gated and Flash Next's gated chain
32 d5a1fc781 hip: a side stream in Ops with fork and join
33 37aed1a03 flashnext hip: one-row decode prefetches down codes on the side stream
34 2f42c9cb6 docs: receipts for the Flash Next HIP series on gfx1151

Groups: 1-3 preparation (file moves without change, refreshed records, a test-tool flag); 4-5 load and prompt policy;
6-17 decode kernels; 18-26 prompt kernels; 27-33 lossless speculation and the side stream; 34 the receipts.

Results: this series against main

overview

Prefill, decode and memory charts

prefill
decode
peak memory

Machine: an AMD Strix Halo (gfx1151) machine with 192 GiB of unified memory, ROCm 7.15.
Checkpoint: TensorFold/Qwen3.8-Flash-Next-MLX-4bit-MTP, revision 2b170fa6. Greedy. Prompt cache off.
Tested: the tip of this series (37aed1a03) against main at 94b90c9db, the commit it is based on.
The two arms alternate in each round. Both builds give the same output tokens.

One 16,384-token prompt of public source code, 1,024 output tokens, 6 runs a row, median:

Build Decode Prefill tok/s Decode tok/s TTFT (s) Wall time (s) Spec-decode hit rate (%) Peak memory (GiB)
this series serial (--no-drafts) 533.5 35.71 30.75 59.39 - 82.68 GPU, 104.9 RSS
main serial (--no-drafts) 353.3 29.99 46.55 80.70 - 78.48 GPU, 78.63 RSS
this series Default MTP 533.5 49.05 30.94 51.86 61.8 82.72 GPU, 103.8 RSS
main Default MTP 353.3 38.26 46.54 73.32 68.1 78.48 GPU, 78.67 RSS

Cold prefill (max_tokens 1, a fresh server a run, 2 runs, median):

Prompt tokens 2,048 8,192 32,768 65,536 131,072 261,888
this series, tok/s 673.7 558.2 513.5 496.7 471.6 437.3
main, tok/s 293.2 261.3 260.4 259.3 258.0 244.8
ratio 2.30 2.14 1.97 1.92 1.83 1.79
this series, GPU / RSS (GiB) 82.45 / 102.9 82.62 / 105.0 83.33 / 105.0 84.27 / 104.3 86.14 / 103.8 89.68 / 103.9
main, GPU / RSS (GiB) 78.21 / 76.81 78.39 / 77.56 79.09 / 79.81 80.03 / 81.64 81.91 / 83.66 85.45 / 86.69

Four concurrent streams (tools/bench_concurrent.py --levels 1,4 --alone --serial), aggregate decode: 117.4 against
89.65 tok/s (code), 99.75 against 72.95 tok/s (chat). Concurrent equals alone and drafted equals plain for both builds.

Definitions: prefill tok/s = prompt tokens / TTFT. Decode tok/s = (output tokens - 1) / (last token time - first token
time). Spec-decode hit rate = accepted drafts / drafted, over every drafted request of the row (16, 256 and 1,024
tokens). GPU = peak GTT + VRAM in use less the idle machine's. RSS = the server's peak resident memory. The two come
from the same unified memory and overlap, so do not add them.

Why the hit rate is lower and decode is faster

The two builds draft differently. The server logs show it (1,024-token requests; the counters are identical on all
6 runs of each build; time a round = the median decode window over 6 runs / rounds):

Build Draft policy (load line) Rounds Drafted Accepted Accepted / drafted Tokens a round Time a round
this series mtp drafts 7 under 0.9, mtp_depth=gated 401 1,018 622 61.1 % 2.55 52.1 ms
main mtp drafts 3 under 0.3 523 747 500 66.9 % 1.96 51.2 ms

Commit 31 lets the chain go to 7 drafts and ends it after the first draft with a head confidence under 0.9.
The longer chain drafts more tokens that the target rejects (396 against 247), so accepted / drafted falls.
But a round now gives 2.55 tokens in place of 1.96 and costs only 2 % more time: commit 30 verifies up to 8 rows in
one window, and commits 6-17 make each row cheaper. So the series needs 23 % fewer rounds for the same 1,024 tokens. Serial decode is also
faster: 28.0 ms against 33.3 ms a token.

Memory, and the settings that change it

The memory A/B ran one fresh server a setting at --context 20480, 2 rounds, each with one cold 16,384-token prefill.
The load lines are identical in both rounds, and GPU and RSS differ by at most 1.7 GiB; the table shows round 1.
Load line = the server's own GiB on the device line.

Setting Load line, device GPU peak over idle (GiB) RSS peak (GiB) 16K TTFT (s) Prefill tok/s
this series, defaults (4,096-row chunks, resident n-gram rows) 81.82 GiB 82.73 104.4 30.74 533.1
--policy prefill_step=20480 105.63 GiB 106.5 104.5 29.69 551.8
--policy prefill_step=1024 77.36 GiB 78.27 102.6 33.90 483.3
--policy rows=mapped, rows not in the page cache 81.82 GiB 82.73 78.57 46.30 353.9
--policy rows=mapped, rows in the page cache 81.82 GiB 82.73 78.62 30.92 529.8
--policy prefill_step=1024,rows=mapped 77.36 GiB 78.27 78.62 34.13 480.1
main 76.58 GiB 78.50 78.54 46.47 352.6

Two commits change memory, one for each counter:

  • GPU memory, commit 5 (a 4,096-row default prompt chunk): the prompt scratch grows with the chunk. The default
    holds 4.2 GiB more than main (82.73 against 78.50 GiB) and prefills 51 % faster at 16K. At 1,024 rows the series
    holds 0.23 GiB less than main. prefill_step=20480 is the opt-in for machines with that memory free:
    +3.5 % prefill at 16K (551.8 against 533.1 tok/s) for +23.8 GiB. The Flash Next recipe documents it.
  • RSS, commit 4 (n-gram tables as a resident copy at load): the checkpoint's n-gram tables are 29.80 GiB. The series
    copies them into anonymous memory at load, so RSS stays near 104 GiB at every prompt length. main reads them from
    the file mapping, so its RSS grows as prompts touch more rows (76.8 GiB at 2K, 86.7 GiB at 262K).
    rows=mapped keeps the mapping: RSS 78.6 GiB. With the rows in the page cache it costs 0.6 % prefill at 16K.
    When they are not (the copy at load dropped them from the page cache), the first prompt reads them from disk and
    prefill falls to 353.9 tok/s. The GPU counter does not include the copy.

The two counters overlap on unified memory: the uploaded weights are in both. With its defaults the series holds
82.7 GiB of GPU memory plus the 29.8 GiB copy in anonymous memory. main holds 78.5 GiB plus the rows it has touched
in the page cache, which the kernel can drop under memory pressure. --policy prefill_step=1024,rows=mapped brings
both counters to within 0.25 GiB of main's.

RAM class: every number in this pull request comes from one machine with 192 GiB of unified memory (184.6 GiB reported
by the kernel). We did not measure a smaller machine, and a budget on this one is not a measurement on a smaller one.

Exactness

On the tip of the code commits, 37aed1a03, with the upstream scripts, server
--context 32768 --parallel 4 --prompt-cache-gib 1 --no-thinking:

Check Result
zig/tests/qwen35/serve_check.py (drafted == plain, greedy and sampled; solo == together; a long prompt among decoders; resumed == fresh) 15 ok, 0 fail; drafting exercised, 1,285 of 1,688 drafted tokens accepted
Token-hash audit of every recorded exchange (identical request bodies, draft ignored) 11 groups, 0 differ
zig/tests/qwen35/serve_cancel.py 3 ok, 0 fail
zig/tests/flashnext_hip/rewind.py, prompt cache on against off (turns, regenerate, edit, delete) 0 replies differ; up to 10,240 cached tokens
generate --hash on a 10,368-token prompt at --chunk 512, 2,048, 8,192 and 20,480 one logits hash, c8f6888043c972e0

The Flash Next greedy hash (generate --chat --max 64 --hash) is 243ecea88fe50d9f on main and on the tip.

Kernel contract

On the tip of the code commits, 37aed1a03, gfx1151, zig 0.17.0, ROCm 7.15:

SUMMARY gfx1151 83 kernels: 83 pass gates 1-4, 0 fail, 4 known findings; 5 controls: 5 red as planted, 0 missed; 0 limit problems
PASS kernel-contract-limits: every registered launch fits gfx1030, gfx1100, gfx1151 and gfx1201, every record current, every control red

zig build test -Dhip=true, zig build hip-kernel-test, zig build test-cuda-host and zig build native-hip tf-flashnext-hip-test (gfx1151, ReleaseFast) exit 0 on the same tip. Each new or changed kernel is registered: a gates 1-4 test type in
zig/tests/hip/contract_*.zig and a gate-5 record in zig/src/core/contract/hip_records*.zig. A kernel that replaces
another names it, so gate 3 requires its bytes to equal the replaced kernel's.

Known findings that the new kernels carry (each spec declares it; bytes stay equal to the kernel they replace):

  • tf_select_rows and tf_router_select_bf16 keep select_body (the MoE pick on main): a NaN shares rank 0
    with the top number, so slot 0 races, and a row with fewer than k numbers leaves pick slots unwritten.
  • tf_fn_idx_scores_quad and tf_fn_idx_scores8 keep tf_fn_idx_scores's relu, fmaxf(x, 0): a NaN head dot
    scores 0 where float64 keeps the NaN.

Per-commit checks

Each of the 33 code commits, built and tested alone on an earlier main (abf822585), with zig 0.17.0:
zig build native-hip tf-flashnext-hip-test (gfx1151, ReleaseFast), zig build test -Dhip=true,
zig build hip-kernel-test and tf-flashnext-hip-test generate --chat --max 64 --hash. All exit 0.
The hash is 243ecea88fe50d9f after every commit (checks/per-commit.tsv in the receipts).
Zig prints a passing test's stderr under a failed command: line; the exit code is 0.
The added and removed lines of each tested commit equal those of the commit here, with two exceptions:
commits 19-33 ran before their gate-5 records were refreshed (zig/src/core/contract/hip_records_*.zig only), and
commit 5 ran with a 20,480-row default chunk, which this pull request sets to 4,096 rows.
tools/zig/lean_check.py: 424 problems on main and 424 on the tip (zero added).

The receipts folder (commit 34, docs/receipts/gfx1151-flashnext-hip-37aed1a0/) has the logs, the request and
reply records, the memory samples, the environment and the provenance, with SHA256SUMS.

Not run

  • Metal: no macOS build or test. The series changes no Metal source, but core/lanes is shared.
  • CUDA device tests: no CUDA GPU. Only test-cuda-host ran.
  • HIP devices other than gfx1151: only the limits check covers gfx1030, gfx1100 and gfx1201.

Overlap with open work

atc overlap and git merge-tree against the open pull requests that share files with this series.
Conflicts that this series adds:

#1702, #1613, #1615 and #1611 merge cleanly with the series. #1675, #1394, #1391-#1393, #1381 and #1425 conflict with
main already, and the series adds no conflict to them. We will rebase or split onto whatever lands first.

ATC: issue #1661 is claimed, with anonymous session 220947162292872.

🤖 Generated with Claude Code

The later changes grow several files past the lean rule's 600 lines. This commit only moves code:
affine.zig's kernel loading into affine_load.zig and its stream launcher into affine_stream.zig,
the relu^2 stream tile records into hip_records_relu2.zig, the Flash Next lane round into lanes_round.zig,
the sparse attention loop into mixers_attend.zig, the sparse attention launcher into flashnext_sparse.zig,
and decode.hip's expert pick into decode/select.hpp.

Exactness: same bytes (no code changes).
Tests: none new; the existing tests cover the moved code.
Five records of the 1-row, 2-column stream tiles no longer matched what hipcc compiles for them.
They take the usage that tf-hip-kernel-contract limits reads from the code objects.

Exactness: same bytes (records only).
Tests: the existing gate-5 limits check.
tf-flashnext-hip-test generate prints the two best tokens of the first N steps and the gap between their
logits, read from the row's bytes. A near-tie shows as a small gap, which tells a real divergence from a tie.

Exactness: same bytes (test tool only).
Tests: none (a diagnostic option of the test tool).
The n-gram row tables were read from the checkpoint mapping, so a round's gather could fault pages in.
A single rank now copies them into anonymous memory at load and drops the page cache's copy.
policy rows=mapped keeps the mapping; ranks of a group keep it too, as they share the page cache.

Exactness: same bytes (the same rows from another place in memory).
Tests: policy words test with rows=mapped.
@AbrahamCete

Copy link
Copy Markdown
Contributor

Hi @davidtai, a heads-up that #1694 overlaps with the stream-tile part of this series. It is the stream-tile work I mentioned in question 4 of #1563. Both change zig/kernels/hip/tiles/stream.hpp in the same two ways, and they don't merge cleanly:

  • wide rounds put a column block's row blocks next to each other on the grid's x;
  • a chunk's code pairs are made once for every row of the block.

Two things from my side that may help when you split the series:

  • I tried naming the layout in a StreamSides field read by stream_tile first. On gfx1151 it doubled the stream kernels' VGPRs (4x4: 140 -> 256) and cost about 10% on wide verify rounds. Mine derives the layout from the grid instead (launches/stream_layout.zig).
  • It has a GPU test that requires the interleaved and reference layouts to write the same bytes. The test covers plain, relu2, pair, routed and grouped products at 5 to 32 rows. There is also a golden digest for the 4-bit BF16 code pairs.

On Qwen3.8-27B it gives +30% aggregate at 8 streams and +47% at 16, with the bytes unchanged. I'm happy to rebase onto your series, or for yours to build on this one, whichever the maintainers prefer.

davidtai added a commit to davidtai/TensorFold that referenced this pull request Oct 31, 2026
Flash Next on HIP cuts prompts at 4,096 rows unless --policy or TF_POLICY names prefill_step.
Each expert's rows of a chunk are one launch, and the prompt scratch stays small.
prefill_step=20480 runs a 20K prompt in one pass: on a 192 GiB gfx1151 machine, about 3 % faster 16K prefill
for about 24 GiB more GPU memory. The Flash Next recipe states it as an opt-in.
Policy.names tells whether the flags or TF_POLICY name a key, so a family default never overrides one.

Exactness: same bytes (a prompt's rows and sums do not depend on the chunk).
Tests: Policy.names in the flags and in TF_POLICY, and only as a key.
tf_router_select computes a decode or verify window's router logits from the bf16 rows, and its last block
picks the top-k and writes tf_moe_route's plan, for 1 to 8 rows. It stages up to 2 rows of x in dynamic
shared memory and picks a row a wave (half a wave a row at 5 to 8 rows). A row with a NaN logit takes the
rank-loop pick, so its bytes are select_body's. Shapes it cannot take keep the two launches.

Exactness: same bytes (bf16 rows widened exactly; the same pick and plan).
Tests: decode_route.zig, the one-launch router and pick against the two launches (513 x 2560, top 10) and
against served plans' picks and items byte for byte (served_plans.bin, rows 1 to 8).
Contract records: tf_router_decode_f16/bf16, tf_router_select_f16/bf16.
tf_topk read a whole row in one block. tf_topk_part takes each part's top k over the card, and tf_topk_join
takes each row's k from the parts' candidates in tf_topk's order. The row's top k are among its parts', so
the ids, values and order are tf_topk's.

Exactness: same bytes.
Tests: ops_flashnext.zig, top-k in parts against tf_topk byte for byte (Flash Next draft and full vocabularies).
Contract records: tf_topk_part, tf_topk_join.
Contract tests: contract_topk.zig (bit-equal to tf_topk, and tf_topk's key order).
The 1-row stream tile loads a round's x chunk before it fetches the next round's codes, so the dots wait for
this round's loads only.

Exactness: same bytes (the same sums in the same order).
Tests: the existing stream tile tests.
Contract records: the stream tile records re-derived from the code objects.
A wave's column sums run in one transposing butterfly (DPP row_xmask and permlanex16, ported from Strata's
tsum, MIT, notice in THIRD_PARTY_NOTICES.md). Each sum is bitwise the xor butterfly's it replaces.

Exactness: same bytes.
Tests: the existing stream tile tests.
Contract records: the stream tile records re-derived from the code objects.
The plain stream tile also compiles one column a lane, which a narrow launch takes to fill the card with
waves. A SwiGLU pair needs two columns and relu^2 keeps its tiles. The relu^2 pair test reads the launcher's
stream constants instead of a copy.

Exactness: same bytes (a column's sum does not depend on the columns beside it).
Tests: the relu^2 pair contract test on the launcher's constants.
Contract records: the 1-column plain tiles at every width and row count.
Past the tile's rows, a launch puts its row blocks on the grid's x and its column blocks on y, so a column
block's row blocks run together and read its codes once from cache.

Exactness: same bytes (block order only).
Tests: the existing stream tile tests.
A SwiGLU pair lane takes at most 8 code words (2 columns at 4 bits), which keeps more waves in flight.
Kernels.stream_cb_force lets a test force the columns a lane.

Exactness: same bytes (a column's sum does not depend on the columns a lane).
Tests: decode_products.zig, the routed gate+up and down at every columns a lane and as single-pair items
(Flash Next, 1 to 8 rows), byte for byte.
A routed plain product (the experts' down) takes at most 4 columns a lane at the 2- and 4-row tiles.

Exactness: same bytes.
Tests: covered by the routed byte test of the SwiGLU pair change.
One row of an unrouted plain product under 20,000 columns takes one column a lane; wider heads keep the
waves rule.

Exactness: same bytes.
Tests: decode_products.zig, a plain product at every columns a lane (in-proj K, out-proj, a ragged N; 1 to 4
rows), byte for byte.
A 2- or 4-row pass with one live row loads and dots that row only. A row's sums do not depend on the rows
beside it, so the bytes are the same.

Exactness: same bytes.
Tests: covered by the routed byte test (single-pair items run every pass on one row).
Contract records: the stream tile records re-derived from the code objects.
A routed 4-bit plan of 5 to 8 rows runs as a work list in its own kernel (stream_list): grid z blocks walk
the items, so no block starts for a dead item or a dead row block. A row's sums are those of the grid launch.

Exactness: same bytes.
Tests: covered by the routed byte test (the engine's launch at a.m = the window's rows).
Contract records: the 4-bit SwiGLU and plain work lists.
Plain 4-bit products of 3 and 4 rows take a held-codes body that makes a round's code pairs once and dots
every row's x with them; 5 to 8 rows run in one 8-row pass by width. The sums are affine_dot2_stream's.

Exactness: same bytes.
Tests: heldPick's picks by rows and width.
Contract records: the held tiles 4x1, 4x2, 4x4, 8x2, 8x4.
Flash Next's prompt expert gate+up (with SwiGLU as the epilogue) and down run on one shape-parameterized
gfx11 matrix-core tile (tiles/moe_gemm.hpp) with affine_wmma_gemm's arithmetic. Its loads stay in flight
across the window barriers (lds_barrier). A prompt plan's launches cover its live items only.
Records of tiles only gfx11 launches carry no usage on the other chips (HipLaunch.served).

Exactness: same bytes (the matrix tile's sums and moe_act's rounding).
Tests: decode_products.zig, prompt gate+up and down against the matrix tile and moe_act byte for byte;
decode_route.zig, a prompt plan's live items before planItems; moe.zig's planItems bound; the served check.
Contract records: affine_wmma_moe_gu, affine_wmma_moe_down (gfx11 only).
A prompt's plain 4-bit group-32 products at Flash Next's K (320, 2560, 6144, 10240) run on a K-specialized
gfx11 matrix-core tile (tiles/dense_gemm.hpp, block layout after Strata's gfx11 kernels). It writes bf16
where the prompt cast its fp32, with the cast's rounding. hc up runs with the HC mix as its epilogue and
reads its gates from a table of rnd(sigmoid(g)) for every bf16 g, filled once a device.

Exactness: same bytes (the matrix tile's sums, tf_cast's rounding, tf_fn_hc_mix's mix).
Tests: decode_products.zig, dense products against the matrix tile and the cast; flashnext_hc.zig, hc up with
the mix in the epilogue against the product then tf_fn_hc_mix; the registry's pick of the dense tile.
Contract records: affine_wmma_dense at each K, tf_affine_dense_hcmix, tf_hc_gate_fill (gfx11 only).
Contract tests: contract_wmma_dense.zig, contract_hc_mix.zig (the hcmix tile and the gate table), and
contract_wmma_moe.zig for the previous change's expert tiles (it reads this change's dense upload).
A prompt's DeltaNet rows (Dk = Dv = 128, 64 rows or more) run at half a value head a block, 4 lanes a row,
in the same sum order as the wave-a-row kernel.

Exactness: same bytes.
Tests: gdn.zig, the half-head kernel against a wave a row, bit for bit (Flash Next: 16 key, 48 value heads).
Contract records: tf_gdn_prompt_128.
Contract tests: contract_gdn.zig (y and state, bit-equal to gated_delta_tile).
tf_gdn_prompt_prep runs a DeltaNet layer's prompt conv, split, q and k RMS norms, v, new conv state, gate
and beta in one launch, with the values of the launch chain it replaces.

Exactness: same bytes.
Tests: chain.zig, the merged launch against the chain's q, k, v, gate, beta and conv state.
Contract records: tf_gdn_prompt_prep.
Contract tests: contract_gdn_prep.zig (bit-equal to the chain it replaces).
A window of more than one row no longer writes a DeltaNet state snapshot per row. It holds the state, keeps
its k, v, gate and beta, and the keep runs every layer's state through the kept rows in one launch
(tf_gdn_keep). A keep that drops rows names the conv-tail snapshot row the next window starts from.
Snapshots, restores and the worker settle the conv tails first, and reset writes the window start word.

Exactness: same bytes (the same recurrence over the same rows).
Tests: gdn.zig, a held window and its keep against the stored window, and a window started from a snapshot
row; state_test.zig, save and restore with a named row, and a cancelled stream's slot leased again fresh.
Contract records: tf_gdn_hold_128, tf_gdn_hold_16, tf_gdn_keep_128, tf_gdn_keep_16.
Contract tests: contract_gdn_hold.zig (hold and keep at 128 and 16, bit-equal to gated_delta_tile).
At 16 rows or more of Flash Next's 4 x 128 index heads, tf_fn_idx_scores_quad scores a block at 4 lanes a
score, in tf_fn_idx_scores's sum order.

Exactness: same bytes.
Tests: flashnext_attn.zig, the 4-lane scores against a wave a block, bit for bit.
Contract records: tf_fn_idx_scores_quad.
Contract tests: contract_fn_scores.zig, the quad form (bit-equal to tf_fn_idx_scores).
tf_fn_qsa_prep runs a sparse layer's q, index q and k norms and rotations, the v and raw index key cache
writes and the completed blocks' pools in one launch at decode and verify rows. The decode chain also finds
select's cut bin by the block and scores a window's 5 to 15 rows 8 rows a wave.

Exactness: same bytes.
Tests: flashnext_attn_tiles.zig, the prep launch against the seven launches it replaces (1 to 4 rows);
flashnext_attn.zig, the 8-row scores against one row at a time; prepWrites' cases.
Contract records: tf_fn_qsa_prep, tf_fn_idx_scores8.
Contract tests: contract_fn_qsa.zig (bit-equal to the seven launches); contract_fn_scores.zig, the
window form (bit-equal to tf_fn_idx_scores).
A window's and a prompt's MoE combine run inside the next hyper-connection norm, one launch for both.
The bf16 HC norm and mix use 16-byte accesses.

Exactness: same bytes (moe_combine_kernel's then tf_fn_hc_norm's).
Tests: flashnext_hc.zig, the combine in the norm (1 to 8 rows, and a prompt's rows) and the 16-byte norm
and mix against the per-element kernels, byte for byte.
Contract records: tf_fn_hc_mix8, tf_fn_hc_norm_v, tf_fn_hc_norm_moe, tf_fn_hc_norm_moe_v.
Contract tests: contract_hc_mix8.zig and contract_hc_norm.zig (bit-equal to the per-element kernels).
A prompt's expert picks run a wave a row (tf_select_rows, select_body's bytes), and its plan runs in three
launches over 512-pair segments (count, scan, scatter) with tf_moe_route's members and items. The prompt
router tile takes 128 x 64 blocks.

Exactness: same bytes (the router tile keeps every logit's fp32 fma chain).
Tests: decode_route.zig, the wave-a-row picks against select_body; ops.zig, the three-launch route;
ops_flashnext.zig, the router tile against the host's fma chain.
Contract records: tf_route_count, tf_route_scan, tf_route_scatter, tf_select_rows.
Contract tests: contract_route.zig (bit-equal to tf_moe_route, and the stated plan); contract_select.zig
for tf_select_rows and the router change's tf_router_select_f16 and _bf16 (it uses the route test's
plan check): bit-equal to upstream, the stated pick rule, and upstream select_body's NaN and short-row
ranks as a known finding at gate 1.
A stream that cannot draft (--no-drafts, "draft": false or --mtp-drafts 0) gets no MTP head caches and no
head prompt pass. The head's prompt pass runs on the prefill tiles and writes its caches only.

Exactness: same bytes (drafted output equals plain output; the caches hold the same keys).
Tests: state_test.zig, a state saved without the head refuses a drafting stream and resumes one without.
A sampled stream's draft is the target's keyed Gumbel-max over the head's top-k survivors, so the draft
agrees with the target whenever the head ranks the target's winner the same. The head's argmax probability
is computed only where the confidence gate reads it.

Exactness: same bytes (the target never reads the draft; a verify keeps the target's draws).
Tests: none new (the lane verify tests cover the draws).
One rank times one stream's window forwards and a head step at load, and the depth rule prices each depth
from them; plain rounds compete with drafted ones once the forwards are timed.

Exactness: same bytes (only the draft depth changes; drafted output equals plain output).
Tests: draft_sources_hip_test.zig sets the new costs field.
A Flash Next verify window holds up to 7 drafts past its pending token (--mtp-drafts up to 7); the other HIP
families keep 3. Every rank refuses more than the widest family cap before the load.

Exactness: same bytes (drafted output equals plain output).
Tests: native/hip.zig, the draft caps per family and the refusal past 7.
--policy mtp_depth=gated drafts to the cap every round and the head's confidence (--mtp-confidence) ends the
chain; core/lanes pins the depth (Model.pinned_depth). Flash Next's HIP default is a chain gated to 7 drafts
under 0.9 where no flag or mtp_depth key names one. The rule's sampled-stream behaviour under a confidence
floor is unchanged.

Exactness: same bytes (drafted output equals plain output).
Tests: depth.zig, a pinned depth drafts the most; native/hip.zig, Flash Next's own policy and the others' default.
Ops carries a second stream and a ring of events: fork makes later side work start after the stream's work
so far, join the reverse. A slot is recorded and waited on in one call, so the ring may wrap between joins.

Exactness: same bytes (nothing runs on the side stream yet).
Tests: side.zig, more forks than the ring before one join (515 forks).
At one-row decode, tf_moe_prefetch reads the live items' experts' down codes into the caches on the side
stream while the gate and up run; an asm register sink keeps the loads. It writes nothing; the forward joins
the side stream after its last layer.

Exactness: same bytes (the kernel only reads).
Tests: none new (the decode tests run with it).
Contract records: tf_moe_prefetch.
Contract tests: contract_prefetch.zig (gates 1 to 3: every buffer and guard stays as prepared).
@AbrahamCete

Copy link
Copy Markdown
Contributor

Hi @davidtai, a follow-up on the GatedDeltaNet scan. I had a rework of gated_delta_tile on v1.2.0 ready, with the same bits. Before opening it, I measured it against this branch's tf_gdn_prompt_128 on Qwen3.8-27B (gfx1151, rocprofv3 --kernel-trace over tf-qwen35-test prefill at 2,048 tokens, 192 calls, two runs each):

  • v1.2.0 gated_delta_tile<128, 8, 16>: 10.50 and 10.71 ms a call;
  • my rework, <128, 8, 15>: 6.02 and 6.11 ms;
  • tf_gdn_prompt_128 at a39a232: 4.30 and 4.32 ms.

Yours is faster on every prompt it takes, and my rework doesn't move decode measurably, so I won't open a competing PR. Two parts of it may still help you:

  • A GPU test that pins the scan's exact bits. It runs fixed inputs built from integer draws through the prompt tile, the one-row wave, the decode plan and its replay. An FNV-1a digest of y, the final state and the snapshots must match. A reordered sum passes the current 5e-3 checks but fails this test, so it could back the "same bits" claim of tf_gdn_prompt_128.
  • lane0_sum without LDS. v_permlanex16 handles mask 16 and DPP row_xmask handles 8, 4, 2 and 1, and each lane adds its partner in the same order. In gated_delta_tile that cut the ds_bpermute count from 12 a step to 2 (the broadcasts) and the kernel time by 38%. tf_gdn_prompt_128 already keeps masks 16, 8 and 4 in registers. Its last two __shfl_xor, masks 2 and 1, could use DPP the same way. lane0_sum itself still serves the decode and short-prompt paths.

I can send either one as a PR on top of your split, or you can take them, whichever suits you.

Exactness, performance and memory of the series tip against main 94b90c9 on an AMD Strix Halo (gfx1151) machine
with 192 GiB of unified memory, both arms alternating in each round, and the contract, test and hash results.
The README states what each folder holds. Logs are sanitized copies: local paths, host and user names are
placeholders; values are unchanged.
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.

2 participants