Skip to main content

oxigdal_gpu/
workgroup_tuner.rs

1//! Workgroup-size auto-tuning derived from GPU adapter limits.
2//!
3//! Different GPUs expose different compute limits. This module derives
4//! optimal workgroup sizes at context-creation time so every kernel
5//! dispatches with sizes that the actual device can support.
6
7use wgpu::Limits;
8
9/// Optimal workgroup sizes derived from GPU adapter limits.
10///
11/// Workgroup sizes are clamped to the adapter's reported maximums,
12/// so kernels compile and dispatch correctly on every device.
13///
14/// # Derivation
15///
16/// Call [`WorkgroupTuner::derive_from_limits`] after obtaining the adapter
17/// to get device-specific sizes.  Use [`WorkgroupTuner::unlimited`] (or
18/// [`Default`]) when no real device is available (e.g. in unit tests).
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct WorkgroupTuner {
21    /// 2D workgroup size for raster/image kernels (e.g. 16×16 or 8×8).
22    pub raster_2d: (u32, u32),
23    /// 1D workgroup size for reduction kernels (e.g. 256 or 64).
24    pub reduction: u32,
25    /// 1D workgroup size for FFT kernels (e.g. 64 or 32).
26    pub fft: u32,
27}
28
29impl WorkgroupTuner {
30    /// Derive optimal workgroup sizes from the adapter's [`Limits`].
31    ///
32    /// The function tries preferred sizes in descending order of quality and
33    /// falls back to smaller values when the adapter cannot accommodate the
34    /// larger ones.
35    ///
36    /// | Class         | Preferred | Fallback 1 | Fallback 2 |
37    /// |---------------|-----------|------------|------------|
38    /// | `raster_2d`   | 16 × 16   | 8 × 8      | 4 × 4      |
39    /// | `reduction`   | 256       | —          | —          |
40    /// | `fft`         | 64        | —          | —          |
41    ///
42    /// `reduction` and `fft` are clamped to `min(preferred, max_x, max_total)`.
43    pub fn derive_from_limits(limits: &Limits) -> Self {
44        let max_x = limits.max_compute_workgroup_size_x;
45        let max_y = limits.max_compute_workgroup_size_y;
46        let max_total = limits.max_compute_invocations_per_workgroup;
47
48        // Raster 2D: prefer 16×16 = 256 total, fall back to 8×8 = 64, then 4×4 = 16.
49        let raster_2d = if max_x >= 16 && max_y >= 16 && max_total >= 256 {
50            (16, 16)
51        } else if max_x >= 8 && max_y >= 8 && max_total >= 64 {
52            (8, 8)
53        } else {
54            (4, 4)
55        };
56
57        // Reduction 1D: min(256, max_x, max_total) — must never exceed either limit.
58        let reduction = 256u32.min(max_x).min(max_total);
59
60        // FFT 1D: min(64, max_x, max_total).
61        let fft = 64u32.min(max_x).min(max_total);
62
63        Self {
64            raster_2d,
65            reduction,
66            fft,
67        }
68    }
69
70    /// Default tuner assuming unbounded adapter limits (production default).
71    ///
72    /// Equivalent to calling [`derive_from_limits`] with `Limits::default()`,
73    /// which reports `max_compute_workgroup_size_x = 256`,
74    /// `max_compute_workgroup_size_y = 256`, and
75    /// `max_compute_invocations_per_workgroup = 256`.
76    ///
77    /// [`derive_from_limits`]: WorkgroupTuner::derive_from_limits
78    pub fn unlimited() -> Self {
79        Self {
80            raster_2d: (16, 16),
81            reduction: 256,
82            fft: 64,
83        }
84    }
85}
86
87impl Default for WorkgroupTuner {
88    fn default() -> Self {
89        Self::unlimited()
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    fn limits_with(max_x: u32, max_y: u32, max_total: u32) -> Limits {
98        Limits {
99            max_compute_workgroup_size_x: max_x,
100            max_compute_workgroup_size_y: max_y,
101            max_compute_invocations_per_workgroup: max_total,
102            ..Limits::default()
103        }
104    }
105
106    #[test]
107    fn test_tuner_picks_16x16_when_limits_unbounded() {
108        let tuner = WorkgroupTuner::derive_from_limits(&Limits::default());
109        assert_eq!(tuner.raster_2d, (16, 16));
110        assert_eq!(tuner.reduction, 256);
111        assert_eq!(tuner.fft, 64);
112    }
113
114    #[test]
115    fn test_tuner_falls_back_8x8_on_low_end_adapter() {
116        // max_x=8, max_y=8, max_total=64 → can't fit 16×16=256, so use 8×8.
117        let tuner = WorkgroupTuner::derive_from_limits(&limits_with(8, 8, 64));
118        assert_eq!(tuner.raster_2d, (8, 8));
119    }
120
121    #[test]
122    fn test_tuner_falls_back_4x4_on_minimum_adapter() {
123        // Extremely constrained: max 4×4=16 total.
124        let tuner = WorkgroupTuner::derive_from_limits(&limits_with(4, 4, 16));
125        assert_eq!(tuner.raster_2d, (4, 4));
126    }
127
128    #[test]
129    fn test_tuner_respects_max_invocations_per_workgroup() {
130        // max_x=32, max_y=32, but max_total=64 → 16×16=256 > 64, so fall back.
131        let tuner = WorkgroupTuner::derive_from_limits(&limits_with(32, 32, 64));
132        assert_eq!(tuner.raster_2d, (8, 8));
133    }
134
135    #[test]
136    fn test_tuner_reduction_capped_at_256() {
137        let tuner = WorkgroupTuner::derive_from_limits(&Limits::default());
138        assert!(tuner.reduction <= 256);
139        // Also verify it doesn't exceed adapter limits.
140        let constrained = limits_with(128, 128, 128);
141        let tuner2 = WorkgroupTuner::derive_from_limits(&constrained);
142        assert!(tuner2.reduction <= 128);
143    }
144
145    #[test]
146    fn test_tuner_unlimited_returns_max_defaults() {
147        let tuner = WorkgroupTuner::unlimited();
148        assert_eq!(tuner.raster_2d, (16, 16));
149        assert_eq!(tuner.reduction, 256);
150        assert_eq!(tuner.fft, 64);
151    }
152
153    #[test]
154    fn test_tuner_default_equals_unlimited() {
155        let a = WorkgroupTuner::default();
156        let b = WorkgroupTuner::unlimited();
157        assert_eq!(a, b);
158    }
159
160    #[test]
161    fn test_tuner_reduction_respects_max_x_cap() {
162        // max_x is the binding limit for 1D kernels.
163        let tuner = WorkgroupTuner::derive_from_limits(&limits_with(64, 256, 256));
164        assert!(tuner.reduction <= 64);
165    }
166
167    #[test]
168    fn test_tuner_fft_capped_at_64() {
169        let tuner = WorkgroupTuner::derive_from_limits(&Limits::default());
170        assert!(tuner.fft <= 64);
171    }
172
173    #[test]
174    fn test_tuner_fft_respects_low_adapter_limits() {
175        let tuner = WorkgroupTuner::derive_from_limits(&limits_with(32, 32, 32));
176        assert!(tuner.fft <= 32);
177        assert_eq!(tuner.fft, 32);
178    }
179
180    #[test]
181    fn test_tuner_clone_and_copy() {
182        let a = WorkgroupTuner::unlimited();
183        let b = a; // Copy
184        let c = a; // Copy again (a still valid)
185        assert_eq!(b, c);
186        assert_eq!(a, b);
187    }
188
189    #[test]
190    fn test_tuner_debug_format() {
191        let tuner = WorkgroupTuner::unlimited();
192        let s = format!("{tuner:?}");
193        assert!(s.contains("WorkgroupTuner"));
194        assert!(s.contains("raster_2d"));
195    }
196}