Repository navigation
qwen27: a masked key's non-finite value adds nothing to the rows that must not see it - #2106
Conversation
50b216b to
ac37546
Compare
|
Thanks @cshintov, this is careful work: one vote per chunk keeps main's instructions whenever no masked value is non-finite, the rejected alternatives are measured, and the M4 Pro digests are byte-equal with the 3,300 stray non-finite values gone. Two things before it lands. On our side, the M5 runs you couldn't do: the contract on the tensor-unit paths with the decode control, the precision tool with |
|
Thanks @cshintov. Here's the first of the runs we owed you, on an M5 Ultra: the 27B served at your head against its base, base and new alternated, four runs each. Tokens: identical in all 20 rows. Prompt speed is lower, and it grows with prompt length:
The repetitions agree to about a tenth of a percent, so this is a steady cost and not noise. Finite outputs don't change, so this is a speed question only, but we can't take a default-path slowdown of this size for inputs that already hold a NaN or infinity. Since it grows with length, the vote looks like the likely cost on these tensor-unit paths: each simdgroup checks every key a live row can't see, on a path your M4 Pro doesn't run. That's our reading, not a measurement. Could you look for a form that doesn't cost finite prompts, such as a cheaper check, or one that runs only when a non-finite value could already be present? We can run any candidate on the M5 Ultra for you. The contract with the decode control and the precision tool's |
|
@cshintov, the contract on the M5 Ultra, on your head ( All 30 kernels pass. The one miss is the decode control: The prompt control does go red here (6 of 49,152 outputs differ in class; first at 3: nan where float64 gives pos_inf). So on tensor-unit decode, the control doesn't show what it plants: either this path doesn't need the mask, or the control doesn't reach it on an M5. Please make the decode control bite there, or tell us the M5's decode isn't affected and leave that path alone. If it isn't affected, that may also be a place to save the prompt cost we posted above. |
ac37546 to
36bea73
Compare
|
Two changes on the head ( Cost: it was not the vote, which reads only keys from Decode control: gate 1's decode round was a tree (5 rows at start 2). A tree's keys go to the tail kernel, so the prefix-kernel edit never ran. Only a chain of 2 or more rows reaches masked keys in I also moved gate 1's prompt chunk so it holds a whole step past the first row's keys: a wrongly placed split passed gate 1 before. The CHANGELOG line is in too. |
|
Thanks @cshintov, that's a sharp diagnosis: the mask on every fully seen step, and a tree that never reached the prefix kernel. We'll rerun the M5 Ultra A/B on |
|
@cshintov, our M5 Ultra rerun on Contract ( Served, on the 27B, alternating builds, tokens identical in all 20 rows:
These don't overlap: every repetition sits apart, so it isn't noise, and the cost grows with prompt length. Decode is level, and four streams are level too (182.1 -> 183.0). The fix is a correctness one, so we want it, but a slower default prompt on every M5 is a lot to pay for values that are rarely non-finite. Is there a way to keep the masked path's cost off rows that are all finite, for example a cheap check per launch or per block? Could you look at where the time goes? I haven't run |
36bea73 to
54ff80b
Compare
|
The time went to the masked code being in the kernel at all: on
On M4, finite launches are +0.4% to +0.7% over main from 8K to 64K keys (the scan and pick), and planted ones +1.5% at 8K. The prompt control now turns the scan off. Gate 1 plants at both ends of the window, so it can't see a scan that is short at one end. A new test-qwen27 test covers that by planting at every key around the window on each KV head. Could you rerun the M5 A/B on |
|
Thanks @cshintov. Finding that the masked code's presence in the kernel was the cost, and picking the kernel per launch so a finite prompt runs main's own, is the right fix, and the window test that plants at every key around |
b442cee to
e77e21a
Compare
e77e21a to
66df541
Compare
|
The M5 Ultra run on
The One thing to look at: one-stream decode leans slightly negative, while four streams don't. Your note said |
66df541 to
d89ce2b
Compare
|
It can take the same treatment, and here it needs no scan. A stream of one row sees every key its tile reads (a decode row's keys run to its own position, and every query row of the tile is that one row), so no value in its range is masked. The host now picks So one-stream decode without the drafter now runs exactly main's kernels. Four streams of one row each do too. The M5 decode control still goes through the masked kernel, since gate 1's decode round is a chain. Could you rerun the one-stream decode cells on |
|
Thanks @cshintov. Picking |
|
Our M5 Ultra runs on this head (
Both are level. The M3 Ultra run, which is the chip this change is for, is running now. |
|
Our M3 Ultra run, the chip this is for, on this head (
Six measured samples a side per cell, in alternating order. Level, with equal tokens. With the M5 Ultra runs above, nothing else is owed from our side, so this goes in the landing queue. Thanks @cshintov. |
…masked P V NaN a known finding Registers Attention.encode's whole calls for decode rows (a round's trees and chains) and a prompt chunk at one Mac's head counts (24 over 4, 32 over 2, 16 over 4) under gates 1-4. Gate 1 plants -Inf in round row 1's value, which row 0 must not see: it carries probability 0 for that key, and 0 times -Inf in P V gives NaN where float64 gives row 0's own value. That is a known finding on the prompt kernels (attention_prompt_sg before M5, attention_prompt on M5) and on the M5 decode prefix (attention_mpp), predicted from the source there. tf-qwen27-attention-precision --prompt --plant puts -Inf in a value of each case's last key and checks the rows before it, which must not see it, against float64.
… must not see it A row gives probability 0 to a key it must not see, and 0 times an infinite or NaN value in P V gave NaN in that row. Each P V kernel that masks keys per row now leaves such a value out of the rows that do not see it, and the rows that see it keep the IEEE result. attention_prompt_sg (prompt rows before M5) and attention_prompt (on M5): before its steps, a simdgroup votes once on the values at keys some live row must not see. If one is not finite, every step reads non-finite values as a zero of their sign, then adds value times probability back into each row that sees the key. attention_mpp's q27_attn_prefix (decode rows on M5) reads values through the tensor op, so in such a pass the steps a row does not fully see sum P V themselves and skip each row's unseen keys. Without such a value every kernel runs the code it ran before, so finite inputs keep their bytes, negative zeros included. The contract drops the three known findings and adds a control that sends non-finite values to P V as they are (prompt rows on every chip, decode rows on M5); it fails gate 1 as planted.
…metal_main stays at 600 lines
…tion rows into NaN
…er the mask The masked read ran in every 32-key step of every chunk, so a finite prompt paid for it per key. Steps whose keys every live row sees now run the unmasked step, the same code as main's; only the steps past that point take the mask and the add-back.
…at reaches the prefix kernel's masked keys A tree's keys below its own go to the tail kernel, so the decode control, which edits the prefix kernel, never ran on the round gate 1 used.
…'s keys, and the last row's value is planted A step split at the wrong key, or an add-back skipped in a whole step, passed gate 1: its chunk of 8 rows ended inside one partial step.
…st not see isn't finite A scan of the launch's own value rows and a one-thread pick write the two indirect grids, so the masked kernel's code is no longer in the kernel that finite prompts run.
… only a stream of rows can mask a value
d89ce2b to
8aa3021
Compare
|
Thank you @cshintov. 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. |
Main grew both files to the edge. The engine API names the control vector as glp.Glp instead of keeping an alias, and qwen27's attention_io registration moves into a Suite in metal_q27_attn_io.zig, as the attention rows did in ashhart#2106, so the runner keeps three lines for it.
…ct runs reach their summary 9be323a gated the decode line on MASKED, and the decode control's edit still looked for the old text, so on M5 Attn.init returned ControlEdit and tf-kernel-contract stopped before any summary (#2301). Co-authored-by: BobClawblaw <288991054+BobClawblaw@users.noreply.github.com>
…ct runs reach their summary 9be323a gated the decode line on MASKED, and the decode control's edit still looked for the old text, so on M5 Attn.init returned ControlEdit and tf-kernel-contract stopped before any summary (#2301). Co-authored-by: BobClawblaw <288991054+BobClawblaw@users.noreply.github.com>
What this changes
A row gives probability 0 to a key it must not see, and 0 times an infinite or NaN value in P V gave NaN in that row. Now such a value adds nothing to the rows that must not see it. Rows that do see it keep the IEEE result, the same class float64 gives, as the decode path already did. Inputs with finite values keep their bytes. This covers the three kernels that mask keys row by row:
attention_prompt_sg(prompt rows before M5),attention_prompt(prompt rows on M5) andattention_mpp'sq27_attn_prefix(decode rows on M5).002c9e03e). Eight commits: the contract registration with the finding marked known, the fix, the registration moved into its own suite sometal_main.zigstays at 600 lines, the changelog, then four changes from review: the masked read moved off the steps every row sees whole, gate 1's decode round made a chain on an M5, gate 1's prompt chunk made to hold a whole step past its first row's keys, and a per-launch pick so a finite prompt launch runs main's kernel.atc overlap --json: not run. ATC isn't installed here, so whoever opens the PR needs to run it.Design
A finite prompt must run the kernel main runs. Two earlier heads did not:
36bea7342ran main's step on the steps every row sees, but in a kernel that also held the masked step, the vote and the add-back. Measured served on the 27B on an M5 Ultra: -1.2% at 2K to -4.3% at 32K. On M4, a timing-only build with the masked code removed cut the same kind of cost from +2.2% to +0.6% at 8,192 keys, so the code's presence costs time even when it never runs. Why it grows with prompt length on the M5 is inferred, not measured: attention is a larger share of a longer prompt, so a fixed slowdown of its kernel shows more.Now each prompt launch picks one of two kernels:
q27_attn_prompt_scanreads the launch's own new value rows, keysstart + 1tostart + rows - 1, all 256 dims of every KV head, one simdgroup a key. Those are the only keys any row of the launch can be masked from: row r seesstart + r + 1keys, so every key belowstart + 1is seen by every row. Any non-finite bf16 sets one flag word.q27_attn_prompt_pick(one thread) reads and clears the flag and writes two indirect grids, the prompt kernel's grid into one and zeros into the other. Both kernels are then dispatched indirectly; the empty one does nothing. Cost: two small dispatches and two barriers per launch, about 0.03 ms on M4.q27_attn_prompt_8(M5) orq27_attn_prompt_sg_8(before M5), theMASKED = falseinstance of the kernel: main's loop, no vote, no add-back. Its IR is identical to main's, both the kernel and all four step instances, once value names, callee names and the unused mask parameter are normalised.q27_attn_prompt_masked_8orq27_attn_prompt_sg_masked_8, which is the previous head's kernel, described below.Inside the masked kernel:
seenis the fewest keys any live row sees. The scan covers only keys fromseento the end of the chunk, so it is constant per chunk, one 16-byte load per lane per two keys.seen(every live row sees every key) runs the step withMASKEDfalse at compile time: plain value fragments, no mask argument, no add-back. Only the rest of the chunk, from the step that holdsseenon, runs the second copy: values read under the vote's mask, then the add-back.xsrow, and each lane adds value times probability back for every non-finite value whose key its row sees, using the same probability the matrix step used (fp32 before M5, half on M5).q27_attn_prefix(decode on M5) is chosen by the host. A round where every stream has one row runsq27_attn_prefix_N, main's kernel by IR for all 16 tile counts: that row sees every key its tile reads, so nothing in its range is masked. A round with a stream of rows runsq27_attn_prefix_masked_N, the kernel described next. A test-qwen27 test walks the kernel's ranges over one-row plans and finds no masked key, and finds one in a two-row chain.q27_attn_prefixreads values straight from device memory through the tensor op, so there is no point at which to mask them. In a pass that voted yes, each step that holds a key some row must not see sums its P V in plain code and skips each row's unseen keys. Steps that every row fully sees still use the tensor op, as they did on main. Its fast steps differ from main's only by one uniform compare and branch per 64-key step (!masked || kt + TK <= seen). Its steps aren't split into two copies within the kernel, because they are one long body with the tensor ops in it.fusedin the plan). Its prefix kernel runs to the last row's key, so row r seesstart + r + 1keys and the chunk holds keys up tostart + rows: every row but the last has keys it must not see. Any other round (a tree, or one row) sends its own keys to the tail kernel, which reads only a row's ancestors, and stops the prefix at the tile before them. A plan of 5 rows at start 2 gives, as a tree,prefix_chunks=0, tail_chunks=1, and as a chainprefix_chunks=1, tail_chunks=0, limit 7 with rows seeing 3, 4, 5, 6 and 7 keys.Rejected alternatives:
p == 0 ? 0 : p * vfor every product. It turns a -0 product into +0, so finite outputs would change bytes. It also costs a select on every product of every step.seenon.36bea7342). The M5 cost above: the finite path's code was main's, the kernel's was not.q27_attn_prefix. It needs 16 KiB per simdgroup, more than the 32 KiB budget allows from 2 tiles (4 simdgroups) up.Checks on this head
d89ce2b9c, on maincde4fe7d9. The newest commit picksq27_attn_prefixper round; lean, test-qwen27 and the contract were rerun on this head.xcrun metal -O3 -S -emit-llvm:q27_attn_prefix_1to_16are main's kernels, andq27_attn_prefix_masked_1to_16are the previous head's (54ff80b87), all 32 identical.python3 tools/zig/lean_check.py: 425 problems on the base (cde4fe7d9), 425 on this head, 0 new (tools/zig/lean_new.py).metal_main.zigis at 600 lines.zig fmt --check build.zig zig: clean.zig build test-qwen27: 46/46 steps, 152/152 tests passed.a stream of one row has no key in the prefix kernel's range it must not seewalks the prefix kernel's per-tile ranges over one-row plans (1 to 3 streams, 14 starts across the tile and 8,192 boundaries, shared prefix and fused chains on and off) and finds no masked key; it finds one in a two-row chain. The other new one isprompt pick: one non-finite value at every key, head and kind; masked iff start < key < start + rows. It runsAttention.encodePickback to back on one flag: for 1, 2, 3, 8, 33 and 40 rows at starts 0 and 5, one value (+Inf, -Inf, NaN or a negative NaN) at every key fromstart - 1tostart + rowson each of 4 KV heads, each followed by a finite launch. It checks the grid picked, that the other grid is zero, and that the flag is clear after each launch.zig build test-kernel-contract: 22/22 steps, 173/173 tests passed.tf-kernel-contract --only qwen27:SUMMARY 30 kernels: 30 pass gates 1-4, 0 fail, 0 known findings; 3 controls: 3 red as planted, 0 missed; 0 limit problems, exit 0.22 pass gates 1-4, 0 fail, 3 known findings. The three are gate 1 on prompt rows at 24 over 4, 32 over 2 and 16 over 4, each "nan where float64 gives pos_inf".start + 1and the last key, so a scan short at either end still finds the other. The new test catches all five:start + 2: "rows 2 start 0 key 1 head 0: masked false".start + rows - 2: the same line.start: "rows 1 start 0 key 0 head 0: masked true".36bea7342's kernel, which is this head's masked kernel):seen, so steps that rows must not see read unmasked: gate 1 fails, "6 of 49152 outputs differ in class; first at 3: nan where float64 gives pos_inf". Before gate 1's prompt chunk held a whole step past its first row's keys, this mutant passed. That is why the chunk is nowk = 57.attention_promptkernels were built here and compared as IR, but none of their GPU paths ran: those kernels assume the M5's tensor-op element layout.tf-qwen27-attention-precision --prompt --planton its tensor-unit paths. On54ff80b87the M5 contract passed with all four controls red, and the prompt cost was gone.Receipt
Environment: TensorFold
d89ce2b9c, Zig 0.17.0, Xcode Metal toolchain, an M4 Pro (no tensor units). No checkpoint: these are synthetic kernel checks.Paths and platforms the change reaches, which I ran and which I could not:
attention_prompt_sg) through the contract and the precision tool.attention_prompt) and decode rows on M5 (q27_attn_prefix).Exactness:
tf-qwen27-attention-precisionoutput digests are byte-equal across main, the earlier heads of this fix and this head:--prompt(24 over 4), prompt kernel / decode path, queries width 1ddeecd15f434b130/d126ea575c5b560a--prompt(24 over 4), queries width 4bd7e7beac1aaa2dc/acceedb02042af55--prompt --heads 32 --kv-heads 248b07607b1f53adbdf5b68aac294eb05c3355635363e142ddfaac0400478a740--prompt --heads 16 --kv-heads 48dcf3c049caaf93ae46988c14121296802b4e9c2c174e005139544178cb7cc85d56632e65c16e02f60c1f0342f93123a0591e9cba21ae225e38e42c9532d7e28With
--plant(-Inf in a value of each case's last key), main's prompt kernel writes 3,300 non-finite values (width 1) into rows that never see that key; this head writes 0 at all three head counts, and gives the same bytes as the earlier heads (2c34835aec7d2f56fe3104dea4d43b662522dafd65de43ec700d248ac565b389at 24 over 4). Those rows hash the same as the unplanted rows, the same rows the decode path gives with and without the plant.Decode speed before and after: the decode path before M5 is unchanged (byte-equal digests above). Decode on M5 is not measured.
Prompt speed before and after: not measured end to end (no checkpoint, and no M5). The attention call's GPU time (scan and pick, prompt kernel, merge) of one 128-row chunk at the end of N cached keys, 24 over 4, on an M4 Pro through its pre-M5 prompt kernel. Median of 5 interleaved runs, each the median of 5 timed repetitions after a dropped warm-up. In ms; the key count is set by a flag I added to the tool locally and have not included:
36bea7342The
36bea7342column is from the earlier run, on mainc2aa0fed3. With finite values this head runs main's kernel, and what is left is the scan and pick (about 0.03 ms a launch). At 16,384 and 65,536 keys the runs of main and this head overlap. Only a launch with a non-finite value it must mask pays the masked kernel: +1.5% over a finite launch at 8,192 keys and +0.5% at 32,768 (median of 3 runs). The M5 speed is yours to measure.Licence of any new weights or drafter: n/a.
Kernel contract, only when a GPU kernel is new or changed
metal qwen27 attention {27B, GQA16, 9B} {decode rows, prompt rows}, which runAttention.encode's whole calls against float64. They are new registrations of existing kernels, at one Mac's head counts: 24 over 4, 32 over 2 and 16 over 4. The rank's 12 over 2 merges as a pair, so it is not one call.q27_attn_prompt_scanandq27_attn_prompt_pick, covered by the test-qwen27 test above and run by the prompt rows' registrations.q27_attn_prompt_masked_8andq27_attn_prompt_sg_masked_8are the previous head's prompt kernels under new names.control: qwen27 attention 27B prompt rows with non-finite values unmaskedturns the scan off, so every launch runs the finite kernel, which sends values to P V as main does. It is red at gate 1, as planted. On M5 the same control coversattention_prompt, plus a decode-rows control that disablesq27_attn_prefix's vote. Before M5, decode rows have no masked path, so that control isn't registered there.PlantedRoundMissesPrefix). Before M5 the round is still a tree: that kernel is the tail, and the sibling that must not see another's key is tested there.SUMMARYline and exit status: Metal (M4 Pro),30 kernels: 30 pass gates 1-4, 0 fail, 0 known findings; 3 controls: 3 red as planted, 0 missed; 0 limit problems, exit 0.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. Finite inputs are byte-equal, so these are unchanged.