Repository navigation
Conversation
20e6de1 to
20df220
Compare
…e_attention.cu, block_attention.cu), checked against core/attention_ref.zig
… merge combines them in order; a slice takes its own one-warp block where four-warp blocks would leave SMs idle (TreeAttention.sms), the same bits
…lves again, only one-warp blocks leave them to the merge (a small fixed slice area), so the scratch stays at one partial a group
…async stages), the same arithmetic in the same order
…w), partials sized to the query heads, one-warp blocks with 3 tile stages where they fit one wave of the SMs, else 2
…ts) before folding them, the same arithmetic in the same order
…, and dense rows for every family, checked against float64
20df220 to
df0ef6b
Compare
|
Rebased onto main |
…te value there stays out of its sum A block whose output holds a NaN or infinity runs again with the values of the tiles some row cannot see all of taken out of the mma; each row adds back the ones it sees. On finite values the first pass is the kernel as it was, byte for byte.
…ax flag, so a NaN head leaves the other heads' keys
…ld zeros, not a copy of the window's first key, so an infinite value there reaches no sum
…, so a later row's infinite value stays out of earlier rows' sums A block whose output holds a NaN or infinity runs again, out of line, with the values of the tiles some row cannot see all of taken out of the mma; each row adds back the ones it sees. On finite values the first pass is the kernel as it was, byte for byte.
|
The kernels are now registered with the kernel contract in |
… flag (a head with no key keeps m, exp2 of +0 is exactly 1), the old kernel's speed
|
Thanks @jschmied for the three window repairs and the final decode-fold update. This still needs changes before landing. It inherits #746's visible zero-weight infinity replay mismatch and reference-constructor allocation leak. The prompt replay uses the same helper, so both failing host controls apply. Please repair those once in the shared code, preserving the split-slot and per-head regressions. The final 7d59918 fold needs its own finite-bit and NaN-head checks; earlier receipts cannot simply qualify it. Keep all 27 launch forms pending, counting #746's inherited fifteen once. Then rerun window boundaries, ring wrap, partial tiles, neighboring heads and numerical/resource gates on the combined head. Posted by Ash's AI Assistant Synthena |
…ook when a later allocation fails, with a host test that fails each allocation in turn
…ut of the mma and adds it back at its key's float64 weight, so an infinite value gives NaN where float64 does
… value out of the mma and adds it back at its key's float64 weight (attn_odd), so an infinite value gives NaN where float64 does
|
Repaired once, in the shared code: #746's
Not changed: the decode kernels have no replay. A visible infinite value whose weight underflows in float32 but not in float64 still gives NaN there, where float64 gives inf (the same as #746's tree attention). |
…ded back before it The replay adds the context's NaN and infinite values back before the block's own tile, whose rescale is exp(m - m') in float32. When a block key holds the row's max and the context's scores sit more than ~103 below it, that factor is 0 and turns an added-back infinity into NaN, where float64 (attention_ref) still weighs it above 0 and gives inf. The replay's block tile now leaves an infinite o as it is (attn_odd::keep); the common path is unchanged. Gate 1 plants the case on stream 1, key head 7: block key 0 all 8, every other key all -8 with +inf at element 23, queries all 2 (inf) and all 8 (float64 weights of 0: NaN).
…values out of the mma, added back at float64's weights A visible infinite value whose weight underflows in float32 (more than ~103 below the max) but not in float64 (745) went through the mma as 0 x inf, or reached a merge at a factor of 0: NaN, where attention_ref gives inf. As in block attention, a block whose partial holds a NaN or infinite value now runs again (ODD, out of line): its V tiles' NaN and infinite values leave the mma (attn_odd::clean_rows), and each pair adds them back after its last tile at the key's float64 weight (attn_odd::add_back), so weight 0 gives NaN. The four-warp combine and the merge fold as before; only a NaN result folds again with the infinities unscaled (attn_odd::fold, keep). tf_tree_shared takes the window's keys (kn) for the path keys' weights. Finite outputs keep their bits. Gate 1 plants key head 1, row 0: committed key p0 - 1 all 8, every other key (committed and its path) all -8 with +inf at element 21, queries all 2 (inf) and all 8 (NaN).
…again with those values out of the sums, added back at float64's weights A visible infinite value whose weight underflows in float32 (more than ~103 below the max) but not in float64 (745) went into a sum as 0 x inf, or reached a fold of partials at a factor of 0: NaN, where attention_ref gives inf. The prompt rows had the replay (462d952); the decode kernels now have it too, out of line: - tf_window_decode_tc: a NaN output (the one wrong class: an infinite output holds a weight above float32's least, so above float64's) runs the block, or the one warp that saw keys, again as ODD. Its V tiles' NaN and infinite values leave the mma (attn_odd::clean_rows), each head adds them back after its last tile at the key's float64 weight (attn_odd::add_back), and the warps' fold keeps them (attn_odd::fold). - tf_window_part: a tile with a NaN or infinite output recomputes those outputs from the finite values, then adds each NaN or infinite value at its key's float64 weight against the row's float64 max over its window (attn_odd::score64 with a run-time head dim). tf_window_merge folds a NaN result again with the infinities unscaled. Finite outputs keep their bits. Gate 1 plants the case in each decode form: a row's key p all 8, its window's other keys all -8 with +inf at element 21, one head querying all 2 (inf), the next all 8 (NaN).
…split.cuh (the file was past 600 lines), comments on one line
|
The open case is closed: the decode kernels now have the replay, in
|
…ombine, and dense rows on CUDA, checked against float64 The pull request's head is rebased on 1.0.12 and #746's rebased head. Two registration conflicts, resolved keeping both sides: core/root.zig's references and tf-cuda-test's imports. No family calls moe_glue yet; GLM keeps its own routing. The merged tree is the one the earlier landing of its previous head gave.
|
Thank you, @jschmied, for sliding-window attention, sigmoid top-k routing, combine and dense CUDA rows. We merged the reviewed head They share #746's zero-weight-infinity replay mismatch and reference-constructor leak. Please repair that common code once, then cover the final NVIDIA GB10/RTX PRO 6000 release contracts passed 55/55 with five controls red; these operator checks do not establish family adoption. Posted by Ash's AI Assistant Synthena |
|
This is already done on this branch for the 1.1.1 increment. The head is
Results are in #798 (comment) and #798 (comment). |
|
Thanks @jschmied for extending the replay across prompt and decode paths and adding the isolation controls. The old constructor/zero-weight findings are repaired in source; the final split-file move preserves the function bodies. This is a 1.1.2 candidate: carry #746's shared changes once, then the remaining window/test changes. Please supply the raw exact-head contract, mutation, finite-byte, resource and timing outputs for the merged build. Kolibri now consumes the operators, so its drafted/plain, resumed/fresh and concurrent/solo checks and prompt/decode comparisons are required too. The shared registry/build merge also needs review before landing. Posted by Ash's AI Assistant Synthena |
|
A correction first, about the tree attention kernels this PR carries from #746. On #746 I wrote that the tree launches have "no local memory on any target". I read that from cuobjdump's Spills from
On the GB10 (sm_121), the A test fix only the merge needs (PR branches unchanged): main's test "the six served MoE and MTP launches are in the live registry" looks records up by name alone. --- a/zig/src/core/contract/qwen_served_records.zig
+++ b/zig/src/core/contract/qwen_served_records.zig
@@ -20,7 +20,7 @@
for (records) |record| {
var found: usize = 0;
- for (registry.all_cuda) |active| if (std.mem.eql(u8, active.launch.symbol, record.launch.symbol)) {
+ for (registry.all_cuda) |active| if (std.mem.eql(u8, active.launch.symbol, record.launch.symbol) and std.mem.eql(u8, active.launch.image, record.launch.image)) {
try std.testing.expectEqualDeep(record, active);
found += 1;
};Merged build: Conflicts and how I resolved them:
Results, GB10 (all raw outputs below):
Kolibri-1 on the GB10,
The cache benchmark ( kernel-contract SUMMARY lineskernel-contract, merge, this PR's kernelskernel-contract-limits, merge, this PR's records (sm_80, 86, 89, 120, 121)ptxas -v per entry point, merge: regs/stack/spill store/spill load (bytes)ptxas -v per entry point, main d46d17aSASS, merge vs headsnumerical suites, mergefinite bytes: merge vs heads, and the log2(e) mutationmutations, merge (controls lines omitted)timing, best of 3 rounds x 7 x 100 calls, #798 head -> merge (us a call, launch + sync)Kolibri raw comparison |
…g and combine, and dense rows on CUDA, checked against float64 The pull request's head is rebased on 1.0.12 and ashhart#746's rebased head. Two registration conflicts, resolved keeping both sides: core/root.zig's references and tf-cuda-test's imports. No family calls moe_glue yet; GLM keeps its own routing. The merged tree is the one the earlier landing of its previous head gave.
… states, and a platform layer for Windows (ashhart#734, ashhart#746, ashhart#797, ashhart#798, ashhart#836, ashhart#849, ashhart#852, ashhart#853, ashhart#859, ashhart#885, ashhart#886, ashhart#888)
Carry the unique #798 changes after #746, omitting its duplicated ancestors. Retain bounded window splitting, sigmoid routing and dense-row contract fixtures. Source-only proposed increment; final CPU/device gates remain owed. Co-authored-by: Jürgen Schmied <600316+jschmied@users.noreply.github.com>
|
The shared CUDA sliding-window attention, sigmoid top-k routing and combine work is included in 1.1.2 (https://github.com/ashhart/TensorFold/releases/tag/v1.1.2), with bounded window and routing fixtures. Closing this as carried, with your coauthor credit retained. Thank you for the contribution. It landed as ffe63cb. - Synthema, Ash's AI Assistant. |
What this changes
Stacked on #746 (the window test checks against its
core/attention_ref.zig, and the prompt kernel takes itsattn_odd.cuh). Operator-only, no model calls them yet:window_attention.cu/.zig: sliding-window attention, decode rows over a ring of keys (split by key tile, mergedin tile order, in
window_split.cuh; or on tensor cores) and prompt rows, grouped KV heads, window 0 = full.moe_glue.cu/.zig: top-k by logit + bias (ties: lower id), sigmoid weights, optionally normalised and scaled,a shared-expert slot; a warp form up to 512 experts, a block form beyond; the weighted combine of the slots.
dense_rows.cu/.zig: bf16 rows times bf16 weight rows into fp32 (routers, output heads), a row's bits at any rowcount.
core/moe_ref.zig,core/dense_ref.zig: the float64 references.NaN and infinities in
window_attention.cu(found by the kernel contract's gate 1, fixed here):tf_window_decode_tc: the flag that lets a tile move the running max was taken from the first of the two heads athread holds and applied to both, so a head whose scores were all NaN stopped another head's max. Each head now
takes its own max with no flag: a tile with no key for a head leaves its max as it was, and exp2(+0) is exactly 1
(
ex2.approx.f32gives +1.0 for +0 and -0 on every target, PTX ISA 9.7.3.21; checked on sm_121), so the kernelkeeps the old arithmetic and the old speed.
tf_window_part: tile slots outside the window held a copy of the window's first key, with weight 0, so aninfinite value at that key gave 0 x inf = NaN in the partial. Those slots now hold zeros.
wattn_kernel, both forms): keys past a row's position (and before its window) sit in the same tileand mma as rows that see them, with P = 0, so a later row's infinite value made earlier rows NaN. As in cuda: tree attention and DFlash block attention for every family, checked against float64 #746's
block attention: a block whose output holds a NaN or an infinity runs again (out of line), with every NaN and
infinite value of V set to 0 before the mma, and each row adding back the ones it sees at the key's float64 weight
(cuda: tree attention and DFlash block attention for every family, checked against float64 #746's
attn_odd.cuh: an infinite value whose float64 weight is 0 adds 0 x inf = NaN, as float64 does; onlywhere its weights sum is positive).
but not in float64 (745) went into a sum as 0 x inf, or met a fold factor of 0: NaN, where float64 gives inf. Now
the same replay, out of line.
tf_window_decode_tc: a NaN output (the only class a 0 x inf can get wrong: aninfinite output holds a weight above float32's least, so above float64's) runs the block, or the one warp that saw
keys, again with V's NaN and infinite values out of the mma, each head adding them back after its last tile at the
key's float64 weight, and the warps' fold keeping them.
tf_window_part: a tile with a NaN or infinite outputrecomputes those outputs from the finite values and adds each NaN or infinite value at its key's float64 weight
against the row's float64 max over its window;
tf_window_mergefolds a NaN result again with those infinitiesunscaled.
Users: families with sliding-window layers or sigmoid-routed MoE on CUDA (Kolibri 1's shapes in the tests; GLM and
DeepSeek-style routing are the same top-k form).
Receipt
46a08c18(1.0.12) plus cuda: tree attention and DFlash block attention for every family, checked against float64 #746 and these commits; one DGX Spark (GB10, sm_121, CUDA 13). Nocheckpoint.
tf-cuda-test window: 48/4x128 window 513 over rings of 640 and 600 (a ring end inside a tile), 8/2x64 window 100with 3 streams, 16/1x128 no window; prompt rows from position 0 and across the ring's wrap: worst 0.465, bits at
any row count and chunk.
tf-cuda-test moe-glue: 384 experts top 6 + shared, 256 top 8 normalised x2.5, 512 top 32 (warp form's widest),513 top 6 (block form's narrowest), 4096 top 64; picks exact, weights and sums worst 0.996.
tf-cuda-test dense-rows: [384, 2560], [4103, 2560], [257, 7168]: worst 0.007, bits at every row count.docs/kernel-contract.md), CUDA on GB10 (sm_121),zig build kernel-contract -Dnvcc=/usr/local/cuda/bin/nvcc, exit 0 (cuda: tree attention and DFlash block attention for every family, checked against float64 #746's 21 kernels plus this PR's 16):SUMMARY 37 kernels: 27 pass gates 1-4, 10 fail, all at gate 1.zig/tests/cuda/contract_window.zig,contract_moe.zig): window attention in five calls (tensor-coredecode, 48/4 heads, window 513, ring 640, two streams; tensor-core decode, 16/1 heads, no window, ring 300;
split decode
tf_window_part+tf_window_merge, 8/2 heads of 64, window 100, ring 128, three streams; promptrows
wattn_kernel<128, 8, 4, 8>, 48/4 heads, window 513, ring 2,624;wattn_kernel<128, 8, 8, 8>, 16/2 heads,no window); sigmoid top-k in three (warp form, Kolibri's 384 experts top 6 + shared; warp form, 256 top 8
normalized x2.5; block form, 513 to 4,096 experts); the combine in four (fp32, bf16 and fp16 rows, bf16, fp32
and fp16 out, the generic loop included); dense rows in each of the four row tiles (1, 4, 16, 32).
chunks of 128 rows from position 0 and 96 rows across the ring's wrap at 2,600, 64 and 48 rows for the unwindowed
form; top-k 16 and 300 rows (33 rows at 513 experts, 16 at 4,096 top 64); the combine 16 and 300 rows of 2,560 with
7 slots and 9 rows of 7,168 with 9; dense rows over Kolibri's router (384 x 2,560), an output head's 4,103 columns
and 257 x 7,168 at each tile's row counts. Tile edges: 32- and 64-key tiles and 16- and 32-row prompt blocks; for top-k
the 8-row block and 32-expert lane (warp form) and 256 experts (block form); 256 columns and 8 slots for the
combine; the row tile, 8 outputs and a 256-deep k for dense rows.
propagate); gate 1 plants a NaN query and key,+inf and -inf values at a row's own key, a later row's key and the window's first key, and both in one column;
for prompt rows also +inf at a row's own key at a float64 weight of 0 (48/4 form), and +inf over a row's keys at
weights below float32's least but not float64's (both forms; in the unwindowed form's gate shape some of those
keys sit in tiles every row of the block sees); for each decode call, a row's key p all 8 and its window's other
keys all -8 with +inf, one head querying all 2 (inf, below float32's least) and the next all 8 (NaN, float64
weight 0), the keys spread over several warps' tiles (48/4) or tiles (split decode). Top-k
states its rule: picks by fp32 logit + bias, larger first, ties to the lower id, a NaN never picked; weights
sigmoid(logit).
core/attention_ref.zig's float64 bound through theirown check hook (the framework cannot express P rounded to bf16 before P x V; the split decode's fp32 weights reach
it through exp the same way). The combine and dense rows run the framework's fp32 bound (worst 0.997 and 0.228 of
it). Gate 4: exact sums at the served depths, where bf16 sums would miss by 256 to 1.0 million times (attention),
2,177 to 102 million (combine), 504 to 801 million (dense rows); top-k sums nothing (N/A).
registry.zig, with compile records fromzig build fatbins kernel-contract-fatbins -Dsm=80,86,89,120,121andtf-cuda-test kernel-contract-limits zig-out/fatbin(PASS kernel-contract-limits: every registered launch fits sm_80, sm_86, sm_89, sm_120 and sm_121, every control red). The fixes moved register counts, re-recorded: bothwattn_kernelforms 203 to 223 (sm_80/86/89) and 218to 222 (sm_120/121),
tf_window_part52 to 51 (sm_80), plus cuda: tree attention and DFlash block attention for every family, checked against float64 #746's (tf_block_attentionnow 230 and 228); thefloat64-weight replay leaves every window record as it was;
tf_window_decode_tckeeps its records(164 and 168). The decode replay re-records
tf_window_decode_tc164 to 176 (sm_80/86/89) and 168 to 178(sm_120/121),
tf_window_part51/44/44/48/48 to 55/50/50/56/56 with 128 more static shared bytes (45,824). On theGB10 the live check reads
PASS limits: 53 registered CUDA launches against sm_80/86/89/120/121, 0 problems.element 0: nan where float64 gives finite"), the window's first key in the outside slots (the split decode, "nan
where float64 gives pos_inf"), the prompt kernel without its second pass (both forms, "nan where float64 gives
finite"). A stale register record for
wattn_kernel<128, 8, 4, 8>(218 where the compile gives 222) turns gate 5red. All restored before the run above. The prompt replay as reviewed (
7d599181): "head 252 element 17: pos_infwhere float64 gives nan" (48/4 form, the weight-0 key) and "head 112 element 13: nan where float64 gives pos_inf"
(unwindowed form: an infinite value in a tile every row sees, at a weight below float32's least, through the mma),
besides cuda: tree attention and DFlash block attention for every family, checked against float64 #746's two block forms. On
462d952cthe decode plants are red: "head 360 element 21: nan where float64gives pos_inf" (48/4), "head 64 element 7" (16/1) and "head 56 element 13" (split decode), the same class, besides
cuda: tree attention and DFlash block attention for every family, checked against float64 #746's new tree and block plants. The float64 weight test forced true: the three decode calls, "head 361 element
21", "head 65 element 7" and "head 57 element 13: pos_inf where float64 gives nan".
only, not run on a GPU.
df0ef6bc) and after, the same inputs:the five window calls over the registration's served shapes and tile edges with 12 seeds in two finite fills, plus
sweeps with 2 seeds: decode rows 1 to 64 at position bounds 1 to 3,000 (both sides of 32, 64, 100, 128, 300, 513,
640), prompt chunks of 1 to 128 rows from positions 0 to 2,400 (both sides of 32, 64, 450, 512, 513). 2,260 runs,
694,272,000 output bytes, 0 differ (with cuda: tree attention and DFlash block attention for every family, checked against float64 #746's kernels on this branch: 5,808 runs, 1,545,863,168 bytes, 0 differ).
7d599181againstdf0ef6bc): both decode calls overtheir served shapes and edges with 12 seeds in both fills, and rows 1 to 64 at position bounds 1 to 3,000 (both
sides of 32, 64, 128, 300, 513, 640 and 2,048) with 2 seeds: 1,038 runs, 169,549,824 bytes, 0 differ; all five
window calls: 2,958 runs, 751,878,144 bytes, 0 differ.
(no key for the row's heads in the tile), the max is unchanged, so its factor is exp2(+0) = 1 exactly; a head
with no max yet gets 0 instead of 1, and its sum and values are +0 or NaN, which both factors leave as they are.
With finite inputs every tile a warp takes has a key. The split decode's outside slots had weight 0, so each added a signed
zero to a sum that starts at +0 and is never -0; zeros add the same. The prompt kernel's first pass is the old
kernel; a finite output skips the second.
tf-cuda-test windowgives the same numbers as before (worst 0.465).462d952cagainst7d599181): cuda: tree attention and DFlash block attention for every family, checked against float64 #746's two block forms (asin cuda: tree attention and DFlash block attention for every family, checked against float64 #746, 2,080 runs) and the five window calls over their served shapes with 12 seeds in both finite fills, plus
with 2 seeds decode rows 1, 2, 3, 16 and 64 at position bounds on both sides of 32, 64, 100, 128, 300, 513, 640
and 1,280 up to 3,000, and prompt chunks of 1, 15 to 17, 31 to 33, 64, 96 and 128 rows from positions 0, 1, 63 to
65, 511 to 514, 1,000, 2,600, 2,623 to 2,625 (the ring's wrap) and 5,000: 5,016 runs, 1,179,664,384 output bytes, 0
differ. A one-ulp change to the window kernels' log2(e) differs in 159,376 bytes.
7d599181) on its own: the tensor-core decode on this head against8f933773(theper-head flag) over the same decode sweep: 992 runs, 174,522,368 bytes, 0 differ. Its machine code on sm_121 is
the same on
7d599181and this head (cuobjdump -sass,tf_window_decode_tc,tf_window_part,tf_window_merge). NaN heads: for every query head of each of the five window calls, a NaN in that head's query(one element, or all of it) in every row, at the served shapes and decode rows 3 and 17 at bounds 33, 300 and 641
or prompt chunks of 17 and 33 rows from 0, 513 and 2,600: that head's outputs are all NaN and every other output
equals the run without the NaN, 2,080 runs, 317,394,944 neighbouring outputs, 0 differ. The shared flag put back
into the decode fold: 688,128 outputs differ.
8cba69e1;74b749famoves the split decode towindow_split.cuhand joins comments, the sameSASS on all five targets) against
462d952c: cuda: tree attention and DFlash block attention for every family, checked against float64 #746's eight forms (tree and block, as in cuda: tree attention and DFlash block attention for every family, checked against float64 #746) and the five windowcalls over their served shapes with 4 seeds, plus decode rows 1, 2, 5, 16, 17 and 64 at position bounds 1, 2, 31
to 33, 63 to 65, 99 to 101, 127 to 129, 299, 300, 512 to 514, 639 to 641, 1,000, 1,300 and 3,000, and prompt chunks
of 1 to 3, 15 to 17, 31 to 33, 63 to 65 and 128 rows from positions 0, 1, 99 to 101, 511 to 514, 600, 2,600,
2,623, 2,624 and 2,700, both finite fills: 3,224 runs, 795,961,344 output bytes, 0 differ. A one-ulp change to the
tensor-core kernels' log2(e) differs in 69,839 bytes. NaN heads as above on this head: 2,080 runs, 325,156,864
outputs, 0 bad; a replay that makes every head NaN: 22,364,160 bad. Speed (best of 7 x 100 launches with sync,
5 alternating runs): tensor-core decode 20.4 to 18.9, 47.2 to 44.0 and 11.0 to 10.5 µs, split
decode 17.1 to 17.0 and 34.0 to 34.3 µs, prompt rows -0.5 to +0.6 %.
18.7 to 18.8 and 12.4 to 12.1 µs at the served shapes; the decode kernels' code is unchanged.
µs (128 rows), 574 to 586 µs (a 2,048-row chunk, +2 %); tensor-core decode 12.3 to 12.3 µs (1 and 4 rows), 22.5 to
22.5 µs (16 rows), 258.5 to 259.0 and 257.9 to 258.4 µs (64 rows, two runs); split decode unchanged.
exponent x1.0625 (5,527x block form, 6,798x warp form), tie order (wrong pick), the combine dropping a slot, a dense
row's first product dropped, the dense warp sum one step short.
window,dense-rows,moe-glueon an RTX 5090, all passing with mutations (Qwen3.8-27B on CUDA: the qwen27 family's engine for NVIDIA GPUs, every stream drafted #608).zig build testfrom an empty cache on74b749fa(86/86 steps, 1,380 of 1,387 tests, 7 skipped) andzig build test-cuda-host(41/41 steps), all passing, with the fixes and the registration;tf-cuda-test tree(0.470),block(0.380),window(0.465),moe-glue(0.996) anddense-rows(0.007) pass on this branch.zig fmt --checkclean.lean_check.py: 517 findings, ason
main.mainuses it.Until #746 lands, this branch carries its commits (its eight new ones cherry-picked, the same changes); this
PR's own are
df0ef6bc,ce160986,63d15071,40de67dd,8f933773,7d599181,462d952c,8cba69e1and74b749fa.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (Noserving path changes.)
SUMMARYline is in the receipt. (CUDA; no Metal kernels here.)