1use std::collections::BTreeMap;
21
22use serde::Deserialize;
23
24pub const SCHEMA: u32 = 1;
26
27#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
29pub struct TableKey {
30 pub schema: u32,
31 pub box_class: String,
33 pub recipe: String,
35 pub plan_digests: BTreeMap<String, String>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum KeyMismatch {
44 Schema {
45 table: u32,
46 serve: u32,
47 },
48 BoxClass {
49 table: String,
50 serve: String,
51 },
52 Recipe {
53 table: String,
54 serve: String,
55 },
56 PlanDigest {
58 mode: String,
59 table: Option<String>,
60 serve: Option<String>,
61 },
62}
63
64impl TableKey {
65 pub fn check(&self, serve: &TableKey) -> Vec<KeyMismatch> {
68 let mut out = Vec::new();
69 if self.schema != serve.schema {
70 out.push(KeyMismatch::Schema {
71 table: self.schema,
72 serve: serve.schema,
73 });
74 }
75 if self.box_class != serve.box_class {
76 out.push(KeyMismatch::BoxClass {
77 table: self.box_class.clone(),
78 serve: serve.box_class.clone(),
79 });
80 }
81 if self.recipe != serve.recipe {
82 out.push(KeyMismatch::Recipe {
83 table: self.recipe.clone(),
84 serve: serve.recipe.clone(),
85 });
86 }
87 let modes: std::collections::BTreeSet<&String> = self
88 .plan_digests
89 .keys()
90 .chain(serve.plan_digests.keys())
91 .collect();
92 for mode in modes {
93 let (t, s) = (self.plan_digests.get(mode), serve.plan_digests.get(mode));
94 if t != s {
95 out.push(KeyMismatch::PlanDigest {
96 mode: mode.clone(),
97 table: t.cloned(),
98 serve: s.cloned(),
99 });
100 }
101 }
102 out
103 }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
108pub struct DrafterKey {
109 pub weights_sha256: String,
111 pub vocab: usize,
113 pub quantization: String,
115 pub context: bool,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
122pub struct Cell {
123 pub n: usize,
124 pub k: usize,
125 pub verify_ms: f64,
127 pub verify_j: f64,
128 pub draft_ms: f64,
130 pub draft_j: f64,
131}
132
133impl Cell {
134 pub fn step_ms(&self) -> f64 {
136 self.verify_ms + self.draft_ms
137 }
138
139 pub fn step_j(&self) -> f64 {
141 self.verify_j + self.draft_j
142 }
143}
144
145#[derive(Debug, Deserialize)]
146struct TableFile {
147 key: TableKey,
148 cells: Vec<Cell>,
149}
150
151#[derive(Debug, Clone, PartialEq)]
153pub struct CostTable {
154 pub key: TableKey,
155 widths: Vec<usize>,
157 max_k: usize,
159 cells: BTreeMap<(usize, usize), Cell>,
161}
162
163impl CostTable {
164 pub fn parse(text: &str) -> Result<Self, String> {
168 let file: TableFile = toml::from_str(text).map_err(|e| format!("spec-cost table: {e}"))?;
169 if file.key.schema != SCHEMA {
170 return Err(format!(
171 "spec-cost table schema {} (this serve reads {SCHEMA})",
172 file.key.schema
173 ));
174 }
175 let mut cells = BTreeMap::new();
176 for c in &file.cells {
177 let finite = [c.verify_ms, c.verify_j, c.draft_ms, c.draft_j]
178 .iter()
179 .all(|v| v.is_finite());
180 let positive = c.verify_ms > 0.0 && c.verify_j > 0.0;
181 let draft_ok = if c.k == 0 {
182 c.draft_ms == 0.0 && c.draft_j == 0.0
183 } else {
184 c.draft_ms > 0.0 && c.draft_j > 0.0
185 };
186 if c.n == 0 || !finite || !positive || !draft_ok {
187 return Err(format!("spec-cost table: bad cell {c:?}"));
188 }
189 if cells.insert((c.n, c.k), *c).is_some() {
190 return Err(format!(
191 "spec-cost table: duplicate cell n={} k={}",
192 c.n, c.k
193 ));
194 }
195 }
196 let widths: Vec<usize> = {
197 let mut w: Vec<usize> = cells.keys().map(|&(n, _)| n).collect();
198 w.dedup();
199 w
200 };
201 let max_k = cells
202 .keys()
203 .map(|&(_, k)| k)
204 .max()
205 .ok_or("spec-cost table: no cells")?;
206 for &n in &widths {
207 for k in 0..=max_k {
208 if !cells.contains_key(&(n, k)) {
209 return Err(format!("spec-cost table: width {n} lacks depth {k}"));
210 }
211 }
212 }
213 Ok(Self {
214 key: file.key,
215 widths,
216 max_k,
217 cells,
218 })
219 }
220
221 pub fn render(key: &TableKey, cells: &[Cell]) -> Result<String, String> {
225 use std::fmt::Write as _;
226 let mut s = String::new();
227 let _ = writeln!(s, "[key]\nschema = {}", key.schema);
228 let _ = writeln!(
229 s,
230 "box_class = {:?}\nrecipe = {:?}",
231 key.box_class, key.recipe
232 );
233 let _ = writeln!(s, "\n[key.plan_digests]");
234 for (mode, digest) in &key.plan_digests {
235 let _ = writeln!(s, "{mode} = {digest:?}");
236 }
237 let mut sorted = cells.to_vec();
238 sorted.sort_by_key(|c| (c.n, c.k));
239 for c in &sorted {
240 let _ = write!(
241 s,
242 "\n[[cells]]\nn = {}\nk = {}\nverify_ms = {:?}\nverify_j = {:?}\ndraft_ms = {:?}\ndraft_j = {:?}\n",
243 c.n, c.k, c.verify_ms, c.verify_j, c.draft_ms, c.draft_j
244 );
245 }
246 let parsed = Self::parse(&s)?;
247 if parsed.key != *key {
248 return Err("spec-cost table: the rendered key does not read back".into());
249 }
250 Ok(s)
251 }
252
253 pub fn max_k(&self) -> usize {
255 self.max_k
256 }
257
258 pub fn cell(&self, n: usize, k: usize) -> Option<Cell> {
262 if k > self.max_k {
263 return None;
264 }
265 let lo = self.widths.iter().copied().filter(|&w| w <= n).max();
266 let hi = self.widths.iter().copied().filter(|&w| w >= n).min();
267 let at = |w| self.cells[&(w, k)];
268 Some(match (lo, hi) {
269 (Some(a), Some(b)) if a == b => at(a),
270 (Some(a), Some(b)) => {
271 let t = (n - a) as f64 / (b - a) as f64;
272 let (x, y) = (at(a), at(b));
273 let mix = |p: f64, q: f64| p + (q - p) * t;
274 Cell {
275 n,
276 k,
277 verify_ms: mix(x.verify_ms, y.verify_ms),
278 verify_j: mix(x.verify_j, y.verify_j),
279 draft_ms: mix(x.draft_ms, y.draft_ms),
280 draft_j: mix(x.draft_j, y.draft_j),
281 }
282 }
283 (Some(a), None) => Cell { n, ..at(a) },
284 (None, Some(b)) => Cell { n, ..at(b) },
285 (None, None) => unreachable!("parse guarantees at least one width"),
286 })
287 }
288}
289
290#[derive(Debug, Deserialize)]
291struct CalibrationFile {
292 drafter: DrafterKey,
293 acceptance: AcceptanceFields,
294}
295
296#[derive(Debug, Deserialize)]
297struct AcceptanceFields {
298 edges: Vec<f32>,
299 p_accept: Vec<f64>,
300 prior_by_position: Vec<f64>,
301}
302
303#[derive(Debug, Clone, PartialEq)]
308pub struct AcceptanceCalibration {
309 pub drafter: DrafterKey,
310 edges: Vec<f32>,
313 p_accept: Vec<f64>,
314 prior_by_position: Vec<f64>,
316}
317
318impl AcceptanceCalibration {
319 pub fn parse(text: &str) -> Result<Self, String> {
323 let f: CalibrationFile =
324 toml::from_str(text).map_err(|e| format!("acceptance calibration: {e}"))?;
325 let a = f.acceptance;
326 let in01 = |p: &f64| (0.0..=1.0).contains(p);
327 if a.edges.is_empty()
328 || a.edges.len() != a.p_accept.len()
329 || !a.edges.windows(2).all(|w| w[0] < w[1])
330 || a.edges.last().is_some_and(|&e| e < 0.0)
331 || !a.p_accept.iter().all(in01)
332 || a.prior_by_position.is_empty()
333 || !a.prior_by_position.iter().all(in01)
334 {
335 return Err("acceptance calibration: malformed buckets or priors".into());
336 }
337 Ok(Self {
338 drafter: f.drafter,
339 edges: a.edges,
340 p_accept: a.p_accept,
341 prior_by_position: a.prior_by_position,
342 })
343 }
344
345 pub fn p_given_lp(&self, lp: f32) -> f64 {
347 let i = self
348 .edges
349 .iter()
350 .position(|&e| lp <= e)
351 .unwrap_or(self.edges.len() - 1);
352 self.p_accept[i]
353 }
354
355 pub fn prior(&self, j: usize) -> f64 {
358 let i = j.saturating_sub(1).min(self.prior_by_position.len() - 1);
359 self.prior_by_position[i]
360 }
361
362 pub fn expected_accepted(&self, lps: &[f32], k: usize) -> f64 {
365 let mut run = 1.0;
366 let mut total = 0.0;
367 for j in 1..=k {
368 let p = lps
369 .get(j - 1)
370 .map_or_else(|| self.prior(j), |&lp| self.p_given_lp(lp));
371 run *= p;
372 total += run;
373 }
374 total
375 }
376}
377
378pub struct SpecCostState {
382 pub table: CostTable,
383 pub calibration: AcceptanceCalibration,
384 pub slack: f64,
385}
386
387#[path = "spec_cost_fit.rs"]
388mod fit;
389pub use fit::MIN_OUTCOMES;
390#[path = "spec_cost_plan.rs"]
391pub mod plan;
392
393#[cfg(test)]
394#[path = "spec_cost_tests.rs"]
395mod tests;