Skip to content

HIP + Vulkan backends — Phase 2

Companion to hip_vulkan_phase1.md. Reuses the Phase-1 backend seam and runtime FFI; adds codegen coverage + corpus harness + wave64.

  • Wave64 (CDNA) transformHipGenerator::with_profile(TargetProfile::hip_wave64()) now propagates lane_width = 64 to the inner CUDA generator (so the reduction tree is sized for 64-lane warps) and widens the shuffle mask from 0xffffffffull (wave32) to 0xffffffffffffffffull (wave64) in the textual transform. Unit-tested in hip::tests::wave32_emits_32bit_mask / wave64_emits_64bit_mask. No CDNA hardware on hand → HW validation deferred.
  • HIP corpus harnesswh-iron-std/tests/hip_kernel_corpus.rs runs every registered #[test_kernel] through HipDevice::run_kernel. PASS / MISMATCH / UNSUPPORTED / ERROR triage matches the CUDA corpus, so results are comparable. Hard ERROR budget = 64 (anything past that signals a codegen regression, not numerics).
  • KernelMode::Reduction lowering — per-thread grid-stride accumulator (Op::StrideReduce) → workgroup-shared barrier-tree (Op::Reduce). The reduction is subgroup-width agnostic (the portable path called out in VULKAN_BACKEND_SPEC.md §4.1), depending only on local_size_x and barrier(). row_reduce_sum bit-accurate (max_rel = 3.32e-7) on RX 9070 XT.
  • KernelMode::Grid3D lowering — per-axis gl_GlobalInvocationID.{x,y,z}. Same wiring as CUDA’s blockIdx.*/threadIdx.*.
  • 3-D dispatch / 3-D workgroupGlslGenerator::with_local_size_3d, VulkanDevice::run_kernel(&kernel, &bufs, grid: [u32;3], block: [u32;3]). Same calling convention as HipDevice and CudaDevice so the corpus harness shares its loop.
  • Broader op coverage — added Select, DeclareLocal/SetLocal, ThreadgroupAlloc/Load/Store, StackAlloc/Load/Store, Barrier, Loop/If (nested-block recursion mirroring CUDA), Zeros/Splat scalar.
  • safe_glsl_ident shim — wh-iron param names like out / in / inout collide with GLSL keywords. We rename them to _b_out / _b_in / _b_inout only inside SSBO arrays + array accesses, preserving readability for any non-conflicting param name.
  • Vulkan corpus harnesswh-iron-std/tests/vulkan_kernel_corpus.rs, same triage as HIP / CUDA.
  1. GLSL is strictly typed; SSA values from BinOps come out as float. Anywhere we use those values as array indices or loop bounds, we need an explicit uint(...) cast. Without it, shaderc fails with '=' : cannot convert from ' temp highp float' to ' temp highp uint'. Phase-2 emits arr[uint(idx)] and for (uint _i = uint(start); ...) universally. A future Phase 3 may add type tracking; the explicit cast path is correct-but-verbose.
  2. GLSL reserved-word collisions. Any param named out, in, inout, uniform, etc. gets a _b_ prefix in the emitted SSBO. We hit out immediately in row_reduce_sum — the standard “output buffer” name in the wh-iron corpus.
  3. shared arrays must live at file scope in GLSL. Phase-2 hoists every Op::ThreadgroupAlloc and the implicit _red_<vid> reduction scratch buffers to file scope by walking every block before main(). Per-thread Op::StackAlloc stays inside main.
  4. 3-D workgroup → tid collapses 3 axes. Reduction-mode kernels from the corpus use 1-D tpg, but the harness also has 3-D tpg cases. We collapse to a single tid as lid.z * sx*sy + lid.y * sx + lid.x so the shared-mem reduction tree works regardless of how the workgroup is dimensioned.
  1. lane_width must flow into the inner CUDA generator too. A wave64 HipGenerator without this would generate a wave32-shaped reduction tree (32-warp shared scratch, etc.) and just rewrite the shuffle mask — the structure would be wrong. Phase-2 builds the inner CUDA generator with a cuda profile whose lane_width matches the HIP one.

Both backends run the full registered #[test_kernel] corpus (~4164 kernels × per-dtype variants).

HIP (tests/hip_kernel_corpus.rs):

PASS=4067 KNOWN_HARD=0 MISMATCH=1 UNSUPPORTED=0 ERROR=96
  • PASS = 4067 (97.66% of the registered corpus) — including the full Reduction + Elementwise + Grid3D + cooperative-matrix-software-emulated op surface from the CUDA backend, with zero HIP-specific codegen beyond the Phase-1 textual transform (3 rules) + the Phase-2 wave64 widener. wave32 RDNA is the lucky-match case the spec called out.
  • UNSUPPORTED = 0 — the entire CUDA op-walker output ports to HIP bit-for-bit on RDNA wave32; nothing fell through to the UnsupportedOp arm. The textual-transform composition strategy proved out the way the spec predicted.
  • ERROR = 96 — every one is hipModuleLaunchKernel: invalid argument on the *_moe_gather_qmm_bm64_mpp family. Same Phase-5 backlog the CUDA scope doc calls out (mpp::matmul2d cooperative MMA needs the shared-memory opt-in tuned). NOT a regression vs CUDA — those kernels pass on GX10 only because the CUDA path tunes per-launch.
  • MISMATCH = 1 — single numerics edge in the entire corpus.

Vulkan (tests/vulkan_kernel_corpus.rs):

  • PASS = 148, ERROR = 0 — Phase-2 stable.
  • UNSUPPORTED = 3831 (3538 are f16/bf16/u8 dtypes blocked on VK_KHR_shader_float16_int8 — Phase 3; 244 are subgroup ops / cooperative MMA / SimdGroup2D — Phases 3–4).
  • MISMATCH = 185 — the conv* / dequant_gather_int* / bulk_dequant families. Most are kernels whose IR uses constructs we lower but don’t yet match the oracle exactly (decode-helper sign convention, transform chains, multi-stage stride-reduce). Specific Phase-3 follow-ups documented below.

The headline is: the Phase-2 codegen + runtime adds 148 bit-accurate Vulkan kernels (FFT, RMS norm, fused activations, binary, reductions, conv causal small/medium, iron gated/rope/residual norms) on top of the f32 elementwise/reduction subset, with no native code touch.

Bit-accuracy (RX 9070 XT, gfx1201, RDNA 4, wave32)

Section titled “Bit-accuracy (RX 9070 XT, gfx1201, RDNA 4, wave32)”
Kernel Backend Mode max_rel / max|Δ| Tol
vector_add f32 HIP Elementwise 0 (bit-exact)
scale_add_exp f32 HIP Elementwise + expf 1.18e-7 5e-7
row_reduce_sum f32 HIP Reduction (warp shuffle + tree) 3.38e-7 1e-5
vector_add f32 Vulkan Elementwise 0 (bit-exact)
scale_add_exp f32 Vulkan Elementwise + exp 1.19e-7 1e-5
row_reduce_sum f32 Vulkan Reduction (workgroup barrier-tree) 3.32e-7 1e-5

The two backends produce essentially identical numerics on the same hardware — a useful coincidence (both go through the AMD f32 hardware math) and a sanity check that the codegen paths are aligned.

$env:Path += ";$env:USERPROFILE\.cargo\bin;C:\Program Files\AMD\ROCm\7.1\bin"
# Quick smokes (under 1 minute):
cargo test -p wh-iron-runtime --features hip --test hip_smoke -- --nocapture
cargo test -p wh-iron-runtime --features vulkan --test vulkan_smoke -- --nocapture
# Full corpus runs (multiple minutes — ~4164 kernels × dtypes):
cargo test -p wh-iron-std --features hip --test hip_kernel_corpus -- --nocapture
cargo test -p wh-iron-std --features vulkan --test vulkan_kernel_corpus -- --nocapture

Dump generated source for a single kernel:

$env:DUMP_HIP = "rms_norm"
cargo test -p wh-iron-std --features hip --test hip_kernel_corpus -- --nocapture
# or DUMP_VK for Vulkan/GLSL

Remaining Vulkan Phase-2 gaps (Phase 3 work)

Section titled “Remaining Vulkan Phase-2 gaps (Phase 3 work)”

The 185 MISMATCH + 26 shaderc-failure kernels cluster into a few patterns:

  1. DeclareLocal / SetLocal type mismatch ('=' : cannot convert from float to uint). My emit declares iron_loc_<name> as float, but in some kernels the IR carries integer-typed values through a mutable local — the implicit float → uint reassignment fails. Phase 3 either tracks the value’s GLSL type for the local or emits uint locals conditionally based on declared dtype.
  2. '[]' : scalar integer expression required (iron_gemv, iron_gemv_axpy_inplace, iron_gate_up_swiglu_fused). The array index in those kernels is itself derived from a constexpr-load / uint-arithmetic path I emit as float — the uint(...) outer cast isn’t enough when the inner expression already had an implicit conversion. Fix is the same as (1).
  3. iron_sort / iron_sort_segmented unexpected SHARED — the IR has a ThreadgroupAlloc declared inside a nested block. I only hoist from the body block + named blocks; the syntax error means the decl leaked into local scope. Phase 3: walk every block transitively.
  4. bulk_dequant_kv_int* numerics — the decode helper rounds correctly per-byte, but the dequant pipeline assumes a specific pack/sign convention I’m not matching yet on the int8/int4 path.
  5. Conv MISMATCH familyiron_conv2d_patch16 etc. typically use 3-D index arithmetic that may need axis order verification on Vulkan.

None of these block Phase-2 ship (the codegen + runtime are stable; PASS counts grow as each is fixed); they’re the natural Phase 3 punch list alongside subgroup ops, fp16, and cooperative-matrix MMA.

  • Vulkan subgroup fast path — query VK_EXT_subgroup_size_control and VkSubgroupFeatureFlagBits at instance/device creation; use subgroupAdd/subgroupShuffle* when supported with the right size. Falls back cleanly to the Phase-2 portable tree if not.
  • Vulkan fp16 / bf16 / int8 — add a Phase-3 path behind VK_KHR_shader_float16_int8 + VK_KHR_16bit_storage / VK_KHR_shader_bfloat16 queries. The emitter changes are small (per-dtype glsl_scalar_type arms); the device-side feature declaration + spec-constant gating is the new surface.
  • HIP atomics — wire Op::Atomic through the existing CUDA emitter (already handles atomicAdd/Max/Min/etc., which HIP supports under the same names).
  • Subgroup ops on HIPOp::SimdReduce/SimdScan/SimdShuffle* already lower to __shfl_*_sync on the CUDA path; they should port via the existing text transform. Validate against the corpus.
  • CDNA hardware validation — once a wave64 device is available, run the corpus against TargetProfile::hip_wave64.
  • Cooperative-matrix MMA on VulkanVK_KHR_cooperative_matrix with the runtime shape-query ladder (§4.2). Major work; the spec already details the fallback path.