Repository navigation
Conversation
added 3 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
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. Only the last commit is new here; the first is #1431's.
Why
#1431 adds
Array.q4mvon the CPU. An earlier revision of it also had a Metal kernel, which I removed to keepcomp.tssmall. Without it, a program that runs on Metal still does every 4-bit product on the host. For a 2B-parameter 4-bit model, the output head alone (248320x2048) costs about 30 ms per decode step on 8 CPU threads.Change
bend2/comp.tsonly.n * cols >= 2^22weights, the host hands it to a new kernel,bend_q4mv, instead of the CPU loop.Verification
Apple M1 Pro, macOS, GPU runs one at a time.
tests/base/array_q4mv.bendpasses on the interpreter, C--gpu off, C--gpu 8GBand JS. Its products are below the threshold, so the Metal lane still runs the CPU loop there.A product above the threshold. A scratch program builds a 4096x2048 product (8.4M weights), with a dead bang so the build carries Metal. It runs
Array.q4mv400 times and prints a hash ofy's bits. The hash is1289093127on Array.q4mv: affine 4-bit matvec primitive (CPU 3.2-3.6x, bit-identical; over comp.ts cap) #1431 (CPU and--gpu 8GB) and on this branch (CPU and--gpu 8GB). Six alternating rounds, milliseconds per call (the 400-call run minus the 1-call run, divided by 400):--gpu off--gpu 8GBThe Metal time includes a command buffer and a wait per call.
Model head. In a microbench, the 248320x2048 head takes 5.44 ms on this kernel. In a Bend inference engine (TensorFold), the same head takes 30.7 ms on the CPU with 8 threads.
Whole model. In that engine, Qwen3.5-2B (MLX 4-bit) greedy decode with this kernel gives the same token IDs and printed logits as the CPU. On its own it does not speed up decode:
--gpu 8GBhad a median of 127.5 ms/step over 6 pairs, and the CPU 127 in separate runs. Most products are below the threshold, and every GPU product waits on its own. Array.q4mv sums in a fixed tree order, so NEON and one Metal SIMD group per row keep the body's bits (stacked; over comp.ts cap) #1436 and Array.q4go/Array.q4wait queue Metal Q4 products in one command buffer and wait once on a flag (stacked; over comp.ts cap) #1437 build on this.Regression. All 186 tests that upstream
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: 64,660 (#1431) → 66,466 (+1,806).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. See the question on Array.q4mv: affine 4-bit matvec primitive (CPU 3.2-3.6x, bit-identical; over comp.ts cap) #1431.