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