1use std::{fmt, string::{String, ToString}, time::Duration, vec::Vec};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Mode {
7 Explore,
9 CacheOnly,
12 Disabled
15}
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum MemoryEviction {
19 Fifo,
21 Lru,
23}
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Timing {
27 Device,
29 EndToEnd
31}
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Validation {
35 Passed,
37 Unsupported
40}
41
42#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct Tolerance {
45 pub absolute: f64,
47 pub relative: f64,
49 pub max_bytes: u64,
51}
52impl Default for Tolerance {
53 fn default() -> Self { Self { absolute: 1e-4, relative: 1e-3, max_bytes: 64 << 20 } }
54}
55
56#[derive(Debug, Clone)]
59pub struct StackPolicy {
60 pub mode: Mode,
62 pub timing: Timing,
64 pub require_validation: bool,
66 pub tolerance: Tolerance,
68 pub warmups: usize,
70 pub samples: usize,
72 pub max_candidates: usize,
74 pub budget: Duration,
76 pub min_speedup: f64,
79 pub max_relative_mad: f64,
81 pub workspace_limit: Option<u64>,
84 pub capacity: usize,
86 pub max_parallel_tunes: usize,
88 pub ttl: Duration,
90 pub regression_pairs: usize,
92 pub regression_ratio: f64,
94}
95impl Default for StackPolicy {
96 fn default() -> Self {
97 Self {
98 mode: Mode::Explore, timing: Timing::EndToEnd, require_validation: true,
99 tolerance: Tolerance::default(), warmups: 2, samples: 7,
100 max_candidates: 32, budget: Duration::from_secs(30), min_speedup: 1.05,
101 max_relative_mad: 0.15, workspace_limit: None, capacity: 1024,
102 max_parallel_tunes: 1, ttl: Duration::from_secs(7 * 24 * 3600),
103 regression_pairs: 7, regression_ratio: 1.15,
104 }
105 }
106}
107impl StackPolicy {
108 pub fn validate(&self) -> Result<(), TuneFailure> {
111 if self.warmups == 0 || self.warmups > 100 || self.samples < 3
112 || self.samples > 101 || self.samples % 2 == 0
113 || self.max_candidates == 0 || self.max_candidates > 4096
114 || self.budget.is_zero() || self.capacity == 0 || self.capacity > 65536
115 || self.max_parallel_tunes == 0 || self.max_parallel_tunes > 256
116 || self.ttl.is_zero() || self.regression_pairs < 3 || self.regression_pairs > 101
117 || self.regression_pairs % 2 == 0
118 || !self.min_speedup.is_finite() || self.min_speedup < 1.0
119 || !self.max_relative_mad.is_finite() || self.max_relative_mad < 0.0
120 || !self.regression_ratio.is_finite() || self.regression_ratio <= 1.0
121 || !self.tolerance.absolute.is_finite() || self.tolerance.absolute < 0.0
122 || !self.tolerance.relative.is_finite() || self.tolerance.relative < 0.0
123 || self.tolerance.max_bytes == 0 {
124 return Err(TuneFailure::invalid("invalid stack autotune policy"));
125 }
126 Ok(())
127 }
128 pub fn accuracy_key(&self) -> String {
130 std::format!("timing={:?};checked={};atol={:016x};rtol={:016x};workspace={:?};min_speedup={:016x};mad={:016x}",
131 self.timing, self.require_validation, self.tolerance.absolute.to_bits(),
132 self.tolerance.relative.to_bits(), self.workspace_limit,
133 self.min_speedup.to_bits(), self.max_relative_mad.to_bits())
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139#[repr(u8)]
140pub enum Scope {
141 Operator = 0,
143 Graph = 1,
145 Pipeline = 2
147}
148
149#[derive(Debug, Clone)]
151pub struct Problem {
152 pub scope: Scope,
154 pub operation: String,
156 pub environment: String,
158 pub workload: String,
160 pub execution_context: String,
162 pub persistent: bool,
164}
165#[derive(Clone, Copy)]
166pub(super) struct ProblemView<'a> {
167 pub(super) scope: Scope,
168 pub(super) operation: &'a str,
169 pub(super) environment: &'a str,
170 pub(super) workload: &'a str,
171 pub(super) execution_context: &'a str,
172 pub(super) persistent: bool,
173}
174impl<'a> From<&'a Problem> for ProblemView<'a> {
175 fn from(problem: &'a Problem) -> Self {
176 Self { scope: problem.scope, operation: &problem.operation, environment: &problem.environment,
177 workload: &problem.workload, execution_context: &problem.execution_context, persistent: problem.persistent }
178 }
179}
180#[derive(Debug, Clone)]
182pub struct Candidate {
183 pub name: String,
185 pub revision: String,
187 pub workspace_bytes: Option<u64>,
189 pub eligible: bool,
191}
192impl Candidate {
193 pub fn new(name: impl Into<String>) -> Self {
195 let name = name.into();
196 let view = CandidateView::new(&name);
197 let revision = view.revision.into();
198 let workspace_bytes = view.workspace_bytes;
199 let eligible = view.eligible;
200 Self { name, revision, workspace_bytes, eligible }
201 }
202 pub fn fits(&self, policy: &StackPolicy) -> bool {
204 self.view().fits(policy)
205 }
206}
207
208#[derive(Clone, Copy)]
209pub(super) struct CandidateView<'a> {
210 pub(super) name: &'a String,
211 pub(super) revision: &'a str,
212 pub(super) workspace_bytes: Option<u64>,
213 pub(super) eligible: bool,
214}
215
216impl<'a> CandidateView<'a> {
217 pub(super) fn new(name: &'a String) -> Self {
218 Self { name, revision: "1", workspace_bytes: None, eligible: true }
219 }
220
221 pub(super) fn fits(&self, policy: &StackPolicy) -> bool {
222 self.eligible && match policy.workspace_limit {
223 Some(limit) => self.workspace_bytes.is_some_and(|n| n <= limit),
224 None => true,
225 }
226 }
227}
228
229pub(super) trait CandidateSource {
230 fn view(&self) -> CandidateView<'_>;
231}
232
233impl CandidateSource for Candidate {
234 fn view(&self) -> CandidateView<'_> {
235 CandidateView { name: &self.name, revision: &self.revision,
236 workspace_bytes: self.workspace_bytes, eligible: self.eligible }
237 }
238}
239
240impl CandidateSource for CandidateView<'_> {
241 fn view(&self) -> CandidateView<'_> { *self }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum FailureKind {
247 InvalidInput,
249 Rejected,
251 Unavailable,
253 Device,
255 Quarantined,
257}
258#[derive(Debug, Clone)]
260pub struct TuneFailure {
261 pub kind: FailureKind,
263 pub message: String
265}
266impl TuneFailure {
267 pub fn invalid(message: impl Into<String>) -> Self { Self { kind: FailureKind::InvalidInput, message: message.into() } }
269 pub fn rejected(message: impl Into<String>) -> Self { Self { kind: FailureKind::Rejected, message: message.into() } }
271 pub fn device(message: impl Into<String>) -> Self { Self { kind: FailureKind::Device, message: message.into() } }
273}
274impl fmt::Display for TuneFailure {
275 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}: {}", self.kind, self.message) }
276}
277impl std::error::Error for TuneFailure {}
278
279pub fn fields(parts: &[&str]) -> String {
281 let mut out = String::new();
282 for part in parts { append_field(&mut out, part); }
283 out
284}
285
286fn append_field(out: &mut String, part: &str) {
287 use std::fmt::Write;
288 let _ = write!(out, "{}:", part.len());
289 out.push_str(part);
290}
291
292struct ByteCounter(usize);
293impl fmt::Write for ByteCounter {
294 fn write_str(&mut self, text: &str) -> fmt::Result { self.0 += text.len(); Ok(()) }
295}
296
297fn append_candidate(out: &mut String, candidate: CandidateView<'_>) {
298 use std::fmt::Write;
299 let mut tag_length = ByteCounter(0);
300 let _ = write!(tag_length, "{:?}/{}", candidate.workspace_bytes, candidate.eligible);
301 let length = [candidate.name.len(), candidate.revision.len(), tag_length.0].into_iter().map(|length| {
302 length + 1 + length.checked_ilog10().map_or(1, |digits| digits as usize + 1)
303 }).sum::<usize>();
304 let _ = write!(out, "{length}:");
305 append_field(out, candidate.name);
306 append_field(out, candidate.revision);
307 let _ = write!(out, "{}:{:?}/{}", tag_length.0, candidate.workspace_bytes, candidate.eligible);
308}
309pub fn cache_key(problem: &Problem, candidates: &[Candidate], reference: usize, policy: &StackPolicy) -> String {
312 cache_key_candidates(problem.into(), candidates, reference, policy)
313}
314
315pub(super) fn cache_key_candidates<C: CandidateSource>(problem: ProblemView<'_>, candidates: &[C], reference: usize, policy: &StackPolicy) -> String {
316 let mut manifest = String::new();
317 for c in candidates {
318 append_candidate(&mut manifest, c.view());
319 }
320 fields(&["ruda-stack-autotune-v1", &std::format!("{:?}", problem.scope), problem.operation, problem.environment, problem.workload,
321 problem.execution_context, &reference.to_string(), &manifest,
322 &policy.accuracy_key()])
323}
324pub fn median(values: &mut [f64]) -> Option<f64> {
327 if values.is_empty() || values.iter().any(|v| !v.is_finite() || *v <= 0.0) { return None; }
328 values.sort_by(f64::total_cmp);
329 let m = values.len() / 2;
330 Some(if values.len() % 2 == 0 { values[m - 1] / 2.0 + values[m] / 2.0 } else { values[m] })
331}
332pub fn paired_score(pairs: &[(Duration, Duration)], policy: &StackPolicy) -> Option<(f64, f64)> {
336 if pairs.len() < policy.samples || pairs.iter().any(|(a,b)| a.is_zero() || b.is_zero()) { return None; }
337 let mut ratios: Vec<_> = pairs.iter().map(|(a,b)| b.as_secs_f64()/a.as_secs_f64()).collect();
338 let center = median(&mut ratios)?;
339 let mut deviations: Vec<_> = ratios.iter().map(|r| (r-center).abs()).collect();
340 deviations.sort_by(f64::total_cmp);
342 let mad = deviations[deviations.len()/2] / center;
343 if !mad.is_finite() || mad > policy.max_relative_mad { None } else { Some((center, mad)) }
344}