Skip to main content

optirs_gpu/
utils.rs

1//! Small utilities shared by the GPU memory and kernel-launch paths.
2//!
3//! Everything here is pure CPU arithmetic; nothing touches a device except
4//! [`get_optimal_backend`], which probes `scirs2_core::gpu` for a usable
5//! backend.
6
7use scirs2_core::gpu::{GpuBackend, GpuContext};
8
9/// Round `size` up to the next multiple of `alignment`.
10///
11/// Returns `size` unchanged when `alignment` is zero or not a power of two,
12/// since no meaningful rounding is defined in that case.
13pub fn align_size(size: usize, alignment: usize) -> usize {
14    if alignment == 0 || !alignment.is_power_of_two() {
15        return size;
16    }
17    (size + alignment - 1) & !(alignment - 1)
18}
19
20/// Whether `addr` sits on an `alignment` boundary.
21pub fn is_aligned(addr: usize, alignment: usize) -> bool {
22    if !alignment.is_power_of_two() {
23        return false;
24    }
25    addr & (alignment - 1) == 0
26}
27
28/// External fragmentation of a free list given as `(block_size, count)` pairs.
29///
30/// `0.0` means all free memory is in one block; values approaching `1.0` mean
31/// the free memory is split into many small blocks.
32pub fn calculate_fragmentation(free_blocks: &[(usize, usize)]) -> f32 {
33    if free_blocks.is_empty() {
34        return 0.0;
35    }
36
37    let total_free: usize = free_blocks.iter().map(|(size, count)| size * count).sum();
38    let largest_block = free_blocks.iter().map(|(size, _)| *size).max().unwrap_or(0);
39
40    if total_free == 0 {
41        0.0
42    } else {
43        1.0 - (largest_block as f32 / total_free as f32)
44    }
45}
46
47/// Format a byte count with a binary unit suffix.
48pub fn format_bytes(bytes: usize) -> String {
49    const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
50    let mut size = bytes as f64;
51    let mut unit_index = 0;
52
53    while size >= 1024.0 && unit_index < UNITS.len() - 1 {
54        size /= 1024.0;
55        unit_index += 1;
56    }
57
58    if unit_index == 0 {
59        format!("{} {}", bytes, UNITS[unit_index])
60    } else {
61        format!("{:.2} {}", size, UNITS[unit_index])
62    }
63}
64
65/// Smallest power of two greater than or equal to `n`, or `None` on overflow.
66///
67/// `n == 0` maps to `1`. Inputs above `usize::MAX / 2 + 1` have no representable
68/// answer and yield `None` instead of silently wrapping to zero.
69pub fn checked_next_power_of_two(n: usize) -> Option<usize> {
70    if n == 0 {
71        return Some(1);
72    }
73    if n.is_power_of_two() {
74        return Some(n);
75    }
76    let shift = usize::BITS - (n - 1).leading_zeros();
77    if shift >= usize::BITS {
78        None
79    } else {
80        Some(1usize << shift)
81    }
82}
83
84/// Launch geometry for a 1-D kernel over `n` elements.
85///
86/// Returns `(grid_size, block_size)` where `block_size` never exceeds
87/// `max_threads` (nor the 256-wide workgroup the shipped kernels declare), and
88/// `grid_size * block_size >= n` so the tail is always covered.
89///
90/// A `max_threads` of `0` is treated as `1`: a launch geometry of zero threads
91/// would silently drop the whole workload.
92pub fn calculate_block_size(n: usize, max_threads: usize) -> (usize, usize) {
93    let block_size = crate::shaders::WORKGROUP_SIZE.min(max_threads.max(1));
94    let grid_size = n.div_ceil(block_size);
95    (grid_size, block_size)
96}
97
98/// First backend from `SUPPORTED_BACKENDS`-style probing that opens a context.
99///
100/// Backends are tried in the order WebGPU → Metal → OpenCL, then CPU as the
101/// always-available fallback. CUDA and ROCm are not probed: `scirs2-core` 0.6.x
102/// has no working context for either, so probing them would only cost a failed
103/// `nvidia-smi`/`rocm-smi` invocation per call.
104pub fn get_optimal_backend() -> GpuBackend {
105    for backend in [GpuBackend::Wgpu, GpuBackend::Metal, GpuBackend::OpenCL] {
106        if GpuContext::new(backend).is_ok() {
107            return backend;
108        }
109    }
110    GpuBackend::Cpu
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_align_size() {
119        assert_eq!(align_size(100, 256), 256);
120        assert_eq!(align_size(256, 256), 256);
121        assert_eq!(align_size(300, 256), 512);
122        // Non power-of-two and zero alignments are pass-through.
123        assert_eq!(align_size(300, 3), 300);
124        assert_eq!(align_size(300, 0), 300);
125    }
126
127    #[test]
128    fn test_is_aligned() {
129        assert!(is_aligned(0x1000, 256));
130        assert!(!is_aligned(0x1001, 256));
131        assert!(!is_aligned(0x1000, 3));
132    }
133
134    #[test]
135    fn test_format_bytes() {
136        assert_eq!(format_bytes(1024), "1.00 KB");
137        assert_eq!(format_bytes(1048576), "1.00 MB");
138        assert_eq!(format_bytes(512), "512 B");
139    }
140
141    #[test]
142    fn checked_next_power_of_two_handles_edges() {
143        assert_eq!(checked_next_power_of_two(0), Some(1));
144        assert_eq!(checked_next_power_of_two(1), Some(1));
145        assert_eq!(checked_next_power_of_two(100), Some(128));
146        assert_eq!(checked_next_power_of_two(128), Some(128));
147        let highest = 1usize << (usize::BITS - 1);
148        assert_eq!(checked_next_power_of_two(highest), Some(highest));
149        // Anything above the highest power of two has no representable answer.
150        assert_eq!(checked_next_power_of_two(highest + 1), None);
151        assert_eq!(checked_next_power_of_two(usize::MAX), None);
152    }
153
154    #[test]
155    fn calculate_block_size_honours_max_threads_and_covers_the_tail() {
156        // Block size is clamped by max_threads...
157        assert_eq!(calculate_block_size(1000, 64), (16, 64));
158        // ...and by the shipped kernels' workgroup width.
159        assert_eq!(calculate_block_size(1000, 1024), (4, 256));
160        // The tail element is never dropped.
161        let (grid, block) = calculate_block_size(257, 256);
162        assert_eq!((grid, block), (2, 256));
163        assert!(grid * block >= 257);
164        // A zero thread budget must not produce a zero-thread launch.
165        let (grid, block) = calculate_block_size(10, 0);
166        assert_eq!(block, 1);
167        assert_eq!(grid, 10);
168    }
169
170    #[test]
171    fn calculate_fragmentation_bounds() {
172        assert_eq!(calculate_fragmentation(&[]), 0.0);
173        assert_eq!(calculate_fragmentation(&[(1024, 1)]), 0.0);
174        let frag = calculate_fragmentation(&[(256, 4)]);
175        assert!(frag > 0.7 && frag < 0.8, "unexpected fragmentation {frag}");
176    }
177
178    #[test]
179    fn get_optimal_backend_returns_something_usable() {
180        let backend = get_optimal_backend();
181        // Whatever comes back must be a backend a context can actually open.
182        assert!(
183            GpuContext::new(backend).is_ok(),
184            "get_optimal_backend returned unusable backend {backend}"
185        );
186    }
187}