ruda_runtime/runtime/tune/
operation.rs1use alloc::boxed::Box;
2use alloc::string::String;
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5use core::fmt::{Debug, Display, Write};
6use core::hash::Hash;
7use spin::Once;
8
9use alloc::format;
10
11use super::{
12 AutotuneError, input_generator::InputGenerator, key_generator::KeyGenerator,
13 tune_inputs::TuneInputs,
14};
15use super::{Tunable, TunePlan};
16
17type TuneDelegate<I, Out> =
22 dyn for<'inp> Fn(<I as TuneInputs>::At<'inp>) -> Result<Out, AutotuneError> + Send + Sync;
23
24#[derive(new)]
27pub struct TuneFn<I: TuneInputs, Out> {
28 pub(crate) name: String,
29 func: Box<TuneDelegate<I, Out>>,
30}
31
32impl<I: TuneInputs, Out: 'static> TuneFn<I, Out> {
33 pub fn execute<'a>(&self, inputs: <I as TuneInputs>::At<'a>) -> Result<Out, AutotuneError> {
35 (self.func)(inputs)
36 }
37}
38
39pub struct TunableSet<K: AutotuneKey, F: TuneInputs, Output: 'static> {
42 tunables: Vec<Tunable<K, F, Output>>,
43 key_gen: Arc<dyn KeyGenerator<K, F> + Send + Sync>,
44 input_gen: Arc<dyn InputGenerator<K, F> + Send + Sync>,
45 stack_reference: Option<usize>,
46 stack_revision: String,
47 stack_workload: Option<Arc<dyn for<'a> Fn(&F::At<'a>) -> String + Send + Sync>>,
48 checksum: Once<String>,
49 stack_manifest: Once<String>,
50 #[cfg(std_io)]
51 stack_plan_cache: Option<spin::Mutex<alloc::collections::VecDeque<(K, String, Arc<StaticStackPlan>)>>>,
52}
53
54#[cfg(std_io)]
55pub(crate) struct StaticStackPlan {
56 pub(crate) indices: Vec<usize>,
57 pub(crate) candidates: Vec<super::stack::Candidate>,
58 pub(crate) key: String,
59}
60
61impl<K: AutotuneKey, F: TuneInputs, Output: 'static> TunableSet<K, F, Output> {
62 pub fn len(&self) -> usize {
64 self.tunables.len()
65 }
66
67 pub fn is_empty(&self) -> bool {
69 self.tunables.is_empty()
70 }
71
72 pub fn new(key_gen: impl KeyGenerator<K, F>, input_gen: impl InputGenerator<K, F>) -> Self {
74 Self {
75 tunables: Default::default(),
76 input_gen: Arc::new(input_gen),
77 key_gen: Arc::new(key_gen),
78 stack_reference: None,
79 stack_revision: String::new(),
80 stack_workload: None,
81 checksum: Once::new(),
82 stack_manifest: Once::new(),
83 #[cfg(std_io)]
84 stack_plan_cache: None,
85 }
86 }
87
88 pub fn with_stack_tuning(
92 mut self, reference: usize, revision: &str,
93 workload: impl for<'a> Fn(&F::At<'a>) -> String + Send + Sync + 'static,
94 ) -> Self {
95 self.stack_reference = Some(reference);
96 self.stack_revision = revision.into();
97 self.stack_workload = Some(Arc::new(workload));
98 self.stack_manifest = Once::new();
99 #[cfg(std_io)]
100 if let Some(cache) = &self.stack_plan_cache { cache.lock().clear(); }
101 self
102 }
103 pub fn stack_reference(&self) -> Option<usize> { self.stack_reference }
104 pub fn stack_workload<'a>(&self, inputs: &F::At<'a>) -> Option<String> {
105 self.stack_workload.as_ref().map(|f| f(inputs))
106 }
107
108 pub fn with_static_stack_plan(mut self) -> Self {
111 #[cfg(std_io)]
112 { self.stack_plan_cache = Some(spin::Mutex::new(Default::default())); }
113 self
114 }
115
116 #[cfg(std_io)]
117 pub(crate) fn static_stack_plan_enabled(&self) -> bool { self.stack_plan_cache.is_some() }
118
119 #[cfg(std_io)]
120 pub(crate) fn cached_stack_plan(&self, key: &K, identity: &str) -> Option<Arc<StaticStackPlan>> {
121 self.stack_plan_cache.as_ref()?.lock().iter()
122 .find(|(stored_key, stored_identity, _)| stored_key == key && stored_identity == identity)
123 .map(|(_, _, plan)| plan.clone())
124 }
125
126 #[cfg(std_io)]
127 pub(crate) fn cache_stack_plan(&self, key: K, identity: String, plan: Arc<StaticStackPlan>) {
128 if let Some(cache) = &self.stack_plan_cache {
129 let mut cache = cache.lock();
130 if cache.iter().any(|(stored_key, stored_identity, _)| stored_key == &key && stored_identity == &identity) { return; }
131 if cache.len() == 32 { cache.pop_front(); }
132 cache.push_back((key, identity, plan));
133 }
134 }
135
136 pub fn new_cloning_inputs(key_gen: impl KeyGenerator<K, F>) -> Self {
139 Self::new(key_gen, super::CloneInputGenerator)
140 }
141
142 pub fn with(mut self, tunable: Tunable<K, F, Output>) -> Self {
144 self.tunables.push(tunable);
145 self.checksum = Once::new();
146 self.stack_manifest = Once::new();
147 #[cfg(std_io)]
148 if let Some(cache) = &self.stack_plan_cache { cache.lock().clear(); }
149 self
150 }
151
152 pub fn autotunables(&self) -> impl Iterator<Item = &TuneFn<F, Output>> {
154 self.tunables.iter().map(|tunable| &tunable.function)
155 }
156
157 pub(crate) fn plan(&self, key: &K) -> TunePlan {
159 TunePlan::new(key, &self.tunables)
160 }
161
162 pub fn fastest(&self, fastest_index: usize) -> &TuneFn<F, Output> {
165 &self.tunables[fastest_index].function
166 }
167
168 pub fn compute_checksum(&self) -> String {
171 self.checksum.call_once(|| {
173 let mut checksum = String::new();
174 for tune in &self.tunables { checksum += &tune.function.name; }
175 format!("{:x}", md5::compute(checksum))
176 }).clone()
177 }
178
179 pub fn stack_checksum(&self) -> String {
181 String::from(self.stack_checksum_ref())
182 }
183
184 pub(crate) fn stack_checksum_ref(&self) -> &str {
185 self.stack_manifest.call_once(|| {
186 let mut checksum = format!("stack-v1:{}:{};reference={:?};", self.stack_revision.len(), self.stack_revision, self.stack_reference);
187 for tune in &self.tunables {
188 let _ = write!(checksum, "{}:{}", tune.function.name.len(), tune.function.name);
189 }
190 format!("{:x}", md5::compute(checksum))
191 }).as_str()
192 }
193
194 pub fn generate_key<'a>(&self, inputs: &F::At<'a>) -> K {
196 self.key_gen.generate(inputs)
197 }
198
199 pub fn generate_inputs<'a>(&self, key: &K, inputs: &F::At<'a>) -> F::At<'a> {
201 self.input_gen.generate(key, inputs)
202 }
203}
204
205#[cfg(std_io)]
206pub trait AutotuneKey:
208 Clone
209 + Debug
210 + PartialEq
211 + Eq
212 + Hash
213 + Display
214 + serde::Serialize
215 + serde::de::DeserializeOwned
216 + Send
217 + Sync
218 + 'static
219{
220}
221#[cfg(not(std_io))]
222pub trait AutotuneKey:
224 Clone + Debug + PartialEq + Eq + Hash + Display + Send + Sync + 'static
225{
226}
227
228impl AutotuneKey for String {}
229
230#[cfg(all(test, std_io))]
231mod static_plan_tests {
232 use super::*;
233 fn set() -> TunableSet<String, String, String> {
234 TunableSet::new_cloning_inputs(|value: &String| value.clone())
235 .with_static_stack_plan()
236 }
237 fn plan() -> Arc<StaticStackPlan> {
238 Arc::new(StaticStackPlan { indices: alloc::vec![0],
239 candidates: alloc::vec![super::super::stack::Candidate::new("reference")], key: "full-key".into() })
240 }
241 #[test]
242 fn cache_requires_exact_generated_key_and_full_identity() {
243 let set = set();
244 let plan = plan();
245 set.cache_stack_plan("shape".into(), "device/offset/stride".into(), plan.clone());
246 assert!(Arc::ptr_eq(&set.cached_stack_plan(&"shape".into(), "device/offset/stride").unwrap(), &plan));
247 assert!(set.cached_stack_plan(&"other-shape".into(), "device/offset/stride").is_none());
248 assert!(set.cached_stack_plan(&"shape".into(), "other-device/offset/stride").is_none());
249 }
250 #[test]
251 fn cache_is_bounded_and_duplicate_insert_does_not_evict() {
252 let set = set();
253 for index in 0..32 { set.cache_stack_plan(alloc::format!("{index}"), "binding".into(), plan()); }
254 set.cache_stack_plan("0".into(), "binding".into(), plan());
255 assert!(set.cached_stack_plan(&"0".into(), "binding").is_some());
256 set.cache_stack_plan("32".into(), "binding".into(), plan());
257 assert!(set.cached_stack_plan(&"0".into(), "binding").is_none());
258 assert!(set.cached_stack_plan(&"1".into(), "binding").is_some());
259 }
260 #[test]
261 fn changing_stack_contract_invalidates_plan_metadata() {
262 let set = set();
263 set.cache_stack_plan("shape".into(), "binding".into(), plan());
264 let set = set.with_stack_tuning(0, "new-revision", |value: &String| value.clone());
265 assert!(set.cached_stack_plan(&"shape".into(), "binding").is_none());
266 }
267}