1use 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
24const OP_SUBGROUP_BLOCK_READ_INTEL: u32 = 5575;
28const OP_SUBGROUP_BLOCK_WRITE_INTEL: u32 = 5576;
30
31const CAPABILITY_ADDRESSES: u32 = 4;
34const CAPABILITY_KERNEL: u32 = 6;
35const CAPABILITY_SUBGROUP_SHUFFLE_INTEL: u32 = 5568;
37const CAPABILITY_SUBGROUP_BUFFER_BLOCK_IO_INTEL: u32 = 5569;
39
40const ADDRESSING_MODEL_PHYSICAL64: u32 = 2;
43const MEMORY_MODEL_OPENCL: u32 = 2;
44
45const DECORATION_BUILTIN: u32 = 11;
48const BUILTIN_GLOBAL_INVOCATION_ID: u32 = 28;
49
50const STORAGE_CLASS_INPUT: u32 = 1;
53const STORAGE_CLASS_CROSS_WORKGROUP: u32 = 5;
54
55const WORKGROUP_SIZE: u32 = 16;
58
59pub fn esimd_block_copy_spirv() -> Vec<u32> {
72 let mut m = SpvModule::new();
73
74 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 m.emit_extension("SPV_INTEL_subgroups");
82
83 m.emit_memory_model(ADDRESSING_MODEL_PHYSICAL64, MEMORY_MODEL_OPENCL);
85
86 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 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 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 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 let block_val = m.alloc_id();
138 m.emit(OP_SUBGROUP_BLOCK_READ_INTEL, &[ty_float, block_val, in_ptr]);
139
140 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub struct DpasTileConfig {
159 pub m: u32,
161 pub n: u32,
163 pub k: u32,
165}
166
167impl DpasTileConfig {
168 pub const DG2_BF16: Self = Self { m: 8, n: 8, k: 16 };
170 pub const DG2_INT8: Self = Self { m: 8, n: 8, k: 32 };
172
173 #[must_use]
175 pub fn mac_lanes(&self) -> u32 {
176 self.m * self.n
177 }
178}
179
180pub fn dpas_gemm_spirv(tile: DpasTileConfig) -> Vec<u32> {
194 let mut m = SpvModule::new();
195
196 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 m.emit_extension("SPV_INTEL_subgroups");
204
205 m.emit_memory_model(ADDRESSING_MODEL_PHYSICAL64, MEMORY_MODEL_OPENCL);
207
208 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 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 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 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 let prod = m.alloc_id();
284 m.emit(OP_F_MUL, &[ty_float, prod, a_blk, b_blk]);
285
286 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum Fp8Format {
318 E4m3,
320 E5m2,
322}
323
324impl Fp8Format {
325 #[must_use]
327 pub fn exponent_bits(self) -> u32 {
328 match self {
329 Fp8Format::E4m3 => 4,
330 Fp8Format::E5m2 => 5,
331 }
332 }
333
334 #[must_use]
336 pub fn mantissa_bits(self) -> u32 {
337 match self {
338 Fp8Format::E4m3 => 3,
339 Fp8Format::E5m2 => 2,
340 }
341 }
342
343 #[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
353const 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; const CAPABILITY_SHADER: u32 = 1;
375const CAPABILITY_COOPERATIVE_MATRIX_KHR: u32 = 6022;
376const 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
388fn 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
395fn 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
406pub 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 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 {
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 push_inst(
452 &mut words,
453 OP_MEMORY_MODEL,
454 &[ADDRESSING_MODEL_LOGICAL, MEMORY_MODEL_GLSL450],
455 );
456
457 let ty_void = alloc();
459 let ty_u32 = alloc();
460 let ty_f8 = alloc(); let ty_f32 = alloc(); 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 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 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 {
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 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 let _ = (ty_cmat_a, ty_cmat_b, ty_cmat_c, format.exponent_bits());
530
531 words[3] = next_id;
532 words
533}
534
535#[cfg(test)]
538mod tests {
539 use super::*;
540 use crate::spirv::{OP_CAPABILITY, OP_EXTENSION, SPIRV_MAGIC};
541
542 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 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 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 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 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 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}