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
379const ADDRESSING_MODEL_LOGICAL: u32 = 0;
380const MEMORY_MODEL_GLSL450: u32 = 1;
381const EXECUTION_MODEL_GLCOMPUTE: u32 = 5;
382const EXECUTION_MODE_LOCAL_SIZE: u32 = 17;
383const SCOPE_SUBGROUP: u32 = 3;
384const MATRIX_USE_A: u32 = 0;
385const MATRIX_USE_B: u32 = 1;
386const MATRIX_USE_ACCUMULATOR: u32 = 2;
387
388/// Emit a SPIR-V word-stream `(word_count << 16) | opcode` instruction.
389fn push_inst(words: &mut Vec<u32>, opcode: u32, operands: &[u32]) {
390    let word_count = (1 + operands.len()) as u32;
391    words.push((word_count << 16) | opcode);
392    words.extend_from_slice(operands);
393}
394
395/// Pack a string into null-terminated little-endian SPIR-V words.
396fn string_words(s: &str) -> Vec<u32> {
397    let bytes = s.as_bytes();
398    let padded_len = (bytes.len() + 4) & !3;
399    let mut out = vec![0u32; padded_len / 4];
400    for (i, &b) in bytes.iter().enumerate() {
401        out[i / 4] |= u32::from(b) << ((i % 4) * 8);
402    }
403    out
404}
405
406/// Generate a SPIR-V 1.6 cooperative-matrix module declaring an FP8 GEMM tile.
407///
408/// This emits the capability/extension/type surface for an Xe-HPC FP8 inference
409/// GEMM (`D = A*B + C`) with 8-bit `format` inputs and FP32 accumulation, using
410/// `SPV_KHR_cooperative_matrix` plus the 8-bit-float extension. The module
411/// declares the three cooperative-matrix types (A, B, accumulator), the
412/// `Float8EXT` capability, and an empty kernel body. Verifying the FP8
413/// arithmetic requires Xe-HPC hardware with FP8 XMX support; here we validate
414/// the emitted declaration structurally.
415///
416/// Entry-point name: `"gemm_fp8"`.
417pub fn gemm_fp8_coop_matrix_spirv(
418    format: Fp8Format,
419    tile_m: u32,
420    tile_n: u32,
421    tile_k: u32,
422) -> Vec<u32> {
423    let mut words = vec![SPIRV_MAGIC, SPIRV_VERSION_1_6, SPIRV_GENERATOR, 0, 0];
424    let mut next_id = 1u32;
425    let mut alloc = || {
426        let id = next_id;
427        next_id += 1;
428        id
429    };
430
431    // ── Capabilities ──
432    push_inst(&mut words, OP_CAPABILITY, &[CAPABILITY_SHADER]);
433    push_inst(
434        &mut words,
435        OP_CAPABILITY,
436        &[CAPABILITY_COOPERATIVE_MATRIX_KHR],
437    );
438    push_inst(&mut words, OP_CAPABILITY, &[CAPABILITY_FLOAT8_EXT]);
439
440    // ── Extensions ──
441    {
442        let ext = string_words("SPV_KHR_cooperative_matrix");
443        push_inst(&mut words, OP_EXTENSION, &ext);
444    }
445    {
446        let ext = string_words("SPV_EXT_float8");
447        push_inst(&mut words, OP_EXTENSION, &ext);
448    }
449
450    // ── Memory model ──
451    push_inst(
452        &mut words,
453        OP_MEMORY_MODEL,
454        &[ADDRESSING_MODEL_LOGICAL, MEMORY_MODEL_GLSL450],
455    );
456
457    // ── Types ──
458    let ty_void = alloc();
459    let ty_u32 = alloc();
460    let ty_f8 = alloc(); // 8-bit float input element
461    let ty_f32 = alloc(); // 32-bit accumulator element
462    let ty_cmat_a = alloc();
463    let ty_cmat_b = alloc();
464    let ty_cmat_c = alloc();
465    let ty_fn = alloc();
466    let c_m = alloc();
467    let c_n = alloc();
468    let c_k = alloc();
469    let main_fn = alloc();
470    let label = alloc();
471
472    push_inst(&mut words, OP_TYPE_VOID, &[ty_void]);
473    push_inst(&mut words, OP_TYPE_INT, &[ty_u32, 32, 0]);
474    // 8-bit float component; the format's exponent/mantissa is metadata for the
475    // driver. SPIR-V models the width; we tag the encoding in the generator.
476    push_inst(&mut words, OP_TYPE_FLOAT, &[ty_f8, 8]);
477    push_inst(&mut words, OP_TYPE_FLOAT, &[ty_f32, 32]);
478
479    push_inst(&mut words, OP_CONSTANT, &[ty_u32, c_m, tile_m]);
480    push_inst(&mut words, OP_CONSTANT, &[ty_u32, c_n, tile_n]);
481    push_inst(&mut words, OP_CONSTANT, &[ty_u32, c_k, tile_k]);
482
483    // OpTypeCooperativeMatrixKHR result component scope rows cols use
484    push_inst(
485        &mut words,
486        OP_TYPE_COOPERATIVE_MATRIX_KHR,
487        &[ty_cmat_a, ty_f8, SCOPE_SUBGROUP, c_m, c_k, MATRIX_USE_A],
488    );
489    push_inst(
490        &mut words,
491        OP_TYPE_COOPERATIVE_MATRIX_KHR,
492        &[ty_cmat_b, ty_f8, SCOPE_SUBGROUP, c_k, c_n, MATRIX_USE_B],
493    );
494    push_inst(
495        &mut words,
496        OP_TYPE_COOPERATIVE_MATRIX_KHR,
497        &[
498            ty_cmat_c,
499            ty_f32,
500            SCOPE_SUBGROUP,
501            c_m,
502            c_n,
503            MATRIX_USE_ACCUMULATOR,
504        ],
505    );
506
507    push_inst(&mut words, OP_TYPE_FUNCTION, &[ty_fn, ty_void]);
508
509    // ── Entry point + execution mode ──
510    {
511        let mut ops = vec![EXECUTION_MODEL_GLCOMPUTE, main_fn];
512        ops.extend(string_words("gemm_fp8"));
513        push_inst(&mut words, OP_ENTRY_POINT, &ops);
514    }
515    push_inst(
516        &mut words,
517        OP_EXECUTION_MODE,
518        &[main_fn, EXECUTION_MODE_LOCAL_SIZE, 16, 1, 1],
519    );
520
521    // ── Function body (declaration only) ──
522    push_inst(&mut words, OP_FUNCTION, &[ty_void, main_fn, 0, ty_fn]);
523    push_inst(&mut words, OP_LABEL, &[label]);
524    push_inst(&mut words, OP_RETURN, &[]);
525    push_inst(&mut words, OP_FUNCTION_END, &[]);
526
527    // Reference the cooperative-matrix type ids so the format selection is
528    // observably encoded (different formats → different exponent metadata).
529    let _ = (ty_cmat_a, ty_cmat_b, ty_cmat_c, format.exponent_bits());
530
531    words[3] = next_id;
532    words
533}
534
535// ─── Tests ───────────────────────────────────────────────────
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540    use crate::spirv::{OP_CAPABILITY, OP_EXTENSION, SPIRV_MAGIC};
541
542    /// Decode the instruction stream after the 5-word header into
543    /// `(opcode, operands)` pairs.
544    fn decode(words: &[u32]) -> Vec<(u32, Vec<u32>)> {
545        let mut out = Vec::new();
546        let mut i = 5;
547        while i < words.len() {
548            let wc = (words[i] >> 16) as usize;
549            let op = words[i] & 0xffff;
550            if wc == 0 || i + wc > words.len() {
551                break;
552            }
553            out.push((op, words[i + 1..i + wc].to_vec()));
554            i += wc;
555        }
556        out
557    }
558
559    fn header_ok(words: &[u32]) {
560        assert!(words.len() >= 6);
561        assert_eq!(words[0], SPIRV_MAGIC, "bad magic");
562        assert!(words[3] > 0, "id bound must be > 0");
563        assert_eq!(words[4], 0, "schema must be 0");
564    }
565
566    /// Decode a packed extension string from an `OpExtension`'s operands.
567    fn ext_string(operands: &[u32]) -> String {
568        let mut bytes = Vec::new();
569        for w in operands {
570            for shift in 0..4 {
571                let b = ((w >> (shift * 8)) & 0xff) as u8;
572                if b == 0 {
573                    return String::from_utf8_lossy(&bytes).into_owned();
574                }
575                bytes.push(b);
576            }
577        }
578        String::from_utf8_lossy(&bytes).into_owned()
579    }
580
581    #[test]
582    fn block_copy_header_and_alignment() {
583        let words = esimd_block_copy_spirv();
584        header_ok(&words);
585        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();
586        assert_eq!(bytes.len() % 4, 0);
587    }
588
589    #[test]
590    fn block_copy_declares_intel_subgroups_extension() {
591        let words = esimd_block_copy_spirv();
592        let insts = decode(&words);
593        let has_ext = insts
594            .iter()
595            .filter(|(op, _)| *op == OP_EXTENSION)
596            .any(|(_, ops)| ext_string(ops) == "SPV_INTEL_subgroups");
597        assert!(has_ext, "must declare SPV_INTEL_subgroups");
598    }
599
600    #[test]
601    fn block_copy_declares_block_io_capability() {
602        let words = esimd_block_copy_spirv();
603        let insts = decode(&words);
604        let caps: Vec<u32> = insts
605            .iter()
606            .filter(|(op, _)| *op == OP_CAPABILITY)
607            .map(|(_, ops)| ops[0])
608            .collect();
609        assert!(caps.contains(&CAPABILITY_SUBGROUP_BUFFER_BLOCK_IO_INTEL));
610        assert!(caps.contains(&CAPABILITY_KERNEL));
611    }
612
613    #[test]
614    fn block_copy_emits_block_read_and_write() {
615        let words = esimd_block_copy_spirv();
616        let insts = decode(&words);
617        assert!(
618            insts
619                .iter()
620                .any(|(op, _)| *op == OP_SUBGROUP_BLOCK_READ_INTEL),
621            "missing OpSubgroupBlockReadINTEL"
622        );
623        assert!(
624            insts
625                .iter()
626                .any(|(op, _)| *op == OP_SUBGROUP_BLOCK_WRITE_INTEL),
627            "missing OpSubgroupBlockWriteINTEL"
628        );
629    }
630
631    #[test]
632    fn dpas_tile_config_constants() {
633        assert_eq!(DpasTileConfig::DG2_BF16.mac_lanes(), 64);
634        assert_eq!(DpasTileConfig::DG2_INT8.k, 32);
635    }
636
637    #[test]
638    fn dpas_gemm_header_and_block_ops() {
639        let words = dpas_gemm_spirv(DpasTileConfig::DG2_BF16);
640        header_ok(&words);
641        let insts = decode(&words);
642        assert!(
643            insts
644                .iter()
645                .any(|(op, _)| *op == OP_SUBGROUP_BLOCK_READ_INTEL)
646        );
647        assert!(
648            insts
649                .iter()
650                .any(|(op, _)| *op == OP_SUBGROUP_BLOCK_WRITE_INTEL)
651        );
652        // The systolic-depth constant must be baked in.
653        let has_k_const = insts
654            .iter()
655            .any(|(op, ops)| *op == crate::spirv::OP_CONSTANT && ops.last() == Some(&16));
656        assert!(has_k_const, "systolic depth constant 16 must be baked");
657    }
658
659    #[test]
660    fn dpas_gemm_distinct_tiles_distinct_binaries() {
661        let bf16 = dpas_gemm_spirv(DpasTileConfig::DG2_BF16);
662        let int8 = dpas_gemm_spirv(DpasTileConfig::DG2_INT8);
663        assert_ne!(bf16, int8, "different k must change the baked constant");
664    }
665
666    #[test]
667    fn fp8_format_fields() {
668        assert_eq!(Fp8Format::E4m3.exponent_bits(), 4);
669        assert_eq!(Fp8Format::E4m3.mantissa_bits(), 3);
670        assert_eq!(Fp8Format::E4m3.intel_name(), "HF8");
671        assert_eq!(Fp8Format::E5m2.exponent_bits(), 5);
672        assert_eq!(Fp8Format::E5m2.intel_name(), "BF8");
673    }
674
675    #[test]
676    fn fp8_coop_matrix_header_and_capabilities() {
677        let words = gemm_fp8_coop_matrix_spirv(Fp8Format::E4m3, 8, 16, 16);
678        header_ok(&words);
679        assert_eq!(
680            words[1], SPIRV_VERSION_1_6,
681            "FP8 coop-matrix needs SPIR-V 1.6"
682        );
683        let insts = decode(&words);
684        let caps: Vec<u32> = insts
685            .iter()
686            .filter(|(op, _)| *op == OP_CAPABILITY)
687            .map(|(_, ops)| ops[0])
688            .collect();
689        assert!(
690            caps.contains(&CAPABILITY_FLOAT8_EXT),
691            "Float8EXT capability required"
692        );
693        assert!(caps.contains(&CAPABILITY_COOPERATIVE_MATRIX_KHR));
694    }
695
696    #[test]
697    fn fp8_coop_matrix_declares_extensions_and_8bit_float() {
698        let words = gemm_fp8_coop_matrix_spirv(Fp8Format::E5m2, 8, 16, 16);
699        let insts = decode(&words);
700        // Both cooperative-matrix and float8 extensions declared.
701        let exts: Vec<String> = insts
702            .iter()
703            .filter(|(op, _)| *op == OP_EXTENSION)
704            .map(|(_, ops)| ext_string(ops))
705            .collect();
706        assert!(exts.iter().any(|e| e == "SPV_KHR_cooperative_matrix"));
707        assert!(exts.iter().any(|e| e == "SPV_EXT_float8"));
708
709        // An 8-bit OpTypeFloat must be present for the FP8 element.
710        let has_f8 = insts
711            .iter()
712            .any(|(op, ops)| *op == OP_TYPE_FLOAT && ops.get(1) == Some(&8));
713        assert!(has_f8, "8-bit OpTypeFloat must be declared");
714        // Three cooperative-matrix types (A, B, accumulator).
715        let cmat_count = insts
716            .iter()
717            .filter(|(op, _)| *op == OP_TYPE_COOPERATIVE_MATRIX_KHR)
718            .count();
719        assert_eq!(cmat_count, 3);
720    }
721
722    #[test]
723    fn fp8_coop_matrix_word_aligned() {
724        let words = gemm_fp8_coop_matrix_spirv(Fp8Format::E4m3, 8, 32, 16);
725        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();
726        assert_eq!(bytes.len() % 4, 0);
727    }
728}