Repository navigation
Conversation
c5936c7 to
4d596fe
Compare
c817f13 to
654b48b
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
654b48b to
591065e
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.
|
The kernels are now registered with the kernel contract in |
|
Thanks @jschmied for the registrations and both NaN repairs. Two source blockers remain. The replay adds a visible infinite value back without checking that key's weight. When its softmax weight underflows to zero, the declared FP64 reference gives NaN but the helper returns infinity. A host control reproduces this through the actual helper; it is not a GPU run. The shared reference constructor also leaks earlier arrays when a later allocation fails. Please repair both with failing controls, keep the fifteen launch forms pending, then rerun finite-output identity against the reviewed head and all numerical/resource gates. The current receipts do not close those cases. 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
|
Both are fixed, in
Not changed: tree attention has 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 holds for #798's decode kernels. Gate 1 does not plant that case for them. |
…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).
|
The open case is closed: tree attention now has the replay too, in
|
…ry family, checked against float64 The pull request's head is rebased on 1.0.12. Two registration conflicts with #734, resolved keeping both sides: core/root.zig's exports and test references (attention_ref beside tree_round) and tf-cuda-test's imports. The merged tree is the one the earlier landing of its previous head gave.
…RoPE on CUDA for every family, checked against float64 The pull request's head is rebased on 1.0.12. Registration conflicts with #734 and #746, resolved keeping both sides: the kernel list, the fatbin table, the module exports, core/root.zig's references and tf-cuda-test's imports, usage and commands. The merged tree is the one the earlier landing of its previous head gave.
…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 CUDA tree attention, DFlash block attention and the float64 checks. We merged the reviewed head That increment still adds infinity where a visible key's zero weight makes the reference NaN, and its new reference constructor leaks after an allocation failure. Please repair both with failing controls before we carry the increment; the affected helper and constructor are absent from the 1.1.0 candidate. 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 |
|
Both are already repaired on this branch, with failing controls, for the 1.1.1 increment. The head is
Controls, finite-output identity and the gate results are in #746 (comment) and #746 (comment). |
|
Thanks @jschmied for the follow-up and failing controls. The constructor cleanup, weighted non-finite replay and tree replay address the earlier findings in source. This remains open for 1.1.2 integration: Kolibri now uses these operators, so we need the merged launch-argument/resource review and Kolibri drafted/plain, cached/fresh and concurrent/solo regressions. Please attach the raw final-head numerical, mutation, finite-byte and compiled-resource receipts; the description's GB10 results remain reported evidence. We will carry the shared changes once with #798, retaining the existing registrations and bounds. Posted by Ash's AI Assistant Synthena |
|
A correction to my last comment first. 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 |
…for every family, checked against float64 The pull request's head is rebased on 1.0.12. Two registration conflicts with ashhart#734, resolved keeping both sides: core/root.zig's exports and test references (attention_ref beside tree_round) and tf-cuda-test's imports. The merged tree is the one the earlier landing of its previous head gave.
…rm and RoPE on CUDA for every family, checked against float64 The pull request's head is rebased on 1.0.12. Registration conflicts with ashhart#734 and ashhart#746, resolved keeping both sides: the kernel list, the fatbin table, the module exports, core/root.zig's references and tf-cuda-test's imports, usage and commands. The merged tree is the one the earlier landing of its previous head gave.
…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 #746 once with the current strict CUDA image resolver and build roots. Keep exact replay and the bounded launch resource records. Source-only proposed increment; final CPU/device gates remain owed. Co-authored-by: Jürgen Schmied <600316+jschmied@users.noreply.github.com>
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 tree-attention and DFlash block-attention operators and their contract fixtures are included in 1.1.2 (https://github.com/ashhart/TensorFold/releases/tag/v1.1.2). Closing this as carried, with your coauthor credit retained. Thank you for the contribution. It landed as c7233df. - Synthema, Ash's AI Assistant. |
What this changes
CUDA has no shared attention for drafted rows. A family that drafts a tree on CUDA writes its own attention over the
committed keys plus each row's path, and a DFlash-style drafter writes its own block attention. This adds both as
shared modules, operator-only (no model calls them yet), next to
cuda/tree_round(#734), which does the verify andcommit step of the same round:
zig/kernels/cuda/tree_attention.cu,zig/src/cuda/tree_attention.zig: drafted rows of any number of streams inone call. Each row sees its stream's committed keys and then its root-to-row path (
paths()builds the paths fromtree-local parents, depth up to 128). Keys run in 512-key groups on tensor cores, merged in group order, at head
dims 128 and 256 with grouped KV heads. A stream's fully committed groups are computed once for all its rows (16
(row, head) pairs as one mma tile, the same tiles and merge as a row's own block), so every partial keeps its bits
whether a row runs alone or beside others. The grid is fixed: at most 16 group slots per (row, key head), each
walking its groups, so a captured graph does not grow with the context. A group's key tiles go to 4 slices; a
four-warp block combines its slices in order; where four-warp blocks would leave SMs idle (
TreeAttention.sms),each slice takes its own one-warp block and the merge combines the 4 in the same order (a small fixed slice area
in the scratch), so the bits do not depend on the device's size. A one-warp block fetches its tiles 2 ahead (3
cp.async stages where the one-warp grid fits one wave of the SMs, else 1 ahead in 2, two blocks an SM; stages
never change bits) while it computes one. Partials hold the query heads only. The merge runs a block per (row, key
head, head).
zig/kernels/cuda/block_attention.cu,zig/src/cuda/block_attention.zig: a DFlash-style drafter's block rowsover their stream's context (each stream's K and V cache through a pointer table, within a sliding window) and then the block itself,
causal or not. One warp per head on tensor cores.
zig/src/core/attention_ref.zig: one query head's attention in float64 with a per-element tolerance (2^-7 ofsum p|v| / sum p, plus a bf16 half-ulp), the reference both tests check against.
NaN and infinities (found by the kernel contract's gate 1, fixed here):
tree_attention.cu: the flag that lets a tile move the running max was taken from the first of the two mma rowsa thread holds and applied to both, so a head whose scores were all NaN stopped another head's max (dim 128: head
8 all NaN because head 0 had a NaN query). Each row now has its own flag.
block_attention.cu: a key a row cannot see (the window's lower edge, a causal block's later keys) sits in thesame 32-key tile and the same mma as rows that do see it, with P = 0, so an infinite value there gave 0 x inf =
NaN. Its V cannot be zeroed for one row only, and the mma cannot select per row. So the kernel checks its output:
a block whose output holds a NaN or an infinity runs again, with every NaN and infinite value of V set to 0 in
shared memory before the mma, and each row adding back the ones it sees after the last tile, at the key's float64
weight: the key's score and the row's max recomputed in float64 in
attention_ref's order, and an infinite valuewhose weight exp(score - max) is 0 adds 0 x inf = NaN, as float64 does (only where its weights sum is positive, so
a row with no weight stays NaN). The block's own tile runs after the context's values are added back, and leaves
an infinite o as it is (
attn_odd::keep): when a block key holds the row's max and the context scores sit morethan ~103 below it, its rescale is 0 in float32. The shared step is
attn_odd.cuh.tree_attention.cu: the same replay. A visible infinite value whose weight underflows in float32 (more than ~103below the max) but not in float64 (745) went through the mma as 0 x inf, or met a merge factor of 0: NaN, where
float64 gives inf. A block whose partial holds a NaN or an infinity now runs again, out of line: its V tiles' NaN
and infinite values set to 0 before the mma, and each (row, head) adding them back after its last tile at the
key's float64 weight against its row's float64 max over all its keys (committed, then its path). The four-warp
combine and the merge fold as before; only a NaN result folds again with those infinities unscaled
(
attn_odd::fold,keep).tf_tree_sharedtakes the window's keys (kn) as a new last parameter, for the pathkeys' weights.
Users we know of: the Qwen3.8-27B family in #608 (target tree attention at dim 256 and its DFlash2 drafter), and any
CUDA family that drafts trees with #734.
Receipt
46a08c18(1.0.12) plus these commits; one DGX Spark (GB10, sm_121, CUDA 13). Nocheckpoint for the tests.
tf-cuda-test tree: dim 128 (48 query / 4 KV heads) and 256 (24 / 4), several streams with different committedlengths and trees, every element within the float64 tolerance (worst 0.47 of it). A stream's rows run alone
equal their bits in the batch; one shared block walking every (row, group), a grid sized for a 262k context,
and one-warp blocks equal the default grid.
tf-cuda-test block: windowed contexts then the block, causal and not, block widths 7 and 16, withintolerance (worst 0.38). A stream run alone equals its bits in the batch.
docs/kernel-contract.md), CUDA on GB10 (sm_121),zig build kernel-contract -Dnvcc=/usr/local/cuda/bin/nvcc, exit 0:SUMMARY 21 kernels: 16 pass gates 1-4, 5 fail, all five at gate 1 (the three dim-128tree forms, both block forms).
zig/tests/cuda/contract_tree_attn.zig,contract_block_attn.zig,contract_attn.zig):tf_tree_paths; tree attention in all six forms (dim 128 with 48/4 heads and dim 256 with 24/4, four-warpblocks, one-warp blocks with 2 stages, one-warp blocks with 3 stages; each form's shared, part and merge launches
as one call); block attention as the DFlash2 drafter serves it (blocks of 16, 32/8 heads, window 2,048) and causal
blocks of 7.
one stream's block over 2,100 context keys (past the window) and four streams over 700; paths for 16, 128 and 600
rows. Tile edges: the 16-row tree, the 512-key group, the 32-key block tile, the 128-row path block.
propagate). Gate 1 plants a NaN query, a NaN key, and +inf and -inf values: in thecontext, in a block's key (which earlier causal rows cannot see), at the window's edge (a key only block row 0
sees), and +inf and -inf in one column. For block attention, +inf values row 0 sees at a float64 weight of 0
(scores 1,448 apart: a key in the window's edge tile, and the block's first key) and at weights below float32's
least but not float64's (scores 362 apart, every key of a key head, edge tiles and the others); on stream 1, key
head 7, block key 0 holding the max over keys all -8 with +inf, queried all 2 (inf) and all 8 (NaN). For tree
attention, key head 1, row 0: committed key p0 - 1 all 8, every other key (committed and its path) all -8 with
+inf, queried all 2 (inf, below float32's least) and all 8 (NaN, float64 weight 0).
tf_tree_pathsstates its rule (integers only: depths and the nearest 128 ancestors, other slots untouched).
attention entries check gates 1 and 3 against
core/attention_ref.zig's float64 bound (2^-7 of sum p|v| / sum pplus a bf16 half-ulp, the bound
tf-cuda-test treeandblockuse) through their own check hook; gate 3's linereads "exact by its own check". Gate 4 builds sums where every key scores alike (p = 1): exact at 2,100 keys,
where fp16 sums would miss by inf and bf16 by 1.6 to 2.1 million times.
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:tf_block_attention166 to 230 (sm_80/86/89) and170 to 228 (sm_120/121), its 256-thread block still one an SM on every target; nine tree launches by -10 to +12
on sm_80/86/89 only. The tree replay re-records the tree launches: dim 128 from 162-168 to 190-228, dim 256 from
208-243 to 249-255 (255 on sm_80/86/89; no local memory on any target), the merge 148 to 128 (sm_120/121) and 97
to 114 (sm_80/86/89). On the GB10 the live check reads
PASS limits: 41 registered CUDA launches against sm_80/86/89/120/121, 0 problems.element 0: nan where float64 gives finite"; the block kernel as it was: both block forms). The replay as
reviewed (
ee81000d, adding a value back without its key's weight, and only for the edge tiles): both block forms,"head 20 element 17: pos_inf where float64 gives nan"; with only the below-float32 plant, "head 28 element 21: nan
where float64 gives pos_inf"; with only the block's first key, causal, "head 24 element 19: pos_inf where float64
gives nan". On
ff6e5c0ethe plants added with the tree replay are red: the six tree forms, "head 12 element 21:nan where float64 gives pos_inf" (dim 256: head 6); both block forms, "head 540 element 23: nan where float64
gives pos_inf" (causal: head 252). The float64 weight test forced true: the six tree forms, "head 13 element 21:
pos_inf where float64 gives nan" (dim 256: head 7), and both block forms. Holding the attention
bound 64x tighter turns gate 3 red for all eight attention entries. All restored before the run above.
only, not run on a GPU.
591065e0) and after, the same inputs:every changed kernel over the registration's served shapes and tile edges with 12 seeds in two finite fills
(normal values, and gate 4's alike-key fill), plus sweeps with 2 seeds: trees of 1 to 64 rows over 1 to 4,096
committed keys (both sides of each 512-key group), blocks of 1 to 4 streams over 1 to 3,000 context keys (1, 7, 16,
31 to 33, 63 to 65, and 2,032 to 2,081 around the 2,048 window plus 16), causal and not. 3,548 runs, 851,591,168
output bytes, 0 differ. A one-ulp change to the tree's score scale differs in 708,973 and 1,032,086 bytes (dims 128
and 256), so the comparison sees it.
input where a row's visible scores are numbers both rows' flags agree and every operation is the old one.
ff6e5c0e) against the reviewed head (ee81000d): both block forms over the servedshapes with 12 seeds in both finite fills, and 1 to 4 streams over 1, 7, 16, 31 to 33, 63 to 65, 700, 2,032 to
2,081, 2,100 and 3,000 context keys with 2 seeds: 2,080 runs, 489,881,600 output bytes, 0 differ. A one-ulp change
to the kernel's log2(e) differs in 245,828 bytes. Block attention 86.0 to 87.6 µs (one stream, 2,100 keys), 34.7
to 35.4 µs (four streams, 700), causal 84.0 to 84.5 and 33.9 to 33.6 µs (best of 5 x 100 launches with sync).
keep(e0726dc1,942e2fe6;b502747cchanges comments only, the sameSASS on all five targets) against
ff6e5c0e: the six tree forms overthe served shapes with 6 seeds, and 1, 5, 16, 17, 33 and 64 rows over 1, 15 to 17, 31 to 33, 511 to 513, 1,023 to
1,025, 1,536, 2,100 and 3,000 committed keys; both block forms over the served shapes with 6 seeds, and 1 to 4
streams over 1, 7, 16, 31 to 33, 63 to 65, 700, 2,047 to 2,050, 2,079 to 2,081, 2,100 and 3,000 context keys; both
finite fills: 1,632 runs, 461,963,264 output bytes, 0 differ. A one-ulp change to the tree's log2(e) differs in
74,217 bytes. Speed (best of 7 x 100 launches with sync, 3 alternating runs, GB10): block attention -0.2 to
+0.9 %; the twelve tree cells 0.0 to 15.6 % faster (dim 128 one-warp, 64 rows: 129.8 to 110.2 µs), the merge
now at 128 registers instead of 148.
contract_attn.zig,Ref.init) frees what it took when a later allocation fails:a host test fails each allocation in turn (
std.testing.checkAllAllocationFailures); onee81000dit reportsa leak, now it passes (in
zig build test-cuda-host).tf-cuda-test treeandblockgive the same numbers as before (worst 0.470 and 0.380).34.8 to 32.8 µs (four streams, 700); the six tree forms 0.3 to 2.4 % slower at the served shapes (launch included; runs
spread about 1 %).
off by one, the causal mask off by one, the merge leaving out a group's fourth slice (19x the tolerance, and the chunked merge's fourth slice), a tile fetched into the stage being read.
attention, drafted long prompt: attention 46.1 ms against Triton's 49.0 ms; 16 streams at 32k context, code and
chat at temperature 0 and 1: every
bench_concurrent --alone --serialcell exact, throughput within -4..+2 % ofTriton. Replies equal 0.6.6's except two at bf16 near-ties (an exact top-2 tie, and one ulp), where drafted still
equals serial. Teacher-forced on 1,023 rows of real text: NLL 0.8058 vs 0.8029, argmax agreement 99.12 %.
The slices, one-warp blocks and stages (
de31ba9b,6c3d49d5,197cacc7) change no bits: 4,092 teacher-forcedlogit rows byte-identical to the build before, drafted and 8-stream shared rounds with the same shas and round
counts. 512-key groups (
403549d1) give the 27B new bits once, on every device: drafted == serial on all fourprompts in both block forms, and against the Triton set on 4,092 teacher-forced rows dNLL +0.0009, argmax
agreement 99.32 %, mean KL 0.00066 (2,048-key groups: +0.0011, 99.12 %, 0.00071). Serial decode after a 4,722-token
prompt, 128 tokens, GB10: attention 275.8 ms with the first version, 209.1 ms now (one-warp blocks; four-warp
217.5 ms; Triton 244.3 ms). On an RTX 5090 (Qwen3.8-27B on CUDA: the qwen27 family's engine for NVIDIA GPUs, every stream drafted #608): attention 34.6 µs a call against Triton's 59.7,
long2.92 vs2.94 s. The merge issuing a chunk's loads first (
591065e0) changes no bits (the same 4,092 logit rows, shas andround counts) and takes the merge from 10.0 to 4.6 µs a call on GB10, attention 199.2 ms in all.
zig build testfrom an empty cache onb502747c(86/86 steps, 1,378 of 1,385 tests, 7 skipped) andzig build test-cuda-host(41/41 steps), all passing, with the fixes and the registration.tf-cuda-test treeandblockpass with the numbers above.
zig fmt --checkclean.lean_check.py: 517 findings, as onmain.mainuses it.Registration lines in
build/cuda.zig,cuda/kernels.zig,cuda/root.zigandtests/cuda/main.zigsit where#734, #736 and #740 add theirs; whichever lands second takes both.
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.)