Skip to content

Optimise machine code where reordering survives - #24

Merged
AndreSlavescu merged 5 commits into
mainfrom
agx-machine-level-pass
Jul 30, 2026
Merged

Optimise machine code where reordering survives#24
AndreSlavescu merged 5 commits into
mainfrom
agx-machine-level-pass

Conversation

@AndreSlavescu

Copy link
Copy Markdown
Owner

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) | 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.

pass what it does
simplify retires instructions computing nothing, via the flag verified indistinguishable from nopping
reorder moves independent instructions, preserving every register dependence

Both 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.scheduling at the IR level, where reassociation
needs no new instruction, and optimize is the one entry point naming both.

Two things the tests caught in my own code

  1. is_identity checked for a * 1 + 0, which cannot exist: the immediate field holds
    (1 + m/8) · 2^(e−11), smallest value 2^−11, so there is no encoding for zero and a decoded
    addend is never it. The branch was unreachable.
  2. The run-continuation bit is positional, not a property of the operation, so moving an
    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

AndreSlavescu and others added 4 commits July 29, 2026 19:46
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
AndreSlavescu force-pushed the agx-machine-level-pass branch from 5597f79 to 50bc96e Compare July 30, 2026 02:58
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>
@AndreSlavescu
AndreSlavescu merged commit 1f13f51 into main Jul 30, 2026
4 checks passed
@AndreSlavescu
AndreSlavescu deleted the agx-machine-level-pass branch July 30, 2026 03:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant