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}