Repository navigation
Conversation
added 4 commits
October 9, 2026 11:08
…CPU and one kernel thread per row on Metal, bit-identical to the Bend body
…ne falls back to the body The C lowering is now one function: eight rows side by side, each row its own chain of adds in base.bend's order, writing only the rows asked. The Metal kernel, its host dispatch and the scalar tail macro are gone; inside a bang the same loop runs on the device. comp.ts goes from 66,465 to 64,660 ttok (ttok 0.3; upstream 63,905). The collector now reads intrinsics per lane (intr_of(k, FL.js)), so a C-only intrinsic keeps its body on the JS lane. Before, the interpreter and JS builds failed with "$Array$q4mv$ is not defined". The emitted JS of every other test is byte-identical. tests/base/array_q4mv.bend checks 13 rows (a block of eight and a tail of five) and rows 3..8 alone against an independent float32 reference, on the interpreter, C and JS lanes.
…en a Metal heap is live, one thread per row, bit-identical to the Bend body
…and one Metal SIMD group per row give the Bend body's bits on every lane Lane j of a group sums its 8 nibbles in code-word order, the 8 lanes meet as ((0+4)+(2+6))+((1+5)+(3+7)), group terms go into 32 partials, and the partials fold 16, 8, 4, 2, 1. tests/base/array_q4mv_tree.bend pins the order.
This was referenced Oct 9, 2026
totaland
marked this pull request as ready for review
October 9, 2026 07:14
Collaborator
|
Thanks @totaland for the careful work and measurements on this stack. We're closing #1435, #1436 and #1437 for the same reasons as #1431:
One difference from #1431: there, plain Bend on |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1431 and #1435. Only the last commit is new here.
Why
Array.q4mv's body adds each row's terms in index order: one dependent F32 chain per row. The CPU loop can only go wide across rows, and the Metal kernel from #1435 needs one thread per row, which reads the weights strided. A faster kernel has to add in a different order. To keep the same bits on every lane, the Bend body has to define that order.Change
The order (T32), for row
iand groupg(64 weights, code wordsk = 0..7, nibblesj = 0..7):j:L_j = 0; for k in 0..7: L_j = L_j + q(k,j) * x[64g+8k+j]. Each mul and add is rounded; there is no FMA.qx = ((L0+L4)+(L2+L6)) + ((L1+L5)+(L3+L7)).t_g = scale_g*qx + bias_g*xsum_g.p[l] = 0.0 + t_l + t_{l+32} + ..., with groups in order.p[l] += p[l+o]foro = 16, 8, 4, 2, 1, then roundp[0]to BF16 (nearest even).bend2/base.bend: the body and its doc follow T32.bend2/comp.ts:q4_row) and a NEON loop that runs 4 rows at once (q4_neon4, under__ARM_NEON);lowns groupsl, l+32, …, and step 5 issimd_shuffle_down;tests/base/array_q4mv_tree.bend(new):The expected bits come from an independent C model of the order.
This changes results. After BF16 rounding, 1 of 6144, 1 of 2048 and 58 of 248320 rows differ from #1431's order on real weights.
array_q4mv.bend's expected lines do not change.Verification
Apple M1 Pro, macOS, GPU runs one at a time.
array_q4mv.bendandarray_q4mv_tree.bendpass on the interpreter, C--gpu off, C--gpu 8GBand JS.The new test catches the defects it should:
Kernel microbench. The Metal tree kernel, the NEON tree loop and the C reference are bit-identical at these shapes:
All 18 AB/BA runs exited bit-exact.
Whole model. In a Bend inference engine (TensorFold), Qwen3.5-2B (MLX 4-bit) gives bit-identical full logits on the CPU (NEON) and with
--gpu 8GB(248,320 of 248,320). Greedy token IDs match the old order.The 4096x2048 scratch program from Array.q4mv on Metal: one kernel thread per row for products of 4M weights or more, bit-identical (stacked on #1431; over comp.ts cap) #1435 prints the same hash on CPU and Metal (
1289093127). Six alternating rounds, milliseconds per call (400 calls, single-threaded host):--gpu off--gpu 8GBKernel microbench timings (best of 10 reps, 3 AB/BA pairs, against Array.q4mv on Metal: one kernel thread per row for products of 4M weights or more, bit-identical (stacked on #1431; over comp.ts cap) #1435's kernels):
On Metal each lane now reads 32 contiguous bytes, and the head reaches 78 GB/s.
The CPU gain differs between the two measurements: 2.2x in the 400-call program, but 1.06–1.17x in the microbench. I have not explained the difference. Treat the CPU gain as somewhere in that range. In the inference engine, CPU decode improved by about 4% (127 → 121.5 ms/step, 10 pairs), which is inside the noise.
60fa05dchas intests/baseandtests/iogive byte-identical interpreter, C and JS output on upstream and on Array.q4go/Array.q4wait queue Metal Q4 products in one command buffer and wait once on a flag (stacked; over comp.ts cap) #1437, the top of this stack. The only difference is the temp path in 21 JS build-error messages. 120 of 186 match their#|lines on both trees. The new q4 tests pass on all three lanes.uvx ttok@0.3 < bend2/comp.ts: 66,466 (#1435) → 67,215 (+749).Limits / open
comp.tscap, as Array.q4mv: affine 4-bit matvec primitive (CPU 3.2-3.6x, bit-identical; over comp.ts cap) #1431 is.