1use std::fmt;
7
8use nxpu_ir::Module;
9
10use crate::Pass;
11
12#[derive(Debug, Clone)]
14pub struct WorkgroupHwParams {
15 pub max_threads: u32,
17 pub max_dim: [u32; 3],
19 pub warp_size: u32,
21 pub max_shared_memory: u32,
23 pub registers_per_thread: u32,
25 pub max_warps_per_cu: u32,
27}
28
29impl WorkgroupHwParams {
30 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 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#[derive(Debug, Clone)]
64pub struct OccupancyResult {
65 pub workgroup_size: [u32; 3],
67 pub threads: u32,
69 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
87pub 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 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 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
128pub 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 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 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#[derive(Debug)]
180pub struct WorkgroupOptimization {
181 target: String,
182}
183
184impl WorkgroupOptimization {
185 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 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 let result = calculate_occupancy([256, 1, 1], &hw, 16384, 16);
244 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 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 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 let opt_q = optimize_workgroup_size(&q, 0, 32);
345 let opt_s = optimize_workgroup_size(&s, 0, 32);
346 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}