1use core::time::Duration;
2
3use alloc::vec::Vec;
4
5use crate::config::autotune::AutotuneLevel;
6use crate::tune::TuneInputs;
7
8pub use crate::throughput::ResourceBound;
12
13#[derive(Debug, Clone, PartialEq)]
15#[cfg_attr(serializable, 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(serializable, 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(serializable, 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 pub const UNBOUNDED: Self = Self::uniform(0.0);
105
106 pub const fn for_level(level: &AutotuneLevel) -> Self {
112 match level {
113 AutotuneLevel::Minimal => Self::uniform(0.6),
114 AutotuneLevel::Balanced => Self::uniform(0.8),
115 AutotuneLevel::Extensive => Self::uniform(0.95),
116 AutotuneLevel::Full => Self::UNBOUNDED,
117 }
118 }
119}
120
121impl Default for Thresholds {
122 fn default() -> Self {
125 Self::uniform(1.0)
126 }
127}
128
129impl TimeBound for AutotuneBound {
130 fn time_limit(&self) -> Option<Duration> {
131 if self.threshold <= 0.0 || !self.threshold.is_normal() {
134 return None;
135 }
136 self.resource
137 .time_at_peak()
138 .map(|limit| limit.div_f64(self.threshold as f64))
139 }
140}
141
142impl<B: TimeBound> TimeBound for Vec<B> {
143 fn time_limit(&self) -> Option<Duration> {
144 self.iter().filter_map(|b| b.time_limit()).max()
145 }
146}
147
148impl TimeBound for Bounds {
149 fn time_limit(&self) -> Option<Duration> {
150 self.bounds
151 .time_limit()
152 .map(|limit| limit + self.launch_overhead)
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use alloc::vec;
160
161 fn bound(ops_count: usize, throughput: f64, threshold: f32) -> AutotuneBound {
162 AutotuneBound {
163 resource: ResourceBound {
164 amount: ops_count,
165 peak_per_s: throughput,
166 },
167 threshold,
168 }
169 }
170
171 #[test]
172 fn time_limit_is_ops_over_throughput_scaled_by_threshold() {
173 let limit = bound(8, 4.0, 0.5).time_limit();
175 assert_eq!(limit, Some(Duration::from_secs(4)));
176 }
177
178 #[test]
179 fn time_limit_is_none_when_inputs_are_not_normal() {
180 assert_eq!(bound(8, 0.0, 0.5).time_limit(), None);
183 assert_eq!(bound(8, f64::NAN, 0.5).time_limit(), None);
184 assert_eq!(bound(8, f64::INFINITY, 0.5).time_limit(), None);
185 assert_eq!(bound(8, 4.0, 0.0).time_limit(), None);
186 }
187
188 #[test]
189 fn time_limit_declines_a_negative_threshold_rather_than_panicking() {
190 assert_eq!(bound(8, 4.0, -0.5).time_limit(), None);
194 assert_eq!(bound(8, 4.0, f32::NEG_INFINITY).time_limit(), None);
195 }
196
197 #[test]
198 fn vec_time_limit_takes_the_roofline_max_not_min() {
199 let compute = bound(8, 4.0, 1.0); let memory = bound(8, 8.0, 1.0); let limit = vec![compute, memory].time_limit();
205 assert_eq!(limit, Some(Duration::from_secs(2)));
206 }
207
208 #[test]
209 fn vec_time_limit_skips_non_normal_bounds_and_is_none_when_empty() {
210 let limit = vec![bound(8, 0.0, 1.0), bound(8, 4.0, 1.0)].time_limit();
212 assert_eq!(limit, Some(Duration::from_secs(2)));
213
214 assert_eq!(Vec::<AutotuneBound>::new().time_limit(), None);
215 }
216
217 #[test]
218 fn bounds_time_limit_adds_launch_overhead() {
219 let bounds = Bounds {
220 bounds: vec![bound(8, 4.0, 1.0)], launch_overhead: Duration::from_millis(500),
222 };
223 assert_eq!(bounds.time_limit(), Some(Duration::from_millis(2500)));
224 }
225
226 #[test]
227 fn a_level_tightens_the_limit_as_it_rises() {
228 let threshold = |level| Thresholds::for_level(&level).compute;
229 assert!(threshold(AutotuneLevel::Minimal) < threshold(AutotuneLevel::Balanced));
230 assert!(threshold(AutotuneLevel::Balanced) < threshold(AutotuneLevel::Extensive));
231 assert_eq!(
232 Thresholds::for_level(&AutotuneLevel::Full),
233 Thresholds::UNBOUNDED,
234 "full measures everything"
235 );
236 }
237
238 #[test]
239 fn an_unbounded_threshold_gives_no_time_limit() {
240 let unbounded = bound(8, 4.0, Thresholds::UNBOUNDED.compute);
242 assert_eq!(unbounded.time_limit(), None);
243 }
244
245 #[test]
246 fn the_default_threshold_is_the_roofline_itself() {
247 assert_eq!(Thresholds::default(), Thresholds::uniform(1.0));
248 assert_eq!(Thresholds::default().compute, 1.0);
249 }
250
251 #[test]
252 fn bounds_time_limit_is_none_without_usable_bounds() {
253 let bounds = Bounds {
256 bounds: vec![],
257 launch_overhead: Duration::from_millis(500),
258 };
259 assert_eq!(bounds.time_limit(), None);
260 }
261}