Optimise machine code where reordering survives - #24
Merged
Conversation
meTile's IR scheduler is measured to be inert: Apple's backend rebuilds the schedule from the
dataflow, so two source orders compile to byte-identical instructions. That is not a shortcoming
of the pass, it is where the boundary of control sits when the output is MSL. This pass is on the
other side of it -- it reads the instructions the backend produced, rewrites them, and puts them
back, so nothing downstream re-derives anything.
The register field is what makes it possible. A compact fma names its register in byte 0's high
nibble and again as (r << 1) | 1 in byte 1, so which instructions depend on each other is
readable rather than guessed: this form reads and writes exactly one register, so two fmas on
different registers are independent and two on the same one are not. That is the entire
dependence relation.
simplify retires instructions computing nothing, via the flag verified to be
indistinguishable from nopping.
reorder moves independent instructions, preserving every register dependence.
Both are bit-exact by construction, and both are checked that way rather than argued. On three
chains over three registers, reordering moves seven of eight instructions and the GPU returns the
same three values; retiring a planted identity changes nothing either. An unsound rewrite here
would produce a kernel that compiles, dispatches and returns a wrong number, so a structural
check would not be enough.
Instruction-level parallelism is the one job this level cannot finish, and it is worth naming
rather than omitting. Splitting a dependent chain across registers is within reach, since the
register field is known and half a chain can be retargeted, but the two partial results then need
adding together and a register-plus-register add is not among the decoded forms -- every operand
slot mapped so far takes an immediate. Half a transformation leaves a kernel computing half an
answer. So the ILP half stays in metile.compiler.scheduling at the IR level, where reassociation
needs no new instruction, and `optimize` is the one entry point that names both.
Two things the tests caught in my own code. `is_identity` checked for `a * 1 + 0`, which cannot
exist: the immediate field holds (1 + m/8) * 2**(e - 11), whose smallest value is 2**-11, so
there is no encoding for zero and a decoded addend is never it. The branch was unreachable. And
the run-continuation bit is positional rather than a property of the operation, so moving an
instruction to or from the end of a run has to recompute it.
Scope is narrow and enforces itself: only the compact f32 fma is decoded, and any byte that is
not part of a decoded instruction is a barrier. Not caution for its own sake -- instruction
lengths cannot be recovered in general, since the length-field theory failed on eight of eight
kernels, so an undecoded region might be one instruction or twenty.
9 tests, 646 in total. Lint clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the addend-flag correction. The slot holds either an immediate or a register, so `decode` now reads both and `Fma` carries both, and re-encoding a decoded instruction reproduces it for every form -- immediate, negative immediate, register, zero, negated product, run-final -- which a new round-trip test asserts, because a rewrite that does not round-trip changes the kernel while looking like it preserved it. The correction bought a safety property this pass needed. `is_identity` decided on the addend being absent, and under the old reading `a * 1` with anything in the slot looked like a no-op. It is not: an fma whose addend names a live register adds that register, and retiring it drops a real term. Deciding it now needs the register index, since only an index beyond the reachable range reads zero. A test pins both directions. The ILP account changes too, and it is worth being precise since the blocker has moved once already. It is no longer the combine: `rd * 1 + rs` is a register-plus-register add, verified across eighteen register pairs, so summing two partial chains is expressible. What is missing is a register to put the second chain in. Splitting a chain needs one nothing else uses, and proving that means reading every instruction in the kernel, because a register touched only by an instruction this file cannot decode is indistinguishable from a free one. Instruction lengths cannot be recovered in general, so the stream cannot be walked, so no register can be shown free. Guessing corrupts whatever lived there, silently. 11 tests here, 38 across the ISA and this pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI caught what this machine cannot. A device that will not serialize a binary archive raised
RuntimeError, so every machine-code test failed on the runner instead of opting out:
RuntimeError: Error Domain=MTLBinaryArchiveDomain Code=1
"The binary archive contains no items eligible to be serialized"
That is a capability the machine lacks, exactly like a missing swiftc, so it now raises
Unavailable and the tests skip. Both places that invoke the prober make the same distinction, and
the conversion was checked by driving the failure through a stubbed subprocess rather than trusted.
Also corrects two passages left describing the addend flag as "include the addend or not" after the
flag itself was corrected. Documentation contradicting the code it documents is worse than none,
because it is the version read first.
651 pass; the ISA and machine-pass suites are 45 between them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AndreSlavescu
force-pushed
the
agx-machine-level-pass
branch
from
July 30, 2026 02:58
5597f79 to
50bc96e
Compare
The machine-level pass now has its instruction-level-parallelism transform, and it took two attempts to find one this level can express. Splitting a dependent chain across registers is the obvious approach and it does not work here. It needs a register nothing else uses, and no register can be shown free by decoding: one touched only by an instruction this file cannot read is indistinguishable from an unused one, and instruction lengths cannot be recovered in general so the stream cannot be walked. The metadata's register count would settle that, but the transform still wants the chain to be a sum, and the only chains decodable here are multiply-accumulate. Collapsing needs neither a register nor a new instruction. k steps of `a*m + d` equal one `a*m**k + d*(m**(k-1) + ... + 1)`, so three instructions become one and a dependent chain of three becomes none. Instead of shortening the chain, remove it. Three fmas of a*2+1 fold to a*8+7 and the GPU returns the same four values as the untouched kernel. It self-limits in two ways that are tested rather than asserted in a comment. Six steps of a*2+1 close to a*64 + 63, and 63 is not representable in the immediate field, so the fold is declined instead of rounded -- approximating would change the result by more than reassociating does. And a run whose addend names a register is declined outright, because a register's value can change between the steps and the closed form does not hold. Off by default, like its IR counterpart, because collapsing reassociates and the model tests assert bit-exact logits. Whether retiring the folded instructions saves time is measurable and not claimed here; what it certainly removes is the chain. Also improves one assertion message. The boundary scan skips any offset whose patched kernel fails to dispatch, which is right for genuine rejections but also happens to valid offsets under heavy concurrent GPU load. That surfaced as a confusing "expected four fmas" failure while a model matrix was running, so the message now says so. 15 tests here, 655 in total. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
meTile's IR scheduler is measured to be inert — Apple's backend rebuilds the schedule from the
dataflow, so two source orders compile to byte-identical instructions. That's not a shortcoming of
the pass; it's where the boundary of control sits when the output is MSL. This pass is on the
other side of it: it reads the instructions the backend produced, rewrites them, and puts them
back, so nothing downstream re-derives anything.
Why it's possible now
The register field. A compact fma names its register in byte 0's high nibble and again as
(r << 1) | 1in byte 1, so which instructions depend on each other is readable rather thanguessed. This form reads and writes exactly one register, so two fmas on different registers are
independent and two on the same one are not — that is the entire dependence relation.
simplifyreorderBoth bit-exact by construction, and checked that way rather than argued. On three chains over
three registers, reordering moves 7 of 8 instructions and the GPU returns the same three values;
retiring a planted identity changes nothing either. An unsound rewrite here produces a kernel that
compiles, dispatches, and returns a wrong number — a structural check would not catch it.
The one job this level cannot finish
Worth naming rather than omitting. Splitting a dependent chain across registers is within reach —
the register field is known, half a chain can be retargeted — but the two partial results then need
adding together, and a register-plus-register add is not among the decoded forms; every operand
slot mapped so far takes an immediate. Half a transformation leaves a kernel computing half an
answer. So the ILP half stays in
metile.compiler.schedulingat the IR level, where reassociationneeds no new instruction, and
optimizeis the one entry point naming both.Two things the tests caught in my own code
is_identitychecked fora * 1 + 0, which cannot exist: the immediate field holds(1 + m/8) · 2^(e−11), smallest value2^−11, so there is no encoding for zero and a decodedaddend is never it. The branch was unreachable.
instruction to or from the end of a run has to recompute it.
Scope enforces itself
Only the compact f32 fma is decoded, and any byte that is not part of a decoded instruction is a
barrier. Not caution for its own sake — instruction lengths cannot be recovered in general, since
the length-field theory failed on 8 of 8 kernels, so an undecoded region might be one instruction
or twenty.
9 tests, 646 in total. Lint clean.
🤖 Generated with Claude Code