1use cubecl_core::ir::{ElemType, features::Features};
2use cubecl_runtime::{
3 client::Client,
4 runtime::Runtime,
5 throughput::{
6 MemoryAccess, MemoryCurve, MemoryPoint, MemorySpec, ThroughputError, ThroughputKey,
7 ThroughputMode, ThroughputValue, sweep_size, working_set_sweep,
8 },
9 tune::{AutotuneBound, Bounds, ResourceBound, Thresholds, Work},
10};
11
12use crate::throughput::{
13 Arithmetic, CooperativeMatrix, LaunchConfig, PooledProbes, ShapeSweep, WorkerSweep,
14 compute_cmma, compute_direct, launch_overhead, memory_direct, memory_probe, memory_read,
15 memory_write,
16};
17
18pub fn device_throughput<R: Runtime>(
20 device: &R::Device,
21 keys: &[ThroughputKey],
22) -> alloc::vec::Vec<Result<ThroughputValue, ThroughputError>> {
23 let client = R::client(device);
24 keys.iter()
25 .map(|key| measure_peak_throughput(&client, *key))
26 .collect()
27}
28
29pub fn measure_memory_curve(client: &Client, access: MemoryAccess) -> MemoryCurve {
38 let (points, probed) = {
39 let _pooled = PooledProbes::enter(client);
41
42 sweep(client, access, |bytes| {
43 ThroughputMode::Memory(MemorySpec::new(access, bytes))
44 })
45 };
46
47 if probed {
48 PooledProbes::cleanup_unless_held(client);
49 }
50
51 MemoryCurve::new(access, points)
52}
53
54fn sweep(
55 client: &Client,
56 access: MemoryAccess,
57 mode: impl Fn(u64) -> ThroughputMode,
58) -> (alloc::vec::Vec<MemoryPoint>, bool) {
59 let mut probed = false;
60
61 let points = working_set_sweep(working_set_cap(client, access))
62 .into_iter()
63 .filter_map(|bytes| {
64 let key = ThroughputKey { mode: mode(bytes) };
65 let (value, ran) = measure(client, key);
66 probed |= ran;
67
68 Some(MemoryPoint {
69 bytes,
70 value: value.ok()?,
71 })
72 })
73 .collect();
74
75 (points, probed)
76}
77
78fn working_set_cap(client: &Client, access: MemoryAccess) -> u64 {
81 let max_alloc = client.properties().memory.max_page_size;
82
83 memory_probe::window_cap(max_alloc) * access.buffers()
84}
85
86pub fn measure_peak_throughput(
102 client: &Client,
103 key: ThroughputKey,
104) -> Result<ThroughputValue, ThroughputError> {
105 let (value, probed) = measure(client, key);
106
107 if probed {
108 PooledProbes::cleanup_unless_held(client);
109 }
110
111 value
112}
113
114fn measure(
118 client: &Client,
119 key: ThroughputKey,
120) -> (Result<ThroughputValue, ThroughputError>, bool) {
121 #[cfg(target_family = "wasm")]
122 {
123 let _ = (client, key);
124 (Err(ThroughputError::Unsupported), false)
125 }
126
127 #[cfg(not(target_family = "wasm"))]
128 {
129 if declined(&client.properties().features, key) {
132 return (Err(ThroughputError::Unsupported), false);
133 }
134
135 let mut probed = false;
136 let value = client.measure_throughput(key, || {
137 let measuring = cubecl_runtime::execution::StreamModeOverride::new(
140 cubecl_runtime::execution::StreamMode::Execute,
141 client,
142 );
143
144 probed = true;
145 let value = probe(client, key);
146 core::mem::drop(measuring);
147 value
148 });
149
150 (value, probed)
151 }
152}
153
154fn probe(client: &Client, key: ThroughputKey) -> Result<ThroughputValue, ThroughputError> {
156 let launch_config = LaunchConfig::for_device(client, key.dtype());
157
158 match key.mode {
159 ThroughputMode::ComputeDirect { dtype } => {
160 ShapeSweep::new(compute_direct_shapes(client, dtype, launch_config))
161 .fastest(|(dtype, config)| compute_direct::build_kernel(client, dtype, config))
162 .map(|(value, _)| value)
163 }
164 ThroughputMode::ComputeCmma {
165 config: cmma_config,
166 ..
167 } => ShapeSweep::new(alloc::vec![launch_config])
168 .fastest(|config| compute_cmma::build_kernel(client, key, cmma_config, config))
169 .map(|(value, _)| value),
170 ThroughputMode::Memory(spec) => {
171 let (value, fastest) = ShapeSweep::new(WorkerSweep::shapes(
172 client,
173 launch_config,
174 spec.access,
175 ))
176 .fastest(|config| match spec.access {
177 MemoryAccess::Copy => memory_direct::build_kernel(client, key, config, spec),
178 MemoryAccess::Read => memory_read::build_kernel(client, key, config, spec),
179 MemoryAccess::Write => memory_write::build_kernel(client, key, config, spec),
180 })?;
181
182 WorkerSweep::remember(client, spec.access, fastest.cube_dim.num_elems());
183
184 Ok(value)
185 }
186 ThroughputMode::Launch => ShapeSweep::new(alloc::vec![launch_config])
187 .fastest(|config| Ok(launch_overhead::build_kernel(client, key, config)))
188 .map(|(value, _)| value),
189 }
190}
191
192fn declined(features: &Features, key: ThroughputKey) -> bool {
195 match key.mode {
196 ThroughputMode::ComputeDirect { dtype } => !features.supports_type(dtype),
197 ThroughputMode::ComputeCmma { dtype, config } => {
198 !CooperativeMatrix::implemented(features, dtype, config)
199 }
200 ThroughputMode::Memory(_) | ThroughputMode::Launch => false,
201 }
202}
203
204fn compute_direct_shapes(
207 client: &Client,
208 dtype: ElemType,
209 launch_config: LaunchConfig,
210) -> alloc::vec::Vec<(ElemType, LaunchConfig)> {
211 Arithmetic::dtypes(client, dtype)
212 .into_iter()
213 .flat_map(|dtype| {
214 Arithmetic::widths(client, dtype)
215 .into_iter()
216 .map(move |vector_size| {
217 (
218 dtype,
219 LaunchConfig {
220 vector_size,
221 ..launch_config
222 },
223 )
224 })
225 })
226 .collect()
227}
228
229pub fn roofline_bounds(
235 client: &Client,
236 compute_key: ThroughputKey,
237 work: Work,
238 thresholds: Thresholds,
239) -> Bounds {
240 Bounds {
241 bounds: alloc::vec![
242 compute_bound(client, compute_key, work, thresholds.compute),
243 memory_bound(client, MemoryAccess::Copy, work, thresholds.memory),
244 ],
245 launch_overhead: measure_launch_overhead(client),
246 }
247}
248
249pub fn compute_bound(
252 client: &Client,
253 compute_key: ThroughputKey,
254 work: Work,
255 threshold: f32,
256) -> AutotuneBound {
257 let peak = measure_peak_throughput(client, compute_key).unwrap_or(ThroughputValue::ZERO);
259 AutotuneBound {
260 resource: ResourceBound {
261 amount: work.compute_ops,
262 peak_per_s: peak.ops_per_s(),
263 },
264 threshold,
265 }
266}
267
268pub fn memory_bound(
275 client: &Client,
276 access: MemoryAccess,
277 work: Work,
278 threshold: f32,
279) -> AutotuneBound {
280 let footprint = (work.bytes as u64).min(working_set_cap(client, access));
283 let memory_key = ThroughputKey {
284 mode: ThroughputMode::Memory(MemorySpec::new(access, sweep_size(footprint))),
285 };
286 let peak = measure_peak_throughput(client, memory_key).unwrap_or(ThroughputValue::ZERO);
287 AutotuneBound {
288 resource: ResourceBound {
289 amount: work.bytes,
290 peak_per_s: peak.bytes_per_s(&memory_key),
291 },
292 threshold,
293 }
294}
295
296pub fn measure_launch_overhead(client: &Client) -> core::time::Duration {
298 let launch_key = ThroughputKey {
299 mode: ThroughputMode::Launch,
300 };
301 measure_peak_throughput(client, launch_key)
302 .map(|value| value.duration_per_op())
303 .unwrap_or_default()
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use cubecl_core::ir::{
310 FloatKind,
311 features::{MmaConfig, TypeUsage},
312 };
313 use cubecl_runtime::throughput::{CmmaDims, ComputeCmmaConfig};
314
315 const F16: ElemType = ElemType::Float(FloatKind::F16);
316 const F32: ElemType = ElemType::Float(FloatKind::F32);
317
318 const DIMS: CmmaDims = CmmaDims {
319 m: 16,
320 n: 16,
321 k: 16,
322 };
323
324 fn cmma_key(dims: CmmaDims) -> ThroughputKey {
325 ThroughputKey {
326 mode: ThroughputMode::ComputeCmma {
327 dtype: F16,
328 config: ComputeCmmaConfig {
329 accumulator_type: F32,
330 cmma_dims: dims,
331 },
332 },
333 }
334 }
335
336 fn cmma_features(dims: CmmaDims) -> Features {
337 let mut features = Features::default();
338
339 features.matmul.cmma.insert(MmaConfig {
340 a_type: F16,
341 b_type: F16,
342 cd_type: F32,
343 m: dims.m as u32,
344 n: dims.n as u32,
345 k: dims.k as u32,
346 });
347
348 features
349 }
350
351 #[test]
352 fn a_type_the_device_cannot_lower_is_declined() {
353 let key = ThroughputKey {
354 mode: ThroughputMode::ComputeDirect { dtype: F16 },
355 };
356
357 assert!(declined(&Features::default(), key));
358
359 let mut features = Features::default();
360 features
361 .types
362 .elem
363 .insert(F16, TypeUsage::Arithmetic.into());
364
365 assert!(!declined(&features, key));
366 }
367
368 #[test]
369 fn a_cmma_shape_the_device_does_not_have_is_declined() {
370 let features = cmma_features(DIMS);
371
372 assert!(!declined(&features, cmma_key(DIMS)));
373 assert!(declined(&features, cmma_key(CmmaDims { k: 8, ..DIMS })));
374 assert!(declined(&Features::default(), cmma_key(DIMS)));
375 }
376
377 #[test]
378 fn memory_and_launch_are_left_to_the_probe() {
379 let features = Features::default();
380
381 for access in [MemoryAccess::Copy, MemoryAccess::Read, MemoryAccess::Write] {
382 let key = ThroughputKey {
383 mode: ThroughputMode::Memory(MemorySpec::new(access, 1024)),
384 };
385
386 assert!(!declined(&features, key));
387 }
388
389 assert!(!declined(
390 &features,
391 ThroughputKey {
392 mode: ThroughputMode::Launch
393 }
394 ));
395 }
396}