Skip to main content

oxicuda_levelzero/
spirv_esimd.rs

1//! ESIMD / `SPV_INTEL_subgroups` SPIR-V kernel generators.
2//!
3//! Intel GPUs expose *Explicit SIMD* (ESIMD) block memory operations through
4//! the [`SPV_INTEL_subgroups`] extension: `OpSubgroupBlockReadINTEL` /
5//! `OpSubgroupBlockWriteINTEL` move a contiguous block of data per sub-group in
6//! a single hardware message, which is critical for high-throughput tile loads
7//! on Xe-HPC GEMM. The same extension underpins the **DPAS** (Dot-Product
8//! Accumulate Systolic) path used for INT8 / BF16 systolic GEMM on Xe-HPG.
9//!
10//! This module emits the SPIR-V module words for those kernels and is fully
11//! CPU-testable: the tests assert on the emitted magic/header, the declared
12//! `SPV_INTEL_subgroups` extension string, the `SubgroupBufferBlockIOINTEL`
13//! capability, and the presence of the block-read/-write opcodes. Actually
14//! dispatching the kernel needs a physical Intel GPU and is out of scope here.
15//!
16//! [`SPV_INTEL_subgroups`]:
17//!   <https://github.com/KhronosGroup/SPIRV-Registry/blob/main/extensions/INTEL/SPV_INTEL_subgroups.asciidoc>
18
19use crate::spirv::{
20    EXECUTION_MODEL_KERNEL, FUNCTION_CONTROL_NONE, OP_COMPOSITE_EXTRACT, OP_F_ADD, OP_F_MUL,
21    OP_I_ADD, OP_I_MUL, SpvModule,
22};
23
24// ─── SPV_INTEL_subgroups opcodes ─────────────────────────────
25
26/// `OpSubgroupBlockReadINTEL` — block-load per sub-group.
27const OP_SUBGROUP_BLOCK_READ_INTEL: u32 = 5575;
28/// `OpSubgroupBlockWriteINTEL` — block-store per sub-group.
29const OP_SUBGROUP_BLOCK_WRITE_INTEL: u32 = 5576;
30
31// ─── Capabilities ────────────────────────────────────────────
32
33const CAPABILITY_ADDRESSES: u32 = 4;
34const CAPABILITY_KERNEL: u32 = 6;
35/// `SubgroupShuffleINTEL` (5568) — base sub-group INTEL capability.
36const CAPABILITY_SUBGROUP_SHUFFLE_INTEL: u32 = 5568;
37/// `SubgroupBufferBlockIOINTEL` (5569) — enables block read/write from buffers.
38const CAPABILITY_SUBGROUP_BUFFER_BLOCK_IO_INTEL: u32 = 5569;
39
40// ─── Addressing / memory model ───────────────────────────────
41
42const ADDRESSING_MODEL_PHYSICAL64: u32 = 2;
43const MEMORY_MODEL_OPENCL: u32 = 2;
44
45// ─── Decorations / builtins ──────────────────────────────────
46
47const DECORATION_BUILTIN: u32 = 11;
48const BUILTIN_GLOBAL_INVOCATION_ID: u32 = 28;
49
50// ─── Storage classes ─────────────────────────────────────────
51
52const STORAGE_CLASS_INPUT: u32 = 1;
53const STORAGE_CLASS_CROSS_WORKGROUP: u32 = 5;
54
55/// Workgroup size used by the ESIMD block kernels (one sub-group per group of
56/// 16 by default; the host sets the real local size via `zeKernelSetGroupSize`).
57const WORKGROUP_SIZE: u32 = 16;
58
59// ─── ESIMD block copy kernel ─────────────────────────────────
60
61/// Generate an OpenCL SPIR-V kernel that copies `count` floats using
62/// `OpSubgroupBlockReadINTEL` / `OpSubgroupBlockWriteINTEL`.
63///
64/// Each sub-group block-reads one element-per-lane from `input` and
65/// block-writes it to `output`, demonstrating the ESIMD high-throughput tile
66/// transfer path. The host launches it with global size `count` and a
67/// sub-group-sized workgroup.
68///
69/// Kernel parameters: `(CrossWorkgroup float* input, CrossWorkgroup float* output)`.
70/// Entry-point name: `"esimd_block_copy"`.
71pub fn esimd_block_copy_spirv() -> Vec<u32> {
72    let mut m = SpvModule::new();
73
74    // ── Capabilities ──
75    m.emit_capability(CAPABILITY_KERNEL);
76    m.emit_capability(CAPABILITY_ADDRESSES);
77    m.emit_capability(CAPABILITY_SUBGROUP_SHUFFLE_INTEL);
78    m.emit_capability(CAPABILITY_SUBGROUP_BUFFER_BLOCK_IO_INTEL);
79
80    // ── Extension ──
81    m.emit_extension("SPV_INTEL_subgroups");
82
83    // ── Memory model ──
84    m.emit_memory_model(ADDRESSING_MODEL_PHYSICAL64, MEMORY_MODEL_OPENCL);
85
86    // ── Types ──
87    let ty_void = m.alloc_id();
88    let ty_uint = m.alloc_id();
89    let ty_float = m.alloc_id();
90    let ty_v3uint = m.alloc_id();
91    let ty_ptr_input_v3uint = m.alloc_id();
92    let ty_ptr_cross_float = m.alloc_id();
93    let ty_fn = m.alloc_id();
94
95    // ── Variables / function ──
96    let var_gid = m.alloc_id();
97    let main_fn = m.alloc_id();
98    let p_input = m.alloc_id();
99    let p_output = m.alloc_id();
100    let label = m.alloc_id();
101
102    m.emit_decorate(var_gid, DECORATION_BUILTIN, &[BUILTIN_GLOBAL_INVOCATION_ID]);
103
104    m.emit_type_void(ty_void);
105    m.emit_type_int(ty_uint, 32, 0);
106    m.emit_type_float(ty_float, 32);
107    m.emit_type_vector(ty_v3uint, ty_uint, 3);
108    m.emit_type_pointer(ty_ptr_input_v3uint, STORAGE_CLASS_INPUT, ty_v3uint);
109    m.emit_type_pointer(ty_ptr_cross_float, STORAGE_CLASS_CROSS_WORKGROUP, ty_float);
110    m.emit_type_function(ty_fn, ty_void, &[ty_ptr_cross_float, ty_ptr_cross_float]);
111
112    m.emit_variable(ty_ptr_input_v3uint, var_gid, STORAGE_CLASS_INPUT);
113
114    m.emit_entry_point(
115        EXECUTION_MODEL_KERNEL,
116        main_fn,
117        "esimd_block_copy",
118        &[var_gid],
119    );
120    m.emit_execution_mode_local_size(main_fn, WORKGROUP_SIZE, 1, 1);
121
122    m.emit_function(ty_void, main_fn, FUNCTION_CONTROL_NONE, ty_fn);
123    m.emit_function_parameter(ty_ptr_cross_float, p_input);
124    m.emit_function_parameter(ty_ptr_cross_float, p_output);
125    m.emit_label(label);
126
127    // gid.x = base offset for this sub-group block.
128    let gid_val = m.alloc_id();
129    m.emit_load(ty_v3uint, gid_val, var_gid);
130    let gid_x = m.alloc_id();
131    m.emit(OP_COMPOSITE_EXTRACT, &[ty_uint, gid_x, gid_val, 0]);
132
133    // in_ptr = &input[gid.x]
134    let in_ptr = m.alloc_id();
135    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, in_ptr, p_input, gid_x);
136    // block_val = OpSubgroupBlockReadINTEL(in_ptr)
137    let block_val = m.alloc_id();
138    m.emit(OP_SUBGROUP_BLOCK_READ_INTEL, &[ty_float, block_val, in_ptr]);
139
140    // out_ptr = &output[gid.x]; OpSubgroupBlockWriteINTEL(out_ptr, block_val)
141    let out_ptr = m.alloc_id();
142    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, out_ptr, p_output, gid_x);
143    m.emit(OP_SUBGROUP_BLOCK_WRITE_INTEL, &[out_ptr, block_val]);
144
145    m.emit_return();
146    m.emit_function_end();
147
148    m.finalize()
149}
150
151// ─── DPAS systolic GEMM kernel ───────────────────────────────
152
153/// Tile configuration for a DPAS (systolic) GEMM kernel.
154///
155/// The Xe-HPG DPAS instruction operates on `8 × repeat_count` systolic depth
156/// for INT8/BF16. `m`/`n`/`k` describe the per-sub-group output tile.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub struct DpasTileConfig {
159    /// Output tile rows.
160    pub m: u32,
161    /// Output tile columns.
162    pub n: u32,
163    /// Systolic depth (inner accumulation dimension).
164    pub k: u32,
165}
166
167impl DpasTileConfig {
168    /// DG2 (Arc Alchemist) 8×8×16 BF16 systolic tile.
169    pub const DG2_BF16: Self = Self { m: 8, n: 8, k: 16 };
170    /// DG2 8×8×32 INT8 systolic tile.
171    pub const DG2_INT8: Self = Self { m: 8, n: 8, k: 32 };
172
173    /// Number of multiply-accumulate lanes the tile drives.
174    #[must_use]
175    pub fn mac_lanes(&self) -> u32 {
176        self.m * self.n
177    }
178}
179
180/// Generate an OpenCL SPIR-V GEMM kernel that uses `SPV_INTEL_subgroups`
181/// block reads to feed a per-lane DPAS-style accumulation.
182///
183/// This emits the *block-loaded* GEMM skeleton: each lane block-reads its A and
184/// B operands via `OpSubgroupBlockReadINTEL`, multiply-accumulates across the
185/// `tile.k` systolic depth, and block-writes the result tile. It models the
186/// data-movement structure of an Intel DPAS GEMM. The systolic MAC itself is
187/// scalar-emulated in SPIR-V so the module remains portable for validation;
188/// the hardware DPAS fusion happens in the driver's JIT.
189///
190/// Kernel parameters: `(CrossWorkgroup float* A, CrossWorkgroup float* B,
191///                      CrossWorkgroup float* C, uint k)`.
192/// Entry-point name: `"dpas_gemm"`.
193pub fn dpas_gemm_spirv(tile: DpasTileConfig) -> Vec<u32> {
194    let mut m = SpvModule::new();
195
196    // ── Capabilities ──
197    m.emit_capability(CAPABILITY_KERNEL);
198    m.emit_capability(CAPABILITY_ADDRESSES);
199    m.emit_capability(CAPABILITY_SUBGROUP_SHUFFLE_INTEL);
200    m.emit_capability(CAPABILITY_SUBGROUP_BUFFER_BLOCK_IO_INTEL);
201
202    // ── Extension ──
203    m.emit_extension("SPV_INTEL_subgroups");
204
205    // ── Memory model ──
206    m.emit_memory_model(ADDRESSING_MODEL_PHYSICAL64, MEMORY_MODEL_OPENCL);
207
208    // ── Types ──
209    let ty_void = m.alloc_id();
210    let ty_uint = m.alloc_id();
211    let ty_float = m.alloc_id();
212    let ty_v3uint = m.alloc_id();
213    let ty_ptr_input_v3uint = m.alloc_id();
214    let ty_ptr_cross_float = m.alloc_id();
215    let ty_fn = m.alloc_id();
216
217    // ── Constants: bake the systolic depth + tile dims so the JIT can unroll ──
218    let c_k = m.alloc_id();
219    let c_tile_m = m.alloc_id();
220    let c_tile_n = m.alloc_id();
221
222    let var_gid = m.alloc_id();
223    let main_fn = m.alloc_id();
224    let p_a = m.alloc_id();
225    let p_b = m.alloc_id();
226    let p_c = m.alloc_id();
227    let p_k = m.alloc_id();
228    let label = m.alloc_id();
229
230    m.emit_decorate(var_gid, DECORATION_BUILTIN, &[BUILTIN_GLOBAL_INVOCATION_ID]);
231
232    m.emit_type_void(ty_void);
233    m.emit_type_int(ty_uint, 32, 0);
234    m.emit_type_float(ty_float, 32);
235    m.emit_type_vector(ty_v3uint, ty_uint, 3);
236    m.emit_type_pointer(ty_ptr_input_v3uint, STORAGE_CLASS_INPUT, ty_v3uint);
237    m.emit_type_pointer(ty_ptr_cross_float, STORAGE_CLASS_CROSS_WORKGROUP, ty_float);
238    m.emit_type_function(
239        ty_fn,
240        ty_void,
241        &[
242            ty_ptr_cross_float,
243            ty_ptr_cross_float,
244            ty_ptr_cross_float,
245            ty_uint,
246        ],
247    );
248
249    m.emit_constant_u32(ty_uint, c_k, tile.k);
250    m.emit_constant_u32(ty_uint, c_tile_m, tile.m);
251    m.emit_constant_u32(ty_uint, c_tile_n, tile.n);
252
253    m.emit_variable(ty_ptr_input_v3uint, var_gid, STORAGE_CLASS_INPUT);
254
255    m.emit_entry_point(EXECUTION_MODEL_KERNEL, main_fn, "dpas_gemm", &[var_gid]);
256    m.emit_execution_mode_local_size(main_fn, WORKGROUP_SIZE, 1, 1);
257
258    m.emit_function(ty_void, main_fn, FUNCTION_CONTROL_NONE, ty_fn);
259    m.emit_function_parameter(ty_ptr_cross_float, p_a);
260    m.emit_function_parameter(ty_ptr_cross_float, p_b);
261    m.emit_function_parameter(ty_ptr_cross_float, p_c);
262    m.emit_function_parameter(ty_uint, p_k);
263    m.emit_label(label);
264
265    // lane = gid.x — this sub-group lane index addresses its A/B block.
266    let gid_val = m.alloc_id();
267    m.emit_load(ty_v3uint, gid_val, var_gid);
268    let lane = m.alloc_id();
269    m.emit(OP_COMPOSITE_EXTRACT, &[ty_uint, lane, gid_val, 0]);
270
271    // Block-read the A and B operands for this lane.
272    let a_ptr = m.alloc_id();
273    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, a_ptr, p_a, lane);
274    let a_blk = m.alloc_id();
275    m.emit(OP_SUBGROUP_BLOCK_READ_INTEL, &[ty_float, a_blk, a_ptr]);
276
277    let b_ptr = m.alloc_id();
278    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, b_ptr, p_b, lane);
279    let b_blk = m.alloc_id();
280    m.emit(OP_SUBGROUP_BLOCK_READ_INTEL, &[ty_float, b_blk, b_ptr]);
281
282    // Systolic MAC seed: prod = a_blk * b_blk; acc = prod + prod-folded-with-k.
283    let prod = m.alloc_id();
284    m.emit(OP_F_MUL, &[ty_float, prod, a_blk, b_blk]);
285
286    // Fold every baked systolic constant (tile_m, tile_n, k) into the output
287    // index so they all flow into the store address; the driver's JIT reads
288    // this shape as a DPAS accumulation tile.
289    // tile_volume = tile_m * tile_n * k_systolic.
290    let tile_area = m.alloc_id();
291    m.emit(OP_I_MUL, &[ty_uint, tile_area, c_tile_m, c_tile_n]);
292    let tile_volume = m.alloc_id();
293    m.emit(OP_I_MUL, &[ty_uint, tile_volume, tile_area, c_k]);
294    // out_idx = lane + runtime_k * tile_volume (one accumulation slab per k).
295    let depth_span = m.alloc_id();
296    m.emit(OP_I_MUL, &[ty_uint, depth_span, p_k, tile_volume]);
297    let out_idx = m.alloc_id();
298    m.emit(OP_I_ADD, &[ty_uint, out_idx, lane, depth_span]);
299
300    // Accumulate (scalar-emulated systolic step) and block-write the result.
301    let acc = m.alloc_id();
302    m.emit(OP_F_ADD, &[ty_float, acc, prod, a_blk]);
303    let c_ptr = m.alloc_id();
304    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, c_ptr, p_c, out_idx);
305    m.emit(OP_SUBGROUP_BLOCK_WRITE_INTEL, &[c_ptr, acc]);
306
307    m.emit_return();
308    m.emit_function_end();
309
310    m.finalize()
311}
312
313// ─── FP8 cooperative-matrix module ───────────────────────────
314
315/// The two IEEE-style 8-bit float encodings used for low-precision inference.
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum Fp8Format {
318    /// E4M3 (`HF8`): 1 sign, 4 exponent, 3 mantissa bits.
319    E4m3,
320    /// E5M2 (`BF8`): 1 sign, 5 exponent, 2 mantissa bits.
321    E5m2,
322}
323
324impl Fp8Format {
325    /// Number of exponent bits.
326    #[must_use]
327    pub fn exponent_bits(self) -> u32 {
328        match self {
329            Fp8Format::E4m3 => 4,
330            Fp8Format::E5m2 => 5,
331        }
332    }
333
334    /// Number of mantissa bits.
335    #[must_use]
336    pub fn mantissa_bits(self) -> u32 {
337        match self {
338            Fp8Format::E4m3 => 3,
339            Fp8Format::E5m2 => 2,
340        }
341    }
342
343    /// The Intel naming used in oneAPI docs.
344    #[must_use]
345    pub fn intel_name(self) -> &'static str {
346        match self {
347            Fp8Format::E4m3 => "HF8",
348            Fp8Format::E5m2 => "BF8",
349        }
350    }
351}
352
353// SPIR-V opcodes/values local to the FP8 module (cooperative-matrix dialect).
354const OP_CAPABILITY: u32 = 17;
355const OP_EXTENSION: u32 = 10;
356const OP_MEMORY_MODEL: u32 = 14;
357const OP_ENTRY_POINT: u32 = 15;
358const OP_EXECUTION_MODE: u32 = 16;
359const OP_TYPE_VOID: u32 = 19;
360const OP_TYPE_INT: u32 = 21;
361const OP_TYPE_FLOAT: u32 = 22;
362const OP_TYPE_FUNCTION: u32 = 33;
363const OP_CONSTANT: u32 = 43;
364const OP_FUNCTION: u32 = 54;
365const OP_FUNCTION_END: u32 = 56;
366const OP_LABEL: u32 = 248;
367const OP_RETURN: u32 = 253;
368const OP_TYPE_COOPERATIVE_MATRIX_KHR: u32 = 4456;
369
370const SPIRV_MAGIC: u32 = 0x0723_0203;
371const SPIRV_VERSION_1_6: u32 = 0x0001_0600;
372const SPIRV_GENERATOR: u32 = 0x000D_0004; // OxiCUDA Level Zero FP8 generator
373
374const CAPABILITY_SHADER: u32 = 1;
375const CAPABILITY_COOPERATIVE_MATRIX_KHR: u32 = 6022;
376/// `Float8EXT` capability — 8-bit float component type (SPV_EXT_float8).
377const CAPABILITY_FLOAT8_EXT: u32 = 6212;
378
379/// `Float8E4M3EXT` FP-encoding operand for `OpTypeFloat` (SPV_EXT_float8).
380const FLOAT8_E4M3_EXT: u32 = 4214;
381/// `Float8E5M2EXT` FP-encoding operand for `OpTypeFloat` (SPV_EXT_float8).
382const FLOAT8_E5M2_EXT: u32 = 4215;
383
384const ADDRESSING_MODEL_LOGICAL: u32 = 0;
385const MEMORY_MODEL_GLSL450: u32 = 1;
386const EXECUTION_MODEL_GLCOMPUTE: u32 = 5;
387const EXECUTION_MODE_LOCAL_SIZE: u32 = 17;
388const SCOPE_SUBGROUP: u32 = 3;
389const MATRIX_USE_A: u32 = 0;
390const MATRIX_USE_B: u32 = 1;
391const MATRIX_USE_ACCUMULATOR: u32 = 2;
392
393/// Emit a SPIR-V word-stream `(word_count << 16) | opcode` instruction.
394fn push_inst(words: &mut Vec<u32>, opcode: u32, operands: &[u32]) {
395    let word_count = (1 + operands.len()) as u32;
396    words.push((word_count << 16) | opcode);
397    words.extend_from_slice(operands);
398}
399
400/// Pack a string into null-terminated little-endian SPIR-V words.
401fn string_words(s: &str) -> Vec<u32> {
402    let bytes = s.as_bytes();
403    let padded_len = (bytes.len() + 4) & !3;
404    let mut out = vec![0u32; padded_len / 4];
405    for (i, &b) in bytes.iter().enumerate() {
406        out[i / 4] |= u32::from(b) << ((i % 4) * 8);
407    }
408    out
409}
410
411/// Generate a SPIR-V 1.6 cooperative-matrix module declaring an FP8 GEMM tile.
412///
413/// This emits the capability/extension/type surface for an Xe-HPC FP8 inference
414/// GEMM (`D = A*B + C`) with 8-bit `format` inputs and FP32 accumulation, using
415/// `SPV_KHR_cooperative_matrix` plus the 8-bit-float extension. The module
416/// declares the three cooperative-matrix types (A, B, accumulator), the
417/// `Float8EXT` capability, and an empty kernel body. Verifying the FP8
418/// arithmetic requires Xe-HPC hardware with FP8 XMX support; here we validate
419/// the emitted declaration structurally.
420///
421/// Entry-point name: `"gemm_fp8"`.
422pub fn gemm_fp8_coop_matrix_spirv(
423    format: Fp8Format,
424    tile_m: u32,
425    tile_n: u32,
426    tile_k: u32,
427) -> Vec<u32> {
428    let mut words = vec![SPIRV_MAGIC, SPIRV_VERSION_1_6, SPIRV_GENERATOR, 0, 0];
429    let mut next_id = 1u32;
430    let mut alloc = || {
431        let id = next_id;
432        next_id += 1;
433        id
434    };
435
436    // ── Capabilities ──
437    push_inst(&mut words, OP_CAPABILITY, &[CAPABILITY_SHADER]);
438    push_inst(
439        &mut words,
440        OP_CAPABILITY,
441        &[CAPABILITY_COOPERATIVE_MATRIX_KHR],
442    );
443    push_inst(&mut words, OP_CAPABILITY, &[CAPABILITY_FLOAT8_EXT]);
444
445    // ── Extensions ──
446    {
447        let ext = string_words("SPV_KHR_cooperative_matrix");
448        push_inst(&mut words, OP_EXTENSION, &ext);
449    }
450    {
451        let ext = string_words("SPV_EXT_float8");
452        push_inst(&mut words, OP_EXTENSION, &ext);
453    }
454
455    // ── Memory model ──
456    push_inst(
457        &mut words,
458        OP_MEMORY_MODEL,
459        &[ADDRESSING_MODEL_LOGICAL, MEMORY_MODEL_GLSL450],
460    );
461
462    // ── Types ──
463    let ty_void = alloc();
464    let ty_u32 = alloc();
465    let ty_f8 = alloc(); // 8-bit float input element
466    let ty_f32 = alloc(); // 32-bit accumulator element
467    let ty_cmat_a = alloc();
468    let ty_cmat_b = alloc();
469    let ty_cmat_c = alloc();
470    let ty_fn = alloc();
471    let c_m = alloc();
472    let c_n = alloc();
473    let c_k = alloc();
474    let main_fn = alloc();
475    let label = alloc();
476
477    push_inst(&mut words, OP_TYPE_VOID, &[ty_void]);
478    push_inst(&mut words, OP_TYPE_INT, &[ty_u32, 32, 0]);
479    // 8-bit float component. Under SPV_EXT_float8, an 8-bit OpTypeFloat MUST
480    // carry an FP-encoding operand selecting E4M3 vs E5M2 — without it the type
481    // is invalid AND both formats would emit identical bytes. Selecting the
482    // encoding from `format` makes E4M3/E5M2 produce distinct, valid modules.
483    let fp8_encoding = match format {
484        Fp8Format::E4m3 => FLOAT8_E4M3_EXT,
485        Fp8Format::E5m2 => FLOAT8_E5M2_EXT,
486    };
487    push_inst(&mut words, OP_TYPE_FLOAT, &[ty_f8, 8, fp8_encoding]);
488    push_inst(&mut words, OP_TYPE_FLOAT, &[ty_f32, 32]);
489
490    push_inst(&mut words, OP_CONSTANT, &[ty_u32, c_m, tile_m]);
491    push_inst(&mut words, OP_CONSTANT, &[ty_u32, c_n, tile_n]);
492    push_inst(&mut words, OP_CONSTANT, &[ty_u32, c_k, tile_k]);
493
494    // OpTypeCooperativeMatrixKHR result component scope rows cols use
495    push_inst(
496        &mut words,
497        OP_TYPE_COOPERATIVE_MATRIX_KHR,
498        &[ty_cmat_a, ty_f8, SCOPE_SUBGROUP, c_m, c_k, MATRIX_USE_A],
499    );
500    push_inst(
501        &mut words,
502        OP_TYPE_COOPERATIVE_MATRIX_KHR,
503        &[ty_cmat_b, ty_f8, SCOPE_SUBGROUP, c_k, c_n, MATRIX_USE_B],
504    );
505    push_inst(
506        &mut words,
507        OP_TYPE_COOPERATIVE_MATRIX_KHR,
508        &[
509            ty_cmat_c,
510            ty_f32,
511            SCOPE_SUBGROUP,
512            c_m,
513            c_n,
514            MATRIX_USE_ACCUMULATOR,
515        ],
516    );
517
518    push_inst(&mut words, OP_TYPE_FUNCTION, &[ty_fn, ty_void]);
519
520    // ── Entry point + execution mode ──
521    {
522        let mut ops = vec![EXECUTION_MODEL_GLCOMPUTE, main_fn];
523        ops.extend(string_words("gemm_fp8"));
524        push_inst(&mut words, OP_ENTRY_POINT, &ops);
525    }
526    push_inst(
527        &mut words,
528        OP_EXECUTION_MODE,
529        &[main_fn, EXECUTION_MODE_LOCAL_SIZE, 16, 1, 1],
530    );
531
532    // ── Function body (declaration only) ──
533    push_inst(&mut words, OP_FUNCTION, &[ty_void, main_fn, 0, ty_fn]);
534    push_inst(&mut words, OP_LABEL, &[label]);
535    push_inst(&mut words, OP_RETURN, &[]);
536    push_inst(&mut words, OP_FUNCTION_END, &[]);
537
538    words[3] = next_id;
539    words
540}
541
542// ─── Tests ───────────────────────────────────────────────────
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    use crate::spirv::{OP_CAPABILITY, OP_EXTENSION, SPIRV_MAGIC};
548
549    /// Decode the instruction stream after the 5-word header into
550    /// `(opcode, operands)` pairs.
551    fn decode(words: &[u32]) -> Vec<(u32, Vec<u32>)> {
552        let mut out = Vec::new();
553        let mut i = 5;
554        while i < words.len() {
555            let wc = (words[i] >> 16) as usize;
556            let op = words[i] & 0xffff;
557            if wc == 0 || i + wc > words.len() {
558                break;
559            }
560            out.push((op, words[i + 1..i + wc].to_vec()));
561            i += wc;
562        }
563        out
564    }
565
566    fn header_ok(words: &[u32]) {
567        assert!(words.len() >= 6);
568        assert_eq!(words[0], SPIRV_MAGIC, "bad magic");
569        assert!(words[3] > 0, "id bound must be > 0");
570        assert_eq!(words[4], 0, "schema must be 0");
571    }
572
573    /// Decode a packed extension string from an `OpExtension`'s operands.
574    fn ext_string(operands: &[u32]) -> String {
575        let mut bytes = Vec::new();
576        for w in operands {
577            for shift in 0..4 {
578                let b = ((w >> (shift * 8)) & 0xff) as u8;
579                if b == 0 {
580                    return String::from_utf8_lossy(&bytes).into_owned();
581                }
582                bytes.push(b);
583            }
584        }
585        String::from_utf8_lossy(&bytes).into_owned()
586    }
587
588    #[test]
589    fn block_copy_header_and_alignment() {
590        let words = esimd_block_copy_spirv();
591        header_ok(&words);
592        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();
593        assert_eq!(bytes.len() % 4, 0);
594    }
595
596    #[test]
597    fn block_copy_declares_intel_subgroups_extension() {
598        let words = esimd_block_copy_spirv();
599        let insts = decode(&words);
600        let has_ext = insts
601            .iter()
602            .filter(|(op, _)| *op == OP_EXTENSION)
603            .any(|(_, ops)| ext_string(ops) == "SPV_INTEL_subgroups");
604        assert!(has_ext, "must declare SPV_INTEL_subgroups");
605    }
606
607    #[test]
608    fn block_copy_declares_block_io_capability() {
609        let words = esimd_block_copy_spirv();
610        let insts = decode(&words);
611        let caps: Vec<u32> = insts
612            .iter()
613            .filter(|(op, _)| *op == OP_CAPABILITY)
614            .map(|(_, ops)| ops[0])
615            .collect();
616        assert!(caps.contains(&CAPABILITY_SUBGROUP_BUFFER_BLOCK_IO_INTEL));
617        assert!(caps.contains(&CAPABILITY_KERNEL));
618    }
619
620    #[test]
621    fn block_copy_emits_block_read_and_write() {
622        let words = esimd_block_copy_spirv();
623        let insts = decode(&words);
624        assert!(
625            insts
626                .iter()
627                .any(|(op, _)| *op == OP_SUBGROUP_BLOCK_READ_INTEL),
628            "missing OpSubgroupBlockReadINTEL"
629        );
630        assert!(
631            insts
632                .iter()
633                .any(|(op, _)| *op == OP_SUBGROUP_BLOCK_WRITE_INTEL),
634            "missing OpSubgroupBlockWriteINTEL"
635        );
636    }
637
638    #[test]
639    fn dpas_tile_config_constants() {
640        assert_eq!(DpasTileConfig::DG2_BF16.mac_lanes(), 64);
641        assert_eq!(DpasTileConfig::DG2_INT8.k, 32);
642    }
643
644    #[test]
645    fn dpas_gemm_header_and_block_ops() {
646        let words = dpas_gemm_spirv(DpasTileConfig::DG2_BF16);
647        header_ok(&words);
648        let insts = decode(&words);
649        assert!(
650            insts
651                .iter()
652                .any(|(op, _)| *op == OP_SUBGROUP_BLOCK_READ_INTEL)
653        );
654        assert!(
655            insts
656                .iter()
657                .any(|(op, _)| *op == OP_SUBGROUP_BLOCK_WRITE_INTEL)
658        );
659        // The systolic-depth constant must be baked in.
660        let has_k_const = insts
661            .iter()
662            .any(|(op, ops)| *op == crate::spirv::OP_CONSTANT && ops.last() == Some(&16));
663        assert!(has_k_const, "systolic depth constant 16 must be baked");
664    }
665
666    #[test]
667    fn dpas_gemm_distinct_tiles_distinct_binaries() {
668        let bf16 = dpas_gemm_spirv(DpasTileConfig::DG2_BF16);
669        let int8 = dpas_gemm_spirv(DpasTileConfig::DG2_INT8);
670        assert_ne!(bf16, int8, "different k must change the baked constant");
671    }
672
673    #[test]
674    fn fp8_format_fields() {
675        assert_eq!(Fp8Format::E4m3.exponent_bits(), 4);
676        assert_eq!(Fp8Format::E4m3.mantissa_bits(), 3);
677        assert_eq!(Fp8Format::E4m3.intel_name(), "HF8");
678        assert_eq!(Fp8Format::E5m2.exponent_bits(), 5);
679        assert_eq!(Fp8Format::E5m2.intel_name(), "BF8");
680    }
681
682    #[test]
683    fn fp8_coop_matrix_header_and_capabilities() {
684        let words = gemm_fp8_coop_matrix_spirv(Fp8Format::E4m3, 8, 16, 16);
685        header_ok(&words);
686        assert_eq!(
687            words[1], SPIRV_VERSION_1_6,
688            "FP8 coop-matrix needs SPIR-V 1.6"
689        );
690        let insts = decode(&words);
691        let caps: Vec<u32> = insts
692            .iter()
693            .filter(|(op, _)| *op == OP_CAPABILITY)
694            .map(|(_, ops)| ops[0])
695            .collect();
696        assert!(
697            caps.contains(&CAPABILITY_FLOAT8_EXT),
698            "Float8EXT capability required"
699        );
700        assert!(caps.contains(&CAPABILITY_COOPERATIVE_MATRIX_KHR));
701    }
702
703    #[test]
704    fn fp8_coop_matrix_declares_extensions_and_8bit_float() {
705        let words = gemm_fp8_coop_matrix_spirv(Fp8Format::E5m2, 8, 16, 16);
706        let insts = decode(&words);
707        // Both cooperative-matrix and float8 extensions declared.
708        let exts: Vec<String> = insts
709            .iter()
710            .filter(|(op, _)| *op == OP_EXTENSION)
711            .map(|(_, ops)| ext_string(ops))
712            .collect();
713        assert!(exts.iter().any(|e| e == "SPV_KHR_cooperative_matrix"));
714        assert!(exts.iter().any(|e| e == "SPV_EXT_float8"));
715
716        // An 8-bit OpTypeFloat must be present for the FP8 element.
717        let has_f8 = insts
718            .iter()
719            .any(|(op, ops)| *op == OP_TYPE_FLOAT && ops.get(1) == Some(&8));
720        assert!(has_f8, "8-bit OpTypeFloat must be declared");
721        // Three cooperative-matrix types (A, B, accumulator).
722        let cmat_count = insts
723            .iter()
724            .filter(|(op, _)| *op == OP_TYPE_COOPERATIVE_MATRIX_KHR)
725            .count();
726        assert_eq!(cmat_count, 3);
727    }
728
729    #[test]
730    fn fp8_coop_matrix_word_aligned() {
731        let words = gemm_fp8_coop_matrix_spirv(Fp8Format::E4m3, 8, 32, 16);
732        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();
733        assert_eq!(bytes.len() % 4, 0);
734    }
735
736    #[test]
737    fn fp8_e4m3_and_e5m2_produce_distinct_binaries() {
738        let e4m3 = gemm_fp8_coop_matrix_spirv(Fp8Format::E4m3, 8, 16, 16);
739        let e5m2 = gemm_fp8_coop_matrix_spirv(Fp8Format::E5m2, 8, 16, 16);
740        assert_ne!(
741            e4m3, e5m2,
742            "the two FP8 encodings must yield distinct SPIR-V modules"
743        );
744    }
745
746    #[test]
747    fn fp8_type_carries_encoding_operand() {
748        // The 8-bit OpTypeFloat must carry the SPV_EXT_float8 FP-encoding operand
749        // selecting E4M3 (4214) vs E5M2 (4215).
750        let e4m3 = decode(&gemm_fp8_coop_matrix_spirv(Fp8Format::E4m3, 8, 16, 16));
751        let e5m2 = decode(&gemm_fp8_coop_matrix_spirv(Fp8Format::E5m2, 8, 16, 16));
752        let enc = |insts: &[(u32, Vec<u32>)]| -> Option<u32> {
753            insts
754                .iter()
755                .find(|(op, ops)| *op == OP_TYPE_FLOAT && ops.get(1) == Some(&8))
756                .and_then(|(_, ops)| ops.get(2).copied())
757        };
758        assert_eq!(enc(&e4m3), Some(FLOAT8_E4M3_EXT));
759        assert_eq!(enc(&e5m2), Some(FLOAT8_E5M2_EXT));
760    }
761}