Measure the int8 multi-row headroom, and keep the four-bit guard that was protecting us - #29
Merged
Merged
Conversation
… was protecting us The int4 multi-row win reproduces and is stronger than published: 1.45x, 1.73x and 1.54x at rows 8, 16 and 32 through the prepared executor. The int8 half of this task does not exist, and finding that out was more instructive than the number. Why int4 wins is bandwidth, not batching. MLX re-reads weights per row tile, and at four bits that drops its effective weight-read bandwidth to 36 GB/s at sixteen rows against a measured 120 GB/s streaming ceiling -- a 3.4x gap, of which the kernel captures about half. At eight bits MLX runs 62 GB/s, a 1.9x gap. benchmarks/quantized_row_headroom.py measures this per width so the question "should we build a multi-row kernel for N bits" is answerable before anyone builds one. Effective bandwidth rather than time, because eight-bit weights are twice the bytes and a ratio of times cannot tell a slower kernel from a worse use of the memory system. Half of the eight-bit gap would be around 1.4x, which is worth wanting, so I tried it. The repacking generalises to eight bits in a few lines, the weights load, results came back and the kernel measured 1.6x to 2.0x faster than MLX. It was also wrong, at a relative error of 2.5 to 2.9, and the speed and the wrongness are the same fact: `lower_affine_matmul` emits NAX affine fragments with block_size=4 and takes no bit width, so it decoded eight nibbles per word where the data held four bytes, read half the values, and finished early. A large speedup arriving together with a wrong answer is one bug, not one win and one bug. Two things worth keeping from that. The four-bit check in MLXAffineWeight.from_mlx is load-bearing rather than a formatting preference, and I loosened it without checking that anything downstream honoured the parameter -- the bit width was threaded into cache keys but never into the lowering. It is restored, its message now says why, and a test pins it, because relaxing it looks harmless and the failure is silent. Only the tuner's agreement gate stopped the fast wrong kernel being selected, which is the gate doing exactly its job and not a reason to rely on it. The generalised repacking is reverted too. It was correct, but a correct-looking eight-bit path sitting beside a four-bit-only lowering is precisely the trap that just caught me. Supporting eight bits means teaching the lowering a bit width, which is real compiler work against a 1.4x prize, not a check to relax. 680 pass, 7 skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AndreSlavescu
force-pushed
the
int8-multirow-headroom
branch
from
July 30, 2026 04:08
02c6500 to
d33c891
Compare
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.
The int4 multi-row win reproduces and is stronger than published — 1.45x, 1.73x, 1.54x at rows 8,
16, 32 through the prepared executor. The int8 half of this task does not exist, and finding that
out was more instructive than the number.
Why int4 wins is bandwidth, not batching
MLX re-reads weights per row tile.
benchmarks/quantized_row_headroom.pymeasures the consequence perwidth, against a measured 120.6 GB/s streaming ceiling:
The gap columns bound what any kernel could win. int4 leaves 3.4x at sixteen rows and our kernel
captures about half. Effective bandwidth rather than time, because eight-bit weights are twice the
bytes and a ratio of times can't tell a slower kernel from a worse use of the memory system.
I tried the int8 kernel, and the speedup was the bug
Half of the int8 gap would be ~1.4x, worth wanting. The repacking generalises to eight bits in a few
lines, the weights load, and the kernel measured 1.6x to 2.0x faster than MLX.
It was also wrong, at relative error 2.5 to 2.9 — and the speed and the wrongness are the same
fact.
lower_affine_matmulemits NAX affine fragments withblock_size=4and takes no bit width, soit decoded eight nibbles per word where the data held four bytes, read half the values, and finished
early. A large speedup arriving together with a wrong answer is one bug, not one win and one bug.
What I got wrong, and what it cost
The four-bit check in
MLXAffineWeight.from_mlxis load-bearing, not a formatting preference, andI loosened it without checking that anything downstream honoured the parameter — the bit width was
threaded into cache keys but never into the lowering. It's restored, its message now says why, and a
test pins it, because relaxing it looks harmless and the failure is silent.
Only the tuner's agreement gate stopped the fast wrong kernel being selected. That's the gate doing
its job, not a reason to rely on it.
The generalised repacking is reverted too. It was correct, but a correct-looking eight-bit path sitting
beside a four-bit-only lowering is precisely the trap that just caught me.
Supporting eight bits means teaching the lowering a bit width — real compiler work against a 1.4x
prize, not a check to relax.
680 pass, 7 skipped.
🤖 Generated with Claude Code