13 KiB
verify/ harness extension roadmap
Status note, 2026-05-11: this file is historical planning context. Vulkan QJL/Polar bind-set verification, the CUDA harness, the Vulkan built-fork dispatch smoke gate, and the Android Vulkan smoke runner now exist in this directory. The enforceable current contract is
kernel-contract.json, checked bymake kernel-contract; the current blocker ledger is../reports/porting/2026-05-11/remaining-work-ledger.md.
This is the implementation plan for taking the verify/ harness from its
current state (Metal: 5/5 shaders verified on M4 Max; Vulkan: 3/5 turbo*
shaders verified on Intel ARL + lavapipe; QJL/Polar Vulkan + every other
backend: unverified) to full coverage of the device matrix declared in
../DEVICE_SUPPORT_GAP_2026-05-10.md.
For each unverified backend × kernel combo: the bind-set / fixture / host change required, with file-path pointers and effort estimate.
The shape of a "verifier" in this directory is fixed:
- Read a JSON fixture (kernel name, integer shape params, raw input buffers as JSON byte/float arrays, expected scalar output array).
- Allocate device buffers matching the shader's bind-set.
- Upload inputs.
- Bind pipeline + descriptor set, push constants, dispatch.
- Read back outputs.
- Diff against
expected_scoreswith the per-kernel tolerance (currently 1e-3 abs). - Print PASS/FAIL count and exit non-zero on any FAIL.
gen_fixture.c builds the fixtures by calling the C reference impls in
../reference/turbo_kernels.c and qjl_polar_ref.c. Same C
references are the source of truth for every backend's fixtures — so
"add backend X" is bind-set work, not new reference work.
Extension 1 — Vulkan QJL bind-set
Status: blocks every Vulkan device for the QJL kernel.
Current state. vulkan_verify.cpp:268–315 hard-codes:
- 3 storage buffers (4 if
kernel == "turbo3_tcq"for the codebook), - a single
TurboPushConstants{ uint head_dim; uint n_kv; uint blocks_per_kv; }push range, - workgroup count
= n_kv.
The QJL shader ../vulkan/qjl.comp wants:
binding=0 q_sketch[n_heads * 256](fp32, host pre-projected)binding=1 packed_k[n_kv_heads * n_tokens * 34_bytes]as a uint stream (std430 forces 4-byte alignment; the 34-byte block is read byte-by-byte)binding=2 scores[n_heads * n_tokens]fp32 out- push constant
Push { uint n_heads; uint n_kv_heads; uint n_tokens; uint proj_dim; }(proj_dimMUST be 256) - workgroup dispatch
= (n_heads, n_tokens, 1)— one threadgroup per output cell, 32 lanes per group, shared-mem tree reduction.
What to do.
- In
vulkan_verify.cpp, branch the bind-set count, buffer sizes, push constant struct, and dispatch dimensions onfx.kernel. Three variants today (turbo / turbo_tcq / new-qjl); the cleanest shape is a smallKernelBindingsstruct that the parser fills in based onfx.kernel. - Extend the JSON fixture schema with the new shape fields:
n_heads,n_kv_heads,n_tokens,proj_dim, plus the input blobsq_sketch(float array ofn_heads*proj_dim) andpacked_k(byte array ofn_kv_heads*n_tokens*34). - Extend
gen_fixture.cto callqjl_score_qk_reffromqjl_polar_ref.cwith deterministic seed inputs and emit the new fixture shape. (The function already exists; this is aprintfJSON serializer, not new math.) - Tolerance: 1e-3 abs (matches current turbo path; the M4 Max Metal max diff was 1.1e-5, well inside).
Effort: S (3-4h: ~50 LOC of harness branching, ~80 LOC of fixture serialization).
Hosts: runs on any Vulkan-capable host — Intel ARL +
Mesa/lavapipe gives the same coverage as the verified turbo* path. Same
host can drive Adreno/Mali via cross-compile + adb push.
Extension 2 — Vulkan Polar bind-set
Status: blocks every Vulkan device for the PolarQuant kernel.
Current state. Same as #1 — bind-set hard-coded for turbo.
The Polar shader ../vulkan/polar.comp wants:
binding=0 k_blocks[n_rows * 82_bytes]as a uint stream (fp16 norm + 64 nibble bytes + 16 QJL residual bytes per block)binding=1 q[head_dim]fp32binding=2 y[n_rows]fp32 out- push constant
Push { uint n_rows; uint head_dim; uint use_qjl; }(head_dimMUST be 128) - workgroup dispatch
= (n_rows, 1, 1)
What to do.
- Add a third branch in the same
KernelBindingsswitch from #1. - Extend the JSON fixture schema with
n_rows,head_dim,use_qjl,q(head_dimfloats),k_blocks(n_rows*82bytes). - Extend
gen_fixture.cto callpolar_dot_reffromqjl_polar_ref.cand emit two fixture variants:polar_no_qjl.json(use_qjl=0, optional residual omitted)polar_with_qjl.json(use_qjl=1, residual exercised)
- Tolerance: 1e-3 abs. Metal max diff on M4 Max was 7.6e-6.
Effort: S (3-4h once #1's KernelBindings shape exists; <100 LOC
incremental).
Hosts: any Vulkan host. Same cross-compile path covers Adreno / Mali / NVIDIA / AMD / Intel.
Extension 3 — cuda_verify (new harness)
Status: blocks 9b, 27b, 27b-256k from claiming
CUDA backend verification per AGENTS.md §8.
Current state. No CUDA verifier exists in verify/. The
v0.4.0-eliza fork ships W4-B CUDA QJL/Polar/TBQ3_TCQ kernels, but no
parity check confirms they produce the same numbers as the C reference
or the Metal/Vulkan ports.
What to do.
- New file
verify/cuda_verify.cu— same JSON fixture parser (factor out ofvulkan_verify.cppinto a tinyfixture.hheader),cudaMallocfor each bind-set buffer,cudaMemcpy, kernel launch via the fork's CUDA entry points (turbo-quant-cuda.cuh,qjl_quant_kernel.cu, etc. — see../README.mdline 74, 84 for the file pointers). - Add a
cudatarget toverify/Makefilekeyed offnvccpresence. - Same JSON fixtures as the Vulkan/Metal harnesses — no new fixture files needed once #1 + #2 land.
- Tolerance: 1e-3 abs initially; tighten once we have on-host runs.
Effort: M (1-2 days: harness scaffolding ~200 LOC, plus glue to the fork's launch wrappers).
Hosts: any L4/T4/A10G EC2 instance, or a developer workstation with an RTX 30/40-series card. CI-runnable via GitHub-Actions GPU-runner or the existing device-lab.
Extension 4 — Adreno on-device runner
Status: blocks 2b Vulkan claim on Snapdragon devices
(Pixel 8/9, Galaxy S23/24/25).
Current state. No on-device runner. vulkan_verify is a
desktop/lavapipe binary; cross-compile against the NDK Vulkan headers
already works (the cmake flags in
../../app-core/scripts/build-llama-cpp-mtp.mjs:670–689
do exactly this for android-arm64-vulkan), but the verify/Makefile
doesn't have an android-vulkan recipe.
What to do.
- Add an
android-vulkanrecipe toverify/Makefilethat cross-compilesvulkan_verifyagainst the NDK toolchain ($ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake). - Document the
adb pushsteps:adb push vulkan_verify *.spv fixtures/ /data/local/tmp/eliza-kernels/thenadb shell "cd /data/local/tmp/eliza-kernels && ./vulkan_verify qjl.spv fixtures/qjl.json". - Same SPIR-V, same fixtures — Adreno's vendor Vulkan driver consumes identical SPV.
Effort: S (1 day, half is procuring a device).
Hosts: Pixel 8/9 (Adreno 730/740) or Galaxy S24/25 (Adreno 750/830).
Extension 5 — Mali on-device runner
Status: blocks 2b Vulkan claim on Tensor / non-Snapdragon
Galaxy / MediaTek devices.
Current state. Same as #4. Mali's subgroup behavior differs from Adreno; W4-A's shared-mem tree reduction is supposed to sidestep that, but it has not been observed on Mali.
What to do. Identical to #4 — same binary, same fixtures, different phone.
Effort: S (same as #4).
Hosts: Pixel 6/7 (Tensor G1/G2 — Mali-G78/G710), or any Exynos-based Galaxy.
Extension 6 — iOS on-device runner
Status: blocks 2b Metal claim on iOS
hardware. Has a hard prerequisite: the ios-arm64-metal archive
must actually be linked into the app first (see
../DEVICE_SUPPORT_GAP_2026-05-10.md
blocker #1).
Current state. metal_verify.mm is a CLI binary that links
-framework Metal — it does not run on iOS where there is no shell.
What to do.
- Once blocker #1 is fixed, create a tiny iOS XCTest target that
embeds the same
metal_verifylogic as a unit test:verify/ios-xctest/MetalVerifyTests.m(or .swift). - Reuse the JSON fixtures verbatim — bundle them into the test target.
- Run via
xcodebuild test -scheme MetalVerifyTests -destination "platform=iOS,name=…".
Effort: M (2-3 days assuming someone else has fixed blocker #1).
Hosts: any iPhone 14 / iPad M-series.
Extension 7 — linux-aarch64-cuda (GH200) target + verify
Status: blocks 27b-256k tier entirely (no target exists today).
Current state. SUPPORTED_TARGETS in
../../app-core/scripts/build-llama-cpp-mtp.mjs:82
has no linux-aarch64-* triple. parseTarget would happily split
linux-aarch64-cuda into the right shape, but the array doesn't
include it.
What to do.
- Add
"linux-aarch64-cuda"and"linux-aarch64-cpu"toSUPPORTED_TARGETS. - In
cmakeFlagsForTarget, the cuda branch needs-DCMAKE_SYSTEM_PROCESSOR=aarch64(when arch=aarch64) and-DCMAKE_CUDA_ARCHITECTURES=90a(Hopper / H100/H200) — today there is noCMAKE_CUDA_ARCHITECTURESpin at all, so the build picks whatever the build host has, which on a non-GPU host issm_52. - Once the target builds, run the new
cuda_verify(#3) on the GH200.
Effort: M for the build target (1-2 days), S for the verify run once the binary exists. The GH200 itself is the rate-limiter.
Hosts: any aarch64 host with H100/H200 (GH200 superchip, or any Grace + Hopper PCIe board).
Extension 8 — AMD ROCm verify
Status: blocks 9b, 27b from claiming ROCm parity
even on hosts where the build target compiles.
Current state. linux-x64-rocm builds when hipcc/rocminfo is
present, but the W4-B QJL/Polar/TBQ3_TCQ kernels are CUDA-only — they
hipify on the fly via the standard llama.cpp HIP machinery, but no
parity check confirms the hipified versions match the CUDA originals.
What to do.
- Hipify
cuda_verify.cufrom #3 (onehipify-perlinvocation). - Add a
rocmrecipe toverify/Makefile. - Same JSON fixtures.
- Add
-DCMAKE_HIP_ARCHITECTURES="gfx942;gfx1100"(MI300 + RX 7900) to the rocm branch incmakeFlagsForTarget— currently no arch pin.
Effort: S once #3 exists (~1 day). M of "find an MI300 host."
Hosts: AMD MI250/MI300 (cloud rentable), or RX 7900 XTX (any Strix-class workstation).
Extension 9 — Windows on-device runner (x64 + arm64)
Status: blocks 9b claim on the Windows half of the
desktop tier.
Current state. windows-x64-cpu and windows-x64-cuda cross-build
from Linux via mingw, but the produced .exe has never been observed
running on Windows. windows-arm64-* doesn't exist as a triple.
What to do.
- Run the existing
vulkan_verify.exe(cross-compiled from thewindows-x64-vulkantarget — also doesn't exist yet, add it) on a Windows host with any GPU. - For arm64: add
windows-arm64-cpuandwindows-arm64-vulkantoSUPPORTED_TARGETS, run on a Snapdragon X Elite Copilot+ PC.
Effort: S for x64 (1 day if a Windows box is available); M for arm64 (toolchain wiring + device).
Hosts: any Windows 11 box for x64; Surface Pro 11 / Galaxy Book4 Edge for arm64.
Extension 10 — WebGPU port (out of scope for ship-1)
Status: no shaders, no harness, no target. Listed for completeness.
What it would take: WGSL ports of all five kernels (turbo3, turbo4, turbo3_tcq, qjl, polar), a Dawn-based or browser-side fixture runner, and a new "webgpu" backend in the manifest backend matrix. Scope: weeks-to-months. Defer until upstream llama.cpp WebGPU lands and a product driver appears.
Sequencing recommendation
The cheapest order that unlocks the most tier×backend cells:
- #1 + #2 (Vulkan QJL/Polar bind-set) — half a day each, unblocks every non-Metal backend's qjl + polar verification on the existing Intel-ARL + lavapipe host.
- #4 + #5 (Adreno + Mali on-device) — once #1 + #2 are green, pushing the same binaries to a phone is mostly device-procurement time.
- #3 (
cuda_verify) — unlocks the entire CUDA half of the matrix in one sweep. - #7 (
linux-aarch64-cuda) — depends on #3. - #8 (ROCm) — depends on #3 (hipified copy).
- iOS blocker #1 in DEVICE_SUPPORT_GAP_2026-05-10.md, then #6 —
higher product impact than #4/#5 but blocked on the build-script
plumbing that isn't a
verify/change. - #9 (Windows) and #10 (WebGPU) — defer.
Total wall-clock to "every cell in the matrix has at least a verify result on real hardware" is roughly 3–4 weeks of harness + device-lab work, gated by hardware availability not by harness complexity. The harness itself is small.