oxicuda_driver/occupancy.rs
1//! GPU occupancy queries for performance optimisation.
2//!
3//! Occupancy measures how effectively GPU resources (warps, registers,
4//! shared memory) are utilised. These queries help select launch
5//! configurations that maximise hardware utilisation.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! # use oxicuda_driver::module::Module;
11//! # fn main() -> Result<(), oxicuda_driver::error::CudaError> {
12//! # let module: Module = unimplemented!();
13//! let func = module.get_function("my_kernel")?;
14//!
15//! // Query the optimal block size for maximum occupancy.
16//! let (min_grid_size, optimal_block_size) = func.optimal_block_size(0)?;
17//! println!("optimal: grid >= {min_grid_size}, block = {optimal_block_size}");
18//!
19//! // Query active blocks per SM for a specific block size.
20//! let active = func.max_active_blocks_per_sm(256, 0)?;
21//! println!("active blocks per SM with 256 threads: {active}");
22//! # Ok(())
23//! # }
24//! ```
25
26use crate::error::CudaResult;
27use crate::loader::try_driver;
28use crate::module::Function;
29
30impl Function {
31 /// Returns the maximum number of active blocks per streaming
32 /// multiprocessor for a given block size and dynamic shared memory.
33 ///
34 /// This is useful for evaluating different block sizes to find
35 /// the configuration that achieves the highest occupancy.
36 ///
37 /// # Parameters
38 ///
39 /// * `block_size` — number of threads per block.
40 /// * `dynamic_smem` — dynamic shared memory per block in bytes
41 /// (set to `0` if the kernel does not use dynamic shared memory).
42 ///
43 /// # Errors
44 ///
45 /// Returns a [`CudaError`](crate::error::CudaError) if the function
46 /// handle is invalid or the driver call fails.
47 pub fn max_active_blocks_per_sm(
48 &self,
49 block_size: i32,
50 dynamic_smem: usize,
51 ) -> CudaResult<i32> {
52 let api = try_driver()?;
53 let mut num_blocks: i32 = 0;
54 crate::cuda_call!((api.cu_occupancy_max_active_blocks_per_multiprocessor)(
55 &mut num_blocks,
56 self.raw(),
57 block_size,
58 dynamic_smem,
59 ))?;
60 Ok(num_blocks)
61 }
62
63 /// Suggests an optimal launch configuration that maximises
64 /// multiprocessor occupancy.
65 ///
66 /// Returns `(min_grid_size, optimal_block_size)` where:
67 ///
68 /// * `min_grid_size` — the minimum number of blocks needed to
69 /// achieve maximum occupancy across all SMs.
70 /// * `optimal_block_size` — the block size (number of threads)
71 /// that achieves maximum occupancy.
72 ///
73 /// # Parameters
74 ///
75 /// * `dynamic_smem` — dynamic shared memory per block in bytes
76 /// (set to `0` if the kernel does not use dynamic shared memory).
77 ///
78 /// # Errors
79 ///
80 /// Returns a [`CudaError`](crate::error::CudaError) if the function
81 /// handle is invalid or the driver call fails.
82 pub fn optimal_block_size(&self, dynamic_smem: usize) -> CudaResult<(i32, i32)> {
83 let api = try_driver()?;
84 let mut min_grid_size: i32 = 0;
85 let mut block_size: i32 = 0;
86 crate::cuda_call!((api.cu_occupancy_max_potential_block_size)(
87 &mut min_grid_size,
88 &mut block_size,
89 self.raw(),
90 None, // no dynamic smem callback
91 dynamic_smem,
92 0, // no block size limit
93 ))?;
94 Ok((min_grid_size, block_size))
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use crate::context::Context;
101 use crate::device::Device;
102 use crate::module::Module;
103
104 /// A minimal, arch-portable empty kernel. `.target sm_70` JIT-recompiles
105 /// forward to any newer device (Ampere/Ada/Hopper).
106 const NOOP_PTX: &str = "\
107.version 7.0
108.target sm_70
109.address_size 64
110.visible .entry noop()
111{
112 ret;
113}
114";
115
116 /// Real-hardware occupancy query: on a live device the driver must return
117 /// a sensible resident-block count and an optimal block size within the
118 /// architectural limits. No-op when no GPU is present.
119 #[test]
120 fn occupancy_query_on_real_device() {
121 let Ok(dev) = Device::get(0) else {
122 return;
123 };
124 // Keep the context alive (and current) for the occupancy queries.
125 let _ctx = match Context::new(&dev) {
126 Ok(c) => std::sync::Arc::new(c),
127 Err(_) => return,
128 };
129 let module = match Module::from_ptx(NOOP_PTX) {
130 Ok(m) => m,
131 Err(_) => return,
132 };
133 let func = module.get_function("noop").expect("noop function");
134
135 // Max resident blocks per SM for a 128-thread block must be >= 1 and
136 // not exceed the hardware ceiling (no device allows > 64 blocks/SM).
137 let blocks = func
138 .max_active_blocks_per_sm(128, 0)
139 .expect("occupancy query");
140 assert!(
141 (1..=64).contains(&blocks),
142 "resident blocks/SM out of range: {blocks}"
143 );
144
145 // The suggested launch config must be a positive block size within the
146 // 1024-thread/block limit and a positive minimum grid.
147 let (min_grid, block) = func.optimal_block_size(0).expect("optimal block size");
148 assert!(
149 (1..=1024).contains(&block),
150 "optimal block size out of range: {block}"
151 );
152 assert!(
153 min_grid >= 1,
154 "min grid size should be >= 1, got {min_grid}"
155 );
156 }
157}