Repository navigation
Conversation
added 5 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.
…fer and wait once, on a flag kernel Array.q4go is Array.q4mv with the same body, so the interpreter and JS run it unchanged. In a native build with a Metal heap it encodes the product into one open concurrent command buffer. Array.q4wait commits the buffer, ends it with a barrier and a 1-thread kernel that stores a sequence number into shared memory, and spins on it (falling back on the buffer status). BEND_Q4_TRACE=1 prints each wait's products, GPU time and wall time.
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, #1435 and #1436. Only the last commit is new here.
Why
Each GPU
Array.q4mvcommits its own command buffer and waits for it. That costs about 0.23–0.27 ms of host time per product, whatever the size. One decode step of a 2B-parameter 4-bit model has 187 products in 97 dependent stages:Change
bend2/base.bend:Array.q4go(w, x, y, r, n, cols, rows)has the same body asArray.q4mv, so the interpreter and JS run it unchanged.Array.q4wait(x)isx.bend2/comp.ts:q4go: in a native build with a Metal heap,Array.q4goencodes a product of any size into one open, concurrent command buffer and returns at once. Without a heap it runsArray.q4mv.q4wait:Array.q4waitcommits the buffer, ends it with a barrier and a 1-thread kernel that stores a sequence number in shared memory, and spins on that number. It falls back on the buffer's status. The flag saves about 0.27 ms per wait overwaitUntilCompleted.Array.q4goloads its Metal library without needing a bang.BEND_Q4_TRACE=1prints each wait's product count, GPU time and wall time.tests/base/array_q4go.bend(new):array_q4mv.bend's 13x128 case through the queue. On the C--gpu 8GBlane it really queues, becauseq4gohas no size threshold.Queued products run concurrently, so none may read another's
y. Put a wait between dependent stages. The GPU readswandxand writesyuntil the wait, and Bend frees a value at its last use. So wait onx, and usewandyafter the wait.Verification
Apple M1 Pro, macOS, GPU runs one at a time.
array_q4mv.bend,array_q4mv_tree.bendandarray_q4go.bendpass on the interpreter, C--gpu off, C--gpu 8GBand JS.Array.q4waitreturns without waiting dies withbend: memory fault;wbefore the wait got a wrong row 0 every time. The runtime had freedwand written its free-list link into word 0, which the GPU then read. The doc now states the lifetime rule.q4go+q4waitper call, prints the same hash (1289093127) asq4mvon CPU and Metal. Six alternating rounds: 0.25 ms/call, against 0.36 forq4mv's own command buffer. These runs are noisy.waitUntilCompleted: 50.5–53.8 ms/token;q4go, then waits once per stage before reading. Qwen3.5-2B (MLX 4-bit) greedy token IDs and logits equal the CPU run over 40 tokens.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: 67,215 (#1436) → 68,289 (+1,074).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.waitUntilCompleted, at about 0.27 ms per wait.wearly and the GPU reads freed memory. A safer API would take the whole((w, x), y)at the wait.