Skip to main content

cubecl_runtime/tune/
record.rs

1//! What a tune leaves in the environment beside its answer.
2//!
3//! The table stores a key's winner and every candidate's result, ranked. What
4//! it cannot say is how the tune went: which candidates ran and in what order,
5//! what each cost to compile and benchmark, whether the tune stopped early,
6//! and how long the key took from its miss to its answer. A [`TuneRecord`] is
7//! that account, written to [`cubecl_environment::records`] once per tune.
8
9use crate::tune::{
10    AutotuneKey, AutotuneLogContext, AutotuneLogEvent, PersistentCacheKey, Placement, TuneCache,
11    TunePlan,
12};
13use alloc::string::String;
14use alloc::vec::Vec;
15use core::time::Duration;
16use cubecl_environment::records::{Record, RecordEffect, Span};
17use serde::{Deserialize, Serialize};
18
19/// How one autotune key was decided.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct TuneRecord<K> {
22    /// The namespace of the table the answer is stored in.
23    pub table: String,
24    /// The table's entry the answer is stored under: the key, and the
25    /// checksum of the candidate list it was tuned under.
26    pub entry: PersistentCacheKey<K>,
27    /// The index of the candidate the key runs.
28    pub winner: usize,
29    /// The candidates that ran, in the order they ran.
30    pub trials: Vec<Trial>,
31    /// The candidate that met the time limit and ended the tune before the
32    /// rest of the plan ran.
33    pub short_circuit: Option<String>,
34    /// From the cache miss to the answer committed.
35    pub wall: Duration,
36    /// Whether the tune ran inside a dry run, where launches compile but do
37    /// not execute.
38    pub dry_run: bool,
39    /// Whether the table took the answer. One that measured nothing, or that
40    /// was tuned with the cache disabled, answers this process alone: the
41    /// table holds another answer to the key, or none.
42    pub stored: bool,
43    /// Every candidate in the order the plan releases them, whether or not
44    /// it ran: the priorities that decided the order, read when the tune
45    /// began. Empty in a record written before plans were kept.
46    #[serde(default)]
47    pub plan: Vec<PlannedCandidate>,
48}
49
50/// One candidate's place in the plan of a tune.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct PlannedCandidate {
53    /// The candidate's name.
54    pub name: String,
55    /// Its index in the tunable set, the one the table's winner names.
56    pub index: usize,
57    /// The batch the plan releases it in, counting from 0. A tune stops
58    /// after the first batch with a valid result, so later batches only run
59    /// when every earlier one failed. `None` when each of its groups gave it
60    /// a negative priority for this key.
61    pub batch: Option<usize>,
62    /// Its place in each group it belongs to; empty for a candidate in no
63    /// group, which trails every batch.
64    pub groups: Vec<Placement>,
65}
66
67/// One candidate's run within a tune.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct Trial {
70    /// The candidate's name.
71    pub name: String,
72    /// Compiling and benchmarking it: from its first launch to its samples
73    /// resolved.
74    pub wall: Duration,
75}
76
77impl<K> Record for TuneRecord<K> {
78    const KIND: &'static str = "autotune";
79}
80
81/// A tune being recorded: stamped when it began, written when it ends. Every
82/// call is a no-op when the environment records nothing.
83#[derive(Debug)]
84pub(crate) struct TuneRecording<K> {
85    open: Option<OpenRecording<K>>,
86}
87
88/// What a [`TuneRecording`] holds while the environment records.
89#[derive(Debug)]
90struct OpenRecording<K> {
91    span: Span,
92    table: String,
93    entry: PersistentCacheKey<K>,
94    dry_run: bool,
95    plan: Vec<PlannedCandidate>,
96}
97
98/// What [`TuneRecording::finish`] needs to know of the answer.
99pub(crate) struct Answer {
100    pub winner: usize,
101    /// Whether the table took it: see [`TuneRecord::stored`].
102    pub stored: bool,
103}
104
105impl<K: AutotuneKey> TuneRecording<K> {
106    /// Begin recording the tune of `key` in `cache`'s table. The key is
107    /// cloned only when the environment records.
108    pub(crate) fn new(cache: &TuneCache<K>, key: &K, checksum: &str) -> Self {
109        let open = Span::new().map(|span| OpenRecording {
110            span,
111            table: cache.table().into(),
112            entry: PersistentCacheKey {
113                key: key.clone(),
114                checksum: checksum.into(),
115            },
116            dry_run: crate::dry_run::dry_run(),
117            plan: Vec::new(),
118        });
119        Self { open }
120    }
121
122    /// Whether the tune is recorded, and so has to track its steps: the
123    /// record's trials are the ones the log context collects.
124    pub(crate) fn is_open(&self) -> bool {
125        self.open.is_some()
126    }
127
128    /// Keep the plan the tune runs, its candidates named by `names`.
129    pub(crate) fn plan(&mut self, plan: &TunePlan, names: &[&str]) {
130        if let Some(open) = &mut self.open {
131            open.plan = plan.account(names);
132        }
133    }
134
135    /// Write the record of the tune that just answered.
136    pub(crate) fn finish(self, answer: Answer, log_context: Option<&AutotuneLogContext>) {
137        let Some(open) = self.open else {
138            return;
139        };
140        // A tune the environment switched away from went with its session.
141        let Some(wall) = open.span.elapsed() else {
142            return;
143        };
144        let mut trials = Vec::new();
145        let mut short_circuit = None;
146        for event in log_context.iter().flat_map(|context| &context.events) {
147            match event {
148                AutotuneLogEvent::TuningStep(name, wall) => trials.push(Trial {
149                    name: name.clone(),
150                    wall: *wall,
151                }),
152                AutotuneLogEvent::ShortCircuit(name) => short_circuit = Some(name.clone()),
153            }
154        }
155        let record = TuneRecord {
156            table: open.table,
157            entry: open.entry,
158            winner: answer.winner,
159            trials,
160            short_circuit,
161            wall,
162            dry_run: open.dry_run,
163            stored: answer.stored,
164            plan: open.plan,
165        };
166        // A stored winner is the environment changing; an answer kept in
167        // memory is not.
168        let effect = if record.stored {
169            RecordEffect::Changed
170        } else {
171            RecordEffect::Observed
172        };
173        open.span.close(effect, &record);
174    }
175}