Skip to main content

nxpu_opt/
workgroup.rs

1//! Workgroup size optimization.
2//!
3//! Provides hardware-specific workgroup size selection based on occupancy
4//! analysis, including shared memory and register pressure constraints.
5
6use std::fmt;
7
8use nxpu_ir::Module;
9
10use crate::Pass;
11
12/// Hardware parameters for a specific NPU target.
13#[derive(Debug, Clone)]
14pub struct WorkgroupHwParams {
15    /// Maximum total threads per workgroup.
16    pub max_threads: u32,
17    /// Maximum per-axis workgroup dimensions `[x, y, z]`.
18    pub max_dim: [u32; 3],
19    /// Warp/wavefront size (threads execute in lockstep).
20    pub warp_size: u32,
21    /// Maximum shared memory per workgroup in bytes.
22    pub max_shared_memory: u32,
23    /// Registers available per thread.
24    pub registers_per_thread: u32,
25    /// Maximum warps that can be resident on a single compute unit.
26    pub max_warps_per_cu: u32,
27}
28
29impl WorkgroupHwParams {
30    /// Return hardware parameters for a named target.
31    pub fn for_target(target: &str) -> Self {
32        match target {
33            "qualcomm" => Self {
34                max_threads: 1024,
35                max_dim: [1024, 1024, 64],
36                warp_size: 64,
37                max_shared_memory: 32768,
38                registers_per_thread: 128,
39                max_warps_per_cu: 32,
40            },
41            "samsung" => Self {
42                max_threads: 512,
43                max_dim: [512, 512, 64],
44                warp_size: 32,
45                max_shared_memory: 65536,
46                registers_per_thread: 64,
47                max_warps_per_cu: 48,
48            },
49            _ => Self {
50                // Generic / fallback.
51                max_threads: 256,
52                max_dim: [256, 256, 64],
53                warp_size: 32,
54                max_shared_memory: 16384,
55                registers_per_thread: 64,
56                max_warps_per_cu: 32,
57            },
58        }
59    }
60}
61
62/// Result of occupancy analysis for a given workgroup size.
63#[derive(Debug, Clone)]
64pub struct OccupancyResult {
65    /// Chosen workgroup size `[x, y, z]`.
66    pub workgroup_size: [u32; 3],
67    /// Total threads in the workgroup.
68    pub threads: u32,
69    /// Occupancy ratio (0.0 to 1.0).
70    pub occupancy: f64,
71}
72
73impl fmt::Display for OccupancyResult {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        write!(
76            f,
77            "workgroup [{}x{}x{}] = {} threads, occupancy {:.1}%",
78            self.workgroup_size[0],
79            self.workgroup_size[1],
80            self.workgroup_size[2],
81            self.threads,
82            self.occupancy * 100.0,
83        )
84    }
85}
86
87/// Calculate occupancy for a given workgroup size and resource usage.
88///
89/// Occupancy is the ratio of active warps to maximum warps, limited by:
90/// - Total thread count
91/// - Shared memory usage
92/// - Register usage per thread
93pub fn calculate_occupancy(
94    size: [u32; 3],
95    hw: &WorkgroupHwParams,
96    shared_mem_bytes: u32,
97    regs_per_thread: u32,
98) -> OccupancyResult {
99    let threads = size[0] * size[1] * size[2];
100    let warps_per_group = threads.div_ceil(hw.warp_size);
101
102    // How many workgroups can fit based on shared memory?
103    let groups_by_mem = if shared_mem_bytes == 0 {
104        hw.max_warps_per_cu / warps_per_group.max(1)
105    } else {
106        hw.max_shared_memory / shared_mem_bytes.max(1)
107    };
108
109    // How many workgroups can fit based on register pressure?
110    let total_regs_per_group = regs_per_thread * threads;
111    let total_regs_available = hw.registers_per_thread * hw.warp_size * hw.max_warps_per_cu;
112    let groups_by_regs = total_regs_available
113        .checked_div(total_regs_per_group)
114        .unwrap_or(hw.max_warps_per_cu / warps_per_group.max(1));
115
116    let max_concurrent_groups = groups_by_mem.min(groups_by_regs);
117    let active_warps = max_concurrent_groups * warps_per_group;
118    let occupancy = (active_warps as f64) / (hw.max_warps_per_cu as f64);
119    let occupancy = occupancy.min(1.0);
120
121    OccupancyResult {
122        workgroup_size: size,
123        threads,
124        occupancy,
125    }
126}
127
128/// Find the workgroup size that maximizes occupancy.
129///
130/// Explores candidate sizes that are multiples of `warp_size`, within
131/// hardware dimension limits.
132pub fn optimize_workgroup_size(
133    hw: &WorkgroupHwParams,
134    shared_mem_bytes: u32,
135    regs_per_thread: u32,
136) -> [u32; 3] {
137    let mut best_size = [hw.warp_size.min(hw.max_dim[0]), 1, 1];
138    let mut best_occupancy = 0.0f64;
139
140    // Candidate 1D sizes: multiples of warp_size up to max_threads.
141    let mut threads = hw.warp_size;
142    while threads <= hw.max_threads && threads <= hw.max_dim[0] {
143        let size = [threads, 1, 1];
144        let result = calculate_occupancy(size, hw, shared_mem_bytes, regs_per_thread);
145        if result.occupancy > best_occupancy {
146            best_occupancy = result.occupancy;
147            best_size = size;
148        }
149        threads += hw.warp_size;
150    }
151
152    // Also try 2D configurations.
153    for x in (hw.warp_size..=hw.max_dim[0]).step_by(hw.warp_size as usize) {
154        for y_pow in 0..=3 {
155            let y = 1u32 << y_pow;
156            if y > hw.max_dim[1] {
157                break;
158            }
159            let total = x * y;
160            if total > hw.max_threads {
161                break;
162            }
163            let size = [x, y, 1];
164            let result = calculate_occupancy(size, hw, shared_mem_bytes, regs_per_thread);
165            if result.occupancy > best_occupancy {
166                best_occupancy = result.occupancy;
167                best_size = size;
168            }
169        }
170    }
171
172    best_size
173}
174
175/// Workgroup size optimization pass.
176///
177/// Overrides `entry_point.workgroup_size` with the optimized size for the
178/// generic hardware target.
179#[derive(Debug)]
180pub struct WorkgroupOptimization {
181    target: String,
182}
183
184impl WorkgroupOptimization {
185    /// Create a workgroup optimization pass for the given target.
186    pub fn new(target: &str) -> Self {
187        Self {
188            target: target.to_string(),
189        }
190    }
191}
192
193impl Pass for WorkgroupOptimization {
194    fn name(&self) -> &str {
195        "workgroup-opt"
196    }
197
198    fn run(&self, module: &mut Module) -> bool {
199        let hw = WorkgroupHwParams::for_target(&self.target);
200        let optimal = optimize_workgroup_size(&hw, 0, hw.registers_per_thread);
201        let mut changed = false;
202        for ep in &mut module.entry_points {
203            if ep.workgroup_size != optimal {
204                ep.workgroup_size = optimal;
205                changed = true;
206            }
207        }
208        changed
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    use nxpu_ir::EntryPoint;
216
217    #[test]
218    fn occupancy_full() {
219        let hw = WorkgroupHwParams {
220            max_threads: 256,
221            max_dim: [256, 256, 64],
222            warp_size: 32,
223            max_shared_memory: 65536,
224            registers_per_thread: 32,
225            max_warps_per_cu: 32,
226        };
227        // 256 threads = 8 warps. With max 32 warps, 4 groups fit → 32/32 = 1.0.
228        let result = calculate_occupancy([256, 1, 1], &hw, 0, 32);
229        assert!((result.occupancy - 1.0).abs() < 0.01);
230    }
231
232    #[test]
233    fn occupancy_limited_by_shared_memory() {
234        let hw = WorkgroupHwParams {
235            max_threads: 256,
236            max_dim: [256, 256, 64],
237            warp_size: 32,
238            max_shared_memory: 16384,
239            registers_per_thread: 64,
240            max_warps_per_cu: 32,
241        };
242        // Request half the shared memory per group.
243        let result = calculate_occupancy([256, 1, 1], &hw, 16384, 16);
244        // Only 1 group fits by shared memory.
245        assert!(result.occupancy < 1.0);
246    }
247
248    #[test]
249    fn occupancy_limited_by_registers() {
250        let hw = WorkgroupHwParams {
251            max_threads: 256,
252            max_dim: [256, 256, 64],
253            warp_size: 32,
254            max_shared_memory: 65536,
255            registers_per_thread: 64,
256            max_warps_per_cu: 32,
257        };
258        // High register usage: 128 regs per thread × 256 threads = 32768 regs/group.
259        // Total available: 64 * 32 * 32 = 65536. Only 2 groups fit → 16/32 = 0.5.
260        let result = calculate_occupancy([256, 1, 1], &hw, 0, 128);
261        assert!(result.occupancy < 1.0);
262    }
263
264    #[test]
265    fn optimize_selects_warp_multiple() {
266        let hw = WorkgroupHwParams::for_target("generic");
267        let size = optimize_workgroup_size(&hw, 0, 32);
268        let total = size[0] * size[1] * size[2];
269        assert_eq!(total % hw.warp_size, 0);
270    }
271
272    #[test]
273    fn optimize_respects_max_dims() {
274        let hw = WorkgroupHwParams {
275            max_threads: 256,
276            max_dim: [128, 64, 32],
277            warp_size: 32,
278            max_shared_memory: 16384,
279            registers_per_thread: 64,
280            max_warps_per_cu: 32,
281        };
282        let size = optimize_workgroup_size(&hw, 0, 32);
283        assert!(size[0] <= hw.max_dim[0]);
284        assert!(size[1] <= hw.max_dim[1]);
285        assert!(size[2] <= hw.max_dim[2]);
286    }
287
288    #[test]
289    fn optimize_respects_max_threads() {
290        let hw = WorkgroupHwParams::for_target("qualcomm");
291        let size = optimize_workgroup_size(&hw, 0, 32);
292        let total = size[0] * size[1] * size[2];
293        assert!(total <= hw.max_threads);
294    }
295
296    #[test]
297    fn hw_params_for_target() {
298        let q = WorkgroupHwParams::for_target("qualcomm");
299        let s = WorkgroupHwParams::for_target("samsung");
300        let g = WorkgroupHwParams::for_target("generic");
301        // All three should have distinct configurations.
302        assert_ne!(q.max_threads, s.max_threads);
303        assert_ne!(q.warp_size, s.warp_size);
304        assert_ne!(s.max_shared_memory, g.max_shared_memory);
305    }
306
307    #[test]
308    fn pass_overrides_workgroup_size() {
309        let mut module = Module::default();
310        let func = nxpu_ir::Function::new("ep");
311        module.entry_points.push(EntryPoint {
312            name: "main".into(),
313            workgroup_size: [1, 1, 1],
314            function: func,
315        });
316        let pass = WorkgroupOptimization::new("generic");
317        let changed = pass.run(&mut module);
318        assert!(changed);
319        let ws = module.entry_points[0].workgroup_size;
320        assert!(ws[0] > 1 || ws[1] > 1 || ws[2] > 1);
321    }
322
323    #[test]
324    fn pass_noop_when_optimal() {
325        let hw = WorkgroupHwParams::for_target("generic");
326        let optimal = optimize_workgroup_size(&hw, 0, hw.registers_per_thread);
327        let mut module = Module::default();
328        let func = nxpu_ir::Function::new("ep");
329        module.entry_points.push(EntryPoint {
330            name: "main".into(),
331            workgroup_size: optimal,
332            function: func,
333        });
334        let pass = WorkgroupOptimization::new("generic");
335        let changed = pass.run(&mut module);
336        assert!(!changed);
337    }
338
339    #[test]
340    fn test_at_least_2_backends() {
341        let q = WorkgroupHwParams::for_target("qualcomm");
342        let s = WorkgroupHwParams::for_target("samsung");
343        // Verify they produce different optimal sizes.
344        let opt_q = optimize_workgroup_size(&q, 0, 32);
345        let opt_s = optimize_workgroup_size(&s, 0, 32);
346        // Both should be valid.
347        assert!(opt_q[0] * opt_q[1] * opt_q[2] <= q.max_threads);
348        assert!(opt_s[0] * opt_s[1] * opt_s[2] <= s.max_threads);
349    }
350}