Skip to content

contract: an XPU device runner (gates 1 to 4 on a B70, gate 5 live, planted controls) - #2269

Merged
ashhart merged 10 commits into
ashhart:mainfrom
tolewis:xpu-contract-runner
Nov 7, 2026
Merged

ashhart merged 10 commits into
ashhart:mainfrom
tolewis:xpu-contract-runner

Conversation

@tolewis

@tolewis tolewis commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

XPU kernels now run gates 1 to 4 of the kernel contract on the card. tf-xpu-contract-run opens an Arc Pro B70 through Level Zero. It runs nine registrations of the Qwen3.8-27B glue and five planted gate controls, then runs gate 5 live against the card's own properties. This follows the HIP runner's shape.

The B70 run, exit 0:

SUMMARY bmg-g31 0xe223 9 kernels: 9 pass gates 1-4, 0 fail, 0 known findings; 5 controls: 5 red as planted, 0 missed; 0 limit problems; gates 7955 ms, limits 0 ms

Each planted control going red, from the same run:

FAIL nonfinite control: fmax row max: 1 of 8 outputs differ in class; first at 0: finite where float64 gives nan
CONTROL control: fmax row max: red at nonfinite, as planted
FAIL repeat control: atomic-add row sum: 4 of 4 repeats differ from the first run's bytes, first at byte 12
CONTROL control: atomic-add row sum: red at repeat, as planted
FAIL repeat control: row sum writing its input: 4 of 4 repeats differ from the first run's bytes, first at byte 0
CONTROL control: row sum writing its input: red at repeat, as planted
FAIL shapes control: row sum losing a tail input: at 8x1x1 output 0 is 0.000000e0 where float64 gives -3.203125e-1: 1864134.22 of its bound
CONTROL control: row sum losing a tail input: red at shapes, as planted
FAIL sums control: dot summed in half: 621378.4x over its exact-sum bound at k 4096: not fp32 sums (fp16 would miss 29826162x, bf16 16777216x)
CONTROL control: dot summed in half: red at sums, as planted
Control Gate What it plants
fmax row max 1, non-finite fmax, which drops a NaN that float64 keeps
atomic-add row sum 2, repeat a row sum through a compare-exchange loop on the value's bits, so the order changes between runs (this device's OpenCL C header declares no fp32 atomic_add)
row sum writing its input 2, repeat the sum written over its own input
row sum losing a tail input 3, shapes a partial tail that loses its last input, right only when the length is a whole number of 32s
dot summed in half 4, fp32 sums a dot product summed in fp16

Gate 5 also runs live. The card's PCI id, EUs a Xe-core, threads an EU, SIMD width and Xe-core count are checked against the BMG-G31 row from #2155. Each loaded kernel's required work-group, sub-group, SLM bytes, spill bytes and argument count are checked against its record, and its work-group against what a Xe-core holds at its GRF mode. The runner loads the same native zebins that the #2155 audit reads, so the GRF mode the record states is the loaded image's mode.

File Lines Job
zig/tests/xpu/contract_run.zig 68 tf-xpu-contract-run [--device N]: the device, the runner list and the SUMMARY line
zig/tests/xpu/contract_rig.zig 217 the card, its zebins, and guarded buffers with poisoned outputs
zig/tests/xpu/contract_kit.zig 81 the glue's value kinds, values exact in them, and the glue's own formulas
zig/tests/xpu/contract_norm.zig 288 add_rmsnorm, add_rmsnorm_dec and gated_norm: the normed row and the residual in place
zig/tests/xpu/contract_swiglu.zig 149 swiglu: the activation and the row sums it writes beside it
zig/tests/xpu/contract_argmax.zig 138 argmax, with its stated NaN and tie rule
zig/tests/xpu/contract_embed.zig 153 embed, with its stated rounding rule
zig/tests/xpu/contract_controls.zig 151 the five planted controls
zig/tests/xpu/contract_live.zig 103 gate 5 live: the card's properties and each loaded kernel's
zig/kernels/xpu/contract_gate_controls.cl 92 the controls' kernels
zig/src/core/contract/xpu_ref.zig 353 the float64 references, the gate-4 replays and their host tests
zig/src/core/contract/xpu_limits.zig +101 the live device and kernel checks, with host tests
zig/src/core/contract/contract.zig +9, -5 the store spec term
zig/src/xpu/module.zig +19, -4 loadNative for a zebin, and kernelAt, which sets no work-group size
zig/build/xpu_contract.zig +59 tf-xpu-contract-run, xpu-contract-run and -Dxpu-images=DIR
zig/src/core/contract.zig +4 the host tests of the new modules
docs/kernel-contract.md +13, -4 the runner, store, and where an XPU test type goes

Two changes reach code outside the XPU contract:

  • Spec.store is a new optional field, null by default. The residual that add_rmsnorm writes in place holds bf16 values in fp32 words, as glue.cl's rbf stores them. With store = .bf16, the bound allows one bf16 rounding there, not one fp32 rounding, which is the same allowance HIP gives a bf16 residual. With null, bound and boundExact give the same values as before, so every other registration is unchanged.
  • Module.loadNative loads a zebin with ZE_MODULE_FORMAT_NATIVE through the same path as load, which still builds SPIR-V. kernel now calls kernelAt and then sets the work-group size, so its callers see no change. Gate 5 live uses kernelAt so that a size the device refuses cannot hide the kernel's properties.

The SwiGLU row sums' reference is the unrounded float64 mean, computed from the activation bytes that the card wrote. The bound already allows one fp16 rounding at the store. A reference that is already rounded leaves no room at an fp16 tie. A B70 run before c5032171 failed at -19.28125 where the rounded reference gave -19.296875, with the exact value near the tie between them. The host test a SwiGLU row sum at an fp16 tie is not rounded in the reference, so either neighbour is within the bound holds that case. In the run above, the row sums' worst shape is 0.996 of the bound.

Checks on this head

  • Head commit: f454898b. It is b79eb6bd rebased onto main 53311e7e for a CHANGELOG.md conflict. Outside CHANGELOG.md the change is byte-identical, and CI was green on b79eb6bd. The checks below ran on b79eb6bd.
  • Lean checker, python3 tools/zig/lean_check.py, on the base (6433d511) and on this head: 252 and 252, the same lines; 0 new findings.
  • Tests run, with the command and pass, fail and skip counts:
    • zig build test-kernel-contract: 210 passed, 0 failed, 0 skipped.
    • zig build test: 7414 tests, 7352 passed, 1 failed, 61 skipped. The failure is the listener test that binds the IPv6 loopback, which this machine does not have. It also fails on main on the same machine.
    • zig fmt --check zig/: clean.
    • python3 -m unittest tools/zig/build_options_test.py: 5 passed. It failed on CI before the -Dxpu-images fix, because -Dxpu-images was declared only with -Dxpu-contract=true.
  • CI: not yet run on this head.
  • Not run, and why: any XPU card other than the Arc Pro B70 (BMG-G31), because the registrations and the device row are written for it.

Receipt

  • Environment, with the TensorFold commit, Zig and backend toolchain, machine and GPU, checkpoint and revision: the B70 run is of c5032171, this branch before the rebase, on contract: an XPU limits backend (gate 5, the .ze_info reader, planted controls) #2155 (3cbd915d). The rebase onto 6433d511 changes no kernel. Main changed three files the runner compiles: rows.zig gains layer_norm and gelu, xpu/kernels.zig gains the LFM3 images (the runner links none), and core/contract.zig exports one more module. None changes a path the runner takes, and the zebins are the same three, with the same hashes. Linux, Zig 0.17.0, ocloc 26.35.39758.10, one Intel Arc Pro B70 (PCI id 0xe223). No checkpoint is needed.
  • Paths and platforms the change reaches, which I ran and which I could not: the contract and xpu/module.zig only. No inference path changes, so no model ran. The Zig host has no ocloc, so I made the zebins on the B70 host with the commands below. I linked them with -Dxpu-images=DIR, then ran the binary once on one B70.
  • Exactness, from tools/bench_concurrent.py --alone --serial or token_sha against "draft": false: n/a. No kernel, weight or inference path changes.
  • Decode speed before and after, from tools/bench_openai.py: n/a, for the same reason.
  • Prompt speed before and after: n/a, for the same reason.
  • Licence of any new weights or drafter, or "n/a": n/a.

The zebins, from the repository root of this head (ocloc 26.35.39758.10):

ocloc compile -q -file zig/kernels/xpu/qwen27/glue.cl -device bmg \
  -options "-cl-std=CL3.0 -cl-intel-256-GRF-per-thread -I zig/kernels/xpu/qwen27" \
  -output_no_suffix -output glue -out_dir <dir>
ocloc compile -q -file zig/kernels/xpu/contract_controls.cl -device bmg \
  -options "-cl-std=CL3.0 -cl-intel-256-GRF-per-thread" \
  -output_no_suffix -output contract_controls -out_dir <dir>
ocloc compile -q -file zig/kernels/xpu/contract_gate_controls.cl -device bmg \
  -options "-cl-std=CL3.0 -cl-intel-256-GRF-per-thread" \
  -output_no_suffix -output contract_gate_controls -out_dir <dir>
zig build -Dxpu-contract=true -Dxpu-images=<dir> tf-xpu-contract-run
./zig-out/bin/tf-xpu-contract-run
Image sha256
glue.bin 678dc97fc959d8e196b21cf59750acc054961e97e22c6389dce63248def51725
contract_controls.bin ad38214f86268ec752608207e32b9dfbed3720bc07a5b3ea413a8131077e7024
contract_gate_controls.bin 506e9fbd5792fd5932fe76a5744de0c6996dbeb26edc28fedb79a41515b517a6

On a B70 host with both tools, zig build -Dxpu-contract=true xpu-contract-run -Docloc=ocloc -Dxpu-device=bmg makes the zebins and runs the binary in one step.

The full run, 65 lines, exit 0. The first two lines are from the driver (the second is a NUL byte, left out here). It refuses the 160 KiB SLM control image of #2155, which is planted. Gate 5 then reports that image as N/A and checks its limits from the table.

tf-xpu-contract-run, full output
error: module build: Size of SLM (163840) larger than available (131072)

PASS nonfinite xpu add_rmsnorm (the normed row, fp16): 5122 non-finite and 20478 finite outputs match the float64 classes
PASS repeat xpu add_rmsnorm (the normed row, fp16): five runs byte-identical (10240 bytes), inputs and guards untouched, no overlap
PASS shapes xpu add_rmsnorm (the normed row, fp16): 8 shapes within the float64 bound (worst 0.941 of the bound), 0 edges refused by the kernel
PASS sums xpu add_rmsnorm (the normed row, fp16): within its bound at k 5120; fp16 sums would miss 120x, bf16 0.6x (a 16-bit output follows the sum of squares as ss^-1/2: a bf16 sum's error is a few output ulps; fp16's overflowing sums give 0, which the bound's residual roundings put at 120x)
PASS nonfinite xpu add_rmsnorm (the residual in place, fp32): 3 non-finite and 25597 finite outputs match the float64 classes
PASS repeat xpu add_rmsnorm (the residual in place, fp32): five runs byte-identical (20480 bytes), inputs and guards untouched, no overlap
PASS shapes xpu add_rmsnorm (the residual in place, fp32): 8 shapes within the float64 bound (worst 0.000 of the bound), 0 edges refused by the kernel
N/A  sums xpu add_rmsnorm (the residual in place, fp32): declares none sums
PASS nonfinite xpu add_rmsnorm_dec (the normed row, fp16): 5122 non-finite and 20478 finite outputs match the float64 classes
PASS repeat xpu add_rmsnorm_dec (the normed row, fp16): five runs byte-identical (10240 bytes), inputs and guards untouched, no overlap
PASS shapes xpu add_rmsnorm_dec (the normed row, fp16): 5 shapes within the float64 bound (worst 0.000 of the bound), 0 edges refused by the kernel
PASS sums xpu add_rmsnorm_dec (the normed row, fp16): within its bound at k 5120; fp16 sums would miss 120x, bf16 0.5x (a 16-bit output follows the sum of squares as ss^-1/2: a bf16 sum's error is a few output ulps; fp16's overflowing sums give 0, which the bound's residual roundings put at 120x)
PASS nonfinite xpu add_rmsnorm_dec (the residual in place, fp32): 3 non-finite and 25597 finite outputs match the float64 classes
PASS repeat xpu add_rmsnorm_dec (the residual in place, fp32): five runs byte-identical (20480 bytes), inputs and guards untouched, no overlap
PASS shapes xpu add_rmsnorm_dec (the residual in place, fp32): 5 shapes within the float64 bound (worst 0.000 of the bound), 0 edges refused by the kernel
N/A  sums xpu add_rmsnorm_dec (the residual in place, fp32): declares none sums
PASS nonfinite xpu gated_norm (the gated row, fp16): 130 non-finite and 30590 finite outputs match the float64 classes
PASS repeat xpu gated_norm (the gated row, fp16): five runs byte-identical (12288 bytes), inputs and guards untouched, no overlap
PASS shapes xpu gated_norm (the gated row, fp16): 5 shapes within the float64 bound (worst 0.926 of the bound), 0 edges refused by the kernel
PASS sums xpu gated_norm (the gated row, fp16): within its bound at k 6144; fp16 sums would miss 120x, bf16 0.1x (a 16-bit output follows the sum of squares as ss^-1/2: a bf16 sum's error is a few output ulps; fp16's overflowing sums give 0, which the bound's residual roundings put at 120x)
PASS nonfinite xpu swiglu (the activation, fp16): 4 non-finite and 87036 finite outputs match the float64 classes
PASS repeat xpu swiglu (the activation, fp16): five runs byte-identical (34816 bytes), inputs and guards untouched, no overlap
PASS shapes xpu swiglu (the activation, fp16): 5 shapes within the float64 bound (worst 0.933 of the bound), 0 edges refused by the kernel
N/A  sums xpu swiglu (the activation, fp16): declares none sums
PASS nonfinite xpu swiglu (the row sums, fp16): 4 non-finite and 1356 finite outputs match the float64 classes
PASS repeat xpu swiglu (the row sums, fp16): five runs byte-identical (544 bytes), inputs and guards untouched, no overlap
PASS shapes xpu swiglu (the row sums, fp16): 5 shapes within the float64 bound (worst 0.996 of the bound), 0 edges refused by the kernel
PASS sums xpu swiglu (the row sums, fp16): within its bound at k 17408; fp16 sums would miss 0x, bf16 0.0x (the f16 output's own rounding is 2^-11 of the value, and a f16 sum's error over a group of 64 is a few of those ulps, so its miss stays under the 100x the gate asks)
PASS nonfinite xpu argmax (the pick): the stated rule holds: a NaN compares false, so it is skipped; the pick is the largest value above -Inf, the lowest index on a tie, and 0x7fffffff when no value compares above -Inf (a row of NaNs, or of -Inf)
PASS repeat xpu argmax (the pick): five runs byte-identical (4 bytes), inputs and guards untouched, no overlap
PASS shapes xpu argmax (the pick): 7 shapes exact by its own check (worst 0.000 of the bound), 0 edges refused by the kernel
N/A  sums xpu argmax (the pick): declares none sums
PASS nonfinite xpu embed (the gathered row, fp32): the stated rule holds: a NaN stays a NaN, an infinity stays an infinity, and a finite value rounds once to bf16, to nearest even, as glue.cl's embed stores it
PASS repeat xpu embed (the gathered row, fp32): five runs byte-identical (20480 bytes), inputs and guards untouched, no overlap
PASS shapes xpu embed (the gathered row, fp32): 9 shapes exact by its own check (worst 0.000 of the bound), 0 edges refused by the kernel
N/A  sums xpu embed (the gathered row, fp32): declares none sums
FAIL nonfinite control: fmax row max: 1 of 8 outputs differ in class; first at 0: finite where float64 gives nan
CONTROL control: fmax row max: red at nonfinite, as planted
FAIL repeat control: atomic-add row sum: 4 of 4 repeats differ from the first run's bytes, first at byte 12
CONTROL control: atomic-add row sum: red at repeat, as planted
FAIL repeat control: row sum writing its input: 4 of 4 repeats differ from the first run's bytes, first at byte 0
CONTROL control: row sum writing its input: red at repeat, as planted
FAIL shapes control: row sum losing a tail input: at 8x1x1 output 0 is 0.000000e0 where float64 gives -3.203125e-1: 1864134.22 of its bound
CONTROL control: row sum losing a tail input: red at shapes, as planted
FAIL sums control: dot summed in half: 621378.4x over its exact-sum bound at k 4096: not fp32 sums (fp16 would miss 29826162x, bf16 16777216x)
CONTROL control: dot summed in half: red at sums, as planted
PASS limits: 6 registered XPU launches against bmg-g31, 0 problems
RED  limits control: 2048 work-items a work-group: red on every card class
RED  limits control: 200 work-items, not a multiple of sub-group 16: red on every card class
RED  limits control: 160 KiB of SLM: red on every card class
RED  limits control: 1024 work-items at 256 GRF: red on every card class
RED  limits control: sub-group 32 at 256 GRF: red on every card class
RED  limits control: sub-group 8: red on every card class
PASS limits live bmg-g31: PCI id 0xe223, 8 EUs a Xe-core, 8 threads an EU, SIMD 16, 32 Xe-cores
N/A  limits live contract_controls: the driver does not build the image (BuildFailed); gate 5's table holds its limits
PASS limits live add_rmsnorm: 128 work-items required, sub-group 16 (the kernel's most 16), 10272 SLM bytes, 0 spill bytes, 0 private bytes, 14 arguments
PASS limits live add_rmsnorm_dec: 512 work-items required, sub-group 16 (the kernel's most 16), 10368 SLM bytes, 0 spill bytes, 0 private bytes, 14 arguments
PASS limits live gated_norm: 256 work-items required, sub-group 16 (the kernel's most 16), 12288 SLM bytes, 0 spill bytes, 0 private bytes, 12 arguments
PASS limits live swiglu: 256 work-items required, sub-group 16 (the kernel's most 16), 2048 SLM bytes, 0 spill bytes, 0 private bytes, 7 arguments
PASS limits live argmax: 256 work-items required, sub-group 16 (the kernel's most 16), 2048 SLM bytes, 0 spill bytes, 0 private bytes, 3 arguments
PASS limits live embed: 256 work-items required, sub-group 16 (the kernel's most 16), 0 SLM bytes, 0 spill bytes, 0 private bytes, 6 arguments
RED  limits live control: sub-group 8: declared only, ocloc does not build it for this device: control: sub-group 8 on bmg-g31: sub-group 8, the device runs { 16, 32 }
SUMMARY bmg-g31 0xe223 9 kernels: 9 pass gates 1-4, 0 fail, 0 known findings; 5 controls: 5 red as planted, 0 missed; 0 limit problems; gates 7955 ms, limits 0 ms
Kernel contract, only when a GPU kernel is new or changed
  • Kernels registered with the contract, and the kernel each one replaces: nine registrations of the Qwen3.8-27B glue, for gates 1 to 4. They are add_rmsnorm and add_rmsnorm_dec (the normed row, and the residual in place), gated_norm, swiglu (the activation, and the row sums), argmax and embed. None replaces a kernel. These kernels do not change.
  • A planted control for each new kernel, and each one fails as planted: five, at least one for each of gates 1 to 4, all red above. The six gate-5 controls of contract: an XPU limits backend (gate 5, the .ze_info reader, planted controls) #2155 stay red, and the live run reads the sub-group 8 control's declaration.
  • Each backend's SUMMARY line and exit status: SUMMARY bmg-g31 0xe223 9 kernels: 9 pass gates 1-4, 0 fail, 0 known findings; 5 controls: 5 red as planted, 0 missed; 0 limit problems; gates 7955 ms, limits 0 ms, exit 0.
  • Chips I could not run: any XPU chip other than BMG-G31.

Checklist

  • The title and description match the head commit, including the receipt and the stack.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency. (The host tests need no GPU. On a card that is not 0xe223, gate 5 live fails with the PCI id it found.)
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths in the diff or its commits. No AI attribution lines.

@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @tolewis. The runner does what #2080 agreed: the glue through gates 1 to 4 on the card, five planted controls each red at its own gate, and gate 5 checked live against the card and each loaded image. store leaves every other registration's bound as it was, and kernel keeps its behaviour. Recording the SwiGLU tie with its own host test is exactly right. It's ready to land right after #2155, once CI is green on this head.

@ashhart
ashhart force-pushed the xpu-contract-runner branch from c503217 to a358383 Compare November 6, 2026 04:35
ashhart added a commit to tolewis/TensorFold that referenced this pull request Nov 6, 2026
@ashhart

ashhart commented Nov 6, 2026

Copy link
Copy Markdown
Owner

@tolewis, #2269 came up in our landing queue but stopped on its own CI. Now that your #2155 is on main, two things:

  1. zig (Linux): tools/zig/build_options_test.py fails with test_every_declared_option_is_available_here:
    {'xpu-images': ['zig/build/xpu_contract.zig:60']}, so an option this target does not declare. The option has to be declared on every target the build runs, or skipped where it doesn't apply, as the test expects.
  2. A rebase: the branch conflicts with main.

With both fixed and CI green on the new head, it goes straight back in the queue.

@tolewis
tolewis force-pushed the xpu-contract-runner branch from a358383 to b79eb6b Compare November 6, 2026 12:12
tolewis pushed a commit to tolewis/TensorFold that referenced this pull request Nov 6, 2026
@tolewis

tolewis commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. Both are fixed on b79eb6bd:

  1. -Dxpu-images is now declared in zig/build/xpu.zig, beside -Dxpu-contract, so every target lists it. xpu_contract.steps takes its value. tools/zig/build_options_test.py passes, 5 of 5.
  2. The branch is rebased onto main (6433d511), with your changelog commit kept. The only conflict was CHANGELOG.md. I kept main's entries and the contract: an XPU device runner (gates 1 to 4 on a B70, gate 5 live, planted controls) #2269 line, and dropped the serving-pins line, because main already has it as tests: re-baseline the stale serving source pins, with no re-qualification claimed #2175.

On this head: zig build test-kernel-contract passes 210 of 210. zig build test passes 7352 of 7414, with 61 skipped. The 1 failure is the IPv6 loopback listener test, which fails on main on the same machine. The lean checker gives 252 on main and 252 here, the same lines. zig fmt --check zig/ is clean.

The B70 run in the description is of c5032171, before the rebase. The rebase changes no kernel. Main changed three files the runner compiles: rows.zig gains layer_norm and gelu, xpu/kernels.zig gains the LFM3 images (the runner links none), and core/contract.zig exports one more module. None changes a path the runner takes, and the zebins are the same three, with the same hashes.

tolewis pushed a commit to tolewis/TensorFold that referenced this pull request Nov 6, 2026
@tolewis
tolewis force-pushed the xpu-contract-runner branch from b79eb6b to f454898 Compare November 6, 2026 12:34
@ashhart

ashhart commented Nov 6, 2026

Copy link
Copy Markdown
Owner

/atc exchange 88927731373e58d9619d034e78818cab5f5c591dcb4302eb1d8faca8457817d3

@ashhart
ashhart force-pushed the xpu-contract-runner branch from f454898 to 465ab61 Compare November 7, 2026 05:51
@ashhart
ashhart merged commit bfc53b9 into ashhart:main Nov 7, 2026
11 of 14 checks passed
ashhart added a commit that referenced this pull request Nov 7, 2026
@ashhart

ashhart commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Thank you @tolewis. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes.

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.

2 participants