Skip to content

apple: implement binary opcodes 9-22 in the portable stub; fail closed on unknown ones - #592

Merged
gstoner merged 2 commits into
mainfrom
claude/apple-stub-binary-opcodes
Aug 19, 2026
Merged

gstoner merged 2 commits into
mainfrom
claude/apple-stub-binary-opcodes

Conversation

@gstoner

@gstoner gstoner commented Aug 19, 2026

Copy link
Copy Markdown
Owner

The defect

apple_gpu_runtime_stub.cpp — compiled on every non-Darwin host so the C symbol exists — implemented opcodes 0–8 and ended its switch with:

default: out[i] = x; break;   // returns the LEFT operand

_APPLE_GPU_BINARY_OPCODES runs to 22. So 14 of 23 opcodes returned operand A instead of computing, on CI and on the Strix Halo box:

mod(9) floor_div(10) eq(11) ne(12) lt(13) le(14) gt(15) ge(16)
logical_and(17) logical_or(18) logical_xor(19)
bitwise_and(20) bitwise_or(21) bitwise_xor(22)

This was not a visible envelope miss. _load_apple_gpu_runtime compiles the stub off Darwin precisely "so the symbol still exists with a portable reference path" — and because the symbol then exists, _apple_gpu_dispatch_mpsgraph_binary takes the kernel branch instead of its numpy fallback. A wrong value came back as if computed, with no diagnostic. Decision #21: a lowering the backend cannot carry must diagnose, never silently fall through.

Observed directly on CI (run 32278268687): tessera.floor_div with {"scalar": 2.0} returned [1,2,4] where the truth is [0,1,2].

Pre-existing on main. Surfaced by #589, which was the first thing to exercise those opcodes on Linux.

The fix

Opcodes 9–22 implemented, matching mpsg_binary_node in apple_gpu_runtime.mm and the declared host reference runtime._apple_gpu_binary_numpy — which were already in agreement with each other. The subtleties that matter:

  • mod and floor_div follow numpy (sign of the divisor), not C fmod/truncation
  • comparisons and logical ops produce f32 0/1 masks
  • bitwise ops go through int32, whose truncation toward zero matches both .astype(np.int32) and MPSGraph's castTensor:toType:MPSDataTypeInt32

Unknown opcodes now fail closed. Rejected before the output is touched, reported through the stub's last-error channel, and the buffer poisoned with NaN. That channel was three fixed no-op parity symbols — which is what left the stub unable to say it hadn't computed anything. It now carries real thread_local state mirroring the .mm's g_last_gpu_error_kind pattern, with a new kind 3 = "not implemented by the portable stub" (1 = timeout and 2 = command-buffer error are Metal-only and unreachable here). _apple_gpu_run_checked reads that channel and routes to the host fallback, so an unimplemented opcode now yields the correct answer via numpy rather than a plausible wrong one.

The guard is a separate predicate rather than a default: arm, so adding a case to the switch without adding it to the guard fails closed.

Testing something this host cannot execute

A Mac loads the real Metal symbol and never reaches the stub — which is exactly why the existing coverage couldn't catch this. tests/unit/test_apple_gpu_binary_opcodes.py layers four kinds of evidence:

Layer Runs where Catches
Dispatcher vs numpy reference, both lanes everywhere dispatch-level regressions
Structural gates on the stub source any host, incl. this Mac a newly declared opcode missing from the stub; a default: arm that returns an operand
Extracted + compiled + executed stub any host with a C++ compiler the stub's real semantics — the only layer that does
Kernel-reported-error fallback everywhere a failing kernel's buffer being believed

The compiled layer slices the opcode switch and error channel verbatim out of the real file rather than retyping them, so the tested text cannot drift from the shipped text. It compiles them with the host compiler and runs all 23 opcodes plus silu_mul and an unknown opcode against the numpy reference. It skips when no C++ compiler is present.

Inputs include a negative numerator and a zero — chosen to separate the implementations, not merely exercise them: they distinguish floor-mod from C fmod, floor-division from truncation, and the logical ops from the bitwise ones.

On the pre-fix stub the structural gates fail and the compiled lane cannot find its anchors.

Verification

Mac (M1 Max). Extracted stub compiles; all 23 opcodes match the numpy reference exactly, silu_mul matches, and opcode 23 reports kind 3 with NaN output.

  • tests/unit/test_apple_gpu_binary_opcodes.py — 74 passed
  • Full unit sweep: 14065 passed, 48 failed — failure set identical to the pre-existing baseline (all 48 the missing Apple runtime dylib in this worktree), zero new
  • mypy clean over 467 files; ruff clean; generated-doc drift gate in sync

Limit of this evidence: the in-context Linux compile of the whole TU is proved by CI's build job, not by this host — -U__APPLE__ breaks libc++ on macOS, so the TU cannot be compiled here in its non-Darwin configuration. The extracted-region compile is the closest local proxy.

Cross-backend assessment (AGENTS.md)

Sync key APPLE-STUB-BINARY-OPCODES-2026-08-19, recorded in all four plans:

Backend Outcome Basis
apple parity validated off-device Metal was always correct — mpsg_binary_node implements the full table — so no exact-device row, .metallib, or dylib evidence changes or is re-claimed. What changes is that the portable path now agrees with Metal instead of silently disagreeing on 14 of 23 opcodes.
nvidia not applicable tessera_apple_gpu_mpsgraph_binary_f32 is referenced only by Apple files; no NVIDIA path reaches it.
rocm not applicable The gfx1151 binary lane is separate code (_execute_rocm_compiled_binary over a compiler-generated hsaco) and binds both operands positionally, raising rather than falling through. Checked for the same defect shape — it has no silent-default arm.
x86 not applicable today, one follow-up recorded See below.

x86 follow-up (verified, not assumed)

The AVX-512 binary kernel carries the same shape: avx512_binary_f32.cpp ends scalar_binary with default: return a; and the vector loop with default: y = a; break;.

It is latent, not live: the kernel implements kinds 0–7 (kSub/kDiv/kMax/kMin/kAdd/kMul/kMod/kFloorDiv) and _X86_BINARY_OPS maps exactly onto that range, so no reachable call hits the default today. It becomes live the moment an opcode is added on one side only — which is precisely how the Apple defect arose. Fixing it needs AVX-512 evidence from the Zen 5 box, so it belongs in an x86 change, not here.

No device evidence is produced or claimed for nvidia, rocm, or x86.

🤖 Generated with Claude Code

gstoner and others added 2 commits August 19, 2026 15:06
…d on unknown ones

`apple_gpu_runtime_stub.cpp` — compiled on every NON-Darwin host so the C symbol
exists — implemented opcodes 0-8 and ended its switch with:

    default: out[i] = x; break;

So `mod`(9), `floor_div`(10), the six comparisons(11-16), and the
logical/bitwise ops(17-22) returned the LEFT operand instead of computing:
14 of the 23 opcodes in `_APPLE_GPU_BINARY_OPCODES`, on CI and on the Strix Halo
box.

It was not a visible envelope miss. `_load_apple_gpu_runtime` compiles this stub
off Darwin precisely "so the symbol still exists with a portable reference
path", and because the symbol then exists,
`_apple_gpu_dispatch_mpsgraph_binary` takes the kernel branch instead of its
numpy fallback — so a wrong value came back as if it had been computed, with no
diagnostic. Observed on CI as `tessera.floor_div` with `{"scalar": 2.0}`
returning `[1,2,4]` where the truth is `[0,1,2]`. Decision #21: a lowering the
backend cannot carry must diagnose, never silently fall through.

Two changes:

* Opcodes 9-22 implemented to match `mpsg_binary_node` in
  apple_gpu_runtime.mm and the declared host reference
  `runtime._apple_gpu_binary_numpy`, which were already in agreement. Floor-mod
  and floor-division follow numpy (sign of the divisor), NOT C `fmod`/
  truncation; comparisons and logical ops produce f32 0/1 masks; bitwise ops go
  through int32, whose truncation toward zero matches both `.astype(np.int32)`
  and MPSGraph's `castTensor:toType:MPSDataTypeInt32`.
* An unknown opcode is now rejected BEFORE the output is touched, reported
  through the stub's last-error channel, and the buffer poisoned with NaN. The
  channel was three fixed no-op parity symbols; it now carries real
  thread_local state mirroring the .mm's `g_last_gpu_error_kind` pattern, with
  a new kind 3 = "not implemented by the portable stub" (1 = timeout and
  2 = command-buffer error are Metal-only). `_apple_gpu_run_checked` reads that
  channel and routes to the host fallback, so an unimplemented opcode now
  produces the correct answer via numpy rather than a plausible wrong one.

The guard is a separate predicate rather than a `default:` arm so that adding a
case to the switch without adding it to the guard fails closed.

Testing this is awkward: a Mac loads the real Metal symbol and never executes
the stub, so the previous coverage could not have caught it.
`tests/unit/test_apple_gpu_binary_opcodes.py` layers four kinds of evidence:

* the dispatcher against the numpy reference for every opcode, on both lanes;
* structural drift gates over the stub source, which run on ANY host — a newly
  declared opcode missing from the stub, or a `default:` arm that returns an
  operand, fails there even on a machine that cannot execute the stub;
* the stub's opcode switch and error channel extracted VERBATIM (sliced from
  the file, never retyped, so the tested text cannot drift from the shipped
  text), compiled with the host C++ compiler and RUN against the numpy
  reference for all 23 opcodes plus silu_mul and an unknown opcode. This is the
  only coverage that executes the stub's real semantics. It skips when no C++
  compiler is present;
* a kernel that reports failure through the last-error channel must route to
  the host reference rather than have its buffer believed.

Inputs include a negative numerator and a zero, which separate floor-mod from
C `fmod`, floor-division from truncation, and the logical ops from the bitwise
ones. On the pre-fix stub the structural gates fail and the compiled lane
cannot find its anchors.

Verified on the Mac (M1 Max): the extracted stub compiles and all 23 opcodes
match the numpy reference exactly, silu_mul matches, and opcode 23 reports
kind 3 with NaN output. Full unit sweep: 14065 passed, 48 failed — failure set
identical to the pre-existing baseline (all 48 the missing Apple runtime
dylib), zero new. mypy clean over 467 files; ruff clean; generated-doc drift
gate in sync. The in-context Linux compile of the whole TU is proved by CI's
build job, not by this host.

Cross-backend assessment (AGENTS.md), sync key
`APPLE-STUB-BINARY-OPCODES-2026-08-19`:

* apple  — parity validated off-device; Metal was always correct, so no
  exact-device row, .metallib, or dylib evidence changes or is re-claimed.
* nvidia — not applicable; the symbol is referenced only by Apple files.
* rocm   — not applicable; the gfx1151 binary lane is separate code and binds
  both operands positionally, raising rather than falling through.
* x86    — not applicable today, one follow-up recorded: the AVX-512 binary
  kernel carries the SAME shape (`default: return a;` and `default: y = a;`),
  currently unreachable because it implements kinds 0-7 and `_X86_BINARY_OPS`
  maps exactly onto that range. It becomes live the moment an opcode is added
  on one side only — which is exactly how this defect arose. Fixing it needs
  AVX-512 evidence from the Zen 5 box.

No device evidence is produced or claimed for nvidia, rocm, or x86.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ary-opcodes

# Conflicts:
#	docs/audit/backend/apple/todo.md
#	docs/audit/backend/nvidia/todo.md
#	docs/audit/backend/rocm/todo.md
#	docs/audit/backend/x86/todo.md
#	docs/audit/generated/test_coverage.md
@gstoner
gstoner merged commit 0fd88cf into main Aug 19, 2026
24 of 26 checks passed
@gstoner
gstoner deleted the claude/apple-stub-binary-opcodes branch August 23, 2026 11:44
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