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