Skip to content

hip flashnext: verify windows run their DeltaNet from the round's plan, with no state snapshot a row - #2728

Merged
ashhart merged 10 commits into
ashhart:mainfrom
gilby:strix/hip-fn-gdn-plan
Nov 13, 2026
Merged

ashhart merged 10 commits into
ashhart:mainfrom
gilby:strix/hip-fn-gdn-plan

Conversation

@gilby

@gilby gilby commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

Flash Next's HIP verify windows ran their 36 DeltaNet layers one row at a time, writing every layer's conv tail and recurrent state after every row of the window, so a keep could copy the snapshot of its last kept row. This moves the window onto the plan path the 27B HIP engine already uses: a window's conv split, gates and delta rule read their rows from the round's plan and leave the state where the window began, and the keep replays the kept rows through the same scan (planGdnReplay) and rebuilds the conv tail from the kept inputs (tf_plan_keep). The round no longer writes a snapshot a row, which is where the decode time goes.

  • The plan kernels are the existing ones (plan_ops.convSplit, the plan delta rule, planGdnReplay, tf_plan_keep); no kernel source changes. A one-row window is its own keep: the plan kernel writes its state back in place and the replay stops at one row, as those kernels already do.

  • gdn_replay.zig (new) holds the plan buffers a cache slot, the words a round sends, the window's linear layer on the plan, and the keep. The engine, mixers.zig, forward.zig and model.zig take one-line hooks: the window branch of the linear layer, the words sent inside the round's graph (so a replayed graph sends that round's own), and the keep.

  • The plan path is the default wherever its shape guard accepts (key dim 128, value dim a multiple of 8, value heads a multiple of key heads, conv width 1 to 8; Flash Next's shapes pass). Shapes it refuses keep the per-row path. There is no switch: it is a pure speedup with the same tokens.

  • The plan's buffers are allocated once at open for the streams asked for and counted there, before room is measured; a stream's admission does not add them again. The per-row snapshot buffers are still allocated, so device memory grows by the plan's few MiB a stream rather than shrinking; dropping the snapshots under the plan path is a separate follow-up.

  • Prompt chunks are untouched (the prefill branch of the linear layer is unchanged).

  • Issue: n/a.

  • Stacked on: main.

  • atc overlap --json against main (000b534a2): incomplete (pullFilesLimit, timeLimit); the file shared with most open PRs is CHANGELOG.md. hip flashnext: an eight-row verify window, so a lone stream's copies land up to seven tokens a round #2730 widens the verify window this plan is sized from; the two are tested together (see the comments).

Checks on this head

  • Head commit: caee124a5: the reviewed commits, a merge of main 000b534a2 (conflict in CHANGELOG.md only), the conv split test with norms and gates on (0c5103dca), the plan's bytes counted once and its slots freed when opening fails (ede2870e2, caee124a5), and the window test through a real plan, fill to keep (b3212e660). The receipt ran on this head.
  • Lean checker, python3 tools/zig/lean_check.py: 345 problems on the base and 345 on this head, 0 new findings. python3 tools/pr_check.py --base origin/main --pr 2728: 0 errors, 0 warnings.
  • Tests run on this head: zig build test -Dhip=true --summary all: 889 of 891 steps succeeded, 13,645 of 13,759 tests passed, 113 skipped, 1 failed; the failure is dashboard_test "dashboard page polls the public stats snapshot without private details", which fails the same way on main 000b534a2. zig build hip-kernel-test on gfx1151: 20 of 20 steps, 213 of 219 tests passed, 6 skipped, 0 failed (main: 159 of 165). zig build hip-kernel-contract: SUMMARY gfx1151 159 kernels: 159 pass gates 1-4, 0 fail, 2 known findings; 41 controls: 41 red as planted, 0 missed; 173 limit problems, the same line as main 000b534a2 on this install (this PR registers no kernel).
  • New tests, in the kernel-test step, at Flash Next's shape against today's per-row launches. gdn_plan.zig: the plan's conv split gives the per-row launch's q, k and v bits at one to four rows; the plan's delta rule with the first n rows kept leaves the state the per-row snapshot of row n holds; two streams' plans keep their own rows. gdn_window.zig, through a real gdn_replay.Plan: fill writes every slot's words and the linear layer's descriptor where the kernels read them; the conv split on the plan with norms and gates on writes the per-row launch's q, k, v, gate and beta; a window through its plan, fill → send → conv split → delta rule → keep(n) over two layers (one linear, one zeroed sparse layer), rows 1–4 and every n, equals the per-row window in y, the state and the conv tail. Planted control: with Plan.keep keeping one row fewer, the last test goes red (slices differ).
  • CI: not yet reported for this head; I will post each failure and whether this change caused it.
  • Not run: gfx1030, gfx1100 and gfx1201 (no card), TP > 1 (one GPU), Metal and CUDA (HIP Flash Next only).

Receipt

  • Flash Next, main (000b534a2) against branch, arms in the order main, this, this, main, a fresh server an arm with the page cache dropped (tools/bench_concurrent.py --levels 1,2,4 --reps 3 --prompts tools/bench_prompts.json --alone --serial --temperatures 1.0,0, 16 distinct prompts, 256 tokens; tools/bench_openai.py --tokens 256 --reps 3; tools/bench_cache.py --require-cold). Both arms of each side are shown:
    • Greedy, 1 / 2 / 4 streams: 51.6 / 66.6 / 67.1 and 51.6 / 66.6 / 67.1 (main) vs 52.8 / 70.0 / 69.6 and 52.8 / 70.0 / 69.5 (this) tok/s: +2.3%, +5.1%, +3.7%.
    • Sampled at 1.0, 1 / 2 / 4 streams: 45.3 / 59.6 / 64.4 and 45.2 / 59.7 / 64.4 (main) vs 46.3 / 61.8 / 66.8 and 46.4 / 61.7 / 66.8 (this): +2.4%, +3.5%, +3.7%.
    • bench_openai code 46.31 / 46.27 vs 47.32 / 47.33 tok/s (+2.2%), chat 40.14 / 40.12 vs 40.98 / 41.02 (+2.2%).
    • Cold 7,377-token prompt, first token: 12.04 / 12.02 s vs 12.02 / 12.03 s (prompt chunks are untouched); four cold streams on a fresh server, greedy 53.2 / 52.8 vs 55.0 / 55.0 tok/s, sampled 39.4 / 39.5 vs 41.1 / 41.1.
  • Boxes it reaches: every HIP device that serves Flash Next with the plan kernels' shapes; measured on Strix Halo (gfx1151) only.
  • New switch? None.
  • Environment: TensorFold main 000b534a2 plus this change, Zig 0.17.0, zig build native-hip -Dhip=true -Dhip-arch=gfx1151 -Doptimize=ReleaseFast with the system hipcc (HIP 7.1, clang 21), one Strix Halo iGPU (gfx1151), Linux 7.0, unigilby/Qwen3.8-Flash-Next-oQ4e-HIP (revision 5a0fb414), --context 131072 --parallel 4 --prompt-cache-gib 0, MTP 3 drafts.
  • Exactness, greedy and sampled: every request's token_sha256 equals main's in each arm (42 of 42 warm, 8 of 8 cold); --alone --serial equal in every cell of every arm (alone 3 / 6 / 12 equal at 1 / 2 / 4 streams, serial 4 / 4: drafted equals "draft": false, concurrent equals solo), 0 unequal, 0 failed; the cold prompt's token_sha256 25c0728f9043 in every arm. The kernel tests above hold the replayed state bit-equal to the per-row snapshots.
  • Decode speed: above. Prompt speed: unchanged, above.
  • Licence of new weights: n/a.

Checklist

  • The title and description match the head commit, including the receipt and the stack.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency.
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths in the diff or its commits. No AI attribution lines.
  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (--alone --serial above. Resume was not run separately: the prompt cache keeps prefill states, and prompt chunks do not take this path.)
  • No precision traded for speed.
  • Decode and prompt processing are no slower than main at one stream and the widest admitted.

@ashhart

ashhart commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby. The replay matching the per-row snapshot for every kept row is the right check. A few asks before this can land:

  1. The kernel tests in gdn_plan.zig only exercise plan kernels that already exist on main, so they'd pass without this PR. Reverting the window hook in mixers.zig, the plan branch of keepWindow, or Plan.fill's layout wouldn't fail any test. Could you add a kernel test that builds a real gdn_replay.Plan and runs fill → send → the window's conv split and delta rule → keep(n)? It should cover two layers (one linear, one zeroed sparse layer), rows 1–4 and every n ≤ rows, and compare the state, the conv tail and y bit-for-bit with the per-row path's snapshots.
  2. The conv-split test calls both sides without the q/k norm and gates (gdn_plan.zig:169-170, and :140-154 for the per-row side). Production passes both (gdn_replay.zig:312-322), as main's per-row path does. Please run both sides with norm and gates on, and compare q, k, v, gate and beta.
  3. engine.zig:186-191: if steps.done fails after the slots are made, they leak. Please add an errdefer that frees them after the block.
  4. Plan bytes look counted twice: the slots for want streams are allocated in open before room is measured, and streamBytes also adds Plan.bytes. Please count them once, or say in the description which you intend.
  5. CHANGELOG: please end the line with (#2728). Thanks to @gilby. and drop "a resumed prompt equals a fresh one" unless you've run it.
  6. Main against this branch on gfx1151: bench_concurrent --levels 1,2,4 --prompts tools/bench_prompts.json --alone --serial --temperatures 1.0,0, with per-request token_sha256 equal to main at 1, 2 and 4 streams for both greedy and sampled, concurrent equal to alone, and drafted equal to "draft": false. The current receipt is greedy only. Sampled runs reject more drafts, so they test the rollback hardest. Please include tok/s at 1, 2 and 4 streams too.
  • Synthena (Ash's Agent)

gilby added a commit to gilby/TensorFold that referenced this pull request Nov 12, 2026
…bytes once

The slots follow the rest of the open path's errdefers, and streamBytes no longer
adds the plan: the slots are made for the streams asked for before room is measured
(ashhart#2728). Thanks to @gilby.
@gilby

gilby commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. All six points, on head caee124a5: the reviewed commits plus a merge of main 000b534a2 (a plain merge commit, no force push; the only conflict was CHANGELOG.md) and four commits.

  1. A kernel test through a real plan (b3212e660, new zig/src/hip/kernel_tests/gdn_window.zig). It builds a gdn_replay.Plan and runs fill → send → the window's conv split and delta rule → keep(n) over two layers, one linear and one zeroed sparse layer, at rows 1–4 and every n up to the rows, and compares y, the state and the conv tail bit for bit with the per-row path's snapshots. A second test checks that fill writes every slot's words and the linear layer's descriptor where the kernels read them. Planted control: with Plan.keep keeping one row fewer, "a window through its plan equals the per-row window at every keep n: y, the state and the conv tail" goes red (slices differ).
  2. Norm and gates on (0c5103dca). The conv split test now runs both sides with the q/k norm and the gates, as production does, and compares q, k, v, gate and beta.
  3. The leak (ede2870e2, caee124a5). The slots are freed by an errdefer placed after the block, so it covers steps.done failing as well as the allocation itself.
  4. Plan bytes counted once (ede2870e2). The slots are allocated at open for the streams asked for and counted there; streamBytes no longer adds Plan.bytes. The description says so.
  5. CHANGELOG. The line ends (#2728). Thanks to @gilby. and no longer claims a resumed prompt.
  6. Receipt, greedy and sampled. One Strix Halo iGPU (gfx1151), four fresh servers with the page cache dropped before each, in the order main (000b534a2), this, this, main. bench_concurrent --levels 1,2,4 --reps 3 --alone --serial --temperatures 1.0,0 --prompts tools/bench_prompts.json, aggregate tok/s with steady state in brackets, both arms of each side:
temperature streams main this head aggregate
0 1 51.6 / 51.6 52.8 / 52.8 +2.3%
0 2 66.6 / 66.6 (68.7 / 69.3) 70.0 / 70.0 (71.5 / 72.1) +5.1%
0 4 67.1 / 67.1 (73.3 / 73.3) 69.6 / 69.5 (76.2 / 76.8) +3.7%
1.0 1 45.3 / 45.2 46.3 / 46.4 +2.4%
1.0 2 59.6 / 59.7 (62.6 / 62.7) 61.8 / 61.7 (65.3 / 65.3) +3.5%
1.0 4 64.4 / 64.4 (69.8 / 69.8) 66.8 / 66.8 (72.2 / 72.2) +3.7%
  • Tokens. Every request's token_sha256 equals main's in each arm: 42 of 42 warm and 8 of 8 on a cold four-stream start, greedy and sampled. Concurrent equals alone (3/3, 6/6, 12/12) and alone equals "draft": false (4/4) in every cell of every arm, 0 unequal, 0 failed. Tokens a round are main's in every cell, so the sampled runs reject as many drafts as main's and roll back to the same tokens.
  • One stream, bench_openai --tokens 256 --reps 3: code 46.31 / 46.27 against 47.32 / 47.33 (+2.2%), chat 40.14 / 40.12 against 40.98 / 41.02 (+2.2%).
  • Cold: four streams on a fresh server, greedy 53.2 / 52.8 against 55.0 / 55.0, sampled 39.4 / 39.5 against 41.1 / 41.1; 8K first token (bench_cache --require-cold) 12.04 / 12.02 s against 12.02 / 12.03 s, reply hash equal in all arms.

Checks on caee124a5. python3 tools/pr_check.py --base origin/main --pr 2728: 0 errors, 0 warnings; lean 345 on the base and 345 here. zig build test -Dhip=true --summary all: 889 of 891 steps succeeded, 13,645 of 13,759 tests passed, 113 skipped, 1 failed: dashboard_test "dashboard page polls the public stats snapshot without private details", which fails the same way on main 000b534a2. zig build hip-kernel-test on the GPU: 20 of 20 steps, 213 of 219 tests passed, 6 skipped (main: 159 of 165). zig build hip-kernel-contract prints the same SUMMARY line as main on this install: 159 kernels pass gates 1–4, 41 of 41 controls red as planted, 2 known findings, and 173 limit problems that main 000b534a2 also reports here; this PR registers no kernel.

With #2730. A commit that runs the window test at every row count from one to eight sits on the two branches merged together, and its GPU run passes (213 of 219, 6 skipped). The receipt cells on both together are running now; I'll post them here and link them from #2730.

Environment: Zig 0.17.0, zig build native-hip -Dhip=true -Dhip-arch=gfx1151 -Doptimize=ReleaseFast, system hipcc (HIP 7.1, clang 21), Linux 7.0, unigilby/Qwen3.8-Flash-Next-oQ4e-HIP at revision 5a0fb414, --context 131072 --parallel 4 --prompt-cache-gib 0. The description is updated to this head, and I'll post the CI results when the run finishes.

@gilby

gilby commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

#2728 and #2730 together (the item 4 ask on #2730). The two branches merged, caee124a5 with 2e51ea822, plus one test commit that runs "a window through its plan" at every row count from one to eight, since with both in a lone stream's plan windows reach eight rows. I have not pushed that commit to either PR, because it needs both; it can go into whichever lands second, or tell me where you'd like it.

zig build hip-kernel-test on gfx1151 for the two together: 20 of 20 steps, 213 of 219 tests passed, 6 skipped, 0 failed, with the window test at rows one to eight.

Receipt: one Strix Halo iGPU (gfx1151), four fresh servers with the page cache dropped before each, in the order main (000b534a2), both, both, main. bench_concurrent --levels 1,2,4 --reps 3 --alone --serial --temperatures 1.0,0 --prompts tools/bench_prompts.json, aggregate tok/s with steady state in brackets, both arms of each side:

temperature streams main #2728 + #2730 aggregate
0 1 51.6 / 51.5 53.2 / 53.2 +3.2%
0 2 66.6 / 67.0 (69.3 / 69.0) 69.0 / 70.0 (71.3 / 72.0) +4.0%
0 4 67.1 / 66.5 (73.3 / 72.9) 69.5 / 69.7 (76.1 / 76.1) +4.2%
1.0 1 45.2 / 45.2 46.3 / 46.3 +2.4%
1.0 2 59.7 / 59.1 (62.7 / 62.2) 61.8 / 61.8 (65.1 / 65.2) +4.0%
1.0 4 64.4 / 63.8 (69.8 / 68.9) 66.8 / 66.8 (72.1 / 72.6) +4.2%

Same environment as the receipt above.

@gilby

gilby commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

CI is green on 50a52d765: 8 checks passed and the two nightly host jobs were skipped. This head is caee124a5, the head of the receipt, plus a plain merge of main 6c5cef894 (no force push); main had moved and the branch conflicted in CHANGELOG.md, so CI could not start without it. The merge changes nothing else of this PR.

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby. We read caee124a5 and the merge of main on top of it (50a52d765). All six points are in:

  1. gdn_window.zig runs a real gdn_replay.Plan from fill through send, the conv split and the delta rule to keep(n). It covers a linear layer and a zeroed sparse layer, rows 1 to 4 and every n, and compares bit for bit with the per-row snapshots. Your planted short keep turns it red, so the test reaches the plan path.
  2. The conv split test now runs the q/k norm and the gates on both sides, and compares q, k, v, gate and beta.
  3. The errdefer after the block (zig/src/families/flashnext/hip/engine.zig:194-198) frees the slots if steps.done fails, and clears weights.model.gdn first.
  4. streamBytes (engine.zig:259-270) no longer adds Plan.bytes. The HIP admission measures room after the slots exist (native.zig:175-176), so they are counted once there.
  5. The CHANGELOG line is right.
  6. The receipt covers greedy and sampled. Every token_sha256 equals main's, concurrent equals alone, and drafted equals "draft": false, at +2.3% to +5.1%.

CI is green on 50a52d765, and it merges cleanly with main. We have no gfx1151 to run it on, so your receipt is the device evidence. The change reaches only the HIP Flash Next engine and its kernel tests. We'll land it.

A small follow-up, not for this PR. The note at engine.zig:260 says the plan is made before any room is measured. That holds for the admission, but fittedScratch measures room at engine.zig:279 (called from :169), before the slots are made at :190. So the chunk-narrowing choice leaves out gdn.Slots.bytes(dims, want). It is small and only moves that one choice. Making the slots before the arena, or taking their bytes off room there, would close it.

The test that runs the window at one to eight rows belongs in #2730. That PR lands after this one, so it will have both changes under it.

  • Synthena (Ash's Agent)

A stream's words, a slot's descriptor and the rows its keep replays, one Plan a cache slot. No kernel launches yet.
…laying the kept rows

A window's conv split, gates and delta rule read its rows from the plan, and the keep rebuilds the conv tail from the kept inputs and re-runs the scan over the kept rows, so the state is serial decode's while the round writes no snapshot a row.
The engine makes a plan a cache slot where the shapes are the plan kernels' and sends its words inside a round's graph, a window's linear layer goes through the plan, and a keep replays the kept rows where the plan took it and copies the snapshots where it did not. The plan's bytes count in a stream's admission.
… k, v, state and conv tail

At Flash Next's shape: the plan's conv split against today's launch and the kept conv tail for one to four rows, the plan's delta rule y against serial and the state a keep replays for each kept row (a one-row window is its own keep; a longer one leaves the state as the round found it), and two streams each replayed from its own rows. Host: the words' layout, the kept sections' offsets, and the shape guard, which is the only choice now.
…paths

The kernel tests now reach the family's model-free side through kernel_reach, so a
conv split test can build a real plan and compare today's launch with the plan's
q, k, v, gate and beta bit for bit.
…bytes once

The slots follow the rest of the open path's errdefers, and streamBytes no longer
adds the plan: the slots are made for the streams asked for before room is measured
(ashhart#2728). Thanks to @gilby.
… and keep the kernel-test wiring out of hip.zig
@ashhart
ashhart force-pushed the strix/hip-fn-gdn-plan branch from 50a52d7 to f70f7e6 Compare November 13, 2026 11:35
@ashhart
ashhart merged commit 244a188 into ashhart:main Nov 13, 2026
2 of 10 checks passed
ashhart pushed a commit that referenced this pull request Nov 13, 2026
…bytes once

The slots follow the rest of the open path's errdefers, and streamBytes no longer
adds the plan: the slots are made for the streams asked for before room is measured
(#2728). Thanks to @gilby.
@ashhart

ashhart commented Nov 13, 2026

Copy link
Copy Markdown
Owner

Thank you @gilby. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes.

ashhart added a commit that referenced this pull request Nov 13, 2026
The train that landed #2728 left two lines for it under Unreleased (one had been under 1.2.6): keep the full wording
with its number. #1878 also had a second line still reading "TBD-RECEIPT%"; its measured line (about 5%) stays.
ashhart added a commit that referenced this pull request Nov 13, 2026
Figures and scope corrected to the PRs (#2948, #2863, #2934, #2933, #2921, #2906, #2878, #2892, #2851, #2728, #3072,
#2915, #2864, #2854 and others), promises of later work removed (#2903, #2875, #1630, #1720), #3074 and #3047/#3046
said as their PRs scope them, the duplicate #2861, #2585, #2321 and #2719 entries and two copies of released 1.2.5
lines removed, #1953's line named for its PR, and a line for #3255. Released sections unchanged.
@ashhart

ashhart commented Nov 13, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby! This is in TensorFold 1.2.8, out now: https://github.com/ashhart/TensorFold/releases/tag/v1.2.8

  • Synthena (Ash's Agent)

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