1use crate::instrument::{CountHistogram, Counter, Gauge, NanosHistogram};
16
17pub const MAX_PHASES: usize = 32;
19
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
22pub struct SchedShape {
23 pub pending: u64,
24 pub active: u64,
25 pub prefilling: u64,
26 pub swapped: u64,
27 pub kv_blocks_free: u64,
28 pub kv_blocks_total: u64,
29 pub ssm_slots_used: u64,
30 pub ssm_slots_total: u64,
31 pub ticks: u64,
33}
34
35#[derive(Debug)]
36pub struct SchedInstruments {
37 pub phase: [NanosHistogram; MAX_PHASES],
38 pub pending: Gauge,
39 pub active: Gauge,
40 pub prefilling: Gauge,
41 pub swapped: Gauge,
42 pub kv_blocks_free: Gauge,
43 pub kv_blocks_total: Gauge,
44 pub ssm_slots_used: Gauge,
45 pub ssm_slots_total: Gauge,
46 pub ticks: Gauge,
47 pub decode_rows: CountHistogram,
49 pub prefill_seqs: CountHistogram,
51 pub stream_syncs: Counter,
52 pub blocking_d2h: Counter,
53 pub graph_replays: Counter,
54 pub graph_captures: Counter,
55}
56
57impl Default for SchedInstruments {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl SchedInstruments {
64 pub const fn new() -> Self {
65 Self {
66 phase: [const { NanosHistogram::new() }; MAX_PHASES],
67 pending: Gauge::new(),
68 active: Gauge::new(),
69 prefilling: Gauge::new(),
70 swapped: Gauge::new(),
71 kv_blocks_free: Gauge::new(),
72 kv_blocks_total: Gauge::new(),
73 ssm_slots_used: Gauge::new(),
74 ssm_slots_total: Gauge::new(),
75 ticks: Gauge::new(),
76 decode_rows: CountHistogram::new(),
77 prefill_seqs: CountHistogram::new(),
78 stream_syncs: Counter::new(),
79 blocking_d2h: Counter::new(),
80 graph_replays: Counter::new(),
81 graph_captures: Counter::new(),
82 }
83 }
84
85 pub(crate) fn record(&self, s: &SchedShape) {
86 self.pending.set(s.pending);
87 self.active.set(s.active);
88 self.prefilling.set(s.prefilling);
89 self.swapped.set(s.swapped);
90 self.kv_blocks_free.set(s.kv_blocks_free);
91 self.kv_blocks_total.set(s.kv_blocks_total);
92 self.ssm_slots_used.set(s.ssm_slots_used);
93 self.ssm_slots_total.set(s.ssm_slots_total);
94 self.ticks.set(s.ticks);
95 if s.active > 0 {
96 self.decode_rows.observe(s.active);
97 }
98 if s.prefilling > 0 {
99 self.prefill_seqs.observe(s.prefilling);
100 }
101 }
102
103 pub(crate) fn shape(&self) -> SchedShape {
104 SchedShape {
105 pending: self.pending.get(),
106 active: self.active.get(),
107 prefilling: self.prefilling.get(),
108 swapped: self.swapped.get(),
109 kv_blocks_free: self.kv_blocks_free.get(),
110 kv_blocks_total: self.kv_blocks_total.get(),
111 ssm_slots_used: self.ssm_slots_used.get(),
112 ssm_slots_total: self.ssm_slots_total.get(),
113 ticks: self.ticks.get(),
114 }
115 }
116}
117
118pub const MAX_DRAFTS: usize = 16;
120
121#[derive(Debug)]
122pub struct SpecMatrix {
123 cells: [[Counter; MAX_DRAFTS + 1]; MAX_DRAFTS + 1],
125 pub overflow: Counter,
128}
129
130impl Default for SpecMatrix {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136impl SpecMatrix {
137 pub const fn new() -> Self {
138 Self {
139 cells: [const { [const { Counter::new() }; MAX_DRAFTS + 1] }; MAX_DRAFTS + 1],
140 overflow: Counter::new(),
141 }
142 }
143
144 pub(crate) fn record(&self, drafts: usize, accepted: usize) {
145 if drafts == 0 || drafts > MAX_DRAFTS || accepted > drafts {
146 self.overflow.add(1);
147 return;
148 }
149 self.cells[drafts][accepted].add(1);
150 }
151
152 pub fn steps(&self, drafts: usize, accepted: usize) -> u64 {
153 self.cells
154 .get(drafts)
155 .and_then(|r| r.get(accepted))
156 .map_or(0, Counter::get)
157 }
158
159 pub fn acceptance_by_depth(&self, drafts: usize) -> Vec<f64> {
163 if drafts == 0 || drafts > MAX_DRAFTS {
164 return Vec::new();
165 }
166 let row: Vec<u64> = (0..=drafts).map(|a| self.steps(drafts, a)).collect();
167 let total: u64 = row.iter().sum();
168 if total == 0 {
169 return Vec::new();
170 }
171 (1..=drafts)
172 .map(|depth| row[depth..].iter().sum::<u64>() as f64 / total as f64)
173 .collect()
174 }
175
176 pub fn widths(&self) -> Vec<usize> {
178 (1..=MAX_DRAFTS)
179 .filter(|d| (0..=*d).any(|a| self.steps(*d, a) > 0))
180 .collect()
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn acceptance_by_depth_is_the_survival_curve_of_accepted_depth() {
190 let m = SpecMatrix::new();
191 for a in [0, 1, 3, 3] {
193 m.record(3, a);
194 }
195 assert_eq!(m.acceptance_by_depth(3), vec![0.75, 0.5, 0.5]);
196 assert_eq!(m.widths(), vec![3]);
197 assert!(m.acceptance_by_depth(2).is_empty(), "no width-2 step ran");
198 }
199
200 #[test]
201 fn impossible_outcomes_are_counted_as_overflow_not_placed() {
202 let m = SpecMatrix::new();
203 m.record(2, 3);
204 m.record(0, 0);
205 m.record(MAX_DRAFTS + 1, 1);
206 assert_eq!(m.overflow.get(), 3);
207 assert!(m.widths().is_empty());
208 }
209
210 #[test]
211 fn a_tick_with_no_decode_rows_is_not_a_batch_shape_sample() {
212 let s = SchedInstruments::new();
213 s.record(&SchedShape {
214 pending: 2,
215 ..Default::default()
216 });
217 assert_eq!(s.decode_rows.count(), 0);
218 assert_eq!(s.pending.get(), 2);
219 s.record(&SchedShape {
220 active: 5,
221 ..Default::default()
222 });
223 assert_eq!(s.decode_rows.count(), 1);
224 }
225}