Skip to main content

logicaffeine_compile/spatial/
serving.rs

1//! The cost of a pass: batched, speculative decode on one card that holds its tokens and streams its weights.
2//!
3//! ★ A CARD THAT HOLDS ITS TOKEN-ROWS AND STREAMS ITS WEIGHTS PAYS FOR EACH WEIGHT ONCE A PASS. Every weight
4//! crosses the card once and multiplies every token-row the card holds: each sequence's verified token and the
5//! tokens drafted after it. What does not amortize is per sequence: the recurrent state it reads and writes
6//! back, its KV cache, its conv ring. A pass therefore costs the weights once plus each sequence's own bytes,
7//! against the arithmetic of every held token-row, and the batch grows until the card's memory is full.
8//!
9//! The pass is read off the same [`DecodeStep`] the batch-one price uses — the same stages and bytes, from the
10//! checkpoint's header — plus what serving adds: how many sequences ride a pass ([`Serving::batch`]), how many
11//! drafted tokens each verifies and how often they are accepted ([`Draft`]), and how the recurrent state is
12//! carried between tokens ([`StateFlush`]). The card is facts with sources ([`Card`]): its memory, the shares
13//! of that memory's bandwidth its stream engines sustain on each way a pass moves its bytes ([`Streams`]), the
14//! multiply-adds its integer array and its f32 vector units retire.
15//!
16//! The array multiplies 4-bit weight codes by 8-bit activation codes. A wider integer code is split into 4-bit
17//! digits and a two-word activation into two words, each digit and word one pass, recombined exactly
18//! ([`array_passes`]), so every block format the card decodes rides one datapath. A float matrix rides the
19//! vector units. A format the card does not decode is refused by name, never priced as a guess.
20//!
21//! Every weight block's integer sums leave the array for a block-scale lane: its sub-block scales and mins
22//! multiplied in, the block's two scales, the exact sum and its one rounding, the f32 accumulate ([`lane_cycles`]).
23//! The lanes are a fifth rate beside the memories and the two kinds of arithmetic: a format of small blocks, or
24//! activations scaled per 32 elements against large ones, asks more of them than of the array.
25
26use std::collections::BTreeMap;
27
28use serde::Serialize;
29
30use super::decode::{DecodeRefusal, DecodeStep, Deployment, GgmlType, GgufHeader, HeldClass, Split, StageKind, Tier, WeightBytes};
31use super::decode_engine::numerics::ActScaling;
32use super::rdu_chip::{Fact, Provenance};
33
34/// How the recurrent state is carried from one token to the next.
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
36pub enum StateFlush {
37    /// The dense state is read and written back every pass. With drafts, the state after every candidate prefix
38    /// is written and the accepted one kept.
39    EveryPass,
40    /// The dense state is written back once every `tokens` accepted tokens. Between flushes a log of each token's
41    /// key, value, decay exponent and gate carries it, read with the state every pass; a rejected draft truncates
42    /// the log.
43    Every { tokens: u32 },
44    /// The dense state is written back a window behind: each window of `tokens` logged tokens is replayed into the
45    /// state over the next window, a `1/tokens` share of every head's rows a token, as the read-out streams them. The
46    /// log holds the window being replayed and the one after it, at most `2·tokens` tokens; a row already replayed reads
47    /// through the newer window, every other through both. The dense state is [`StateFlush::Every`]'s at the same
48    /// `tokens`, a window later.
49    Trailing { tokens: u32 },
50}
51
52/// The drafted tokens each sequence verifies in a pass.
53#[derive(Clone, Debug, PartialEq, Serialize)]
54pub struct Draft {
55    pub tokens: u32,
56    /// `acceptance[i]`: the chance draft `i + 1` is accepted, given every draft before it was.
57    pub acceptance: Vec<Fact<f64>>,
58    /// Vocabulary rows the drafting head reads (the most frequent tokens), or `None` for the whole head.
59    /// Verification always reads the whole head, so the tokens emitted are the greedy tokens either way.
60    pub head_rows: Option<u64>,
61}
62
63/// A class of the batch's sequences by context: its share of the batch, the mean position its sequences have
64/// reached, and the context each of them is allowed.
65#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
66pub struct ContextClass {
67    pub share: f64,
68    pub position: f64,
69    pub limit: u64,
70}
71
72/// The contexts of a batch's sequences. Every per-position cost is linear in position, so a pass reads, and a
73/// paged cache holds, what the mean position says; a contiguous cache holds what the mean limit says.
74#[derive(Clone, Debug, PartialEq, Serialize)]
75pub struct ContextMix {
76    pub classes: Vec<ContextClass>,
77}
78
79impl ContextMix {
80    /// Every sequence at `position` of `limit`.
81    pub fn uniform(position: f64, limit: u64) -> ContextMix {
82        ContextMix { classes: vec![ContextClass { share: 1.0, position, limit }] }
83    }
84
85    pub fn mean_position(&self) -> f64 {
86        self.classes.iter().map(|c| c.share * c.position).sum()
87    }
88
89    pub fn mean_limit(&self) -> f64 {
90        self.classes.iter().map(|c| c.share * c.limit as f64).sum()
91    }
92
93    /// The longest context any class is allowed.
94    pub fn longest(&self) -> u64 {
95        self.classes.iter().map(|c| c.limit).max().unwrap_or(0)
96    }
97}
98
99/// Where attention's two products over the cache run.
100#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
101pub enum AttentionMath {
102    /// Scores and the weighted sum in f32 on the vector units, over the cache as stored.
103    Vector,
104    /// Scores and the weighted sum as integer dot products on the array: int8 queries against an int8 cache, and
105    /// 8-bit probabilities against it, two passes each (an 8-bit cache code is two digits). The softmax stays on the
106    /// vector units.
107    Array,
108}
109
110/// Vector multiply-add slots per score a softmax takes: the engine's exponential is twelve f32 operations
111/// (`decode_engine::numerics::exp`: the scaling and its rounding, two for the range reduction, six for the cubic, one
112/// for the table product, one for the power of two), and the running maximum, the sum and the rescale three more;
113/// fifteen operations, two to a slot.
114pub const SOFTMAX_MACS_PER_SCORE: f64 = 7.5;
115
116/// One serving configuration of a checkpoint on a card.
117#[derive(Clone, Debug, PartialEq, Serialize)]
118pub struct Serving {
119    /// Sequences that ride a pass.
120    pub batch: u32,
121    pub contexts: ContextMix,
122    /// Positions per KV page when the caches are paged: each sequence then holds its own length and half a page,
123    /// on average, instead of its whole limit.
124    pub kv_page: Option<u64>,
125    pub attention: AttentionMath,
126    /// Bytes per KV-cache element.
127    pub kv_bytes: f64,
128    /// Bytes per recurrent-state element.
129    pub state_bytes: f64,
130    /// Words each activation crosses the array in: one 8-bit word, or two (a word and its residual).
131    pub activation_words: u32,
132    /// How the activations a matrix reads are scaled: per 32 elements, or per the matrix format's block.
133    pub scaling: ActScaling,
134    /// Whether each cached key and value head row is rotated by the Walsh–Hadamard transform before its int8 codes,
135    /// the queries rotated alike and every head's weighted sum rotated back.
136    pub kv_rotated: bool,
137    pub draft: Option<Draft>,
138    pub flush: StateFlush,
139    /// Whether sequences the card's memory cannot hold, or whose traffic it is better without, may keep their
140    /// state and caches in host memory, streamed over the host link each pass.
141    pub host_memory: bool,
142}
143
144/// The host memory a card reads and writes over its host link.
145#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
146pub struct HostLink {
147    /// Bytes of host memory the serving pins for the card.
148    pub capacity: Fact<f64>,
149    /// Bytes per second host to card, and card to host: the link's two directions run at once.
150    pub read: Fact<f64>,
151    pub write: Fact<f64>,
152    /// The share of either direction's rate the card's DMA sustains.
153    pub stream: Fact<f64>,
154}
155
156/// The share of a card memory's bandwidth its stream engines sustain on each way a pass moves its bytes. The ways differ
157/// in how often the memory's bus turns between reading and writing and how often a row is opened for few bytes.
158#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
159pub struct Streams {
160    /// One long read: the weights, the embedding rows.
161    pub read: Fact<f64>,
162    /// A memory read and written back in place a chunk at a time: a recurrent state its pass rewrites.
163    pub rewrite: Fact<f64>,
164    /// A long read with the pass's rows appended after it: a cache, a conv ring, a log, a recurrent state read and not
165    /// written.
166    pub append: Fact<f64>,
167}
168
169/// A card that streams its weights, as a pass sees it.
170#[derive(Clone, Debug, PartialEq, Serialize)]
171pub struct Card {
172    pub name: &'static str,
173    /// Off-chip memory: the weights, and every KV cache and recurrent state it holds.
174    pub memory: Tier,
175    /// On-chip memory: the batch's token-rows through a layer.
176    pub on_chip: Tier,
177    /// The shares of `memory`'s bandwidth the stream engines sustain.
178    pub streams: Streams,
179    /// Multiply-adds per second the integer array retires: a 4-bit weight code times an 8-bit activation code.
180    pub array: Fact<f64>,
181    /// f32 multiply-adds per second the vector units retire: norms, attention, the recurrence, float matrices.
182    pub vector: Fact<f64>,
183    /// Cycles per second the block-scale lanes retire, all lanes together ([`lane_cycles`]).
184    pub scale: Fact<f64>,
185    pub host: Option<HostLink>,
186    pub sources: &'static [&'static str],
187}
188
189impl Card {
190    /// Every fact the card carries, with the path a lock names it by.
191    pub fn facts(&self) -> Vec<(String, Provenance, &'static str)> {
192        let mut out = vec![
193            ("memory.capacity".into(), self.memory.capacity.provenance, self.memory.capacity.cite),
194            ("memory.bandwidth".into(), self.memory.bandwidth.provenance, self.memory.bandwidth.cite),
195            ("on_chip.capacity".into(), self.on_chip.capacity.provenance, self.on_chip.capacity.cite),
196            ("on_chip.bandwidth".into(), self.on_chip.bandwidth.provenance, self.on_chip.bandwidth.cite),
197            ("streams.read".into(), self.streams.read.provenance, self.streams.read.cite),
198            ("streams.rewrite".into(), self.streams.rewrite.provenance, self.streams.rewrite.cite),
199            ("streams.append".into(), self.streams.append.provenance, self.streams.append.cite),
200            ("array".into(), self.array.provenance, self.array.cite),
201            ("vector".into(), self.vector.provenance, self.vector.cite),
202            ("scale".into(), self.scale.provenance, self.scale.cite),
203        ];
204        if let Some(h) = &self.host {
205            out.push(("host.capacity".into(), h.capacity.provenance, h.capacity.cite));
206            out.push(("host.read".into(), h.read.provenance, h.read.cite));
207            out.push(("host.write".into(), h.write.provenance, h.write.cite));
208            out.push(("host.stream".into(), h.stream.provenance, h.stream.cite));
209        }
210        out
211    }
212}
213
214/// Passes of the 4-bit array one weight of `ty` takes, or `None` for a type the array does not read.
215///
216/// A block format stores an unsigned code per weight (a signed one is offset by half its range) and per-block
217/// scales the block-scale unit applies after the integer sum. Codes of up to four bits are one pass. Five- and
218/// six-bit codes split into a 4-bit low digit and a high digit, and eight-bit codes (Q8_0, and the int8 table
219/// values IQ4 indexes) into two 4-bit digits: two passes, recombined exactly as `lo + 16·hi`.
220pub fn array_passes(ty: GgmlType) -> Option<u32> {
221    use GgmlType::*;
222    match ty {
223        Q2K | Q3K | Q4_0 | Q4_1 | Q4K => Some(1),
224        Q5_0 | Q5_1 | Q5K | Q6K | Q8_0 | Iq4Nl | Iq4Xs => Some(2),
225        _ => None,
226    }
227}
228
229/// Sub-block multipliers a block-scale lane applies in one cycle.
230pub const LANE_PRODUCTS: f64 = 16.0;
231
232/// Cycles a block-scale lane spends on one weight block of `ty` read against one activation word scaled as `scaling`,
233/// or `None` for a type the array does not read.
234///
235/// A lane takes a block's integer sums from the array, multiplies in its sub-block scales and mins
236/// ([`LANE_PRODUCTS`] a cycle), multiplies by the block's two scales times the activation scale, sums the two terms
237/// exactly, rounds once and adds the result into its row. Activations scaled per 32 elements against a larger block
238/// give the block one term per 32 elements, each aligned into the block's exact sum on its own cycle.
239pub fn lane_cycles(ty: GgmlType, scaling: ActScaling) -> Option<f64> {
240    use GgmlType::*;
241    let products = match ty {
242        Q4_0 | Q4_1 | Q5_0 | Q5_1 | Q8_0 | Iq4Nl => 0.0,
243        Iq4Xs => 8.0,
244        Q3K | Q4K | Q5K | Q6K => 16.0,
245        Q2K => 32.0,
246        _ => return None,
247    };
248    let per_block = (products / LANE_PRODUCTS).max(1.0);
249    Some(match scaling {
250        ActScaling::PerBlock => per_block,
251        ActScaling::Sub32 => per_block.max(ty.block_elems() as f64 / 32.0),
252    })
253}
254
255/// Positions one block of attention's weighted sum on the array spans: its probabilities share a scale.
256pub const VALUE_BLOCK: f64 = 32.0;
257
258/// Whether the vector units read `ty` as a float matrix.
259pub fn is_float(ty: GgmlType) -> bool {
260    matches!(ty, GgmlType::F32 | GgmlType::F16 | GgmlType::Bf16)
261}
262
263/// Why a serving could not be priced.
264#[derive(Clone, Debug, PartialEq, Serialize)]
265pub enum ServingRefusal {
266    Step(DecodeRefusal),
267    /// A matrix in a format neither the array nor the vector units read.
268    Undecoded { tensor: String, ty: GgmlType },
269    /// A tensor a stage names that the header does not hold.
270    MissingTensor(String),
271    /// What the serving holds exceeds the card's memory, and the host's if it may use it.
272    NotResident { needed: f64, capacity: f64 },
273    /// The batch's token-rows exceed the card's on-chip memory.
274    OnChipOverflow { needed: f64, capacity: f64 },
275    /// A draft whose acceptance list is not one probability per drafted token.
276    BadDraft(String),
277    /// A serving of no sequences, or no activation words.
278    Empty,
279    /// A mixture of experts: which experts a pass reads depends on how its rows route, which the pricer does
280    /// not model.
281    Routed,
282    /// A serving that may use host memory, on a card with no host link.
283    NoHostLink,
284    /// A context mix whose shares do not sum to one, or a class past its own limit.
285    BadContexts(String),
286    /// A context limit past the checkpoint's own context length.
287    ContextBeyondModel { limit: u64, model: u64 },
288    /// Attention on the array over a cache whose elements are wider than its 8-bit codes.
289    WideCacheOnArray { kv_bytes: f64 },
290}
291
292impl From<DecodeRefusal> for ServingRefusal {
293    fn from(r: DecodeRefusal) -> ServingRefusal {
294        ServingRefusal::Step(r)
295    }
296}
297
298/// The term a pass is bound by.
299#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
300pub enum ServingBind {
301    Memory,
302    Host,
303    Array,
304    Vector,
305    Scale,
306}
307
308/// Tokens a sequence emits per pass: its verified token, plus each draft accepted after every draft before it.
309pub fn emitted_per_pass(acceptance: &[f64]) -> f64 {
310    let mut run = 1.0;
311    let mut sum = 1.0;
312    for a in acceptance {
313        run *= a;
314        sum += run;
315    }
316    sum
317}
318
319/// The shape of a Gated-DeltaNet recurrence, from the header: what a log entry and a dense state hold.
320struct Recurrence {
321    /// Elements of one layer's dense state: one `key × value` matrix per value head.
322    state: f64,
323    /// Elements one token adds to one layer's log: every key group's key, every value head's value, decay exponent
324    /// and gate.
325    log: f64,
326    key: f64,
327    value: f64,
328    heads: f64,
329}
330
331fn recurrence(h: &GgufHeader) -> Option<Recurrence> {
332    let heads = h.meta_u64("ssm.time_step_rank")?;
333    let key = h.meta_u64("ssm.state_size")?;
334    let inner = h.meta_u64("ssm.inner_size")?;
335    let groups = h.meta_u64("ssm.group_count")?;
336    let value = inner / heads;
337    Some(Recurrence {
338        state: (heads * key * value) as f64,
339        log: (groups * key + heads * value + 2 * heads) as f64,
340        key: key as f64,
341        value: value as f64,
342        heads: heads as f64,
343    })
344}
345
346/// An attention head's key and value widths, as the decode step reads them.
347struct HeadWidths {
348    key: f64,
349    value: f64,
350}
351
352fn head_widths(h: &GgufHeader) -> HeadWidths {
353    let embd = h.meta_u64("embedding_length").unwrap_or(0);
354    let heads = h.meta_u64("attention.head_count").unwrap_or(1).max(1);
355    HeadWidths {
356        key: h.meta_u64("attention.key_length").unwrap_or(embd / heads) as f64,
357        value: h.meta_u64("attention.value_length").unwrap_or(embd / heads) as f64,
358    }
359}
360
361/// A pass as a function of its batch. Everything a pass costs is shared by the batch or paid by each sequence,
362/// so the cost is affine in the batch and in how the batch splits between the card's memory and the host's.
363#[derive(Clone, Debug, PartialEq, Serialize)]
364pub struct PassShape {
365    /// Token-rows each sequence carries through a pass: its verified token and its drafts.
366    pub rows_per_sequence: u32,
367    /// Tokens each sequence emits per pass, expected.
368    pub emitted_per_sequence: f64,
369    /// Weight bytes a pass streams, whatever its batch.
370    pub weight_bytes: f64,
371    /// Embedding-table bytes a pass reads for each sequence: a row per verified row and per drafting step.
372    pub embed_bytes: f64,
373    /// State, cache, log and ring bytes a pass reads and writes for each sequence.
374    pub read_bytes: f64,
375    pub write_bytes: f64,
376    /// Of those, the bytes of each sequence's recurrent states read and written back in place; the rest are read in
377    /// long runs with the pass's rows appended after them.
378    pub rewrite_bytes: f64,
379    pub array_macs: f64,
380    pub vector_macs: f64,
381    /// Block-scale lane cycles a pass spends for each sequence.
382    pub scale_cycles: f64,
383    /// Bytes every serving of the checkpoint holds: the weights and the embedding table.
384    pub held_shared: f64,
385    /// Bytes each sequence holds: its states, its log, its ring, its caches.
386    pub held_per_sequence: f64,
387    /// Bytes each sequence's token-rows take on chip through a layer.
388    pub on_chip_per_sequence: f64,
389    /// The weakest acceptance the shape rests on.
390    pub provenance: Provenance,
391}
392
393/// One pass of a serving on a card.
394#[derive(Clone, Debug, PartialEq, Serialize)]
395pub struct ServingCost {
396    pub card: &'static str,
397    pub batch: u32,
398    /// Of the batch, the sequences whose state and caches live in host memory.
399    pub host_sequences: u32,
400    pub rows_per_sequence: u32,
401    pub emitted_per_sequence: f64,
402    /// Weight bytes a pass streams, shared by the batch, embedding rows included.
403    pub weight_bytes: f64,
404    /// Bytes a pass moves for each sequence: its state, its caches, its log, its conv ring.
405    pub sequence_bytes: f64,
406    /// Of `sequence_bytes`, the recurrent states' bytes read and written back in place.
407    pub rewrite_bytes: f64,
408    pub array_macs: f64,
409    pub vector_macs: f64,
410    pub scale_cycles: f64,
411    pub memory_s: f64,
412    pub host_s: f64,
413    pub array_s: f64,
414    pub vector_s: f64,
415    pub scale_s: f64,
416    /// The largest of the five: the units and the memories overlap, so no pass is faster.
417    pub pass_s: f64,
418    pub binds: ServingBind,
419    pub held_bytes: f64,
420    pub capacity_bytes: f64,
421    pub host_held_bytes: f64,
422    pub host_capacity_bytes: f64,
423    pub on_chip_bytes: f64,
424    pub on_chip_capacity: f64,
425    /// The weakest fact the price rests on.
426    pub provenance: Provenance,
427}
428
429impl ServingCost {
430    pub fn tokens_per_s(&self) -> f64 {
431        self.batch as f64 * self.emitted_per_sequence / self.pass_s
432    }
433}
434
435impl PassShape {
436    /// The shape of a pass of `s` over the checkpoint `h` describes. The batch and the host split are the
437    /// cost's arguments; everything else in `s` is read here.
438    pub fn of(h: &GgufHeader, s: &Serving) -> Result<PassShape, ServingRefusal> {
439        if s.activation_words == 0 {
440            return Err(ServingRefusal::Empty);
441        }
442        if h.meta_u64("expert_count").is_some_and(|e| e > 0) {
443            return Err(ServingRefusal::Routed);
444        }
445        let k = s.draft.as_ref().map_or(0, |d| d.tokens);
446        if let Some(d) = &s.draft {
447            if d.acceptance.len() != d.tokens as usize || d.acceptance.iter().any(|a| !(0.0..=1.0).contains(&a.value)) {
448                return Err(ServingRefusal::BadDraft(format!(
449                    "{} drafted tokens with acceptance {:?}",
450                    d.tokens,
451                    d.acceptance.iter().map(|a| a.value).collect::<Vec<_>>()
452                )));
453            }
454        }
455        let shares: f64 = s.contexts.classes.iter().map(|c| c.share).sum();
456        if s.contexts.classes.is_empty()
457            || (shares - 1.0).abs() > 1e-9
458            || s.contexts.classes.iter().any(|c| !(c.share > 0.0) || !(c.position >= 0.0) || c.position > c.limit as f64)
459        {
460            return Err(ServingRefusal::BadContexts(format!("{:?}", s.contexts.classes)));
461        }
462        if let Some(model) = h.meta_u64("context_length") {
463            if s.contexts.longest() > model {
464                return Err(ServingRefusal::ContextBeyondModel { limit: s.contexts.longest(), model });
465            }
466        }
467        // An 8-bit cache code and its share of a per-row scale: up to one byte and a quarter.
468        if s.attention == AttentionMath::Array && s.kv_bytes > 1.25 {
469            return Err(ServingRefusal::WideCacheOnArray { kv_bytes: s.kv_bytes });
470        }
471        // The step at one position: every attention term it prices is then per position, scaled below by the
472        // mix's mean position.
473        let deployment = Deployment {
474            ctx: 1,
475            weights: WeightBytes::AsStored,
476            kv_bytes: s.kv_bytes,
477            state_bytes: s.state_bytes,
478            activation_bytes: 2.0,
479            speculative: k > 0,
480        };
481        let step = DecodeStep::of(h, &deployment)?;
482        let blocks = h.meta_u64("block_count").unwrap_or(0) as u32;
483        let drafts = h.meta_u64("nextn_predict_layers").unwrap_or(0) as u32;
484        let is_draft = |layer: Option<u32>| layer.is_some_and(|l| l >= blocks.saturating_sub(drafts));
485        let tensors: BTreeMap<&str, &super::decode::TensorRow> = h.tensors.iter().map(|t| (t.name.as_str(), t)).collect();
486        let rec = recurrence(h);
487        let head = head_widths(h);
488
489        let n = (1 + k) as f64;
490        let kf = k as f64;
491        let acceptance: Vec<f64> = s.draft.as_ref().map_or(Vec::new(), |d| d.acceptance.iter().map(|a| a.value).collect());
492        let emitted = emitted_per_pass(&acceptance);
493        let words = s.activation_words as f64;
494        let vocab = tensors
495            .get("token_embd.weight")
496            .and_then(|t| t.dims.get(1).copied())
497            .ok_or_else(|| ServingRefusal::MissingTensor("token_embd.weight".into()))? as f64;
498        let head_share = s.draft.as_ref().and_then(|d| d.head_rows).map_or(1.0, |r| (r as f64 / vocab).min(1.0));
499        // The tokens a flush replays, whether the state is lazy, the windows its log holds at its longest, and the
500        // mean log length a pass starts with: the log grows by the emitted tokens each pass from empty to the flush,
501        // or under a trailing flush from one window to two, the window being replayed behind the newer one.
502        let (flush_tokens, lazy, windows, window) = match s.flush {
503            StateFlush::EveryPass => (1.0, false, 0.0, 0.0),
504            StateFlush::Every { tokens } => {
505                let t = tokens.max(1) as f64;
506                (t, true, 1.0, ((t - emitted) / 2.0).max(0.0))
507            }
508            StateFlush::Trailing { tokens } => {
509                let t = tokens.max(1) as f64;
510                (t, true, 2.0, t + ((t - emitted) / 2.0).max(0.0))
511            }
512        };
513        let position = s.contexts.mean_position();
514        // Positions each sequence's caches hold.
515        let kv_positions = match s.kv_page {
516            Some(page) => position + page as f64 / 2.0,
517            None => s.contexts.mean_limit(),
518        };
519
520        let mut shape = PassShape {
521            rows_per_sequence: 1 + k,
522            emitted_per_sequence: emitted,
523            weight_bytes: 0.0,
524            embed_bytes: 0.0,
525            read_bytes: 0.0,
526            write_bytes: 0.0,
527            rewrite_bytes: 0.0,
528            array_macs: 0.0,
529            vector_macs: 0.0,
530            scale_cycles: 0.0,
531            held_shared: step
532                .held
533                .iter()
534                .filter(|c| matches!(c.class, HeldClass::Weights | HeldClass::EmbeddingTable))
535                .map(|c| c.bytes)
536                .sum(),
537            held_per_sequence: 0.0,
538            on_chip_per_sequence: 0.0,
539            provenance: s
540                .draft
541                .as_ref()
542                .and_then(|d| d.acceptance.iter().map(|a| a.provenance).max())
543                .unwrap_or(Provenance::Published),
544        };
545        for st in &step.stages {
546            let draft = is_draft(st.layer);
547            // Rows the stage multiplies per pass, per sequence: every verified row, or one row per drafting step.
548            let rows = if draft { kf } else { n };
549            match st.kind {
550                StageKind::Embed => {
551                    // One table row for every verified row and every drafting step's input token.
552                    shape.embed_bytes += st.weight_bytes * (n + kf);
553                }
554                StageKind::Project | StageKind::Head | StageKind::Norm | StageKind::Router => {
555                    let reads = if st.kind == StageKind::Head && !draft {
556                        // The verifying head, and each drafting step's head over its rows.
557                        1.0 + kf * head_share
558                    } else if draft {
559                        kf
560                    } else {
561                        1.0
562                    };
563                    shape.weight_bytes += st.weight_bytes * reads;
564                    let head_rows = if st.kind == StageKind::Head { n + kf * head_share } else { rows };
565                    for name in &st.tensors {
566                        let t = tensors.get(name.as_str()).ok_or_else(|| ServingRefusal::MissingTensor(name.clone()))?;
567                        let ty = h.tensor_type(t)?;
568                        let elems: f64 = t.dims.iter().product::<u64>() as f64;
569                        if st.kind == StageKind::Norm || t.dims.len() < 2 {
570                            // A norm: a sum of squares and a scale per element.
571                            shape.vector_macs += 2.0 * elems * rows;
572                        } else if let Some(p) = array_passes(ty) {
573                            shape.array_macs += elems * p as f64 * words * head_rows;
574                            let lane = lane_cycles(ty, s.scaling).expect("the array reads every type it passes");
575                            shape.scale_cycles += elems / ty.block_elems() as f64 * lane * words * head_rows;
576                        } else if is_float(ty) {
577                            shape.vector_macs += elems * head_rows;
578                        } else {
579                            return Err(ServingRefusal::Undecoded { tensor: name.clone(), ty });
580                        }
581                    }
582                }
583                StageKind::Attention => {
584                    shape.weight_bytes += st.weight_bytes * if draft { kf } else { 1.0 };
585                    // One read of the cache serves every row of the pass; every row's key and value are written.
586                    let per_position = st.state_write;
587                    shape.read_bytes += per_position * position * if draft { kf } else { 1.0 };
588                    shape.write_bytes += per_position * rows;
589                    // Scores and the weighted sum: a multiply-add per head per key and value element per position.
590                    let products = st.ops / 2.0 * position * rows;
591                    let heads = match st.split {
592                        Split::Heads { q, .. } => q as f64,
593                        _ => 1.0,
594                    };
595                    shape.vector_macs += SOFTMAX_MACS_PER_SCORE * heads * position * rows;
596                    match s.attention {
597                        AttentionMath::Vector => shape.vector_macs += products,
598                        AttentionMath::Array => {
599                            shape.array_macs += 2.0 * products;
600                            // A lane rounds every score, and every value element's sum over each block of positions.
601                            shape.scale_cycles += heads * position * rows * (1.0 + head.value / VALUE_BLOCK);
602                        }
603                    }
604                    if s.kv_rotated {
605                        // Every cached key and value head rotated, every query head, every weighted sum rotated back:
606                        // a transform of `n` elements is `n·log2 n` additions.
607                        let transform = |n: f64| n * n.max(1.0).log2();
608                        let kv_heads = match st.split {
609                            Split::Heads { kv, .. } => kv as f64,
610                            _ => 0.0,
611                        };
612                        shape.vector_macs +=
613                            (kv_heads * (transform(head.key) + transform(head.value)) + heads * (transform(head.key) + transform(head.value))) * rows;
614                    }
615                    shape.held_per_sequence += per_position * kv_positions;
616                }
617                StageKind::Recurrence => {
618                    shape.weight_bytes += st.weight_bytes;
619                    let r = rec.as_ref().ok_or_else(|| {
620                        ServingRefusal::Step(DecodeRefusal::MissingMetadata(
621                            "ssm.time_step_rank / ssm.state_size / ssm.inner_size / ssm.group_count".into(),
622                        ))
623                    })?;
624                    let dense = r.state * s.state_bytes;
625                    // The conv ring is a log of the last inputs: a pass reads the ring and appends its rows.
626                    let taps = h.meta_u64("ssm.conv_kernel").unwrap_or(1) as f64;
627                    let ring = (st.state_read - dense).max(0.0);
628                    let ring_row = ring / (taps - 1.0).max(1.0);
629                    shape.read_bytes += dense + ring;
630                    shape.write_bytes += ring_row * n;
631                    shape.vector_macs += taps * ring_row / s.state_bytes * n;
632                    shape.held_per_sequence += ring + ring_row * n;
633                    if lazy {
634                        let log = r.log * s.state_bytes;
635                        // The state is read once; the log since the last flush is read with it; the pass appends
636                        // its rows; the state is written once every `flush_tokens` emitted tokens, all at once or
637                        // a `1/flush_tokens` share an emitted token.
638                        shape.read_bytes += window * log;
639                        shape.write_bytes += n * log + emitted / flush_tokens * dense;
640                        // A pass that flushes reads the state and writes it back, the others read it alone; under a
641                        // trailing flush every pass reads and writes back its emitted tokens' shares.
642                        let flushing = (emitted / flush_tokens).min(1.0);
643                        shape.rewrite_bytes += flushing * dense + emitted / flush_tokens * dense;
644                        // Each row reads the state once through its folded query and walks the log back to the
645                        // flush point; a flush folds `flush_tokens` rank-one updates into the state.
646                        let walk = window + (n - 1.0) / 2.0;
647                        shape.vector_macs += n * r.state + n * r.heads * walk * (2.0 * r.key + r.value) + 2.0 * emitted * r.state;
648                        shape.held_per_sequence += dense + (windows * flush_tokens + n) * log;
649                    } else {
650                        // Every row's decay, rank-one update and read-out; every candidate prefix's state written.
651                        shape.write_bytes += n * dense;
652                        shape.rewrite_bytes += dense + n * dense;
653                        shape.vector_macs += n * 3.0 * r.state;
654                        shape.held_per_sequence += n * dense;
655                    }
656                }
657            }
658        }
659        // Through a layer the card holds each row's f32 residual and its widest activations in their words.
660        let width = h.meta_u64("embedding_length").unwrap_or(0) as f64;
661        let ffn = h.meta_u64("feed_forward_length").unwrap_or(0) as f64;
662        shape.on_chip_per_sequence = n * (4.0 * width + words * (ffn + width));
663        Ok(shape)
664    }
665
666    /// The pass of `batch` sequences, `host_sequences` of them held in host memory, on `card`.
667    pub fn cost(&self, card: &Card, batch: u32, host_sequences: u32) -> Result<ServingCost, ServingRefusal> {
668        if batch == 0 {
669            return Err(ServingRefusal::Empty);
670        }
671        let host_sequences = host_sequences.min(batch);
672        let b = batch as f64;
673        let bh = host_sequences as f64;
674        let bd = b - bh;
675        let held_bytes = self.held_shared + bd * self.held_per_sequence;
676        let capacity_bytes = card.memory.capacity.value;
677        let host_held_bytes = bh * self.held_per_sequence;
678        let host_capacity_bytes = card.host.map_or(0.0, |h| h.capacity.value);
679        if host_sequences > 0 && card.host.is_none() {
680            return Err(ServingRefusal::NoHostLink);
681        }
682        if held_bytes > capacity_bytes {
683            return Err(ServingRefusal::NotResident { needed: held_bytes, capacity: capacity_bytes });
684        }
685        if host_held_bytes > host_capacity_bytes {
686            return Err(ServingRefusal::NotResident { needed: host_held_bytes, capacity: host_capacity_bytes });
687        }
688        let on_chip_bytes = b * self.on_chip_per_sequence;
689        let on_chip_capacity = card.on_chip.capacity.value;
690        if on_chip_bytes > on_chip_capacity {
691            return Err(ServingRefusal::OnChipOverflow { needed: on_chip_bytes, capacity: on_chip_capacity });
692        }
693        let weight_bytes = self.weight_bytes + b * self.embed_bytes;
694        let sequence_bytes = self.read_bytes + self.write_bytes;
695        let at = |share: Fact<f64>| card.memory.bandwidth.value * share.value;
696        let memory_s = weight_bytes / at(card.streams.read)
697            + bd * (self.rewrite_bytes / at(card.streams.rewrite) + (sequence_bytes - self.rewrite_bytes) / at(card.streams.append));
698        let host_s = match card.host {
699            Some(h) if host_sequences > 0 => {
700                (bh * self.read_bytes / (h.read.value * h.stream.value)).max(bh * self.write_bytes / (h.write.value * h.stream.value))
701            }
702            _ => 0.0,
703        };
704        let array_macs = b * self.array_macs;
705        let vector_macs = b * self.vector_macs;
706        let scale_cycles = b * self.scale_cycles;
707        let array_s = array_macs / card.array.value;
708        let vector_s = vector_macs / card.vector.value;
709        let scale_s = scale_cycles / card.scale.value;
710        let (pass_s, binds) = [
711            (memory_s, ServingBind::Memory),
712            (host_s, ServingBind::Host),
713            (array_s, ServingBind::Array),
714            (vector_s, ServingBind::Vector),
715            (scale_s, ServingBind::Scale),
716        ]
717        .into_iter()
718        .max_by(|a, b| a.0.total_cmp(&b.0))
719        .expect("five terms");
720        let mut facts = vec![
721            card.memory.bandwidth,
722            card.memory.capacity,
723            card.streams.read,
724            card.streams.rewrite,
725            card.streams.append,
726            card.array,
727            card.vector,
728            card.scale,
729            card.on_chip.capacity,
730        ];
731        if let (Some(h), true) = (card.host, host_sequences > 0) {
732            facts.extend([h.capacity, h.read, h.write, h.stream]);
733        }
734        let provenance = facts.iter().map(|f| f.provenance).max().unwrap_or(Provenance::Published).max(self.provenance);
735        Ok(ServingCost {
736            card: card.name,
737            batch,
738            host_sequences,
739            rows_per_sequence: self.rows_per_sequence,
740            emitted_per_sequence: self.emitted_per_sequence,
741            weight_bytes,
742            sequence_bytes,
743            rewrite_bytes: self.rewrite_bytes,
744            array_macs,
745            vector_macs,
746            scale_cycles,
747            memory_s,
748            host_s,
749            array_s,
750            vector_s,
751            scale_s,
752            pass_s,
753            binds,
754            held_bytes,
755            capacity_bytes,
756            host_held_bytes,
757            host_capacity_bytes,
758            on_chip_bytes,
759            on_chip_capacity,
760            provenance,
761        })
762    }
763
764    /// Most sequences the card's memory holds beside the weights.
765    fn card_room(&self, card: &Card) -> u32 {
766        let room = (card.memory.capacity.value - self.held_shared) / self.held_per_sequence.max(1.0);
767        room.max(0.0).floor().min(u32::MAX as f64) as u32
768    }
769
770    /// Most sequences the host memory holds.
771    fn host_room(&self, card: &Card) -> u32 {
772        card.host.map_or(0, |h| (h.capacity.value / self.held_per_sequence.max(1.0)).floor().min(u32::MAX as f64) as u32)
773    }
774
775    /// The pass of `batch` sequences split between the card's memory and the host's as fast as the split allows:
776    /// the card's memory streams far faster, so it holds sequences until its own traffic would outlast the host
777    /// link's.
778    pub fn cost_split(&self, card: &Card, batch: u32, host_memory: bool) -> Result<ServingCost, ServingRefusal> {
779        if !host_memory {
780            return self.cost(card, batch, 0);
781        }
782        if card.host.is_none() {
783            return Err(ServingRefusal::NoHostLink);
784        }
785        let lo = batch.saturating_sub(self.host_room(card));
786        let hi = batch.min(self.card_room(card));
787        if lo > hi {
788            // Neither memory holds the rest: the cost names what would not fit.
789            return self.cost(card, batch, batch - hi);
790        }
791        // The card's time rises with the sequences it holds and the host's falls: the fastest split is where they
792        // cross, or the end of the range nearest it.
793        let card_time = |held: u32| self.cost(card, batch, batch - held).map(|c| c.memory_s.max(c.host_s));
794        let (mut a, mut z) = (lo, hi);
795        while z - a > 2 {
796            let m1 = a + (z - a) / 3;
797            let m2 = z - (z - a) / 3;
798            if card_time(m1)? <= card_time(m2)? {
799                z = m2;
800            } else {
801                a = m1;
802            }
803        }
804        let mut best: Option<ServingCost> = None;
805        for held in a..=z {
806            let c = self.cost(card, batch, batch - held)?;
807            if best.as_ref().map_or(true, |b| c.pass_s < b.pass_s) {
808                best = Some(c);
809            }
810        }
811        Ok(best.expect("a nonempty range"))
812    }
813
814    /// The batch that makes `card` emit the most tokens per second, and its pass. Ties go to the smaller batch.
815    pub fn best(&self, card: &Card, host_memory: bool) -> Result<ServingCost, ServingRefusal> {
816        let most = if host_memory { self.card_room(card).saturating_add(self.host_room(card)) } else { self.card_room(card) };
817        let mut best: Option<ServingCost> = None;
818        for batch in 1..=most.max(1) {
819            match self.cost_split(card, batch, host_memory) {
820                Ok(c) => {
821                    if best.as_ref().map_or(true, |b| c.tokens_per_s() > b.tokens_per_s() * (1.0 + 1e-12)) {
822                        best = Some(c);
823                    }
824                }
825                Err(ServingRefusal::NotResident { .. } | ServingRefusal::OnChipOverflow { .. }) => break,
826                Err(e) => return Err(e),
827            }
828        }
829        best.map_or_else(|| self.cost_split(card, 1, host_memory), Ok)
830    }
831}
832
833/// Prices one pass of `s` over the checkpoint `h` describes, on `card`.
834pub fn price_serving(h: &GgufHeader, card: &Card, s: &Serving) -> Result<ServingCost, ServingRefusal> {
835    if s.batch == 0 {
836        return Err(ServingRefusal::Empty);
837    }
838    PassShape::of(h, s)?.cost_split(card, s.batch, s.host_memory)
839}
840
841/// The most sequences `s` can carry on `card`: every larger batch is refused for capacity.
842pub fn max_batch(h: &GgufHeader, card: &Card, s: &Serving) -> Result<u32, ServingRefusal> {
843    let shape = PassShape::of(h, s)?;
844    let mut n = 0;
845    let most = if s.host_memory { shape.card_room(card).saturating_add(shape.host_room(card)) } else { shape.card_room(card) };
846    for batch in 1..=most.saturating_add(1) {
847        match shape.cost_split(card, batch, s.host_memory) {
848            Ok(_) => n = batch,
849            Err(ServingRefusal::NotResident { .. } | ServingRefusal::OnChipOverflow { .. }) => break,
850            Err(e) => return Err(e),
851        }
852    }
853    if n == 0 {
854        shape.cost_split(card, 1, s.host_memory)?;
855    }
856    Ok(n)
857}
858
859/// The batch at which `s` emits the most tokens per second on `card`, and its pass.
860pub fn best_serving(h: &GgufHeader, card: &Card, s: &Serving) -> Result<ServingCost, ServingRefusal> {
861    PassShape::of(h, s)?.best(card, s.host_memory)
862}