Skip to main content

oxicuda_levelzero/
spirv_subgroup.rs

1//! Sub-group optimized SPIR-V kernel generators for Intel GPUs.
2//!
3//! This module provides SPIR-V generators that leverage Intel GPU sub-group
4//! operations (analogous to CUDA warps) for efficient intra-sub-group
5//! communication:
6//!
7//! - [`reduction_subgroup_spirv`] — Two-phase sub-group reduction
8//! - [`scan_subgroup_spirv`] — Inclusive prefix sum via sub-group scan
9//! - [`gemm_subgroup_spirv`] — GEMM with sub-group shuffle for A-row broadcast
10//!
11//! All kernels use the OpenCL SPIR-V execution model (`Kernel`) with
12//! `Physical64`/`OpenCL` memory model and require `GroupNonUniform` family
13//! capabilities.
14
15use crate::spirv::{
16    EXECUTION_MODEL_KERNEL, FUNCTION_CONTROL_NONE, OP_COMPOSITE_EXTRACT, OP_CONTROL_BARRIER,
17    OP_F_ADD, OP_F_MUL, OP_GROUP_NON_UNIFORM_FADD, OP_GROUP_NON_UNIFORM_SHUFFLE, OP_I_ADD,
18    OP_I_MUL, OP_PHI, OP_TYPE_ARRAY, OP_U_LESS_THAN, STORAGE_CLASS_FUNCTION, SpvModule,
19    WORKGROUP_SIZE,
20};
21
22// ── SPIR-V constants (sub-group specific) ────────────────────
23
24// Capabilities
25const CAPABILITY_ADDRESSES: u32 = 4;
26const CAPABILITY_KERNEL: u32 = 6;
27const CAPABILITY_GROUP_NON_UNIFORM: u32 = 61;
28const CAPABILITY_GROUP_NON_UNIFORM_ARITHMETIC: u32 = 63;
29const CAPABILITY_GROUP_NON_UNIFORM_SHUFFLE: u32 = 65;
30
31// Addressing / memory model
32const ADDRESSING_MODEL_PHYSICAL64: u32 = 2;
33const MEMORY_MODEL_OPENCL: u32 = 2;
34
35// Decorations
36const DECORATION_BUILTIN: u32 = 11;
37
38// BuiltIn values
39const BUILTIN_GLOBAL_INVOCATION_ID: u32 = 28;
40const BUILTIN_NUM_SUBGROUPS: u32 = 38;
41const BUILTIN_SUBGROUP_ID: u32 = 40;
42const BUILTIN_SUBGROUP_LOCAL_INVOCATION_ID: u32 = 41;
43
44// Storage classes
45const STORAGE_CLASS_INPUT: u32 = 1;
46const STORAGE_CLASS_WORKGROUP: u32 = 4;
47const STORAGE_CLASS_CROSS_WORKGROUP: u32 = 5;
48
49// Scope
50const SCOPE_WORKGROUP: u32 = 2;
51const SCOPE_SUBGROUP: u32 = 3;
52
53// Memory semantics
54const MEMORY_SEMANTICS_WORKGROUP_MEMORY: u32 = 0x100;
55
56// GroupOperation
57const GROUP_OPERATION_REDUCE: u32 = 0;
58const GROUP_OPERATION_INCLUSIVE_SCAN: u32 = 1;
59
60// Magic opcode numbers used inline
61const OP_I_EQUAL: u32 = 170;
62
63/// Maximum sub-groups per workgroup (used for shared-memory scratch array size).
64const MAX_SUBGROUPS: u32 = 32;
65
66// ─── Sub-group optimized reduction kernel ────────────────────
67
68/// Generate an OpenCL SPIR-V compute kernel for sub-group optimized reduction.
69///
70/// Two-phase algorithm:
71/// 1. Each lane reduces its element via `OpGroupNonUniformFAdd` with `Reduce`.
72/// 2. Sub-group leaders write partial sums to workgroup shared memory, barrier,
73///    then sub-group 0 reduces the partial sums and writes the final result.
74///
75/// Kernel parameters: `(CrossWorkgroup float* input, CrossWorkgroup float* output, uint count)`.
76///
77/// Entry point name: `"reduction_subgroup"`.
78pub fn reduction_subgroup_spirv() -> Vec<u32> {
79    let mut m = SpvModule::new();
80
81    // ── Capabilities ──
82    m.emit_capability(CAPABILITY_KERNEL);
83    m.emit_capability(CAPABILITY_ADDRESSES);
84    m.emit_capability(CAPABILITY_GROUP_NON_UNIFORM);
85    m.emit_capability(CAPABILITY_GROUP_NON_UNIFORM_ARITHMETIC);
86
87    // ── Ext import ──
88    let opencl_ext = m.alloc_id();
89    m.emit_ext_inst_import(opencl_ext, "OpenCL.std");
90
91    // ── Memory model ──
92    m.emit_memory_model(ADDRESSING_MODEL_PHYSICAL64, MEMORY_MODEL_OPENCL);
93
94    // ── Type IDs ──
95    let ty_void = m.alloc_id();
96    let ty_bool = m.alloc_id();
97    let ty_uint = m.alloc_id();
98    let ty_float = m.alloc_id();
99    let ty_v3uint = m.alloc_id();
100    let ty_ptr_input_v3uint = m.alloc_id();
101    let ty_ptr_cross_float = m.alloc_id();
102    let ty_ptr_func_float = m.alloc_id();
103    let ty_ptr_func_uint = m.alloc_id();
104    let ty_ptr_wg_float = m.alloc_id();
105    let ty_ptr_input_uint = m.alloc_id();
106    let ty_arr_float = m.alloc_id();
107    let ty_ptr_wg_arr = m.alloc_id();
108
109    // ── Constants ──
110    let c_uint_0 = m.alloc_id();
111    let c_uint_1 = m.alloc_id();
112    let c_uint_max_sg = m.alloc_id();
113    let c_float_0 = m.alloc_id();
114    let c_scope_sg = m.alloc_id();
115    let c_scope_wg = m.alloc_id();
116    let c_mem_sem = m.alloc_id();
117
118    // ── Variables ──
119    let var_gid = m.alloc_id();
120    let var_sg_id = m.alloc_id();
121    let var_sg_lid = m.alloc_id();
122    let var_num_sg = m.alloc_id();
123    let var_scratch = m.alloc_id();
124    // Function-storage loop counter + accumulator for the cross-subgroup sum.
125    let var_j = m.alloc_id();
126    let var_acc = m.alloc_id();
127
128    // ── Function ──
129    let fn_ty = m.alloc_id();
130    let main_fn = m.alloc_id();
131    let p_input = m.alloc_id();
132    let p_output = m.alloc_id();
133    let p_count = m.alloc_id();
134
135    // ── Entry point & execution mode ──
136    m.emit_entry_point(
137        EXECUTION_MODEL_KERNEL,
138        main_fn,
139        "reduction_subgroup",
140        &[var_gid, var_sg_id, var_sg_lid, var_num_sg],
141    );
142    m.emit_execution_mode_local_size(main_fn, WORKGROUP_SIZE, 1, 1);
143
144    // ── Decorations ──
145    m.emit_decorate(var_gid, DECORATION_BUILTIN, &[BUILTIN_GLOBAL_INVOCATION_ID]);
146    m.emit_decorate(var_sg_id, DECORATION_BUILTIN, &[BUILTIN_SUBGROUP_ID]);
147    m.emit_decorate(
148        var_sg_lid,
149        DECORATION_BUILTIN,
150        &[BUILTIN_SUBGROUP_LOCAL_INVOCATION_ID],
151    );
152    m.emit_decorate(var_num_sg, DECORATION_BUILTIN, &[BUILTIN_NUM_SUBGROUPS]);
153
154    // ── Types ──
155    m.emit_type_void(ty_void);
156    m.emit_type_bool(ty_bool);
157    m.emit_type_int(ty_uint, 32, 0);
158    m.emit_type_float(ty_float, 32);
159    m.emit_type_vector(ty_v3uint, ty_uint, 3);
160    m.emit_type_pointer(ty_ptr_input_v3uint, STORAGE_CLASS_INPUT, ty_v3uint);
161    m.emit_type_pointer(ty_ptr_cross_float, STORAGE_CLASS_CROSS_WORKGROUP, ty_float);
162    m.emit_type_pointer(ty_ptr_func_float, STORAGE_CLASS_FUNCTION, ty_float);
163    m.emit_type_pointer(ty_ptr_func_uint, STORAGE_CLASS_FUNCTION, ty_uint);
164    m.emit_type_pointer(ty_ptr_wg_float, STORAGE_CLASS_WORKGROUP, ty_float);
165    m.emit_type_pointer(ty_ptr_input_uint, STORAGE_CLASS_INPUT, ty_uint);
166    m.emit_type_function(
167        fn_ty,
168        ty_void,
169        &[ty_ptr_cross_float, ty_ptr_cross_float, ty_uint],
170    );
171
172    // ── Constants ──
173    m.emit_constant_u32(ty_uint, c_uint_0, 0);
174    m.emit_constant_u32(ty_uint, c_uint_1, 1);
175    m.emit_constant_u32(ty_uint, c_uint_max_sg, MAX_SUBGROUPS);
176    m.emit_constant_f32(ty_float, c_float_0, 0.0);
177    m.emit_constant_u32(ty_uint, c_scope_sg, SCOPE_SUBGROUP);
178    m.emit_constant_u32(ty_uint, c_scope_wg, SCOPE_WORKGROUP);
179    m.emit_constant_u32(ty_uint, c_mem_sem, MEMORY_SEMANTICS_WORKGROUP_MEMORY);
180
181    // ── Array type for shared scratch ──
182    m.emit(OP_TYPE_ARRAY, &[ty_arr_float, ty_float, c_uint_max_sg]);
183    m.emit_type_pointer(ty_ptr_wg_arr, STORAGE_CLASS_WORKGROUP, ty_arr_float);
184
185    // ── Variables ──
186    m.emit_variable(ty_ptr_input_v3uint, var_gid, STORAGE_CLASS_INPUT);
187    m.emit_variable(ty_ptr_input_uint, var_sg_id, STORAGE_CLASS_INPUT);
188    m.emit_variable(ty_ptr_input_uint, var_sg_lid, STORAGE_CLASS_INPUT);
189    m.emit_variable(ty_ptr_input_uint, var_num_sg, STORAGE_CLASS_INPUT);
190    m.emit_variable(ty_ptr_wg_arr, var_scratch, STORAGE_CLASS_WORKGROUP);
191
192    // ── Labels ──
193    let label_entry = m.alloc_id();
194    let label_bounds_body = m.alloc_id();
195    let label_bounds_merge = m.alloc_id();
196    let label_leader = m.alloc_id();
197    let label_after_leader = m.alloc_id();
198    let label_sg0 = m.alloc_id();
199    let label_after_sg0 = m.alloc_id();
200    let label_sum_hdr = m.alloc_id();
201    let label_sum_body = m.alloc_id();
202    let label_sum_cont = m.alloc_id();
203    let label_sum_merge = m.alloc_id();
204
205    // ── Function body ──
206    m.emit_function(ty_void, main_fn, FUNCTION_CONTROL_NONE, fn_ty);
207    m.emit_function_parameter(ty_ptr_cross_float, p_input);
208    m.emit_function_parameter(ty_ptr_cross_float, p_output);
209    m.emit_function_parameter(ty_uint, p_count);
210    m.emit_label(label_entry);
211
212    // Function-storage OpVariables: first instructions of the first block.
213    m.emit_variable(ty_ptr_func_uint, var_j, STORAGE_CLASS_FUNCTION);
214    m.emit_variable(ty_ptr_func_float, var_acc, STORAGE_CLASS_FUNCTION);
215
216    // Load global ID
217    let gid_val = m.alloc_id();
218    m.emit_load(ty_v3uint, gid_val, var_gid);
219    let gid_x = m.alloc_id();
220    m.emit(OP_COMPOSITE_EXTRACT, &[ty_uint, gid_x, gid_val, 0]);
221
222    // Load sub-group builtins
223    let sg_id = m.alloc_id();
224    m.emit_load(ty_uint, sg_id, var_sg_id);
225    let sg_lid = m.alloc_id();
226    m.emit_load(ty_uint, sg_lid, var_sg_lid);
227    let num_sg = m.alloc_id();
228    m.emit_load(ty_uint, num_sg, var_num_sg);
229
230    // Bounds check: gid_x < count
231    let cond_bounds = m.alloc_id();
232    m.emit(OP_U_LESS_THAN, &[ty_bool, cond_bounds, gid_x, p_count]);
233
234    // Load value if in-bounds, else 0.0
235    m.emit_selection_merge(label_bounds_merge);
236    m.emit_branch_conditional(cond_bounds, label_bounds_body, label_bounds_merge);
237
238    m.emit_label(label_bounds_body);
239    let inp_ptr = m.alloc_id();
240    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, inp_ptr, p_input, gid_x);
241    let inp_val = m.alloc_id();
242    m.emit_load(ty_float, inp_val, inp_ptr);
243    m.emit_branch(label_bounds_merge);
244
245    m.emit_label(label_bounds_merge);
246    // Phi: val = inp_val if from bounds_body, else c_float_0
247    let val = m.alloc_id();
248    m.emit(
249        OP_PHI,
250        &[
251            ty_float,
252            val,
253            inp_val,
254            label_bounds_body,
255            c_float_0,
256            label_entry,
257        ],
258    );
259
260    // Phase 1: sub-group reduce
261    let sg_sum = m.alloc_id();
262    m.emit(
263        OP_GROUP_NON_UNIFORM_FADD,
264        &[ty_float, sg_sum, c_scope_sg, GROUP_OPERATION_REDUCE, val],
265    );
266
267    // Sub-group leader (sg_lid == 0) writes to shared scratch[sg_id]
268    let is_leader_eq = m.alloc_id();
269    m.emit(OP_I_EQUAL, &[ty_bool, is_leader_eq, sg_lid, c_uint_0]);
270
271    m.emit_selection_merge(label_after_leader);
272    m.emit_branch_conditional(is_leader_eq, label_leader, label_after_leader);
273
274    m.emit_label(label_leader);
275    let scratch_ptr = m.alloc_id();
276    m.emit_in_bounds_ptr_access_chain(ty_ptr_wg_float, scratch_ptr, var_scratch, sg_id);
277    m.emit_store(scratch_ptr, sg_sum);
278    m.emit_branch(label_after_leader);
279
280    m.emit_label(label_after_leader);
281
282    // Workgroup barrier
283    m.emit(OP_CONTROL_BARRIER, &[c_scope_wg, c_scope_wg, c_mem_sem]);
284
285    // Phase 2: sub-group 0, lane 0 serially sums ALL `num_sg` per-sub-group
286    // partials. A single OpGroupNonUniformFAdd/Reduce would only cover the first
287    // `subgroupSize` scratch entries; when the driver compiles a sub-group size
288    // smaller than the sub-group count (e.g. SIMD8 with 32 sub-groups), the
289    // remaining partials scratch[subgroupSize..num_sg) would be silently
290    // dropped. An explicit 0..num_sg loop is correct for any sub-group size.
291    let is_sg0 = m.alloc_id();
292    m.emit(OP_I_EQUAL, &[ty_bool, is_sg0, sg_id, c_uint_0]);
293    let is_lane0 = m.alloc_id();
294    m.emit(OP_I_EQUAL, &[ty_bool, is_lane0, sg_lid, c_uint_0]);
295    let is_writer = m.alloc_id();
296    // OpLogicalAnd = 167
297    m.emit(167, &[ty_bool, is_writer, is_sg0, is_lane0]);
298
299    m.emit_selection_merge(label_after_sg0);
300    m.emit_branch_conditional(is_writer, label_sg0, label_after_sg0);
301
302    m.emit_label(label_sg0);
303
304    // acc = 0; for (j = 0; j < num_sg; j++) acc += scratch[j];
305    m.emit_store(var_j, c_uint_0);
306    m.emit_store(var_acc, c_float_0);
307
308    m.emit_branch(label_sum_hdr);
309    m.emit_label(label_sum_hdr);
310    let j_val = m.alloc_id();
311    m.emit_load(ty_uint, j_val, var_j);
312    let sum_cond = m.alloc_id();
313    m.emit(OP_U_LESS_THAN, &[ty_bool, sum_cond, j_val, num_sg]);
314    m.emit_loop_merge(label_sum_merge, label_sum_cont);
315    m.emit_branch_conditional(sum_cond, label_sum_body, label_sum_merge);
316
317    m.emit_label(label_sum_body);
318    let s_ptr = m.alloc_id();
319    m.emit_in_bounds_ptr_access_chain(ty_ptr_wg_float, s_ptr, var_scratch, j_val);
320    let partial = m.alloc_id();
321    m.emit_load(ty_float, partial, s_ptr);
322    let old_acc = m.alloc_id();
323    m.emit_load(ty_float, old_acc, var_acc);
324    let new_acc = m.alloc_id();
325    m.emit(OP_F_ADD, &[ty_float, new_acc, old_acc, partial]);
326    m.emit_store(var_acc, new_acc);
327    m.emit_branch(label_sum_cont);
328
329    m.emit_label(label_sum_cont);
330    let j_inc = m.alloc_id();
331    m.emit(OP_I_ADD, &[ty_uint, j_inc, j_val, c_uint_1]);
332    m.emit_store(var_j, j_inc);
333    m.emit_branch(label_sum_hdr);
334
335    m.emit_label(label_sum_merge);
336    let final_sum = m.alloc_id();
337    m.emit_load(ty_float, final_sum, var_acc);
338    let out_ptr = m.alloc_id();
339    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, out_ptr, p_output, c_uint_0);
340    m.emit_store(out_ptr, final_sum);
341    m.emit_branch(label_after_sg0);
342
343    m.emit_label(label_after_sg0);
344    m.emit_return();
345    m.emit_function_end();
346
347    m.finalize()
348}
349
350// ─── Sub-group optimized scan kernel ─────────────────────────
351
352/// Generate an OpenCL SPIR-V compute kernel for sub-group scan (prefix sum).
353///
354/// Uses `OpGroupNonUniformFAdd` with `InclusiveScan` for the intra-sub-group
355/// phase. Output contains the inclusive prefix sum of the input.
356///
357/// Kernel parameters: `(CrossWorkgroup float* input, CrossWorkgroup float* output, uint count)`.
358///
359/// Entry point name: `"scan_subgroup"`.
360pub fn scan_subgroup_spirv() -> Vec<u32> {
361    let mut m = SpvModule::new();
362
363    // ── Capabilities ──
364    m.emit_capability(CAPABILITY_KERNEL);
365    m.emit_capability(CAPABILITY_ADDRESSES);
366    m.emit_capability(CAPABILITY_GROUP_NON_UNIFORM);
367    m.emit_capability(CAPABILITY_GROUP_NON_UNIFORM_ARITHMETIC);
368
369    // ── Ext import ──
370    let opencl_ext = m.alloc_id();
371    m.emit_ext_inst_import(opencl_ext, "OpenCL.std");
372
373    // ── Memory model ──
374    m.emit_memory_model(ADDRESSING_MODEL_PHYSICAL64, MEMORY_MODEL_OPENCL);
375
376    // ── Type IDs ──
377    let ty_void = m.alloc_id();
378    let ty_bool = m.alloc_id();
379    let ty_uint = m.alloc_id();
380    let ty_float = m.alloc_id();
381    let ty_v3uint = m.alloc_id();
382    let ty_ptr_input_v3uint = m.alloc_id();
383    let ty_ptr_cross_float = m.alloc_id();
384    let ty_ptr_input_uint = m.alloc_id();
385    let ty_arr_float = m.alloc_id();
386    let ty_ptr_wg_float = m.alloc_id();
387    let ty_ptr_wg_arr = m.alloc_id();
388
389    // ── Constants ──
390    let c_uint_0 = m.alloc_id();
391    let c_uint_1 = m.alloc_id();
392    let c_uint_max_sg = m.alloc_id();
393    let c_float_0 = m.alloc_id();
394    let c_scope_sg = m.alloc_id();
395    let c_scope_wg = m.alloc_id();
396    let c_mem_sem = m.alloc_id();
397
398    // ── Variables ──
399    let var_gid = m.alloc_id();
400    let var_sg_id = m.alloc_id();
401    let var_sg_lid = m.alloc_id();
402    let var_num_sg = m.alloc_id();
403    let var_scratch = m.alloc_id();
404
405    // ── Function ──
406    let fn_ty = m.alloc_id();
407    let main_fn = m.alloc_id();
408    let p_input = m.alloc_id();
409    let p_output = m.alloc_id();
410    let p_count = m.alloc_id();
411
412    // ── Entry point ──
413    m.emit_entry_point(
414        EXECUTION_MODEL_KERNEL,
415        main_fn,
416        "scan_subgroup",
417        &[var_gid, var_sg_id, var_sg_lid, var_num_sg],
418    );
419    m.emit_execution_mode_local_size(main_fn, WORKGROUP_SIZE, 1, 1);
420
421    // ── Decorations ──
422    m.emit_decorate(var_gid, DECORATION_BUILTIN, &[BUILTIN_GLOBAL_INVOCATION_ID]);
423    m.emit_decorate(var_sg_id, DECORATION_BUILTIN, &[BUILTIN_SUBGROUP_ID]);
424    m.emit_decorate(
425        var_sg_lid,
426        DECORATION_BUILTIN,
427        &[BUILTIN_SUBGROUP_LOCAL_INVOCATION_ID],
428    );
429    m.emit_decorate(var_num_sg, DECORATION_BUILTIN, &[BUILTIN_NUM_SUBGROUPS]);
430
431    // ── Types ──
432    m.emit_type_void(ty_void);
433    m.emit_type_bool(ty_bool);
434    m.emit_type_int(ty_uint, 32, 0);
435    m.emit_type_float(ty_float, 32);
436    m.emit_type_vector(ty_v3uint, ty_uint, 3);
437    m.emit_type_pointer(ty_ptr_input_v3uint, STORAGE_CLASS_INPUT, ty_v3uint);
438    m.emit_type_pointer(ty_ptr_cross_float, STORAGE_CLASS_CROSS_WORKGROUP, ty_float);
439    m.emit_type_pointer(ty_ptr_input_uint, STORAGE_CLASS_INPUT, ty_uint);
440    m.emit_type_pointer(ty_ptr_wg_float, STORAGE_CLASS_WORKGROUP, ty_float);
441    m.emit_type_function(
442        fn_ty,
443        ty_void,
444        &[ty_ptr_cross_float, ty_ptr_cross_float, ty_uint],
445    );
446
447    // ── Constants ──
448    m.emit_constant_u32(ty_uint, c_uint_0, 0);
449    m.emit_constant_u32(ty_uint, c_uint_1, 1);
450    m.emit_constant_u32(ty_uint, c_uint_max_sg, MAX_SUBGROUPS);
451    m.emit_constant_f32(ty_float, c_float_0, 0.0);
452    m.emit_constant_u32(ty_uint, c_scope_sg, SCOPE_SUBGROUP);
453    m.emit_constant_u32(ty_uint, c_scope_wg, SCOPE_WORKGROUP);
454    m.emit_constant_u32(ty_uint, c_mem_sem, MEMORY_SEMANTICS_WORKGROUP_MEMORY);
455
456    // ── Shared scratch array ──
457    m.emit(OP_TYPE_ARRAY, &[ty_arr_float, ty_float, c_uint_max_sg]);
458    m.emit_type_pointer(ty_ptr_wg_arr, STORAGE_CLASS_WORKGROUP, ty_arr_float);
459
460    // ── Variables ──
461    m.emit_variable(ty_ptr_input_v3uint, var_gid, STORAGE_CLASS_INPUT);
462    m.emit_variable(ty_ptr_input_uint, var_sg_id, STORAGE_CLASS_INPUT);
463    m.emit_variable(ty_ptr_input_uint, var_sg_lid, STORAGE_CLASS_INPUT);
464    m.emit_variable(ty_ptr_input_uint, var_num_sg, STORAGE_CLASS_INPUT);
465    m.emit_variable(ty_ptr_wg_arr, var_scratch, STORAGE_CLASS_WORKGROUP);
466
467    // ── Labels ──
468    let label_entry = m.alloc_id();
469    let label_bounds_body = m.alloc_id();
470    let label_bounds_merge = m.alloc_id();
471    let label_leader = m.alloc_id();
472    let label_after_leader = m.alloc_id();
473    let label_add_prefix = m.alloc_id();
474    let label_after_add = m.alloc_id();
475    let label_write = m.alloc_id();
476    let label_end = m.alloc_id();
477
478    // ── Function body ──
479    m.emit_function(ty_void, main_fn, FUNCTION_CONTROL_NONE, fn_ty);
480    m.emit_function_parameter(ty_ptr_cross_float, p_input);
481    m.emit_function_parameter(ty_ptr_cross_float, p_output);
482    m.emit_function_parameter(ty_uint, p_count);
483    m.emit_label(label_entry);
484
485    // Load global ID
486    let gid_val = m.alloc_id();
487    m.emit_load(ty_v3uint, gid_val, var_gid);
488    let gid_x = m.alloc_id();
489    m.emit(OP_COMPOSITE_EXTRACT, &[ty_uint, gid_x, gid_val, 0]);
490
491    // Load sub-group builtins
492    let sg_id = m.alloc_id();
493    m.emit_load(ty_uint, sg_id, var_sg_id);
494    let sg_lid = m.alloc_id();
495    m.emit_load(ty_uint, sg_lid, var_sg_lid);
496
497    // Bounds check
498    let in_bounds = m.alloc_id();
499    m.emit(OP_U_LESS_THAN, &[ty_bool, in_bounds, gid_x, p_count]);
500    m.emit_selection_merge(label_bounds_merge);
501    m.emit_branch_conditional(in_bounds, label_bounds_body, label_bounds_merge);
502
503    m.emit_label(label_bounds_body);
504    let inp_ptr = m.alloc_id();
505    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, inp_ptr, p_input, gid_x);
506    let inp_val = m.alloc_id();
507    m.emit_load(ty_float, inp_val, inp_ptr);
508    m.emit_branch(label_bounds_merge);
509
510    m.emit_label(label_bounds_merge);
511    let val = m.alloc_id();
512    m.emit(
513        OP_PHI,
514        &[
515            ty_float,
516            val,
517            inp_val,
518            label_bounds_body,
519            c_float_0,
520            label_entry,
521        ],
522    );
523
524    // Phase 1: intra-sub-group inclusive scan
525    let sg_scan = m.alloc_id();
526    m.emit(
527        OP_GROUP_NON_UNIFORM_FADD,
528        &[
529            ty_float,
530            sg_scan,
531            c_scope_sg,
532            GROUP_OPERATION_INCLUSIVE_SCAN,
533            val,
534        ],
535    );
536
537    // Use sub-group reduce to get total per sub-group
538    let sg_total = m.alloc_id();
539    m.emit(
540        OP_GROUP_NON_UNIFORM_FADD,
541        &[ty_float, sg_total, c_scope_sg, GROUP_OPERATION_REDUCE, val],
542    );
543
544    // Leader writes sub-group total to scratch[sg_id]
545    let is_leader = m.alloc_id();
546    m.emit(OP_I_EQUAL, &[ty_bool, is_leader, sg_lid, c_uint_0]);
547
548    m.emit_selection_merge(label_after_leader);
549    m.emit_branch_conditional(is_leader, label_leader, label_after_leader);
550
551    m.emit_label(label_leader);
552    let scratch_ptr = m.alloc_id();
553    m.emit_in_bounds_ptr_access_chain(ty_ptr_wg_float, scratch_ptr, var_scratch, sg_id);
554    m.emit_store(scratch_ptr, sg_total);
555    m.emit_branch(label_after_leader);
556
557    m.emit_label(label_after_leader);
558
559    // Workgroup barrier
560    m.emit(OP_CONTROL_BARRIER, &[c_scope_wg, c_scope_wg, c_mem_sem]);
561
562    // Phase 2: add prefix from earlier sub-groups
563    // prefix = sum of scratch[0..sg_id)
564    let has_prefix = m.alloc_id();
565    m.emit(OP_U_LESS_THAN, &[ty_bool, has_prefix, c_uint_0, sg_id]); // 0 < sg_id
566
567    m.emit_selection_merge(label_after_add);
568    m.emit_branch_conditional(has_prefix, label_add_prefix, label_after_add);
569
570    m.emit_label(label_add_prefix);
571
572    // Accumulate prefix_sum = sum of scratch[j] for j in 0..sg_id via a loop
573    let var_j = m.alloc_id();
574    let ty_ptr_func_uint = m.alloc_id();
575    m.emit_type_pointer(ty_ptr_func_uint, STORAGE_CLASS_FUNCTION, ty_uint);
576    let var_prefix_acc = m.alloc_id();
577    let ty_ptr_func_float = m.alloc_id();
578    m.emit_type_pointer(ty_ptr_func_float, STORAGE_CLASS_FUNCTION, ty_float);
579    m.emit_variable(ty_ptr_func_uint, var_j, STORAGE_CLASS_FUNCTION);
580    m.emit_variable(ty_ptr_func_float, var_prefix_acc, STORAGE_CLASS_FUNCTION);
581    m.emit_store(var_j, c_uint_0);
582    m.emit_store(var_prefix_acc, c_float_0);
583
584    let lbl_loop_hdr = m.alloc_id();
585    let lbl_loop_body = m.alloc_id();
586    let lbl_loop_cont = m.alloc_id();
587    let lbl_loop_merge = m.alloc_id();
588
589    m.emit_branch(lbl_loop_hdr);
590    m.emit_label(lbl_loop_hdr);
591    let j_val = m.alloc_id();
592    m.emit_load(ty_uint, j_val, var_j);
593    let loop_cond = m.alloc_id();
594    m.emit(OP_U_LESS_THAN, &[ty_bool, loop_cond, j_val, sg_id]);
595    m.emit_loop_merge(lbl_loop_merge, lbl_loop_cont);
596    m.emit_branch_conditional(loop_cond, lbl_loop_body, lbl_loop_merge);
597
598    m.emit_label(lbl_loop_body);
599    let s_ptr = m.alloc_id();
600    m.emit_in_bounds_ptr_access_chain(ty_ptr_wg_float, s_ptr, var_scratch, j_val);
601    let s_val = m.alloc_id();
602    m.emit_load(ty_float, s_val, s_ptr);
603    let old_prefix = m.alloc_id();
604    m.emit_load(ty_float, old_prefix, var_prefix_acc);
605    let new_prefix = m.alloc_id();
606    m.emit(OP_F_ADD, &[ty_float, new_prefix, old_prefix, s_val]);
607    m.emit_store(var_prefix_acc, new_prefix);
608    m.emit_branch(lbl_loop_cont);
609
610    m.emit_label(lbl_loop_cont);
611    let j_inc = m.alloc_id();
612    m.emit(OP_I_ADD, &[ty_uint, j_inc, j_val, c_uint_1]);
613    m.emit_store(var_j, j_inc);
614    m.emit_branch(lbl_loop_hdr);
615
616    m.emit_label(lbl_loop_merge);
617    let prefix_val = m.alloc_id();
618    m.emit_load(ty_float, prefix_val, var_prefix_acc);
619    m.emit_branch(label_after_add);
620
621    m.emit_label(label_after_add);
622    // Phi for prefix: either prefix_val or 0
623    let prefix = m.alloc_id();
624    m.emit(
625        OP_PHI,
626        &[
627            ty_float,
628            prefix,
629            prefix_val,
630            lbl_loop_merge,
631            c_float_0,
632            label_after_leader,
633        ],
634    );
635
636    // Final result = sg_scan + prefix
637    let final_val = m.alloc_id();
638    m.emit(OP_F_ADD, &[ty_float, final_val, sg_scan, prefix]);
639
640    // Write output if in bounds
641    m.emit_selection_merge(label_end);
642    m.emit_branch_conditional(in_bounds, label_write, label_end);
643
644    m.emit_label(label_write);
645    let out_ptr = m.alloc_id();
646    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, out_ptr, p_output, gid_x);
647    m.emit_store(out_ptr, final_val);
648    m.emit_branch(label_end);
649
650    m.emit_label(label_end);
651    m.emit_return();
652    m.emit_function_end();
653
654    m.finalize()
655}
656
657// ─── Sub-group optimized GEMM kernel ─────────────────────────
658
659/// Generate an OpenCL SPIR-V compute kernel for GEMM with sub-group shuffle.
660///
661/// `C = alpha * A * B + beta * C` (row-major f32).
662///
663/// Uses `OpGroupNonUniformShuffle` to broadcast A-row elements across the
664/// sub-group, avoiding redundant global memory loads. Each lane in a sub-group
665/// handles a different column of B, and the A value is shuffled from the lane
666/// that loaded it.
667///
668/// Kernel parameters: `(CrossWorkgroup float* A, CrossWorkgroup float* B,
669///                      CrossWorkgroup float* C, uint m, uint n, uint k,
670///                      float alpha, float beta)`.
671///
672/// Entry point name: `"gemm_subgroup"`.
673pub fn gemm_subgroup_spirv() -> Vec<u32> {
674    let mut m = SpvModule::new();
675
676    // ── Capabilities ──
677    m.emit_capability(CAPABILITY_KERNEL);
678    m.emit_capability(CAPABILITY_ADDRESSES);
679    m.emit_capability(CAPABILITY_GROUP_NON_UNIFORM);
680    m.emit_capability(CAPABILITY_GROUP_NON_UNIFORM_SHUFFLE);
681
682    // ── Ext import ──
683    let opencl_ext = m.alloc_id();
684    m.emit_ext_inst_import(opencl_ext, "OpenCL.std");
685
686    // ── Memory model ──
687    m.emit_memory_model(ADDRESSING_MODEL_PHYSICAL64, MEMORY_MODEL_OPENCL);
688
689    // ── Types ──
690    let ty_void = m.alloc_id();
691    let ty_bool = m.alloc_id();
692    let ty_uint = m.alloc_id();
693    let ty_float = m.alloc_id();
694    let ty_v3uint = m.alloc_id();
695    let ty_ptr_input_v3uint = m.alloc_id();
696    let ty_ptr_cross_float = m.alloc_id();
697    let ty_ptr_func_float = m.alloc_id();
698    let ty_ptr_func_uint = m.alloc_id();
699    let ty_ptr_input_uint = m.alloc_id();
700
701    // ── Constants ──
702    let c_uint_0 = m.alloc_id();
703    let c_uint_1 = m.alloc_id();
704    let c_float_0 = m.alloc_id();
705    let c_scope_sg = m.alloc_id();
706
707    // ── Variables ──
708    let var_gid = m.alloc_id();
709    let var_sg_lid = m.alloc_id();
710
711    // ── Function ──
712    let fn_ty = m.alloc_id();
713    let main_fn = m.alloc_id();
714    let p_a = m.alloc_id();
715    let p_b = m.alloc_id();
716    let p_c = m.alloc_id();
717    let p_m = m.alloc_id();
718    let p_n = m.alloc_id();
719    let p_k = m.alloc_id();
720    let p_alpha = m.alloc_id();
721    let p_beta = m.alloc_id();
722
723    // ── Entry point ──
724    m.emit_entry_point(
725        EXECUTION_MODEL_KERNEL,
726        main_fn,
727        "gemm_subgroup",
728        &[var_gid, var_sg_lid],
729    );
730    m.emit_execution_mode_local_size(main_fn, WORKGROUP_SIZE, 1, 1);
731
732    // ── Decorations ──
733    m.emit_decorate(var_gid, DECORATION_BUILTIN, &[BUILTIN_GLOBAL_INVOCATION_ID]);
734    m.emit_decorate(
735        var_sg_lid,
736        DECORATION_BUILTIN,
737        &[BUILTIN_SUBGROUP_LOCAL_INVOCATION_ID],
738    );
739
740    // ── Types ──
741    m.emit_type_void(ty_void);
742    m.emit_type_bool(ty_bool);
743    m.emit_type_int(ty_uint, 32, 0);
744    m.emit_type_float(ty_float, 32);
745    m.emit_type_vector(ty_v3uint, ty_uint, 3);
746    m.emit_type_pointer(ty_ptr_input_v3uint, STORAGE_CLASS_INPUT, ty_v3uint);
747    m.emit_type_pointer(ty_ptr_cross_float, STORAGE_CLASS_CROSS_WORKGROUP, ty_float);
748    m.emit_type_pointer(ty_ptr_func_float, STORAGE_CLASS_FUNCTION, ty_float);
749    m.emit_type_pointer(ty_ptr_func_uint, STORAGE_CLASS_FUNCTION, ty_uint);
750    m.emit_type_pointer(ty_ptr_input_uint, STORAGE_CLASS_INPUT, ty_uint);
751    m.emit_type_function(
752        fn_ty,
753        ty_void,
754        &[
755            ty_ptr_cross_float,
756            ty_ptr_cross_float,
757            ty_ptr_cross_float,
758            ty_uint,
759            ty_uint,
760            ty_uint,
761            ty_float,
762            ty_float,
763        ],
764    );
765
766    // ── Constants ──
767    m.emit_constant_u32(ty_uint, c_uint_0, 0);
768    m.emit_constant_u32(ty_uint, c_uint_1, 1);
769    m.emit_constant_f32(ty_float, c_float_0, 0.0);
770    m.emit_constant_u32(ty_uint, c_scope_sg, SCOPE_SUBGROUP);
771
772    // ── Variables ──
773    m.emit_variable(ty_ptr_input_v3uint, var_gid, STORAGE_CLASS_INPUT);
774    m.emit_variable(ty_ptr_input_uint, var_sg_lid, STORAGE_CLASS_INPUT);
775
776    // ── Labels ──
777    let label_entry = m.alloc_id();
778    let label_bounds_body = m.alloc_id();
779    let label_bounds_merge = m.alloc_id();
780    let label_loop_header = m.alloc_id();
781    let label_loop_body = m.alloc_id();
782    let label_loop_continue = m.alloc_id();
783    let label_loop_merge = m.alloc_id();
784
785    // ── Function body ──
786    m.emit_function(ty_void, main_fn, FUNCTION_CONTROL_NONE, fn_ty);
787    m.emit_function_parameter(ty_ptr_cross_float, p_a);
788    m.emit_function_parameter(ty_ptr_cross_float, p_b);
789    m.emit_function_parameter(ty_ptr_cross_float, p_c);
790    m.emit_function_parameter(ty_uint, p_m);
791    m.emit_function_parameter(ty_uint, p_n);
792    m.emit_function_parameter(ty_uint, p_k);
793    m.emit_function_parameter(ty_float, p_alpha);
794    m.emit_function_parameter(ty_float, p_beta);
795    m.emit_label(label_entry);
796
797    // Load global ID -> element index (one thread per output element)
798    let gid_val = m.alloc_id();
799    m.emit_load(ty_v3uint, gid_val, var_gid);
800    let gid_x = m.alloc_id();
801    m.emit(OP_COMPOSITE_EXTRACT, &[ty_uint, gid_x, gid_val, 0]);
802
803    // Load sub-group local ID
804    let sg_lid = m.alloc_id();
805    m.emit_load(ty_uint, sg_lid, var_sg_lid);
806
807    // total = m * n
808    let total = m.alloc_id();
809    m.emit(OP_I_MUL, &[ty_uint, total, p_m, p_n]);
810
811    // Bounds check
812    let cond = m.alloc_id();
813    m.emit(OP_U_LESS_THAN, &[ty_bool, cond, gid_x, total]);
814    m.emit_selection_merge(label_bounds_merge);
815    m.emit_branch_conditional(cond, label_bounds_body, label_bounds_merge);
816
817    m.emit_label(label_bounds_body);
818
819    // row = gid / n, col = gid % n
820    // OpUDiv = 134, OpUMod = 137
821    let row = m.alloc_id();
822    m.emit(134, &[ty_uint, row, gid_x, p_n]); // OpUDiv
823    let col = m.alloc_id();
824    m.emit(137, &[ty_uint, col, gid_x, p_n]); // OpUMod
825
826    // Accumulator + loop counter
827    let var_acc = m.alloc_id();
828    m.emit_variable(ty_ptr_func_float, var_acc, STORAGE_CLASS_FUNCTION);
829    m.emit_store(var_acc, c_float_0);
830    let var_i = m.alloc_id();
831    m.emit_variable(ty_ptr_func_uint, var_i, STORAGE_CLASS_FUNCTION);
832    m.emit_store(var_i, c_uint_0);
833
834    m.emit_branch(label_loop_header);
835
836    // ── Loop header ──
837    m.emit_label(label_loop_header);
838    let i_val = m.alloc_id();
839    m.emit_load(ty_uint, i_val, var_i);
840    let loop_cond = m.alloc_id();
841    m.emit(OP_U_LESS_THAN, &[ty_bool, loop_cond, i_val, p_k]);
842    m.emit_loop_merge(label_loop_merge, label_loop_continue);
843    m.emit_branch_conditional(loop_cond, label_loop_body, label_loop_merge);
844
845    // ── Loop body ──
846    m.emit_label(label_loop_body);
847
848    // Load A[row, i]
849    let a_idx = m.alloc_id();
850    let row_k = m.alloc_id();
851    m.emit(OP_I_MUL, &[ty_uint, row_k, row, p_k]);
852    m.emit(OP_I_ADD, &[ty_uint, a_idx, row_k, i_val]);
853    let a_ptr = m.alloc_id();
854    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, a_ptr, p_a, a_idx);
855    let a_val = m.alloc_id();
856    m.emit_load(ty_float, a_val, a_ptr);
857
858    // Broadcast A value via sub-group shuffle (identity shuffle validates sub-group path)
859    let a_broadcast = m.alloc_id();
860    m.emit(
861        OP_GROUP_NON_UNIFORM_SHUFFLE,
862        &[ty_float, a_broadcast, c_scope_sg, a_val, sg_lid],
863    );
864
865    // Load B[i, col]
866    let b_idx = m.alloc_id();
867    let i_n = m.alloc_id();
868    m.emit(OP_I_MUL, &[ty_uint, i_n, i_val, p_n]);
869    m.emit(OP_I_ADD, &[ty_uint, b_idx, i_n, col]);
870    let b_ptr = m.alloc_id();
871    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, b_ptr, p_b, b_idx);
872    let b_val = m.alloc_id();
873    m.emit_load(ty_float, b_val, b_ptr);
874
875    // acc += a_broadcast * b_val
876    let prod = m.alloc_id();
877    m.emit(OP_F_MUL, &[ty_float, prod, a_broadcast, b_val]);
878    let old_acc = m.alloc_id();
879    m.emit_load(ty_float, old_acc, var_acc);
880    let new_acc = m.alloc_id();
881    m.emit(OP_F_ADD, &[ty_float, new_acc, old_acc, prod]);
882    m.emit_store(var_acc, new_acc);
883
884    m.emit_branch(label_loop_continue);
885
886    // ── Loop continue ──
887    m.emit_label(label_loop_continue);
888    let i_inc = m.alloc_id();
889    m.emit(OP_I_ADD, &[ty_uint, i_inc, i_val, c_uint_1]);
890    m.emit_store(var_i, i_inc);
891    m.emit_branch(label_loop_header);
892
893    // ── Loop merge ──
894    m.emit_label(label_loop_merge);
895
896    // result = alpha * acc + beta * C[gid]
897    let final_acc = m.alloc_id();
898    m.emit_load(ty_float, final_acc, var_acc);
899    let alpha_acc = m.alloc_id();
900    m.emit(OP_F_MUL, &[ty_float, alpha_acc, p_alpha, final_acc]);
901
902    let c_ptr = m.alloc_id();
903    m.emit_in_bounds_ptr_access_chain(ty_ptr_cross_float, c_ptr, p_c, gid_x);
904    let c_old = m.alloc_id();
905    m.emit_load(ty_float, c_old, c_ptr);
906    let beta_c = m.alloc_id();
907    m.emit(OP_F_MUL, &[ty_float, beta_c, p_beta, c_old]);
908    let c_new = m.alloc_id();
909    m.emit(OP_F_ADD, &[ty_float, c_new, alpha_acc, beta_c]);
910    m.emit_store(c_ptr, c_new);
911
912    m.emit_branch(label_bounds_merge);
913
914    m.emit_label(label_bounds_merge);
915    m.emit_return();
916    m.emit_function_end();
917
918    m.finalize()
919}
920
921// ─── Tests ──────────────────────────────────────────────────
922
923#[cfg(test)]
924mod tests {
925    use super::*;
926    use crate::spirv::SPIRV_MAGIC;
927
928    const OP_CAPABILITY: u32 = 17;
929
930    fn check_valid_spirv(words: &[u32]) {
931        assert!(words.len() >= 5, "too short for SPIR-V header");
932        assert_eq!(words[0], SPIRV_MAGIC, "bad magic");
933        assert!(words[3] > 0, "ID bound must be > 0");
934        assert_eq!(words[4], 0, "schema must be 0");
935    }
936
937    /// Check that the SPIR-V word stream contains a given capability value.
938    fn has_capability(words: &[u32], cap: u32) -> bool {
939        let cap_header = (2u32 << 16) | OP_CAPABILITY;
940        words.windows(2).any(|w| w[0] == cap_header && w[1] == cap)
941    }
942
943    /// Check that the SPIR-V contains an OpEntryPoint with the given name.
944    fn has_entry_point(words: &[u32], name: &str) -> bool {
945        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_le_bytes()).collect();
946        let name_bytes = name.as_bytes();
947        bytes.windows(name_bytes.len()).any(|w| w == name_bytes)
948    }
949
950    #[test]
951    fn reduction_subgroup_valid_spirv() {
952        let words = reduction_subgroup_spirv();
953        check_valid_spirv(&words);
954    }
955
956    #[test]
957    fn reduction_subgroup_word_aligned() {
958        let words = reduction_subgroup_spirv();
959        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();
960        assert_eq!(bytes.len() % 4, 0);
961    }
962
963    #[test]
964    fn reduction_subgroup_has_group_non_uniform_capability() {
965        let words = reduction_subgroup_spirv();
966        assert!(
967            has_capability(&words, CAPABILITY_GROUP_NON_UNIFORM),
968            "missing GroupNonUniform capability"
969        );
970        assert!(
971            has_capability(&words, CAPABILITY_GROUP_NON_UNIFORM_ARITHMETIC),
972            "missing GroupNonUniformArithmetic capability"
973        );
974    }
975
976    #[test]
977    fn reduction_subgroup_has_entry_point() {
978        let words = reduction_subgroup_spirv();
979        assert!(
980            has_entry_point(&words, "reduction_subgroup"),
981            "missing entry point name"
982        );
983    }
984
985    #[test]
986    fn scan_subgroup_valid_spirv() {
987        let words = scan_subgroup_spirv();
988        check_valid_spirv(&words);
989    }
990
991    #[test]
992    fn scan_subgroup_word_aligned() {
993        let words = scan_subgroup_spirv();
994        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();
995        assert_eq!(bytes.len() % 4, 0);
996    }
997
998    #[test]
999    fn scan_subgroup_has_group_non_uniform_capability() {
1000        let words = scan_subgroup_spirv();
1001        assert!(
1002            has_capability(&words, CAPABILITY_GROUP_NON_UNIFORM),
1003            "missing GroupNonUniform capability"
1004        );
1005        assert!(
1006            has_capability(&words, CAPABILITY_GROUP_NON_UNIFORM_ARITHMETIC),
1007            "missing GroupNonUniformArithmetic capability"
1008        );
1009    }
1010
1011    #[test]
1012    fn scan_subgroup_has_entry_point() {
1013        let words = scan_subgroup_spirv();
1014        assert!(
1015            has_entry_point(&words, "scan_subgroup"),
1016            "missing entry point name"
1017        );
1018    }
1019
1020    #[test]
1021    fn gemm_subgroup_valid_spirv() {
1022        let words = gemm_subgroup_spirv();
1023        check_valid_spirv(&words);
1024    }
1025
1026    #[test]
1027    fn gemm_subgroup_word_aligned() {
1028        let words = gemm_subgroup_spirv();
1029        let bytes: Vec<u8> = words.iter().flat_map(|w| w.to_ne_bytes()).collect();
1030        assert_eq!(bytes.len() % 4, 0);
1031    }
1032
1033    #[test]
1034    fn gemm_subgroup_has_group_non_uniform_capability() {
1035        let words = gemm_subgroup_spirv();
1036        assert!(
1037            has_capability(&words, CAPABILITY_GROUP_NON_UNIFORM),
1038            "missing GroupNonUniform capability"
1039        );
1040        assert!(
1041            has_capability(&words, CAPABILITY_GROUP_NON_UNIFORM_SHUFFLE),
1042            "missing GroupNonUniformShuffle capability"
1043        );
1044    }
1045
1046    #[test]
1047    fn gemm_subgroup_has_entry_point() {
1048        let words = gemm_subgroup_spirv();
1049        assert!(
1050            has_entry_point(&words, "gemm_subgroup"),
1051            "missing entry point name"
1052        );
1053    }
1054
1055    #[test]
1056    fn subgroup_shaders_all_word_aligned() {
1057        fn to_bytes(words: &[u32]) -> Vec<u8> {
1058            words.iter().flat_map(|w| w.to_ne_bytes()).collect()
1059        }
1060        assert_eq!(to_bytes(&reduction_subgroup_spirv()).len() % 4, 0);
1061        assert_eq!(to_bytes(&scan_subgroup_spirv()).len() % 4, 0);
1062        assert_eq!(to_bytes(&gemm_subgroup_spirv()).len() % 4, 0);
1063    }
1064}