1use core::time::Duration;
2
3use alloc::vec::Vec;
4
5use crate::throughput::{ThroughputKey, ThroughputValue};
6use crate::tune::TuneInputs;
7
8pub use crate::throughput::ResourceBound;
12
13#[derive(Debug, Clone, PartialEq)]
15#[cfg_attr(autotune_persistence, derive(serde::Serialize, serde::Deserialize))]
16pub struct Bounds {
17 pub bounds: Vec<AutotuneBound>,
19 pub launch_overhead: Duration,
21}
22
23impl Eq for Bounds {}
25
26#[diagnostic::on_unimplemented(
28 message = "`{Self}` is not a valid bounds generator",
29 label = "invalid bounds generator"
30)]
31pub trait BoundsGenerator<K, I: TuneInputs>: Send + Sync + 'static {
32 fn generate<'a>(&self, key: &K, inputs: &I::At<'a>) -> Bounds;
34}
35
36impl<K, Func, A> BoundsGenerator<K, A> for Func
39where
40 A: Clone + Send + Sync + 'static,
41 K: 'static,
42 Func: Send + Sync + 'static + Fn(&K, &A) -> Bounds,
43{
44 #[inline]
45 fn generate<'a>(&self, key: &K, inputs: &<A as TuneInputs>::At<'a>) -> Bounds {
46 (self)(key, inputs)
47 }
48}
49
50pub trait TimeBound {
52 fn time_limit(&self) -> Option<Duration>;
54}
55
56#[derive(Debug, Clone)]
59#[cfg_attr(autotune_persistence, derive(serde::Serialize, serde::Deserialize))]
60pub struct AutotuneBound {
61 pub resource: ResourceBound,
63 pub threshold: f32,
65}
66
67impl PartialEq for AutotuneBound {
70 fn eq(&self, other: &Self) -> bool {
71 self.resource.peak_per_s.to_bits() == other.resource.peak_per_s.to_bits()
72 && self.threshold.to_bits() == other.threshold.to_bits()
73 && self.resource.amount == other.resource.amount
74 }
75}
76
77impl Eq for AutotuneBound {}
78
79pub use cubecl_common::work::Work;
82
83#[derive(Debug, Clone, Copy, PartialEq)]
85#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
86pub struct Thresholds {
87 pub compute: f32,
89 pub memory: f32,
91}
92
93impl Thresholds {
94 pub const fn uniform(fraction: f32) -> Self {
96 Self {
97 compute: fraction,
98 memory: fraction,
99 }
100 }
101}
102
103impl Default for Thresholds {
104 fn default() -> Self {
107 Self::uniform(1.0)
108 }
109}
110
111pub fn calculate_bounds(
113 work: Work,
114 thresholds: Thresholds,
115 compute_throughput: &ThroughputValue,
116 memory_throughput: &ThroughputValue,
117 memory_key: &ThroughputKey,
118) -> Vec<AutotuneBound> {
119 alloc::vec![
120 AutotuneBound {
121 resource: ResourceBound {
122 amount: work.compute_ops,
123 peak_per_s: compute_throughput.ops_per_s(),
124 },
125 threshold: thresholds.compute,
126 },
127 AutotuneBound {
128 resource: ResourceBound {
129 amount: work.bytes,
130 peak_per_s: memory_throughput.bytes_per_s(memory_key),
131 },
132 threshold: thresholds.memory,
133 },
134 ]
135}
136
137impl TimeBound for AutotuneBound {
138 fn time_limit(&self) -> Option<Duration> {
139 if !self.threshold.is_normal() {
140 return None;
141 }
142 self.resource
143 .time_at_peak()
144 .map(|limit| limit.div_f64(self.threshold as f64))
145 }
146}
147
148impl<B: TimeBound> TimeBound for Vec<B> {
149 fn time_limit(&self) -> Option<Duration> {
150 self.iter().filter_map(|b| b.time_limit()).max()
151 }
152}
153
154impl TimeBound for Bounds {
155 fn time_limit(&self) -> Option<Duration> {
156 self.bounds
157 .time_limit()
158 .map(|limit| limit + self.launch_overhead)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use crate::throughput::ThroughputMode;
165
166 use super::*;
167 use alloc::vec;
168
169 fn bound(ops_count: usize, throughput: f64, threshold: f32) -> AutotuneBound {
170 AutotuneBound {
171 resource: ResourceBound {
172 amount: ops_count,
173 peak_per_s: throughput,
174 },
175 threshold,
176 }
177 }
178
179 #[test]
180 fn time_limit_is_ops_over_throughput_scaled_by_threshold() {
181 let limit = bound(8, 4.0, 0.5).time_limit();
183 assert_eq!(limit, Some(Duration::from_secs(4)));
184 }
185
186 #[test]
187 fn time_limit_is_none_when_inputs_are_not_normal() {
188 assert_eq!(bound(8, 0.0, 0.5).time_limit(), None);
191 assert_eq!(bound(8, f64::NAN, 0.5).time_limit(), None);
192 assert_eq!(bound(8, f64::INFINITY, 0.5).time_limit(), None);
193 assert_eq!(bound(8, 4.0, 0.0).time_limit(), None);
194 }
195
196 #[test]
197 fn vec_time_limit_takes_the_roofline_max_not_min() {
198 let compute = bound(8, 4.0, 1.0); let memory = bound(8, 8.0, 1.0); let limit = vec![compute, memory].time_limit();
204 assert_eq!(limit, Some(Duration::from_secs(2)));
205 }
206
207 #[test]
208 fn vec_time_limit_skips_non_normal_bounds_and_is_none_when_empty() {
209 let limit = vec![bound(8, 0.0, 1.0), bound(8, 4.0, 1.0)].time_limit();
211 assert_eq!(limit, Some(Duration::from_secs(2)));
212
213 assert_eq!(Vec::<AutotuneBound>::new().time_limit(), None);
214 }
215
216 #[test]
217 fn bounds_time_limit_adds_launch_overhead() {
218 let bounds = Bounds {
219 bounds: vec![bound(8, 4.0, 1.0)], launch_overhead: Duration::from_millis(500),
221 };
222 assert_eq!(bounds.time_limit(), Some(Duration::from_millis(2500)));
223 }
224
225 #[test]
226 fn calculate_bounds_applies_a_threshold_per_resource() {
227 let work = Work {
228 compute_ops: 8,
229 bytes: 16,
230 };
231 let thresholds = Thresholds {
232 compute: 0.5,
233 memory: 1.0,
234 };
235 let key = ThroughputKey {
236 mode: ThroughputMode::Memory,
237 };
238
239 let bounds = calculate_bounds(
240 work,
241 thresholds,
242 &ThroughputValue::ZERO,
243 &ThroughputValue::ZERO,
244 &key,
245 );
246
247 assert_eq!(bounds[0].resource.amount, 8);
248 assert_eq!(bounds[0].threshold, 0.5);
249 assert_eq!(bounds[1].resource.amount, 16);
250 assert_eq!(bounds[1].threshold, 1.0);
251 }
252
253 #[test]
254 fn the_default_threshold_is_the_roofline_itself() {
255 assert_eq!(Thresholds::default(), Thresholds::uniform(1.0));
256 assert_eq!(Thresholds::default().compute, 1.0);
257 }
258
259 #[test]
260 fn bounds_time_limit_is_none_without_usable_bounds() {
261 let bounds = Bounds {
264 bounds: vec![],
265 launch_overhead: Duration::from_millis(500),
266 };
267 assert_eq!(bounds.time_limit(), None);
268 }
269}