コンテンツにスキップ

Vulkan / SPIR-V Backend Spec

このコンテンツはまだ日本語訳がありません。

Status: 📋 Proposed (design only; no implementation yet) Scope: Add a portable GPU backend that lowers Iron’s #[kernel] DSL / IR to SPIR-V and dispatches it through Vulkan compute — one backend that runs across AMD, NVIDIA, Intel, Qualcomm Adreno, ARM Mali, and Apple (via MoltenVK). Out of scope: model loading, graph execution, checkpoint readers — Iron is an optimized-kernel generator, not an inference engine.

Read CUDA_BACKEND_SPEC.md (the backend-seam design) and AMD_BACKEND_SPEC.md (the wavefront-width hazard) first. This spec reuses the same seam — IR + macro + quant::{codec,format} unchanged — and covers what’s Vulkan-specific.


CUDA (CUDA_BACKEND_SPEC.md) and HIP/ROCm (AMD_BACKEND_SPEC.md) are per-vendor, peak-perf backends — they reach tensor/matrix cores and hardware microscaling directly. Vulkan is the opposite trade:

One backend, (almost) every GPU. A single SPIR-V/Vulkan backend covers AMD, NVIDIA, Intel (Arc/Xe), Qualcomm Adreno (Android), ARM Mali, and Apple GPUs via MoltenVK — at the cost of vendor-specific peak features.

So Vulkan is the reach / fallback backend, not a replacement for the vendor-specific ones:

  • Use Metal on Apple, CUDA on NVIDIA, HIP on AMD for maximum perf.
  • Use Vulkan to reach everything else (Intel, mobile/embedded, ARM), and as a vendor-neutral baseline where a native backend doesn’t exist yet.

A second reason it’s attractive: SPIR-V is an IR, so Iron’s IR → SPIR-V is an IR-to-IR lowering (via rspirv), not a text-emit-then-reparse step — a more natural structural fit than MSL/CUDA-C++ text, and a good candidate for the reference portable backend.

Goals

  • A spirv codegen target lowering the IR to SPIR-V (direct via rspirv; or GLSL/ HLSL → SPIR-V via shaderc/glslang as an alternative).
  • A vulkan runtime Device impl (instance/device, VkShaderModule, compute VkPipeline, descriptor sets over storage VkBuffers, vkCmdDispatch, fences) — via ash (thin) or vulkano (safe).
  • Reuse the IR, the #[kernel] macro, and the entire quant::{codec,format} layer unchanged.
  • Runtime feature detection + graceful fallback for the non-guaranteed bits (subgroup size, cooperative matrix, fp16/int8) — the heart of portability.

Non-goals

  • Model execution / weight loading (engine concern).
  • Matching a native backend’s peak perf — Vulkan trades peak for reach.
  • Hardware E8M0 microscaling (no portable Vulkan path yet — software dequant).

3. What’s shared with the CUDA/AMD backends

Section titled “3. What’s shared with the CUDA/AMD backends”

The backend seam (CUDA_BACKEND_SPEC.md §4.1) and the bulk of the DSL→target mapping apply. The structural analogs:

Concept CUDA / HIP Vulkan / SPIR-V
dispatch <<<grid,block>>> / hipLaunchKernel vkCmdDispatch(gx,gy,gz)
workgroup id blockIdx.{x,y,z} gl_WorkGroupID
local thread id threadIdx.{x,y,z} gl_LocalInvocationID
workgroup size (lsize) blockDim local_size_* (SPIR-V exec mode / spec-constant)
shared memory __shared__ shared (Workgroup storage class)
buffers device pointers storage VkBuffer + descriptor sets
math __expf/exp2f/… SPIR-V ExtInst (GLSL.std.450: Exp, Exp2, InverseSqrt)
runtime compile NVRTC / hipRTC offline SPIR-V (or shaderc at runtime) + vkCreateShaderModule
decode intrinsics device fns SPIR-V arithmetic (ports verbatim)

program_id / tid / lsize, Grid3D/Reduction modes, threadgroup_alloc, the decode intrinsics, and the whole quant layer carry over.

4. What’s genuinely different for Vulkan

Section titled “4. What’s genuinely different for Vulkan”

4.1 Subgroup size is variable AND not guaranteed — the central hazard

Section titled “4.1 Subgroup size is variable AND not guaranteed — the central hazard”

This is the AMD wave32/64 problem taken to its limit: Vulkan’s subgroup (the warp/simdgroup/wavefront analog) has no fixed, portable size:

  • NVIDIA 32, AMD 32 or 64, Intel 8/16/32, Adreno/Mali vary, and a driver may even pick per-dispatch.
  • Subgroup ops (subgroupAdd, subgroupShuffle, …) require Vulkan 1.1 subgroup support and the relevant VkSubgroupFeatureFlagBits (arithmetic, shuffle, …) — themselves optional.

Iron’s pervasive 32-lane assumption cannot hold. Two coping strategies, used together:

  • VK_EXT_subgroup_size_control: query the supported range and require a specific subgroup size at pipeline creation (VkPipelineShaderStageRequired­ SubgroupSizeCreateInfo) where the device allows it — recover a known width.
  • Subgroup-agnostic reductions: lower reduce_sum/simd_sum to a workgroup-level reduction over shared memory (a barrier tree) that does not depend on subgroup width — portable, slightly slower. Use subgroup ops only as a fast path when size + features are confirmed.

The lowering must therefore make subgroup width a queried runtime value / spec-constant, not a compile-time 32. Geometry-audit discipline from the other specs carries over and matters most here.

4.2 Matrix multiply: VK_KHR_cooperative_matrix (optional, queried)

Section titled “4.2 Matrix multiply: VK_KHR_cooperative_matrix (optional, queried)”

The portable tensor/matrix-core access is VK_KHR_cooperative_matrix (with the newer VK_KHR_cooperative_matrix2 and the vendor VK_NV_* variants). It is:

  • optional — not all devices/drivers expose it (notably weaker on mobile);
  • runtime-shape-queriedvkGetPhysicalDeviceCooperativeMatrixPropertiesKHR returns the supported {M,N,K} × {A,B,C,result} × type combinations, which differ per vendor (and differ again from Metal 8×8 / CUDA 16×16×16 / AMD MFMA). The MMA kernels must pick a supported tile at runtime or fall back.

Fallback ladder for the MMA / cooperative kernels: VK_KHR_cooperative_matrix (preferred) → subgroup-op tiled MMA → plain shared-memory Reduction GEMM. The mpp::/InlineMsl Metal coop kernels are reimplemented against this ladder (same “doesn’t auto-port” caveat as CUDA/AMD).

  • fp16: VK_KHR_shader_float16_int8 (arithmetic) + VK_KHR_16bit_storage (buffers) — widely but not universally available.
  • int8 storage/arithmetic: same shader_float16_int8 + 8bit_storage.
  • bf16: VK_KHR_shader_bfloat16 is very new / sparse — treat bf16 as a fall-back-to-fp32 path on Vulkan for now.
  • Required SPIR-V capabilities (Float16, Int8, StorageBuffer16BitAccess, …) must be declared and the matching device features queried/enabled, else pipeline creation fails. Element-type availability becomes a per-device feature gate, mirroring how CUDA gates by compute capability.
  • Software-decode path (everywhere): the quant::codec decode is arithmetic → ports to SPIR-V verbatim, feeding dequant-into-shared + cooperative-matrix (fp16/int8) or the shared-mem GEMM fallback. This is the universal path.
  • No portable hardware microscaling. There is no portable Vulkan equivalent of Blackwell tcgen05 / CDNA4 MXFP today — the E8M0-microscaling hardware path is reachable only through the native CUDA/HIP backends. On Vulkan, mx*/ mxint* run via software dequant. (Re-evaluate if/when a VK_*_cooperative_ matrix scaling extension ships.)
  • nvfp8/fp8_* likewise run software-dequant unless a future FP8 cooperative-matrix type is exposed and queried.

Three ways to produce SPIR-V, mirroring the CUDA spec’s C++/NVRTC-vs-cuda-oxide choice:

Strategy How Notes
Direct SPIR-V (rspirv) — recommended IR → SPIR-V module via the rspirv builder IR-to-IR; no text round-trip; full control over capabilities/exec-modes. Best fit for Iron’s IR.
GLSL/HLSL text → shaderc emit GLSL compute, compile with shaderc/glslang reuses a text-emitter shape (like MSL/CUDA-C++); easy to read/debug; extra compile dep.
Rust → SPIR-V (rust-gpu) emit Rust device code, compile via Embark’s rust-gpu the Vulkan analog of cuda-oxide; idiomatic Rust kernels, but heavier toolchain + maturity caveats. On the radar, not the default.

Recommend direct rspirv as the primary path (natural IR→IR), with the GLSL/shaderc path as a debugging/bring-up convenience.

  1. Seam + SPIR-V smoke kernel. Spirv codegen target (rspirv) for an elementwise kernel; VulkanDevice over ash (module → compute pipeline → descriptor set → dispatch → readback); one #[test_kernel] green on a desktop GPU. Run spirv-val on emitted modules in CI.
  2. Portable reductions. Workgroup/shared-memory reductions independent of subgroup width; bring up Reduction-mode families (dequant, qgemv, rms-norm, gather, conv, flash-scalar). This retires the §4.1 hazard early.
  3. Subgroup fast path. Add VK_EXT_subgroup_size_control + subgroup-op reductions as a queried fast path over the portable baseline.
  4. Cooperative-matrix MMA. VK_KHR_cooperative_matrix with runtime shape-query + the §4.2 fallback ladder; software-dequant block-scaled.
  5. Feature/dtype gating + breadth. fp16/int8 extension gating; validate across ≥3 vendors (e.g. NVIDIA, AMD, Intel) + MoltenVK on Apple.
  6. CLI + CI. --target vulkan across build/test/bench; a multi-vendor (or software-rasterizer, e.g. SwiftShader/lavapipe) CI lane; roofline device-spec rows where queryable.
  • Subgroup-size variability (the big one). §4.1 — no portable fixed width; the portable reduction path is mandatory, subgroup ops are an optional fast path.
  • Optional everything. cooperative-matrix, fp16/int8, subgroup arithmetic are all extensions/features — every kernel needs a feature-detect + fallback, so the runtime is more conditional than CUDA/HIP.
  • No hardware microscaling. block-scaled = software dequant on Vulkan (§4.4); the E8M0 hardware payoff stays with the native backends.
  • Perf ceiling. Vulkan generally trails a tuned native backend on the same GPU (less vendor-specific tensor-core reach, more portable-but-generic codegen).
  • Driver fragmentation. behavior/perf vary widely across vendors + driver versions, especially mobile (Adreno/Mali) — broad testing required.
  • MoltenVK is a translation layer, not native — useful for reach/CI on Apple but slower than Iron’s own Metal backend; it’s a fallback, not the Apple path.
  • Tooling maturity: ash/vulkano/rspirv are mature; rust-gpu is less so (if that codegen strategy is chosen).

It’s the breadth backend: one SPIR-V/Vulkan target reaches Intel, Qualcomm, ARM, and any GPU without a native Iron backend (plus Apple via MoltenVK), reusing the IR + #[kernel] macro + the full quant codec. It complements — not replaces — the peak-perf native backends: Metal/CUDA/HIP for the vendor you’re on, Vulkan for everywhere else and as the vendor-neutral baseline.

  • Vulkan computeVkPipeline (compute), descriptor sets, vkCmdDispatch; the §3 host-mapping. Rust runtimes: ash (thin FFI), vulkano (safe).
  • SPIR-Vrspirv (Rust SPIR-V builder/assembler; the recommended direct emitter), shaderc/glslang (GLSL/HLSL→SPIR-V), spirv-val (validation), rust-gpu (Rust→SPIR-V, the cuda-oxide analog — §5).
  • Subgroups — Vulkan 1.1 subgroup ops + VkSubgroupFeatureFlagBits; VK_EXT_subgroup_size_control (§4.1).
  • Cooperative matrixVK_KHR_cooperative_matrix (+ _cooperative_matrix2, VK_NV_cooperative_matrix*), vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR (§4.2).
  • DtypesVK_KHR_shader_float16_int8, VK_KHR_16bit_storage / 8bit_storage, VK_KHR_shader_bfloat16 (§4.3).
  • MoltenVK — Vulkan-on-Metal (Apple reach/CI).
  • Prior art for GPU LLM compute on Vulkan: llama.cpp’s Vulkan backend, Kompute, wgpu (WebGPU over Vulkan/Metal/DX12).
  • CUDA_BACKEND_SPEC.md / AMD_BACKEND_SPEC.md — the shared seam + the wavefront/subgroup-width lineage this spec extends.