Skip to main content

nxpu_opt/
vectorize.rs

1//! SIMD vectorization hints.
2//!
3//! Analyzes classified kernel patterns to identify vectorizable dimensions
4//! and compute appropriate vector widths for different register sizes.
5
6use std::fmt;
7
8use nxpu_ir::{Module, Scalar, ScalarKind};
9
10use crate::Pass;
11
12/// Vector width specification for a SIMD operation.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct VectorWidth {
15    /// Scalar element type.
16    pub scalar: Scalar,
17    /// Number of SIMD lanes.
18    pub lanes: u32,
19    /// Register width in bits.
20    pub register_bits: u32,
21}
22
23impl VectorWidth {
24    /// Compute the number of lanes that fit in a register of the given bit width.
25    pub fn for_register_width(scalar: Scalar, register_bits: u32) -> Self {
26        let scalar_bits = scalar_width_bits(scalar);
27        let lanes = (register_bits).checked_div(scalar_bits).unwrap_or(1);
28        Self {
29            scalar,
30            lanes,
31            register_bits,
32        }
33    }
34}
35
36impl fmt::Display for VectorWidth {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(
39            f,
40            "{:?}x{} ({}b reg)",
41            self.scalar, self.lanes, self.register_bits
42        )
43    }
44}
45
46/// A vectorization hint for a specific dimension of an operation.
47#[derive(Debug, Clone)]
48pub struct VectorizationHint {
49    /// Name of the operation.
50    pub op_name: String,
51    /// Name of the dimension to vectorize.
52    pub dim_name: String,
53    /// Recommended vector width.
54    pub vector_width: VectorWidth,
55    /// Whether this dimension is a reduction (e.g., K in MatMul).
56    pub is_reduction: bool,
57    /// Whether memory access along this dimension is contiguous.
58    pub is_contiguous: bool,
59}
60
61impl fmt::Display for VectorizationHint {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        let red = if self.is_reduction {
64            " [reduction]"
65        } else {
66            ""
67        };
68        let contig = if self.is_contiguous {
69            " [contiguous]"
70        } else {
71            ""
72        };
73        write!(
74            f,
75            "{}.{}: {}{red}{contig}",
76            self.op_name, self.dim_name, self.vector_width
77        )
78    }
79}
80
81/// Analyze a classified kernel pattern and produce vectorization hints.
82///
83/// `register_bits` specifies the SIMD register width (e.g., 128 for NEON/SSE,
84/// 256 for AVX2, 512 for AVX-512).
85pub fn analyze_vectorization(
86    pattern: &nxpu_analysis::KernelPattern,
87    register_bits: u32,
88) -> Vec<VectorizationHint> {
89    let scalar = Scalar::F32; // Default; could be refined from pattern.
90    let vw = VectorWidth::for_register_width(scalar, register_bits);
91
92    match pattern {
93        nxpu_analysis::KernelPattern::MatMul { shape, .. } => {
94            vec![
95                VectorizationHint {
96                    op_name: "MatMul".into(),
97                    dim_name: shape.n.clone(),
98                    vector_width: vw.clone(),
99                    is_reduction: false,
100                    is_contiguous: true,
101                },
102                VectorizationHint {
103                    op_name: "MatMul".into(),
104                    dim_name: shape.k.clone(),
105                    vector_width: vw,
106                    is_reduction: true,
107                    is_contiguous: false,
108                },
109            ]
110        }
111        nxpu_analysis::KernelPattern::ElementWise { dim_name, .. } => {
112            vec![VectorizationHint {
113                op_name: "ElementWise".into(),
114                dim_name: dim_name.clone(),
115                vector_width: vw,
116                is_reduction: false,
117                is_contiguous: true,
118            }]
119        }
120        nxpu_analysis::KernelPattern::Activation { dim_name, .. } => {
121            vec![VectorizationHint {
122                op_name: "Activation".into(),
123                dim_name: dim_name.clone(),
124                vector_width: vw,
125                is_reduction: false,
126                is_contiguous: true,
127            }]
128        }
129        nxpu_analysis::KernelPattern::Conv2D { shape, .. } => {
130            vec![VectorizationHint {
131                op_name: "Conv2D".into(),
132                dim_name: shape.width.clone(),
133                vector_width: vw,
134                is_reduction: false,
135                is_contiguous: true,
136            }]
137        }
138        nxpu_analysis::KernelPattern::Reduce { op, axis, .. } => {
139            vec![VectorizationHint {
140                op_name: op.op_name().to_string(),
141                dim_name: format!("axis_{axis}"),
142                vector_width: vw,
143                is_reduction: true,
144                is_contiguous: true,
145            }]
146        }
147        _ => Vec::new(),
148    }
149}
150
151/// Vectorization hint pass.
152///
153/// Classifies entry points and produces vectorization hints for each.
154#[derive(Debug)]
155pub struct VectorizationPass {
156    register_bits: u32,
157}
158
159impl VectorizationPass {
160    /// Create a vectorization pass for the given register width.
161    pub fn new(register_bits: u32) -> Self {
162        Self { register_bits }
163    }
164}
165
166impl Default for VectorizationPass {
167    fn default() -> Self {
168        Self::new(128)
169    }
170}
171
172impl Pass for VectorizationPass {
173    fn name(&self) -> &str {
174        "vectorize"
175    }
176
177    fn run(&self, module: &mut Module) -> bool {
178        let mut any = false;
179        for i in 0..module.entry_points.len() {
180            if let Ok(pattern) = nxpu_analysis::classify_entry_point(module, i) {
181                let hints = analyze_vectorization(&pattern, self.register_bits);
182                if !hints.is_empty() {
183                    any = true;
184                }
185            }
186        }
187        any
188    }
189}
190
191/// Return the width of a scalar type in bits.
192fn scalar_width_bits(scalar: Scalar) -> u32 {
193    match scalar.kind {
194        ScalarKind::Float | ScalarKind::BFloat | ScalarKind::Sint | ScalarKind::Uint => {
195            scalar.width as u32 * 8
196        }
197        ScalarKind::Bool => 8,
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use nxpu_analysis::{
205        ActivationOp, Conv2DShape, ElementWiseOp, KernelPattern, MatMulShape, ReduceOp,
206        TensorBinding, TensorRole,
207    };
208    use nxpu_ir::{
209        AddressSpace, Arena, GlobalVariable, Scalar, StorageAccess, Type, TypeInner, UniqueArena,
210    };
211
212    fn dummy_binding(name: &str, role: TensorRole) -> TensorBinding {
213        let mut types = UniqueArena::new();
214        let ty = types.insert(Type {
215            name: None,
216            inner: TypeInner::Scalar(Scalar::F32),
217        });
218        let mut arena = Arena::new();
219        let handle = arena.append(GlobalVariable {
220            name: Some(name.into()),
221            space: AddressSpace::Storage {
222                access: StorageAccess::LOAD,
223            },
224            binding: None,
225            ty,
226            init: None,
227            layout: None,
228        });
229        TensorBinding {
230            handle,
231            name: name.into(),
232            elem_type: 1, // FLOAT
233            role,
234        }
235    }
236
237    #[test]
238    fn vector_width_f32_128() {
239        let vw = VectorWidth::for_register_width(Scalar::F32, 128);
240        assert_eq!(vw.lanes, 4);
241    }
242
243    #[test]
244    fn vector_width_f16_128() {
245        let vw = VectorWidth::for_register_width(Scalar::F16, 128);
246        assert_eq!(vw.lanes, 8);
247    }
248
249    #[test]
250    fn vector_width_int8_128() {
251        let scalar = Scalar {
252            kind: ScalarKind::Sint,
253            width: 1,
254        };
255        let vw = VectorWidth::for_register_width(scalar, 128);
256        assert_eq!(vw.lanes, 16);
257    }
258
259    #[test]
260    fn vector_width_f32_256() {
261        let vw = VectorWidth::for_register_width(Scalar::F32, 256);
262        assert_eq!(vw.lanes, 8);
263    }
264
265    #[test]
266    fn vectorize_matmul() {
267        let pattern = KernelPattern::MatMul {
268            inputs: [
269                dummy_binding("A", TensorRole::Input),
270                dummy_binding("B", TensorRole::Input),
271            ],
272            output: dummy_binding("C", TensorRole::Output),
273            shape: MatMulShape {
274                m: "M".into(),
275                n: "N".into(),
276                k: "K".into(),
277            },
278        };
279        let hints = analyze_vectorization(&pattern, 128);
280        assert_eq!(hints.len(), 2);
281        // First hint: N dimension (contiguous)
282        assert_eq!(hints[0].dim_name, "N");
283        assert!(hints[0].is_contiguous);
284        assert!(!hints[0].is_reduction);
285        // Second hint: K dimension (reduction)
286        assert_eq!(hints[1].dim_name, "K");
287        assert!(hints[1].is_reduction);
288    }
289
290    #[test]
291    fn vectorize_elementwise() {
292        let pattern = KernelPattern::ElementWise {
293            op: ElementWiseOp::Add,
294            inputs: [
295                dummy_binding("A", TensorRole::Input),
296                dummy_binding("B", TensorRole::Input),
297            ],
298            output: dummy_binding("C", TensorRole::Output),
299            dim_name: "N".into(),
300        };
301        let hints = analyze_vectorization(&pattern, 128);
302        assert_eq!(hints.len(), 1);
303        assert_eq!(hints[0].dim_name, "N");
304        assert!(hints[0].is_contiguous);
305    }
306
307    #[test]
308    fn vectorize_conv2d() {
309        let pattern = KernelPattern::Conv2D {
310            input: dummy_binding("input", TensorRole::Input),
311            weight: dummy_binding("weight", TensorRole::Input),
312            output: dummy_binding("output", TensorRole::Output),
313            shape: Conv2DShape {
314                batch: "1".into(),
315                channels_in: "3".into(),
316                channels_out: "16".into(),
317                height: "32".into(),
318                width: "W".into(),
319                kernel_h: "3".into(),
320                kernel_w: "3".into(),
321                kernel_h_val: 3,
322                kernel_w_val: 3,
323                stride_h: 1,
324                stride_w: 1,
325                pad_h: 1,
326                pad_w: 1,
327                groups: 1,
328                dilation_h: 1,
329                dilation_w: 1,
330            },
331            bias: None,
332            activation: None,
333        };
334        let hints = analyze_vectorization(&pattern, 128);
335        assert_eq!(hints.len(), 1);
336        assert_eq!(hints[0].dim_name, "W");
337    }
338
339    #[test]
340    fn vectorize_reduction() {
341        let pattern = KernelPattern::Reduce {
342            op: ReduceOp::Sum,
343            input: dummy_binding("input", TensorRole::Input),
344            output: dummy_binding("output", TensorRole::Output),
345            axis: 1,
346        };
347        let hints = analyze_vectorization(&pattern, 128);
348        assert_eq!(hints.len(), 1);
349        assert!(hints[0].is_reduction);
350    }
351
352    #[test]
353    fn vectorize_activation() {
354        let pattern = KernelPattern::Activation {
355            op: ActivationOp::Relu,
356            input: dummy_binding("input", TensorRole::Input),
357            output: dummy_binding("output", TensorRole::Output),
358            dim_name: "N".into(),
359        };
360        let hints = analyze_vectorization(&pattern, 128);
361        assert_eq!(hints.len(), 1);
362        assert!(hints[0].is_contiguous);
363        assert!(!hints[0].is_reduction);
364    }
365
366    #[test]
367    fn pass_on_module() {
368        // Empty module → no entry points → no hints.
369        let mut module = Module::default();
370        let pass = VectorizationPass::default();
371        let changed = pass.run(&mut module);
372        assert!(!changed);
373    }
374}