1use std::collections::BTreeMap;
17
18use serde::Serialize;
19
20use crate::model::spec::{Mixer, ModelSpec};
21use crate::spatial::decode::{GgmlType, GgufHeader};
22use crate::spatial::serving::{array_passes, is_float, lane_cycles, Card, VALUE_BLOCK};
23use crate::spatial::serving::{AttentionMath, StateFlush};
24
25use super::model::{CacheFormat, Replay, StateFormat};
26use super::numerics::Words;
27use super::program::{Command, Program};
28
29pub const EXP_OPS: f64 = 10.0;
33pub const RECIP_OPS: f64 = 6.0;
34pub const RSQRT_OPS: f64 = 8.0;
35pub const SIGMOID_OPS: f64 = EXP_OPS + 1.0 + RECIP_OPS;
36pub const SILU_OPS: f64 = SIGMOID_OPS + 1.0;
37pub const SOFTPLUS_OPS: f64 = EXP_OPS + 12.0;
38
39#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
41pub struct SequenceRows {
42 pub rows: usize,
43 pub position: usize,
44 pub logged: usize,
46}
47
48#[derive(Clone, Debug, PartialEq, Serialize)]
51pub struct Step {
52 pub sequences: Vec<SequenceRows>,
53 pub drafted: bool,
54}
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize)]
58pub struct Demand {
59 pub weights: f64,
61 pub rewritten: f64,
63 pub appended: f64,
65 pub array: f64,
67 pub lanes: f64,
69 pub vector: f64,
71}
72
73impl std::ops::AddAssign for Demand {
74 fn add_assign(&mut self, o: Demand) {
75 self.weights += o.weights;
76 self.rewritten += o.rewritten;
77 self.appended += o.appended;
78 self.array += o.array;
79 self.lanes += o.lanes;
80 self.vector += o.vector;
81 }
82}
83
84impl Demand {
85 pub fn memory_s(&self, card: &Card, weights: f64) -> f64 {
88 let peak = card.memory.bandwidth.value;
89 weights / (peak * card.streams.read.value)
90 + self.rewritten / (peak * card.streams.rewrite.value)
91 + self.appended / (peak * card.streams.append.value)
92 }
93}
94
95#[derive(Debug, PartialEq)]
97pub enum TwinRefusal {
98 MissingTensor(String),
99 UnknownType { tensor: String, id: u32 },
100 Undecoded { tensor: String, ty: GgmlType },
102}
103
104pub fn demands(spec: &ModelSpec, h: &GgufHeader, program: &Program, step: &Step) -> Result<Vec<Demand>, TwinRefusal> {
106 let drafted = step.drafted;
107 let step = &step.sequences;
108 let tensors: BTreeMap<&str, &crate::spatial::decode::TensorRow> = h.tensors.iter().map(|t| (t.name.as_str(), t)).collect();
109 let tensor = |name: &str| -> Result<(GgmlType, f64, Vec<u64>), TwinRefusal> {
110 let t = tensors.get(name).ok_or_else(|| TwinRefusal::MissingTensor(name.into()))?;
111 let unknown = || TwinRefusal::UnknownType { tensor: name.into(), id: t.type_id };
112 let ty = GgmlType::from_id(t.type_id).ok_or_else(unknown)?;
113 let bytes = ty.bytes(t.dims.iter().product::<u64>()).ok_or_else(unknown)?;
114 Ok((ty, bytes as f64, t.dims.clone()))
115 };
116 let row_bytes = |name: &str| -> Result<f64, TwinRefusal> {
117 let (ty, _, dims) = tensor(name)?;
118 Ok(ty.bytes(dims[0]).ok_or_else(|| TwinRefusal::UnknownType { tensor: name.into(), id: ty.id() })? as f64)
119 };
120 let n = program.numerics;
121 let words = match n.activations {
122 Words::One => 1.0,
123 Words::Two => 2.0,
124 };
125 let state_bytes = match n.state {
126 StateFormat::F32 => 4.0,
127 StateFormat::F16 => 2.0,
128 };
129 let per_position = |kv_heads: usize, width: usize, value_width: usize| -> f64 {
131 match n.cache {
132 CacheFormat::F16 => (kv_heads * (width + value_width) * 2) as f64,
133 CacheFormat::Int8 | CacheFormat::Int8Hadamard => (kv_heads * (width + value_width + 4)) as f64,
134 }
135 };
136 let rows: f64 = step.iter().map(|s| s.rows as f64).sum();
137 let mut out = Vec::with_capacity(program.commands.len());
138 for c in &program.commands {
139 let mut d = Demand::default();
140 match c {
141 Command::Embed { table } => d.weights = rows * row_bytes(table)?,
142 Command::Norm { weight, .. } => {
143 let (_, bytes, dims) = tensor(weight)?;
144 d.weights = bytes;
145 d.vector = 2.0 * dims[0] as f64 * rows + RSQRT_OPS * rows;
146 }
147 Command::Matvec { tensor: name, .. } => {
148 let (ty, bytes, dims) = tensor(name)?;
149 let elems = dims.iter().product::<u64>() as f64;
150 d.weights = bytes;
151 if let Some(p) = array_passes(ty) {
152 d.array = elems * p as f64 * words * rows;
153 d.lanes = elems / ty.block_elems() as f64 * lane_cycles(ty, n.scaling).expect("a type the array passes") * words * rows;
154 } else if is_float(ty) {
155 d.vector = elems * rows;
156 } else {
157 return Err(TwinRefusal::Undecoded { tensor: name.clone(), ty });
158 }
159 }
160 Command::Add { .. } => d.vector = spec.d as f64 * rows,
161 Command::SwiGlu { .. } => {
162 let dff = ffn_width(spec);
163 d.vector = dff * rows * (SILU_OPS + 1.0);
164 }
165 Command::Conv { kernel, taps, .. } => {
166 let (_, bytes, dims) = tensor(kernel)?;
167 let channels = *dims.last().expect("a kernel's channels") as f64;
168 d.weights = bytes;
169 let ring = (*taps as f64 - 1.0) * channels * state_bytes;
171 d.appended = step.iter().map(|s| ring + s.rows as f64 * channels * state_bytes).sum();
172 d.vector = *taps as f64 * channels * rows + SILU_OPS * channels * rows;
173 }
174 Command::L2Heads { heads, width, .. } => d.vector = (2.0 * (*heads * *width) as f64 + RSQRT_OPS * *heads as f64) * rows,
175 Command::Gates { dt, a, .. } => {
176 let (_, b1, _) = tensor(dt)?;
177 let (_, b2, dims) = tensor(a)?;
178 d.weights = b1 + b2;
179 d.vector = dims[0] as f64 * rows * (SOFTPLUS_OPS + 2.0 + EXP_OPS + SIGMOID_OPS);
180 }
181 Command::Delta { k_heads, v_heads, width, .. } => {
182 let state = (*v_heads * *width * *width) as f64;
183 let log = (*k_heads * *width + *v_heads * *width + 2 * *v_heads) as f64;
184 match n.flush {
185 StateFlush::EveryPass => {
186 d.rewritten = step.iter().map(|s| state * state_bytes * (1.0 + if drafted { s.rows as f64 } else { 1.0 })).sum();
189 d.vector = 3.0 * state * rows;
190 }
191 StateFlush::Every { .. } | StateFlush::Trailing { .. } => {
192 for s in step {
193 let mut logged = s.logged;
194 let (mut walked, mut replayed, mut replays) = (0usize, 0usize, 0usize);
197 for _ in 0..s.rows {
198 logged += 1;
199 walked += logged;
200 let replay = Replay::at(n.flush, *width, logged);
201 replayed += replay.rows.len();
202 replays += replay.rows.len() * replay.by;
203 logged -= replay.drop;
204 }
205 let rewritten = replayed.min(*width) as f64 / *width as f64;
208 d.rewritten += state * state_bytes * (rewritten + replayed as f64 / *width as f64);
209 d.appended += state * state_bytes * (1.0 - rewritten);
210 d.appended += (s.logged + s.rows) as f64 * log * state_bytes;
211 d.vector += s.rows as f64 * state
212 + *v_heads as f64 * walked as f64 * (2.0 * *width as f64 + *width as f64)
213 + 2.0 * (*v_heads * *width * replays) as f64;
214 }
215 }
216 }
217 }
218 Command::GatedNorm { weight, width, .. } => {
219 let (_, bytes, _) = tensor(weight)?;
220 let heads = gated_heads(spec, *width);
221 d.weights = bytes;
222 d.vector = heads * *width as f64 * rows * (3.0 + SILU_OPS + 1.0) + RSQRT_OPS * heads * rows;
223 }
224 Command::Split { .. } => {}
225 Command::RmsHeads { weight, width, slot } => {
226 let (_, bytes, _) = tensor(weight)?;
227 let heads = attention_heads(spec, *slot);
228 d.weights = bytes;
229 d.vector = (2.0 * heads * *width as f64 + RSQRT_OPS * heads) * rows;
230 }
231 Command::Rotate { slot, rope, .. } => {
232 let heads = attention_heads(spec, *slot);
233 d.vector = heads * (rope.dims / 2) as f64 * 6.0 * rows;
235 }
236 Command::Hadamard { slot, width } => {
237 let heads = attention_heads(spec, *slot);
238 d.vector = heads * transform(*width as f64) * rows;
239 }
240 Command::CacheAppend { kv_heads, width, value_width, .. } => {
241 d.appended = per_position(*kv_heads, *width, *value_width) * rows;
242 if n.cache.is_int8() {
243 d.vector = (*kv_heads * (*width + *value_width)) as f64 * rows * 3.0;
244 }
245 }
246 Command::Attend { heads, kv_heads, width, value_width, .. } => {
247 let per_position = per_position(*kv_heads, *width, *value_width);
248 for s in step {
249 let read_to = (s.position + s.rows) as f64;
251 d.appended += per_position * read_to;
252 for r in 0..s.rows {
253 let attended = (s.position + r + 1) as f64;
254 let products = *heads as f64 * (*width + *value_width) as f64 * attended;
255 d.vector += crate::spatial::serving::SOFTMAX_MACS_PER_SCORE * *heads as f64 * attended;
256 match n.attention {
257 AttentionMath::Vector => d.vector += products,
258 AttentionMath::Array => {
259 d.array += 2.0 * products;
260 d.lanes += *heads as f64 * attended * (1.0 + *value_width as f64 / VALUE_BLOCK);
261 }
262 }
263 }
264 }
265 if n.cache == CacheFormat::Int8Hadamard {
266 d.vector += *heads as f64 * transform(*value_width as f64) * rows;
267 }
268 }
269 Command::SigmoidGate { .. } => {
270 let width = attention_gate_width(spec);
271 d.vector = width * rows * (SIGMOID_OPS + 1.0);
272 }
273 Command::Advance => {}
274 }
275 out.push(d);
276 }
277 Ok(out)
278}
279
280fn transform(n: f64) -> f64 {
282 n * n.max(1.0).log2()
283}
284
285fn ffn_width(spec: &ModelSpec) -> f64 {
286 spec.layers
287 .iter()
288 .find_map(|l| match l.ffn {
289 crate::model::spec::Ffn::Dense { dff, .. } => Some(dff as f64),
290 _ => None,
291 })
292 .unwrap_or(0.0)
293}
294
295fn gated_heads(spec: &ModelSpec, width: usize) -> f64 {
296 spec.layers
297 .iter()
298 .find_map(|l| match &l.mixer {
299 Mixer::GatedDelta(g) if g.head_dim == width => Some(g.v_heads as f64),
300 _ => None,
301 })
302 .unwrap_or(0.0)
303}
304
305fn attention_heads(spec: &ModelSpec, slot: super::program::Slot) -> f64 {
306 spec.layers
307 .iter()
308 .find_map(|l| match &l.mixer {
309 Mixer::Attention(a) => Some(match slot {
310 super::program::Slot::Query => a.heads as f64,
311 _ => a.kv_heads as f64,
312 }),
313 _ => None,
314 })
315 .unwrap_or(0.0)
316}
317
318fn attention_gate_width(spec: &ModelSpec) -> f64 {
319 spec.layers
320 .iter()
321 .find_map(|l| match &l.mixer {
322 Mixer::Attention(a) => Some((a.heads * a.value_dim) as f64),
323 _ => None,
324 })
325 .unwrap_or(0.0)
326}
327
328#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
330pub enum Bound {
331 Memory,
332 Array,
333 Lanes,
334 Vector,
335}
336
337#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
339pub struct Span {
340 pub start_s: f64,
341 pub end_s: f64,
342 pub bound: Bound,
343 pub prefetched: f64,
345}
346
347#[derive(Clone, Debug, PartialEq, Serialize)]
349pub struct Timeline {
350 pub spans: Vec<Span>,
351 pub step_s: f64,
352 pub memory_s: f64,
354 pub array_s: f64,
355 pub lanes_s: f64,
356 pub vector_s: f64,
357}
358
359pub fn schedule(demands: &[Demand], card: &Card, window: f64) -> Timeline {
363 let reads = card.memory.bandwidth.value * card.streams.read.value;
365 let (array, vector, lanes) = (card.array.value, card.vector.value, card.scale.value);
366 let mut fetched = vec![0f64; demands.len()];
368 let mut held = 0f64;
369 let mut next = 0usize;
370 let mut now = 0f64;
371 let mut spans = Vec::with_capacity(demands.len());
372 let (mut memory_s, mut array_s, mut lanes_s, mut vector_s) = (0f64, 0f64, 0f64, 0f64);
373 for (i, d) in demands.iter().enumerate() {
374 let prefetched = fetched[i];
375 held -= prefetched;
376 let own = d.memory_s(card, (d.weights - prefetched).max(0.0));
377 let times = [(own, Bound::Memory), (d.array / array, Bound::Array), (d.lanes / lanes, Bound::Lanes), (d.vector / vector, Bound::Vector)];
378 let (length, bound) = times.into_iter().fold((0.0, Bound::Memory), |a, b| if b.0 > a.0 { b } else { a });
379 memory_s += own;
380 array_s += d.array / array;
381 lanes_s += d.lanes / lanes;
382 vector_s += d.vector / vector;
383 let mut spare = (length - own) * reads;
385 next = next.max(i + 1);
386 while spare > 0.0 && next < demands.len() {
387 let want = demands[next].weights - fetched[next];
388 let room = window - held;
389 let take = want.min(room).min(spare);
390 if take <= 0.0 {
391 break;
392 }
393 fetched[next] += take;
394 held += take;
395 spare -= take;
396 memory_s += take / reads;
397 if fetched[next] >= demands[next].weights {
398 next += 1;
399 } else {
400 break;
401 }
402 }
403 spans.push(Span { start_s: now, end_s: now + length, bound, prefetched });
404 now += length;
405 }
406 Timeline { spans, step_s: now, memory_s, array_s, lanes_s, vector_s }
407}
408
409pub const PREDICTED_HEADER: &str = "qwen3.8-27b-q4_k_m";
413pub const PREDICTED_BATCHES: [usize; 3] = [1, 16, 64];
416pub const PREDICTED_POSITION: usize = 2047;
418pub const PREDICTED_LIMIT: u64 = 4096;
420
421#[derive(Clone, Debug, PartialEq, Serialize)]
423pub struct PredictedRow {
424 pub numerics: String,
425 pub batch: usize,
426 pub position: usize,
427 pub window_bytes: f64,
429 pub step_s: f64,
430 pub tokens_per_s: f64,
431 pub roofline_tokens_per_s: f64,
433 pub bound_share: BTreeMap<String, f64>,
435 pub memory_s: f64,
436 pub array_s: f64,
437 pub lanes_s: f64,
438 pub vector_s: f64,
439}
440
441#[derive(Clone, Debug, PartialEq, Serialize)]
445pub struct Predicted {
446 pub card: &'static str,
447 pub header: &'static str,
448 pub rows: Vec<PredictedRow>,
449}
450
451pub fn serving_of(spec: &ModelSpec, n: super::model::EngineNumerics, batch: usize, attended: f64) -> crate::spatial::serving::Serving {
453 let (hd, vd) = spec
454 .layers
455 .iter()
456 .find_map(|l| match &l.mixer {
457 Mixer::Attention(a) => Some((a.head_dim as f64, a.value_dim as f64)),
458 _ => None,
459 })
460 .unwrap_or((0.0, 0.0));
461 crate::spatial::serving::Serving {
462 batch: batch as u32,
463 contexts: crate::spatial::serving::ContextMix::uniform(attended, PREDICTED_LIMIT),
464 kv_page: Some(crate::spatial::d5005::KV_PAGE),
465 attention: n.attention,
466 kv_bytes: if n.cache.is_int8() { (hd + vd + 4.0) / (hd + vd) } else { 2.0 },
468 state_bytes: match n.state {
469 StateFormat::F32 => 4.0,
470 StateFormat::F16 => 2.0,
471 },
472 activation_words: match n.activations {
473 Words::One => 1,
474 Words::Two => 2,
475 },
476 scaling: n.scaling,
477 kv_rotated: n.cache == CacheFormat::Int8Hadamard,
478 draft: None,
479 flush: n.flush,
480 host_memory: false,
481 }
482}
483
484pub fn steady_steps(n: super::model::EngineNumerics, batch: usize, position: usize) -> Vec<Step> {
488 let (window, behind) = match n.flush {
489 StateFlush::EveryPass => (1, 0),
490 StateFlush::Every { tokens } => (tokens.max(1) as usize, 0),
491 StateFlush::Trailing { tokens } => (tokens.max(1) as usize, tokens as usize),
492 };
493 (0..window)
494 .map(|p| Step {
495 sequences: (0..batch).map(|i| SequenceRows { rows: 1, position, logged: if window > 1 { behind + (i + p) % window } else { 0 } }).collect(),
496 drafted: false,
497 })
498 .collect()
499}
500
501pub fn predicted(decode_dir: &std::path::Path) -> Result<Predicted, String> {
504 use crate::spatial::d5005::{card, EnginePlan};
505 let h = GgufHeader::read_ledger(&decode_dir.join(format!("headers/{PREDICTED_HEADER}.json")))?;
506 let spec = h.spec().map_err(|e| format!("{PREDICTED_HEADER}: {e}"))?;
507 let c = card(&EnginePlan::standard());
508 let mut rows = Vec::new();
509 for n in super::model::EngineNumerics::real_points() {
510 let program = super::program::lower(&spec, n, PREDICTED_POSITION + 1).map_err(|e| format!("{PREDICTED_HEADER}: {e}"))?.program;
511 let attended = (PREDICTED_POSITION + 1) as f64;
512 let most = crate::spatial::serving::max_batch(&h, &c, &serving_of(&spec, n, 1, attended)).map_err(|e| format!("{e:?}"))? as usize;
513 let mut batches: Vec<usize> = PREDICTED_BATCHES.iter().copied().filter(|&b| b < most).collect();
514 batches.push(most);
515 for batch in batches {
516 let shape = crate::spatial::serving::PassShape::of(&h, &serving_of(&spec, n, batch, (PREDICTED_POSITION + 1) as f64))
517 .map_err(|e| format!("{e:?}"))?;
518 let window = (c.on_chip.capacity.value - batch as f64 * shape.on_chip_per_sequence).max(0.0);
519 let steps = steady_steps(n, batch, PREDICTED_POSITION);
520 let timelines: Vec<Timeline> = steps
521 .iter()
522 .map(|step| Ok(schedule(&demands(&spec, &h, &program, step).map_err(|e| format!("{e:?}"))?, &c, window)))
523 .collect::<Result<_, String>>()?;
524 let mean = |f: &dyn Fn(&Timeline) -> f64| timelines.iter().map(f).sum::<f64>() / timelines.len() as f64;
525 let step_s = mean(&|t| t.step_s);
526 let roofline = shape.cost(&c, batch as u32, 0).map(|cost| cost.tokens_per_s()).unwrap_or(0.0);
527 let mut bound_share = BTreeMap::new();
528 for t in &timelines {
529 for s in &t.spans {
530 *bound_share.entry(format!("{:?}", s.bound)).or_insert(0.0) += (s.end_s - s.start_s) / (step_s * timelines.len() as f64);
531 }
532 }
533 rows.push(PredictedRow {
534 numerics: format!("{n:?}"),
535 batch,
536 position: PREDICTED_POSITION,
537 window_bytes: window,
538 step_s,
539 tokens_per_s: batch as f64 / step_s,
540 roofline_tokens_per_s: roofline,
541 bound_share,
542 memory_s: mean(&|t| t.memory_s),
543 array_s: mean(&|t| t.array_s),
544 lanes_s: mean(&|t| t.lanes_s),
545 vector_s: mean(&|t| t.vector_s),
546 });
547 }
548 }
549 Ok(Predicted { card: c.name, header: PREDICTED_HEADER, rows })
550}