igneum/proto-cuda/CHECKLIST.md
igneum-labs ff263245ac Igneum: design docs, Metal lottery-hash prototype, CUDA test pack, finality simulation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 15:06:01 +00:00

8.1 KiB

Metal to CUDA equivalence checklist

Written 3 October 2026 for the program packs in packs/. Everything below is about the kernel text that proto-metal/igneum-bench --export-pack emits, compared with the Metal text the same run emits and with the CPU interpreter cpuWarp in proto-metal/main.swift. There is no CUDA toolchain on the Mac, so "verified" means one of the three methods in the last section, never an nvcc build.

Types and arithmetic

Item Metal CUDA Status
Register type uint (32-bit unsigned) uint32_t (unsigned int, 32-bit on Linux and Windows) same width, same wraparound
Output type ulong uint64_t 64-bit on both; (uint64_t)hi << 32 | (uint64_t)lo
Literals 0x...u 0x...u emitted by the same hex() helper
Floats none none nothing for fast-math or FMA contraction to touch
Shift amounts always 0..31 (see rotates) always 0..31 no out-of-range shift anywhere
Evaluation order each instruction is one statement with one assignment same statement text no sequence-point question arises

Register init and framing

Item Metal CUDA Status
Nonce baseNonce + gid, gid = thread_position_in_grid baseNonce + blockIdx.x * blockDim.x + threadIdx.x identical for blockDim 32; also identical for blockDim 32 x W because the nonce is still base + global index
Register init x = nonce ^ SEEDW[i]; x += 0x9e3779b9u * (i+1)u; x = splitmix32(x); r_i = x ^ SEEDW[(i+1)&7] same, with SEEDW[i] and the product 0x9e3779b9 * (i+1) written as literals (computed by the exporter mod 2^32) verified by vectors
splitmix32 3 shift-xor, 2 multiply identical text verified by vectors
Iteration for it in 0..<8 { sel = r0; 64 instructions } identical verified by vectors
Output mix lo = r0 ^ rotl(r1,7) ^ rotl(r2,14) ^ rotl(r3,21), hi = r4 ^ rotl(r5,9) ^ rotl(r6,18) ^ rotl(r7,27) identical verified by vectors

Instructions

Op Metal CUDA CPU interpreter Notes
add d = d + a + select(imm, imm2, ((sel >> bit) & 1u) != 0u) d = d + a + ((((sel >> bit) & 1u) != 0u) ? imm2 : imm) d &+ a &+ (s != 0 ? imm2 : imm) Metal select(A, B, c) is c ? B : A, so the true branch is imm2 in all three. sel is r0 sampled at the top of the iteration in all three.
sub d = d - a same d &- a wraparound
mul d = d * a same d &* a low 32 bits
mulhi mulhi(d, a) __umulhi(d, a) (UInt64(d) * UInt64(a)) >> 32 high 32 bits of the unsigned 64-bit product on all three
xor d = d ^ a same same
or d = d | a same same
rotl (immediate) rotl_imm(d, n) = (x << n) | (x >> (32u - n)), n literal 1..31 identical text rotl32 with n in 1..31 the generator draws rot from 1..31, so neither shift amount is ever 0 or 32
rotr (register) rotr_var(d, a) = n &= 31u; (x >> n) | (x << ((32u - n) & 31u)) identical text rotr32: n & 31, n == 0 ? x : ... for n = 0 both GPU forms give x | x = x; for n in 1..31 both shifts are in range
mad d = a * b + d same (a &* r[b]) &+ d b may equal d or a; all three read every operand before the single write
shfl d = d ^ simd_shuffle_xor(a, (ushort)mask) d = d ^ __shfl_xor_sync(0xffffffffu, a, mask) r[lane][dst] ^= tmp[lane ^ mask] where tmp is a copy of register a across the warp taken before any lane writes mask in {1,2,4,8,16} so the partner lane is always inside the same 32-lane group. a != dst by construction, so there is no read-after-write hazard inside the instruction. Control flow is uniform (no branches at all), so the full member mask is valid and no lane is missing from the shuffle.
load d = d ^ dataset[a & MASK], MASK a compile-time literal d = d ^ ds[a & mask], mask a kernel argument d ^ datasetElem(a & mask, d0, d1) the index is a 32-bit unsigned value below 2^28, so pointer arithmetic needs no 64-bit care

No instruction had to be removed or changed. Every op is an unsigned 32-bit integer operation whose result is defined identically in Metal Shading Language, CUDA C++ and Swift's wrapping operators.

Lane mapping (the one place where the two platforms differ in guarantees)

Item Metal CUDA
Group that shuffle_xor spans the SIMD group, width threadExecutionWidth (32 on this M5 Max; the tool warns if not) the warp, width 32 on every NVIDIA GPU (host.cu prints cudaDevAttrWarpSize and warns if not 32)
Lane id of a thread thread_index_in_simdgroup; for a 32-wide threadgroup this equalled thread_position_in_threadgroup, as shown by the bit-exact match with the CPU model across 21 warps on 3 Oct 2026 and the 3 vector warps per pack today threadIdx.x % 32, guaranteed by the CUDA programming model
Partner lane lane ^ mask lane ^ mask
Warps per block 1 (threadgroup of 32) --block-warps W, default 1. Any W is bit-exact because each warp is an aligned run of 32 consecutive nonces either way. The emulation passed with W = 1 and W = 2.

Dataset

Item Metal CUDA
Element ds_elem(i, d0, d1): xor, mul 0x9E3779B1, xor-shift 15, add d1, mul 0x85EBCA77, xor-shift 13, mul 0xC2B2AE3D, xor-shift 16 identical text in kernel.cu; a third copy host_ds_elem in host.cu
Day words seedWords("day/" + day)[0..1] on the Mac written into program.h as IGNEUM_DAY0, IGNEUM_DAY1
Fill one thread per word, threadgroup 256 one thread per word, block 256, with an i < n guard (a no-op for power-of-two sizes)
Self-test not needed on the Mac (CPU and GPU share one process) head 16 words and word [MASK] against values the Mac wrote into vectors.h; 64 pseudo-random words against host_ds_elem

How each claim above was verified

  1. Mac, Metal GPU against the CPU interpreter: the regular bench (proto-metal/igneum-bench) passed 3 warps per program on both seeds again after the exporter was added, and --export-pack itself runs the Metal kernel for the three vector warps (base nonces 0, 4096, 1000000) and refuses to write a pack unless all 96 outputs match the interpreter. Both packs were written with "Metal GPU cross-check PASS 3/3 warps".
  2. Mac, CUDA text executed as C++: emu/emu.sh compiles host.cu and the generated kernel.cu with clang against a shim cuda_runtime.h (the launch syntax is the only text rewritten) and runs the kernels on host threads, 32 per warp, with a barrier inside __shfl_xor_sync. Result on 3 Oct 2026, both packs, 1 GiB dataset: dataset self-test PASS, 3 of 3 warps PASS standalone, warps 0 and 4096 PASS inside a batch with 1 and with 2 warps per block. Sweep sizes 4, 64, 256, 512 MiB: dataset self-test PASS. -Wall -Wextra clean.
  3. Reading: every emitted line was compared by eye against the Metal line for the same instruction index (the comment at the end of each CUDA line carries the index and the op).

Residual risks that the Mac cannot remove

  • nvcc never ran on this code. Syntax that clang accepted could still trip nvcc's front end, and the shim's idea of the runtime API could differ from the real header in a detail. The six cudaDevAttr* enumerators and the template signatures of cudaFuncGetAttributes(cudaFuncAttributes*, T* entry) and cudaOccupancyMaxActiveBlocksPerMultiprocessor(int*, T func, int blockSize, size_t dynamicSMemSize) were checked against the NVIDIA CUDA Runtime API documentation on 3 Oct 2026 and match what the code uses. Anything left would be a compile error, not a silent output difference, and a one-line fix in host.cu or the three wrapper functions at the bottom of kernel.cu.
  • __shfl_xor_sync and __umulhi semantics were emulated, not executed on NVIDIA hardware. Both are documented as lane ^ laneMask within a 32-wide warp and the high 32 bits of the unsigned product; the vectors will settle it on the 5090.
  • -arch=sm_120 requires CUDA 12.8 or newer. On an older toolkit the build fails at the flag, not at the code.
  • Performance figures from the emulation are meaningless and were not recorded. No NVIDIA hash rate exists yet.