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 _measurement = cubecl_runtime::dry_run::RealRun::new();
139
140 probed = true;
141 probe(client, key)
142 });
143
144 (value, probed)
145 }
146}
147
148fn probe(client: &Client, key: ThroughputKey) -> Result<ThroughputValue, ThroughputError> {
150 let launch_config = LaunchConfig::for_device(client, key.dtype());
151
152 match key.mode {
153 ThroughputMode::ComputeDirect { dtype } => {
154 ShapeSweep::new(compute_direct_shapes(client, dtype, launch_config))
155 .fastest(|(dtype, config)| Ok(compute_direct::build_kernel(client, dtype, config)))
156 .map(|(value, _)| value)
157 }
158 ThroughputMode::ComputeCmma {
159 config: cmma_config,
160 ..
161 } => ShapeSweep::new(alloc::vec![launch_config])
162 .fastest(|config| Ok(compute_cmma::build_kernel(client, key, cmma_config, config)))
163 .map(|(value, _)| value),
164 ThroughputMode::Memory(spec) => {
165 let (value, fastest) = ShapeSweep::new(WorkerSweep::shapes(
166 client,
167 launch_config,
168 spec.access,
169 ))
170 .fastest(|config| match spec.access {
171 MemoryAccess::Copy => memory_direct::build_kernel(client, key, config, spec),
172 MemoryAccess::Read => memory_read::build_kernel(client, key, config, spec),
173 MemoryAccess::Write => memory_write::build_kernel(client, key, config, spec),
174 })?;
175
176 WorkerSweep::remember(client, spec.access, fastest.cube_dim.num_elems());
177
178 Ok(value)
179 }
180 ThroughputMode::Launch => ShapeSweep::new(alloc::vec![launch_config])
181 .fastest(|config| Ok(launch_overhead::build_kernel(client, key, config)))
182 .map(|(value, _)| value),
183 }
184}
185
186fn declined(features: &Features, key: ThroughputKey) -> bool {
189 match key.mode {
190 ThroughputMode::ComputeDirect { dtype } => !features.supports_type(dtype),
191 ThroughputMode::ComputeCmma { dtype, config } => {
192 !CooperativeMatrix::implemented(features, dtype, config)
193 }
194 ThroughputMode::Memory(_) | ThroughputMode::Launch => false,
195 }
196}
197
198fn compute_direct_shapes(
201 client: &Client,
202 dtype: ElemType,
203 launch_config: LaunchConfig,
204) -> alloc::vec::Vec<(ElemType, LaunchConfig)> {
205 Arithmetic::dtypes(client, dtype)
206 .into_iter()
207 .flat_map(|dtype| {
208 Arithmetic::widths(client, dtype)
209 .into_iter()
210 .map(move |vector_size| {
211 (
212 dtype,
213 LaunchConfig {
214 vector_size,
215 ..launch_config
216 },
217 )
218 })
219 })
220 .collect()
221}
222
223pub fn roofline_bounds(
229 client: &Client,
230 compute_key: ThroughputKey,
231 work: Work,
232 thresholds: Thresholds,
233) -> Bounds {
234 Bounds {
235 bounds: alloc::vec![
236 compute_bound(client, compute_key, work, thresholds.compute),
237 memory_bound(client, MemoryAccess::Copy, work, thresholds.memory),
238 ],
239 launch_overhead: measure_launch_overhead(client),
240 }
241}
242
243pub fn compute_bound(
246 client: &Client,
247 compute_key: ThroughputKey,
248 work: Work,
249 threshold: f32,
250) -> AutotuneBound {
251 let peak = measure_peak_throughput(client, compute_key).unwrap_or(ThroughputValue::ZERO);
253 AutotuneBound {
254 resource: ResourceBound {
255 amount: work.compute_ops,
256 peak_per_s: peak.ops_per_s(),
257 },
258 threshold,
259 }
260}
261
262pub fn memory_bound(
269 client: &Client,
270 access: MemoryAccess,
271 work: Work,
272 threshold: f32,
273) -> AutotuneBound {
274 let footprint = (work.bytes as u64).min(working_set_cap(client, access));
277 let memory_key = ThroughputKey {
278 mode: ThroughputMode::Memory(MemorySpec::new(access, sweep_size(footprint))),
279 };
280 let peak = measure_peak_throughput(client, memory_key).unwrap_or(ThroughputValue::ZERO);
281 AutotuneBound {
282 resource: ResourceBound {
283 amount: work.bytes,
284 peak_per_s: peak.bytes_per_s(&memory_key),
285 },
286 threshold,
287 }
288}
289
290pub fn measure_launch_overhead(client: &Client) -> core::time::Duration {
292 let launch_key = ThroughputKey {
293 mode: ThroughputMode::Launch,
294 };
295 measure_peak_throughput(client, launch_key)
296 .map(|value| value.duration_per_op())
297 .unwrap_or_default()
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use cubecl_core::ir::{
304 FloatKind,
305 features::{MmaConfig, TypeUsage},
306 };
307 use cubecl_runtime::throughput::{CmmaDims, ComputeCmmaConfig};
308
309 const F16: ElemType = ElemType::Float(FloatKind::F16);
310 const F32: ElemType = ElemType::Float(FloatKind::F32);
311
312 const DIMS: CmmaDims = CmmaDims {
313 m: 16,
314 n: 16,
315 k: 16,
316 };
317
318 fn cmma_key(dims: CmmaDims) -> ThroughputKey {
319 ThroughputKey {
320 mode: ThroughputMode::ComputeCmma {
321 dtype: F16,
322 config: ComputeCmmaConfig {
323 accumulator_type: F32,
324 cmma_dims: dims,
325 },
326 },
327 }
328 }
329
330 fn cmma_features(dims: CmmaDims) -> Features {
331 let mut features = Features::default();
332
333 features.matmul.cmma.insert(MmaConfig {
334 a_type: F16,
335 b_type: F16,
336 cd_type: F32,
337 m: dims.m as u32,
338 n: dims.n as u32,
339 k: dims.k as u32,
340 });
341
342 features
343 }
344
345 #[test]
346 fn a_type_the_device_cannot_lower_is_declined() {
347 let key = ThroughputKey {
348 mode: ThroughputMode::ComputeDirect { dtype: F16 },
349 };
350
351 assert!(declined(&Features::default(), key));
352
353 let mut features = Features::default();
354 features
355 .types
356 .elem
357 .insert(F16, TypeUsage::Arithmetic.into());
358
359 assert!(!declined(&features, key));
360 }
361
362 #[test]
363 fn a_cmma_shape_the_device_does_not_have_is_declined() {
364 let features = cmma_features(DIMS);
365
366 assert!(!declined(&features, cmma_key(DIMS)));
367 assert!(declined(&features, cmma_key(CmmaDims { k: 8, ..DIMS })));
368 assert!(declined(&Features::default(), cmma_key(DIMS)));
369 }
370
371 #[test]
372 fn memory_and_launch_are_left_to_the_probe() {
373 let features = Features::default();
374
375 for access in [MemoryAccess::Copy, MemoryAccess::Read, MemoryAccess::Write] {
376 let key = ThroughputKey {
377 mode: ThroughputMode::Memory(MemorySpec::new(access, 1024)),
378 };
379
380 assert!(!declined(&features, key));
381 }
382
383 assert!(!declined(
384 &features,
385 ThroughputKey {
386 mode: ThroughputMode::Launch
387 }
388 ));
389 }
390}