Skip to main content

oxicuda_launch/
grid.rs

1//! Grid and block dimension types for kernel launch configuration.
2//!
3//! CUDA kernels are launched with a grid of thread blocks.
4//! Each block contains threads organized in up to 3 dimensions.
5//!
6//! # Dimension model
7//!
8//! The CUDA execution model uses a two-level hierarchy:
9//!
10//! - **Grid**: A collection of thread blocks, specified as up to 3D dimensions.
11//! - **Block**: A collection of threads within a block, also up to 3D.
12//!
13//! Both are described by [`Dim3`], which defaults unused dimensions to 1.
14//!
15//! # Helper function
16//!
17//! The [`grid_size_for`] function computes the minimum grid size needed
18//! to cover a given number of elements with a given block size (ceiling
19//! division).
20
21use std::fmt;
22
23use oxicuda_driver::error::{CudaError, CudaResult};
24use oxicuda_driver::module::Function;
25
26/// 3-dimensional size specification for grids and blocks.
27///
28/// Used to specify the number of thread blocks in a grid
29/// and the number of threads in a block. Dimensions default
30/// to 1 when not explicitly provided.
31///
32/// # Examples
33///
34/// ```
35/// use oxicuda_launch::Dim3;
36///
37/// // 1D: 256 threads
38/// let block = Dim3::x(256);
39/// assert_eq!(block.x, 256);
40/// assert_eq!(block.y, 1);
41/// assert_eq!(block.z, 1);
42///
43/// // 2D: 16x16 threads
44/// let block = Dim3::xy(16, 16);
45/// assert_eq!(block.total(), 256);
46///
47/// // 3D
48/// let block = Dim3::new(8, 8, 4);
49/// assert_eq!(block.total(), 256);
50///
51/// // From conversions
52/// let block: Dim3 = 256u32.into();
53/// assert_eq!(block, Dim3::x(256));
54///
55/// let block: Dim3 = (16u32, 16u32).into();
56/// assert_eq!(block, Dim3::xy(16, 16));
57///
58/// let block: Dim3 = (8u32, 8u32, 4u32).into();
59/// assert_eq!(block, Dim3::new(8, 8, 4));
60/// ```
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub struct Dim3 {
63    /// X dimension.
64    pub x: u32,
65    /// Y dimension (default 1).
66    pub y: u32,
67    /// Z dimension (default 1).
68    pub z: u32,
69}
70
71impl Dim3 {
72    /// Creates a new `Dim3` with explicit values for all three dimensions.
73    #[inline]
74    pub fn new(x: u32, y: u32, z: u32) -> Self {
75        Self { x, y, z }
76    }
77
78    /// Creates a 1-dimensional `Dim3` with the given X value.
79    ///
80    /// Y and Z are set to 1.
81    #[inline]
82    pub fn x(x: u32) -> Self {
83        Self::new(x, 1, 1)
84    }
85
86    /// Creates a 2-dimensional `Dim3` with the given X and Y values.
87    ///
88    /// Z is set to 1.
89    #[inline]
90    pub fn xy(x: u32, y: u32) -> Self {
91        Self::new(x, y, 1)
92    }
93
94    /// Total number of elements (`x * y * z`), saturating at [`u32::MAX`]
95    /// rather than panicking (debug builds) or silently wrapping (release
96    /// builds) when the true product would overflow a `u32`.
97    ///
98    /// Use [`total_u64`](Self::total_u64) instead when the exact product
99    /// matters, e.g. for `u64`-typed accounting or comparisons.
100    #[inline]
101    pub fn total(&self) -> u32 {
102        u32::try_from(self.total_u64()).unwrap_or(u32::MAX)
103    }
104
105    /// Total number of elements (`x * y * z`) widened to `u64`, saturating
106    /// at [`u64::MAX`] on overflow.
107    ///
108    /// Note that the product of three arbitrary `u32` values does *not*
109    /// always fit in a `u64` (e.g. `u32::MAX` cubed is far larger than
110    /// `u64::MAX`), so the multiplication is carried out in `u128` — which
111    /// is always wide enough for three `u32` factors — before narrowing
112    /// back down, exactly mirroring how [`total`](Self::total) narrows
113    /// this value to `u32`.
114    #[inline]
115    pub fn total_u64(&self) -> u64 {
116        let product: u128 = u128::from(self.x) * u128::from(self.y) * u128::from(self.z);
117        u64::try_from(product).unwrap_or(u64::MAX)
118    }
119}
120
121impl From<u32> for Dim3 {
122    /// Converts a single `u32` into a 1D `Dim3`.
123    #[inline]
124    fn from(x: u32) -> Self {
125        Self::x(x)
126    }
127}
128
129impl From<(u32, u32)> for Dim3 {
130    /// Converts a `(u32, u32)` tuple into a 2D `Dim3`.
131    #[inline]
132    fn from((x, y): (u32, u32)) -> Self {
133        Self::xy(x, y)
134    }
135}
136
137impl From<(u32, u32, u32)> for Dim3 {
138    /// Converts a `(u32, u32, u32)` tuple into a 3D `Dim3`.
139    #[inline]
140    fn from((x, y, z): (u32, u32, u32)) -> Self {
141        Self::new(x, y, z)
142    }
143}
144
145impl fmt::Display for Dim3 {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        if self.z != 1 {
148            write!(f, "({}, {}, {})", self.x, self.y, self.z)
149        } else if self.y != 1 {
150            write!(f, "({}, {})", self.x, self.y)
151        } else {
152            write!(f, "{}", self.x)
153        }
154    }
155}
156
157// ---------------------------------------------------------------------------
158// Occupancy-based auto grid sizing
159// ---------------------------------------------------------------------------
160
161/// Computes optimal grid and block dimensions for a 1D problem of `n` elements.
162///
163/// Queries the CUDA occupancy API to determine the block size that
164/// maximises multiprocessor occupancy for the given kernel function,
165/// then calculates the grid size needed to cover `n` work items.
166///
167/// Returns `(grid_dim, block_dim)` suitable for use with [`LaunchParams`](crate::LaunchParams).
168///
169/// # Errors
170///
171/// Returns a [`CudaError`] if the occupancy
172/// query fails (e.g., invalid function handle, driver not loaded).
173///
174/// # Examples
175///
176/// ```rust,no_run
177/// use oxicuda_launch::grid::auto_grid_for;
178/// # use oxicuda_driver::module::Module;
179///
180/// # let module: Module = todo!();
181/// let func = module.get_function("my_kernel")?;
182/// let (grid, block) = auto_grid_for(&func, 100_000)?;
183/// # Ok::<(), oxicuda_driver::error::CudaError>(())
184/// ```
185pub fn auto_grid_for(func: &Function, n: usize) -> CudaResult<(Dim3, Dim3)> {
186    let (_min_grid, optimal_block) = func.optimal_block_size(0)?;
187    let block_size = optimal_block as u32;
188    let grid_x = checked_ceil_div_u32(n, block_size)?;
189    Ok((Dim3::x(grid_x), Dim3::x(block_size)))
190}
191
192/// Computes optimal grid and block dimensions for a 2D problem.
193///
194/// Given a kernel function and problem dimensions `(width, height)`,
195/// this function determines a 2D block size and the corresponding
196/// grid dimensions.  The block is sized as a square (or near-square)
197/// tile whose total thread count respects the occupancy-optimal value.
198///
199/// Returns `(grid_dim, block_dim)` as 2D [`Dim3`] values.
200///
201/// # Errors
202///
203/// Returns a [`CudaError`] if the occupancy
204/// query fails.
205///
206/// # Examples
207///
208/// ```rust,no_run
209/// use oxicuda_launch::grid::auto_grid_2d;
210/// # use oxicuda_driver::module::Module;
211///
212/// # let module: Module = todo!();
213/// let func = module.get_function("my_kernel_2d")?;
214/// let (grid, block) = auto_grid_2d(&func, 1920, 1080)?;
215/// # Ok::<(), oxicuda_driver::error::CudaError>(())
216/// ```
217pub fn auto_grid_2d(func: &Function, width: usize, height: usize) -> CudaResult<(Dim3, Dim3)> {
218    let (_min_grid, optimal_block) = func.optimal_block_size(0)?;
219    let total = optimal_block as u32;
220
221    // Find a near-square block tile. Start from sqrt and round down to
222    // powers-of-two-friendly values.
223    let sqrt_approx = (total as f64).sqrt() as u32;
224    let block_x = nearest_power_of_two_le(sqrt_approx).max(1);
225    let block_y = (total / block_x).max(1);
226
227    let grid_x = checked_ceil_div_u32(width, block_x)?;
228    let grid_y = checked_ceil_div_u32(height, block_y)?;
229
230    Ok((Dim3::xy(grid_x, grid_y), Dim3::xy(block_x, block_y)))
231}
232
233/// Computes `ceil(n / divisor)` widened to `u64` before narrowing back to
234/// `u32`, rejecting the result instead of silently truncating when `n` is
235/// large enough (`n >= 2^32`) that the true grid size would not fit in a
236/// `u32`.
237///
238/// Extracted from [`auto_grid_for`]/[`auto_grid_2d`] so the overflow
239/// behaviour is unit-testable without a live GPU/kernel.
240///
241/// # Errors
242///
243/// Returns [`CudaError::InvalidValue`] if the ceiling-divided result does
244/// not fit in a `u32`.
245fn checked_ceil_div_u32(n: usize, divisor: u32) -> CudaResult<u32> {
246    let wide: u64 = if n == 0 {
247        0
248    } else {
249        (n as u64).div_ceil(u64::from(divisor))
250    };
251    u32::try_from(wide).map_err(|_| CudaError::InvalidValue)
252}
253
254/// Returns the largest power of two that is less than or equal to `n`.
255///
256/// Returns 1 if `n` is 0.
257fn nearest_power_of_two_le(n: u32) -> u32 {
258    if n == 0 {
259        return 1;
260    }
261    // Highest bit position gives the largest power-of-2 <= n.
262    1u32 << (31 - n.leading_zeros())
263}
264
265// ---------------------------------------------------------------------------
266// Simple grid sizing helper
267// ---------------------------------------------------------------------------
268
269/// Calculate the grid size needed to cover `n` elements with `block_size` threads.
270///
271/// Returns `(n + block_size - 1) / block_size`, i.e., ceiling division.
272/// This is the standard formula for determining how many thread blocks
273/// are needed to process `n` work items when each block handles
274/// `block_size` items.
275///
276/// # Panics
277///
278/// Panics if `block_size` is zero.
279///
280/// # Examples
281///
282/// ```
283/// use oxicuda_launch::grid_size_for;
284///
285/// assert_eq!(grid_size_for(1000, 256), 4);  // 4 * 256 = 1024 >= 1000
286/// assert_eq!(grid_size_for(256, 256), 1);
287/// assert_eq!(grid_size_for(257, 256), 2);
288/// assert_eq!(grid_size_for(0, 256), 0);
289/// assert_eq!(grid_size_for(1, 256), 1);
290/// ```
291#[inline]
292pub fn grid_size_for(n: u32, block_size: u32) -> u32 {
293    n.div_ceil(block_size)
294}
295
296// ---------------------------------------------------------------------------
297// Tests
298// ---------------------------------------------------------------------------
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn dim3_new() {
306        let d = Dim3::new(4, 5, 6);
307        assert_eq!(d.x, 4);
308        assert_eq!(d.y, 5);
309        assert_eq!(d.z, 6);
310    }
311
312    #[test]
313    fn dim3_x() {
314        let d = Dim3::x(128);
315        assert_eq!(d, Dim3::new(128, 1, 1));
316    }
317
318    #[test]
319    fn dim3_xy() {
320        let d = Dim3::xy(16, 16);
321        assert_eq!(d, Dim3::new(16, 16, 1));
322    }
323
324    #[test]
325    fn dim3_total() {
326        assert_eq!(Dim3::x(256).total(), 256);
327        assert_eq!(Dim3::xy(16, 16).total(), 256);
328        assert_eq!(Dim3::new(8, 8, 4).total(), 256);
329        assert_eq!(Dim3::new(1, 1, 1).total(), 1);
330    }
331
332    #[test]
333    fn dim3_from_u32() {
334        let d: Dim3 = 512u32.into();
335        assert_eq!(d, Dim3::x(512));
336    }
337
338    #[test]
339    fn dim3_from_tuple2() {
340        let d: Dim3 = (32u32, 8u32).into();
341        assert_eq!(d, Dim3::xy(32, 8));
342    }
343
344    #[test]
345    fn dim3_from_tuple3() {
346        let d: Dim3 = (4u32, 4u32, 4u32).into();
347        assert_eq!(d, Dim3::new(4, 4, 4));
348    }
349
350    #[test]
351    fn dim3_display_1d() {
352        assert_eq!(format!("{}", Dim3::x(256)), "256");
353    }
354
355    #[test]
356    fn dim3_display_2d() {
357        assert_eq!(format!("{}", Dim3::xy(16, 16)), "(16, 16)");
358    }
359
360    #[test]
361    fn dim3_display_3d() {
362        assert_eq!(format!("{}", Dim3::new(8, 8, 4)), "(8, 8, 4)");
363    }
364
365    #[test]
366    fn dim3_eq_and_hash() {
367        use std::collections::HashSet;
368        let mut set = HashSet::new();
369        set.insert(Dim3::x(256));
370        assert!(set.contains(&Dim3::new(256, 1, 1)));
371        assert!(!set.contains(&Dim3::x(128)));
372    }
373
374    #[test]
375    fn grid_size_for_exact() {
376        assert_eq!(grid_size_for(256, 256), 1);
377        assert_eq!(grid_size_for(512, 256), 2);
378    }
379
380    #[test]
381    fn grid_size_for_remainder() {
382        assert_eq!(grid_size_for(257, 256), 2);
383        assert_eq!(grid_size_for(1000, 256), 4);
384        assert_eq!(grid_size_for(1, 256), 1);
385    }
386
387    #[test]
388    fn grid_size_for_zero_elements() {
389        assert_eq!(grid_size_for(0, 256), 0);
390    }
391
392    #[test]
393    fn grid_size_for_one_block() {
394        assert_eq!(grid_size_for(1, 1), 1);
395        assert_eq!(grid_size_for(100, 100), 1);
396    }
397
398    #[test]
399    fn nearest_power_of_two_le_values() {
400        assert_eq!(super::nearest_power_of_two_le(0), 1);
401        assert_eq!(super::nearest_power_of_two_le(1), 1);
402        assert_eq!(super::nearest_power_of_two_le(2), 2);
403        assert_eq!(super::nearest_power_of_two_le(3), 2);
404        assert_eq!(super::nearest_power_of_two_le(4), 4);
405        assert_eq!(super::nearest_power_of_two_le(5), 4);
406        assert_eq!(super::nearest_power_of_two_le(16), 16);
407        assert_eq!(super::nearest_power_of_two_le(17), 16);
408        assert_eq!(super::nearest_power_of_two_le(255), 128);
409        assert_eq!(super::nearest_power_of_two_le(256), 256);
410    }
411
412    // ---------------------------------------------------------------------------
413    // Dim3::total_u64 / checked_ceil_div_u32 — overflow-safety regression tests
414    // ---------------------------------------------------------------------------
415
416    #[test]
417    fn dim3_total_u64_matches_total_for_small_values() {
418        let d = Dim3::new(8, 8, 4);
419        assert_eq!(d.total_u64(), 256u64);
420        assert_eq!(d.total_u64(), u64::from(d.total()));
421    }
422
423    #[test]
424    fn dim3_total_saturates_at_u32_max_when_u64_product_is_exact() {
425        // x * y * z here vastly overflows u32::MAX (~4.29e9) but still fits
426        // exactly in a u64 (u32::MAX squared is ~1.84e19, just under
427        // u64::MAX): total() must saturate to u32::MAX rather than
428        // panicking (debug) or wrapping (release), while total_u64() must
429        // report the true, non-saturated product.
430        let d = Dim3::new(u32::MAX, u32::MAX, 1);
431        assert_eq!(d.total(), u32::MAX);
432        let expected_u64 = u64::from(u32::MAX) * u64::from(u32::MAX);
433        assert_eq!(d.total_u64(), expected_u64);
434        assert!(d.total_u64() > u64::from(u32::MAX));
435    }
436
437    #[test]
438    fn dim3_total_u64_saturates_instead_of_panicking_when_product_overflows_u64() {
439        // Three u32::MAX-scale factors can reach ~7.9e28 — far beyond even
440        // u64::MAX (~1.8e19) — so total_u64() itself must saturate at
441        // u64::MAX rather than panicking (debug) or wrapping (release).
442        // This is exactly the same class of overflow bug as total()'s
443        // u32 overflow, just one level up; total_u64() must not
444        // reintroduce it.
445        let d = Dim3::new(u32::MAX, u32::MAX, 2);
446        assert_eq!(d.total_u64(), u64::MAX);
447        assert_eq!(d.total(), u32::MAX);
448    }
449
450    #[test]
451    fn dim3_total_u64_one_is_one() {
452        assert_eq!(Dim3::new(1, 1, 1).total_u64(), 1u64);
453    }
454
455    #[test]
456    fn checked_ceil_div_u32_normal_values() {
457        assert_eq!(super::checked_ceil_div_u32(1000, 256).unwrap(), 4);
458        assert_eq!(super::checked_ceil_div_u32(256, 256).unwrap(), 1);
459        assert_eq!(super::checked_ceil_div_u32(0, 256).unwrap(), 0);
460        assert_eq!(super::checked_ceil_div_u32(257, 256).unwrap(), 2);
461    }
462
463    #[test]
464    #[cfg(target_pointer_width = "64")]
465    fn checked_ceil_div_u32_rejects_overflow_instead_of_truncating() {
466        // n so large that n / block_size overflows u32::MAX — must be
467        // rejected with an error rather than silently truncated to a
468        // too-small (and therefore too-few-blocks) grid dimension.
469        let n: usize = (u32::MAX as usize) + 1;
470        let result = super::checked_ceil_div_u32(n, 1);
471        assert!(
472            result.is_err(),
473            "grid dimension overflowing u32 must be rejected, got {result:?}"
474        );
475    }
476
477    #[test]
478    #[cfg(target_pointer_width = "64")]
479    fn checked_ceil_div_u32_boundary_fits_exactly() {
480        // Exactly u32::MAX elements at block_size 1 must fit (grid_x ==
481        // u32::MAX), not be rejected.
482        let n: usize = u32::MAX as usize;
483        let result = super::checked_ceil_div_u32(n, 1);
484        assert_eq!(result.unwrap(), u32::MAX);
485    }
486
487    #[test]
488    fn auto_grid_for_signature_compiles() {
489        let _f: fn(
490            &oxicuda_driver::module::Function,
491            usize,
492        ) -> oxicuda_driver::error::CudaResult<(Dim3, Dim3)> = super::auto_grid_for;
493    }
494
495    #[test]
496    fn auto_grid_2d_signature_compiles() {
497        let _f: fn(
498            &oxicuda_driver::module::Function,
499            usize,
500            usize,
501        ) -> oxicuda_driver::error::CudaResult<(Dim3, Dim3)> = super::auto_grid_2d;
502    }
503
504    #[cfg(feature = "gpu-tests")]
505    #[test]
506    fn auto_grid_for_with_real_kernel() {
507        use std::sync::Arc;
508        oxicuda_driver::init().ok();
509        if let Ok(dev) = oxicuda_driver::device::Device::get(0) {
510            let _ctx = Arc::new(oxicuda_driver::context::Context::new(&dev).expect("ctx"));
511            let ptx = ".version 7.0\n.target sm_70\n.address_size 64\n.visible .entry test_kernel(.param .u32 n) { ret; }";
512            if let Ok(module) = oxicuda_driver::module::Module::from_ptx(ptx) {
513                let func = module.get_function("test_kernel").expect("func");
514                let (grid, block) = super::auto_grid_for(&func, 10000).expect("auto_grid");
515                assert!(grid.x > 0);
516                assert!(block.x > 0);
517            }
518        }
519    }
520}