1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
36pub enum StateFlush {
37 EveryPass,
40 Every { tokens: u32 },
44 Trailing { tokens: u32 },
50}
51
52#[derive(Clone, Debug, PartialEq, Serialize)]
54pub struct Draft {
55 pub tokens: u32,
56 pub acceptance: Vec<Fact<f64>>,
58 pub head_rows: Option<u64>,
61}
62
63#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
66pub struct ContextClass {
67 pub share: f64,
68 pub position: f64,
69 pub limit: u64,
70}
71
72#[derive(Clone, Debug, PartialEq, Serialize)]
75pub struct ContextMix {
76 pub classes: Vec<ContextClass>,
77}
78
79impl ContextMix {
80 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 pub fn longest(&self) -> u64 {
95 self.classes.iter().map(|c| c.limit).max().unwrap_or(0)
96 }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
101pub enum AttentionMath {
102 Vector,
104 Array,
108}
109
110pub const SOFTMAX_MACS_PER_SCORE: f64 = 7.5;
115
116#[derive(Clone, Debug, PartialEq, Serialize)]
118pub struct Serving {
119 pub batch: u32,
121 pub contexts: ContextMix,
122 pub kv_page: Option<u64>,
125 pub attention: AttentionMath,
126 pub kv_bytes: f64,
128 pub state_bytes: f64,
130 pub activation_words: u32,
132 pub scaling: ActScaling,
134 pub kv_rotated: bool,
137 pub draft: Option<Draft>,
138 pub flush: StateFlush,
139 pub host_memory: bool,
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
146pub struct HostLink {
147 pub capacity: Fact<f64>,
149 pub read: Fact<f64>,
151 pub write: Fact<f64>,
152 pub stream: Fact<f64>,
154}
155
156#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
159pub struct Streams {
160 pub read: Fact<f64>,
162 pub rewrite: Fact<f64>,
164 pub append: Fact<f64>,
167}
168
169#[derive(Clone, Debug, PartialEq, Serialize)]
171pub struct Card {
172 pub name: &'static str,
173 pub memory: Tier,
175 pub on_chip: Tier,
177 pub streams: Streams,
179 pub array: Fact<f64>,
181 pub vector: Fact<f64>,
183 pub scale: Fact<f64>,
185 pub host: Option<HostLink>,
186 pub sources: &'static [&'static str],
187}
188
189impl Card {
190 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
214pub 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
229pub const LANE_PRODUCTS: f64 = 16.0;
231
232pub 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
255pub const VALUE_BLOCK: f64 = 32.0;
257
258pub fn is_float(ty: GgmlType) -> bool {
260 matches!(ty, GgmlType::F32 | GgmlType::F16 | GgmlType::Bf16)
261}
262
263#[derive(Clone, Debug, PartialEq, Serialize)]
265pub enum ServingRefusal {
266 Step(DecodeRefusal),
267 Undecoded { tensor: String, ty: GgmlType },
269 MissingTensor(String),
271 NotResident { needed: f64, capacity: f64 },
273 OnChipOverflow { needed: f64, capacity: f64 },
275 BadDraft(String),
277 Empty,
279 Routed,
282 NoHostLink,
284 BadContexts(String),
286 ContextBeyondModel { limit: u64, model: u64 },
288 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
300pub enum ServingBind {
301 Memory,
302 Host,
303 Array,
304 Vector,
305 Scale,
306}
307
308pub 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
319struct Recurrence {
321 state: f64,
323 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
346struct 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#[derive(Clone, Debug, PartialEq, Serialize)]
364pub struct PassShape {
365 pub rows_per_sequence: u32,
367 pub emitted_per_sequence: f64,
369 pub weight_bytes: f64,
371 pub embed_bytes: f64,
373 pub read_bytes: f64,
375 pub write_bytes: f64,
376 pub rewrite_bytes: f64,
379 pub array_macs: f64,
380 pub vector_macs: f64,
381 pub scale_cycles: f64,
383 pub held_shared: f64,
385 pub held_per_sequence: f64,
387 pub on_chip_per_sequence: f64,
389 pub provenance: Provenance,
391}
392
393#[derive(Clone, Debug, PartialEq, Serialize)]
395pub struct ServingCost {
396 pub card: &'static str,
397 pub batch: u32,
398 pub host_sequences: u32,
400 pub rows_per_sequence: u32,
401 pub emitted_per_sequence: f64,
402 pub weight_bytes: f64,
404 pub sequence_bytes: f64,
406 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 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 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 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 if s.attention == AttentionMath::Array && s.kv_bytes > 1.25 {
469 return Err(ServingRefusal::WideCacheOnArray { kv_bytes: s.kv_bytes });
470 }
471 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 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 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 let rows = if draft { kf } else { n };
549 match st.kind {
550 StageKind::Embed => {
551 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 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 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 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 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 shape.scale_cycles += heads * position * rows * (1.0 + head.value / VALUE_BLOCK);
602 }
603 }
604 if s.kv_rotated {
605 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 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 shape.read_bytes += window * log;
639 shape.write_bytes += n * log + emitted / flush_tokens * dense;
640 let flushing = (emitted / flush_tokens).min(1.0);
643 shape.rewrite_bytes += flushing * dense + emitted / flush_tokens * dense;
644 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 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 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 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 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 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 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 return self.cost(card, batch, batch - hi);
790 }
791 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 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
833pub 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
841pub 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
859pub fn best_serving(h: &GgufHeader, card: &Card, s: &Serving) -> Result<ServingCost, ServingRefusal> {
861 PassShape::of(h, s)?.best(card, s.host_memory)
862}