Skip to content

Decode and encode G17 instructions, verified by making the GPU compute a prediction - #22

Merged
AndreSlavescu merged 2 commits into
mainfrom
agx-isa-encoder
Jul 30, 2026
Merged

Decode and encode G17 instructions, verified by making the GPU compute a prediction#22
AndreSlavescu merged 2 commits into
mainfrom
agx-isa-encoder

Conversation

@AndreSlavescu

Copy link
Copy Markdown
Owner

The instruction set is undocumented, so the method is the substance. Reading bytes and
inferring field layouts produces plausible maps that are wrong, so every claim here was instead
put to the GPU: patch the machine code inside a binary archive, run it, and check the answer
against a number worked out beforehand.

metile/target/agx_execute.swift runs a kernel from an archive with failOnBinaryArchiveMiss,
which makes the archive the only permitted source of machine code. Without that flag the driver
quietly recompiles the MSL
, a patched kernel and an unpatched one agree, and the experiment
proves nothing.

What that establishes

Edited code runs. The driver executes an edited __text and does not re-derive it from the
AIR it also carries.

Boundaries, found behaviourally. Nopping a byte range and running it locates instruction
starts. On four dependent a = fma(a, 2, 1) steps, only 0x5a, 0x62, 0x6a, 0x72 return 15
instead of 31 — exactly one fma removed at each. Four instructions, eight-byte stride. Other
alignments return 0, 1, 7 or 225: they run, and lie, which is why the test asserts the value
rather than that the kernel survived.

The immediate field is (e << 4) | (m << 1) | low, holding (1 + m/8) · 2^(e−11).

value byte value byte
2.0 0xc1 1.0 0xb0
3.0 0xc9 3.0 0xc8
4.0 0xd1 5.0 0xd4
8.0 0xe1

And it encodes, not just decodes — the part that distinguishes understanding from a lookup
table. Predictions made before assembling the bytes, for constants no Metal compiler produced:

rewrite predicted measured
3 fmas → a*6+7 949 949
3 fmas → a*1.25+1.5 11.578125 11.578125
1 fma → a*6+7 103 103

Field map for the compact fma

Each byte established by patching it and running: byte 0 opcode (low nibble 9); byte 2 operand
mode plus a last-instruction flag — 0x03 there turns a*2+1 into a*a, giving 225 from an
accumulator holding 15
; bit 0x20 of byte 6 disables the instruction; bytes 1 and 7 select
registers. Byte 4 was not probed, and says so.

Deliberately not claimed

No general disassembler — one form is mapped and decode reports the rest as unknown rather than
inventing mnemonics. And negative immediates have no known encoding in this field, so
encode_immediate refuses them instead of returning the nearest byte, which would corrupt a
kernel silently.

Two of my own errors, since both looked like the encoding failing

  1. A confirmed boundary is not a known form. All four fmas sit on an eight-byte stride, but
    the compiler encodes the first differently, and reading the constant field there returned
    0.000488. Being a boundary is measured; being the mapped form is what the opcode nibble
    decides. The probe now separates the two.
  2. The first prediction missed — 97 against a measured 103 — and the encoding was right. The
    patched instruction was the second fma of four, not the last. Predictions now replay the
    arithmetic from the actual instruction positions.

benchmarks/agx_isa_probe.py re-derives all four stages from scratch and exits non-zero if any
prediction misses, which is how to port this to different hardware or a newer toolchain.

624 pass. Lint and vulture clean.

🤖 Generated with Claude Code

AndreSlavescu and others added 2 commits July 29, 2026 18:52
…e a prediction

The instruction set is undocumented, so the method is the substance here. Reading bytes and
inferring field layouts produces plausible maps that are wrong, so every claim was instead put
to the GPU: patch the machine code inside a binary archive, run it, and check the answer
against a number worked out beforehand.

metile/target/agx_execute.swift runs a kernel from an archive with failOnBinaryArchiveMiss,
which makes the archive the only permitted source of machine code. Without that flag the
driver quietly recompiles the MSL, a patched kernel and an unpatched one agree, and the
experiment proves nothing.

What that establishes, in order:

  edited code runs   the driver executes an edited __text and does not re-derive it from the
                     AIR it also carries.

  boundaries         nopping a byte range and running it locates instruction starts
                     behaviourally. On four dependent `a = fma(a, 2, 1)` steps only 0x5a,
                     0x62, 0x6a and 0x72 return 15 instead of 31, so exactly one fma went at
                     each: four instructions, eight-byte stride. Other alignments return 0, 1,
                     7 or 225 -- they run, and lie, which is why the test asserts the value
                     rather than that the kernel survived.

  immediates         the eight-bit float operand field is (e << 4) | (m << 1) | low, holding
                     (1 + m/8) * 2**(e - 11). It reads all six constants the compiler was
                     observed to emit.

  encoding           and, the part that makes it an encoder rather than a lookup table, it
                     predicts results for constants no Metal compiler produced. Rewriting the
                     three compact fmas to a*6+7 gives 949 from x=1; to a*1.25+1.5 gives
                     11.578125. Both predicted before assembling the bytes, both exact.

Also mapped, each by patching that byte and running: byte 0 is the opcode, low nibble 9; byte
2 selects operand mode and flags the last instruction of a run, and 0x03 there turns a*2+1
into a*a, giving 225 from an accumulator holding 15; bit 0x20 of byte 6 disables the
instruction; bytes 1 and 7 select registers. Byte 4 was not probed and says so.

Two things are deliberately not claimed. There is no general disassembler: one instruction
form is mapped and `decode` reports the rest as unknown instead of inventing mnemonics. And
negative immediates have no known encoding in this field, so encode_immediate refuses them
rather than returning the nearest byte, which would corrupt a kernel silently.

Two of my own errors are worth recording because both looked like the encoding failing:

  A confirmed boundary is not a known form. All four fmas sit on an eight-byte stride, but the
  compiler encodes the first differently, and reading the constant field there returned
  0.000488. Being a boundary is measured; being the mapped form is what the opcode nibble
  decides, and the probe now separates them.

  The first prediction missed at 97 against a measured 103, and the encoding was right -- the
  patched instruction was the second fma of four, not the last. Predictions now replay the
  arithmetic from the actual instruction positions.

benchmarks/agx_isa_probe.py re-derives all four stages from scratch and exits non-zero if any
prediction misses, which is how to port this to different hardware or a newer toolchain.

624 pass. Lint and vulture clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… data

Three bits of the compact fma turn out to control its arithmetic, so a compiled kernel's
operations can be rewritten and not merely its constants:

    byte 2 bit 0x10   negate the product      -a*m + d
    byte 4 bit 0x10   negate the addend        a*m - d
    byte 4 bit 0x20   include the addend       clear it and a plain multiply is left
    byte 6 bit 0x20   retire the instruction   indistinguishable from nopping it

Each was found by scanning all 256 values of its byte and grouping outputs by what arithmetic
they expressed, then confirmed the only way that counts: set the bit across a chain of three
instructions and predict the result for four different inputs before running. Twelve
predictions, all exact. Four inputs rather than one because negating the product and negating
the addend both move the result by an even amount, so a single input can agree by coincidence.

The rest of byte 2 and byte 6 is reported honestly rather than named. Bit 0x01 of byte 2 makes
`a*2+1` compute `a*a` — 225 from an accumulator holding 15 — but which operand slot is being
redirected was never pinned down and it was never checked on more than one input, so it stays a
comment. Naming it would put it on the same footing as bits that were predicted four times.

The other half of this commit is a theory withdrawn. Instruction length looked like the low
nibble of byte 0, and the case for it was good: nop is 0x06 and two bytes, the block terminator
0x0e and four, the compact fma 0x09 and eight, and giving an eight-byte instruction the nop's
nibble is the single edit the driver rejects outright rather than running with a wrong answer,
which is exactly what desynchronising the stream would do. A table extended to fit one kernel
walked it end to end and covered all four behaviourally confirmed fma boundaries.

It then walked none of eight other kernels exactly -- copy, two fma chains, a reduction loop,
integer and half mixes, a branch, a sqrt -- five overrunning the end of the stream. Sixteen free
values fitted to one 134-byte kernel is not evidence, and 4/4 on the kernel it was fitted to was
coincidence. The table is gone and the failure is recorded in its place, because the next person
to look at those bytes will have the same good idea.

Lengths continue to come from the behavioural finder one form at a time, which cannot be fooled
that way: a wrong boundary produces a running kernel with a wrong answer, so the test asserts
the value rather than that the kernel survived.

629 pass. Lint and vulture clean; the probe re-derives all five stages and exits zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AndreSlavescu
AndreSlavescu merged commit 603b7aa into main Jul 30, 2026
2 checks passed
@AndreSlavescu
AndreSlavescu deleted the agx-isa-encoder branch July 30, 2026 01:59
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