1use crate::config::ModelConfig;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum KvElem {
49 F32,
51 F16,
53 Q8_0,
56 Turbo4,
58}
59
60impl KvElem {
61 pub fn bytes_for(self, elems: u64) -> u64 {
65 match self {
66 KvElem::F32 => elems * 4,
67 KvElem::F16 => elems * 2,
68 KvElem::Q8_0 => {
69 let blocks = elems.div_ceil(ferrox_quant::Q8_0_BLOCK_ELEMS as u64);
70 blocks * ferrox_quant::Q8_0_BLOCK_BYTES as u64
71 }
72 KvElem::Turbo4 => {
73 let blocks = elems.div_ceil(ferrox_quant::TURBO4_KV_GROUP as u64);
74 blocks * ferrox_quant::TURBO4_KV_BLOCK_BYTES as u64
75 }
76 }
77 }
78
79 pub fn as_str(self) -> &'static str {
80 match self {
81 KvElem::F32 => "f32",
82 KvElem::F16 => "f16",
83 KvElem::Q8_0 => "q8_0",
84 KvElem::Turbo4 => "turbo4",
85 }
86 }
87
88 pub fn from_ctk(value: &str) -> Self {
101 match value.trim().to_ascii_lowercase().as_str() {
102 "f32" => KvElem::F32,
105 "q8_0" | "turbo8" | "fp8" => KvElem::Q8_0,
106 "turbo4" => KvElem::Turbo4,
107 _ => KvElem::F16,
108 }
109 }
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum KvLayout {
118 Gqa { n_kv_heads: usize, head_dim: usize },
124 MlaLatent {
136 kv_lora_rank: usize,
137 qk_rope_head_dim: usize,
138 },
139 MlaExpanded {
145 n_heads: usize,
146 k_head_dim: usize,
147 v_head_dim: usize,
148 },
149}
150
151impl KvLayout {
152 pub fn elems_per_token_per_layer(self) -> u64 {
154 match self {
155 KvLayout::Gqa {
156 n_kv_heads,
157 head_dim,
158 } => 2 * n_kv_heads as u64 * head_dim as u64,
159 KvLayout::MlaLatent {
160 kv_lora_rank,
161 qk_rope_head_dim,
162 } => kv_lora_rank as u64 + qk_rope_head_dim as u64,
163 KvLayout::MlaExpanded {
164 n_heads,
165 k_head_dim,
166 v_head_dim,
167 } => n_heads as u64 * (k_head_dim as u64 + v_head_dim as u64),
168 }
169 }
170
171 pub fn describe(self) -> String {
174 match self {
175 KvLayout::Gqa {
176 n_kv_heads,
177 head_dim,
178 } => format!("2 (K+V) x {n_kv_heads} kv-heads x {head_dim} head-dim"),
179 KvLayout::MlaLatent {
180 kv_lora_rank,
181 qk_rope_head_dim,
182 } => format!(
183 "MLA latent: {kv_lora_rank} kv_lora_rank + {qk_rope_head_dim} rope-dim \
184 (one vector, no K/V doubling)"
185 ),
186 KvLayout::MlaExpanded {
187 n_heads,
188 k_head_dim,
189 v_head_dim,
190 } => format!(
191 "MLA expanded: {n_heads} heads x ({k_head_dim} K head-dim + \
192 {v_head_dim} V head-dim)"
193 ),
194 }
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub struct SlidingWindow {
202 pub window: usize,
204 pub chunk: usize,
209 pub pattern: Option<usize>,
214}
215
216impl SlidingWindow {
217 pub fn resident_positions(&self, tokens: usize) -> usize {
220 tokens.min(self.window + self.chunk.max(1) - 1)
221 }
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub struct KvShape {
227 pub n_layers: usize,
228 pub layout: KvLayout,
229 pub elem: KvElem,
230 pub sliding: Option<SlidingWindow>,
232}
233
234impl KvShape {
235 pub fn from_config(config: &ModelConfig, elem: KvElem, chunk: usize) -> Self {
245 KvShape {
246 n_layers: config.n_layers,
247 layout: KvLayout::Gqa {
248 n_kv_heads: config.n_kv_heads,
249 head_dim: config.head_dim,
250 },
251 elem,
252 sliding: config.sliding_window.map(|window| SlidingWindow {
253 window,
254 chunk,
255 pattern: config.swa_pattern,
256 }),
257 }
258 }
259
260 pub fn mla_expanded(
263 n_layers: usize,
264 n_heads: usize,
265 qk_nope_head_dim: usize,
266 qk_rope_head_dim: usize,
267 v_head_dim: usize,
268 elem: KvElem,
269 ) -> Self {
270 KvShape {
271 n_layers,
272 layout: KvLayout::MlaExpanded {
273 n_heads,
274 k_head_dim: qk_nope_head_dim + qk_rope_head_dim,
275 v_head_dim,
276 },
277 elem,
278 sliding: None,
279 }
280 }
281
282 pub fn sliding_layers(&self) -> usize {
284 match self.sliding {
285 None => 0,
286 Some(SlidingWindow { pattern: None, .. }) => self.n_layers,
287 Some(SlidingWindow {
288 pattern: Some(period),
289 ..
290 }) => {
291 if period <= 1 {
292 self.n_layers
293 } else {
294 self.n_layers - self.n_layers / period
297 }
298 }
299 }
300 }
301
302 pub fn full_attention_layers(&self) -> usize {
304 self.n_layers - self.sliding_layers()
305 }
306
307 pub fn per_token_kv_bytes(&self) -> u64 {
314 self.n_layers as u64 * self.elem.bytes_for(self.layout.elems_per_token_per_layer())
315 }
316
317 pub fn marginal_per_token_bytes(&self) -> u64 {
323 self.full_attention_layers() as u64
324 * self.elem.bytes_for(self.layout.elems_per_token_per_layer())
325 }
326
327 pub fn kv_bytes_for_tokens(&self, tokens: usize) -> u64 {
330 let per_layer = self.layout.elems_per_token_per_layer();
331 let full =
332 self.full_attention_layers() as u64 * self.elem.bytes_for(per_layer * tokens as u64);
333 let sliding = match self.sliding {
334 None => 0,
335 Some(w) => {
336 self.sliding_layers() as u64
337 * self
338 .elem
339 .bytes_for(per_layer * w.resident_positions(tokens) as u64)
340 }
341 };
342 full + sliding
343 }
344
345 pub fn describe(&self) -> String {
348 let base = format!(
349 "{} layers x [{}] x {} = {} bytes/token",
350 self.n_layers,
351 self.layout.describe(),
352 self.elem.as_str(),
353 self.per_token_kv_bytes()
354 );
355 match self.sliding {
356 None => base,
357 Some(w) => format!(
358 "{base}; {} of {} layers slide and cap at min(tokens, {} window + {} chunk - 1) \
359 = {} positions, leaving {} bytes/token marginal",
360 self.sliding_layers(),
361 self.n_layers,
362 w.window,
363 w.chunk,
364 w.window + w.chunk.max(1) - 1,
365 self.marginal_per_token_bytes(),
366 ),
367 }
368 }
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub enum Ceiling {
378 ContextLength,
380 DeviceMemory,
382}
383
384impl Ceiling {
385 pub fn code(self) -> &'static str {
387 match self {
388 Ceiling::ContextLength => "context_length_exceeded",
389 Ceiling::DeviceMemory => "device_memory_budget_exceeded",
390 }
391 }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
399#[error("{code}: {detail} (estimated {estimated_bytes} bytes vs limit {limit_bytes} bytes)",
400 code = self.binding.code())]
401pub struct KvBudgetError {
402 pub binding: Ceiling,
403 pub estimated_bytes: u64,
404 pub limit_bytes: u64,
405 pub detail: String,
406}
407
408impl KvBudgetError {
409 pub fn code(&self) -> &'static str {
410 self.binding.code()
411 }
412
413 pub fn overage_bytes(&self) -> u64 {
415 self.estimated_bytes.saturating_sub(self.limit_bytes)
416 }
417}
418
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub struct KvBudget {
423 pub weights_bytes: u64,
426 pub activation_headroom_bytes: u64,
428 pub device_budget_bytes: u64,
431 pub shape: KvShape,
432 pub concurrent_requests: usize,
434}
435
436impl KvBudget {
437 pub fn kv_bytes_available(&self) -> u64 {
440 self.device_budget_bytes
441 .saturating_sub(self.weights_bytes)
442 .saturating_sub(self.activation_headroom_bytes)
443 }
444
445 pub fn estimated_bytes(&self, tokens: usize) -> u64 {
447 self.weights_bytes
448 + self.activation_headroom_bytes
449 + self.shape.kv_bytes_for_tokens(tokens) * self.concurrent_requests.max(1) as u64
450 }
451
452 pub fn check(&self, tokens: usize) -> Result<u64, KvBudgetError> {
455 let estimated = self.estimated_bytes(tokens);
456 if estimated <= self.device_budget_bytes {
457 return Ok(estimated);
458 }
459 Err(KvBudgetError {
460 binding: Ceiling::DeviceMemory,
461 estimated_bytes: estimated,
462 limit_bytes: self.device_budget_bytes,
463 detail: format!(
464 "{} weight bytes + {} KV bytes at {tokens} tokens x{} concurrent + {} \
465 activation headroom exceeds the {} byte device budget",
466 self.weights_bytes,
467 self.shape.kv_bytes_for_tokens(tokens) * self.concurrent_requests.max(1) as u64,
468 self.concurrent_requests.max(1),
469 self.activation_headroom_bytes,
470 self.device_budget_bytes,
471 ),
472 })
473 }
474
475 pub fn max_context(&self, cap: usize, granularity: usize) -> ContextFit {
484 let granularity = granularity.max(1);
485 let concurrency = self.concurrent_requests.max(1) as u64;
486 let available = self.kv_bytes_available();
487 let marginal = self.shape.marginal_per_token_bytes() * concurrency;
488
489 let saturated_sliding = {
493 let mut shape = self.shape;
494 shape.n_layers = shape.sliding_layers();
495 match shape.sliding {
496 None => 0,
497 Some(w) => {
498 shape.n_layers as u64
499 * shape.elem.bytes_for(
500 shape.layout.elems_per_token_per_layer()
501 * w.resident_positions(cap) as u64,
502 )
503 * concurrency
504 }
505 }
506 };
507 let for_full_layers = available.saturating_sub(saturated_sliding);
508
509 let (tokens, capped_by) = if available == 0 || for_full_layers == 0 && marginal > 0 {
510 (0, ContextCap::DeviceBudget)
511 } else {
512 match for_full_layers.checked_div(marginal) {
520 None => (cap, ContextCap::ModelContextLength),
521 Some(raw) => {
522 let raw = raw as usize;
523 let floored = if raw >= granularity {
528 (raw / granularity) * granularity
529 } else {
530 raw
531 };
532 if floored >= cap {
533 (cap, ContextCap::ModelContextLength)
534 } else {
535 (floored, ContextCap::DeviceBudget)
536 }
537 }
538 }
539 };
540
541 ContextFit {
542 tokens,
543 cap,
544 granularity,
545 capped_by,
546 kv_available_bytes: available,
547 marginal_per_token_bytes: self.shape.marginal_per_token_bytes(),
548 concurrent_requests: concurrency as usize,
549 kv_bytes: self.shape.kv_bytes_for_tokens(tokens) * concurrency,
550 weights_bytes: self.weights_bytes,
551 activation_headroom_bytes: self.activation_headroom_bytes,
552 device_budget_bytes: self.device_budget_bytes,
553 }
554 }
555}
556
557#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub enum ContextCap {
560 ModelContextLength,
562 DeviceBudget,
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Eq)]
569pub struct ContextFit {
570 pub tokens: usize,
571 pub cap: usize,
572 pub granularity: usize,
573 pub capped_by: ContextCap,
574 pub kv_available_bytes: u64,
575 pub marginal_per_token_bytes: u64,
576 pub concurrent_requests: usize,
577 pub kv_bytes: u64,
578 pub weights_bytes: u64,
579 pub activation_headroom_bytes: u64,
580 pub device_budget_bytes: u64,
581}
582
583impl std::fmt::Display for ContextFit {
584 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585 write!(
586 f,
587 "ctx auto = {} tokens ({}): ({} device budget - {} weights - {} activation headroom) \
588 = {} for KV; / {} bytes/token/request / {} request(s) -> rounded down to a multiple \
589 of {} (reported exactly below one step), capped at the model's {} trained context. \
590 KV at the chosen context: {} bytes.",
591 self.tokens,
592 match self.capped_by {
593 ContextCap::ModelContextLength => "limited by the model's context length",
594 ContextCap::DeviceBudget => "limited by the device memory budget",
595 },
596 self.device_budget_bytes,
597 self.weights_bytes,
598 self.activation_headroom_bytes,
599 self.kv_available_bytes,
600 self.marginal_per_token_bytes,
601 self.concurrent_requests,
602 self.granularity,
603 self.cap,
604 self.kv_bytes,
605 )
606 }
607}
608
609pub const CTX_AUTO_GRANULARITY: usize = 256;
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 fn llama31_8b() -> KvShape {
623 KvShape {
624 n_layers: 32,
625 layout: KvLayout::Gqa {
626 n_kv_heads: 8,
627 head_dim: 128,
628 },
629 elem: KvElem::F32,
630 sliding: None,
631 }
632 }
633
634 #[test]
635 fn gqa_per_token_kv_matches_the_hand_computed_byte_count() {
636 let shape = llama31_8b();
637 assert_eq!(shape.layout.elems_per_token_per_layer(), 2 * 8 * 128);
638 assert_eq!(shape.per_token_kv_bytes(), 32 * 2 * 8 * 128 * 4);
639 assert_eq!(shape.per_token_kv_bytes(), 262_144);
640 assert_eq!(
643 KvShape {
644 elem: KvElem::F16,
645 ..shape
646 }
647 .per_token_kv_bytes(),
648 131_072
649 );
650 assert_eq!(
651 KvShape {
652 elem: KvElem::Q8_0,
653 ..shape
654 }
655 .per_token_kv_bytes(),
656 32 * (2 * 8 * 128 / 32) * 34
657 );
658 assert_eq!(
659 KvShape {
660 elem: KvElem::Turbo4,
661 ..shape
662 }
663 .per_token_kv_bytes(),
664 32 * (2 * 8 * 128 / 32) * 18
665 );
666 }
667
668 #[test]
669 fn ctk_names_map_onto_the_widths_metal_really_writes() {
670 assert_eq!(KvElem::from_ctk("f16"), KvElem::F16);
671 assert_eq!(KvElem::from_ctk("f32"), KvElem::F32);
672 assert_eq!(KvElem::from_ctk("Q8_0"), KvElem::Q8_0);
673 assert_eq!(KvElem::from_ctk("turbo8"), KvElem::Q8_0);
675 assert_eq!(KvElem::from_ctk("fp8"), KvElem::Q8_0);
676 assert_eq!(KvElem::from_ctk("turbo4"), KvElem::Turbo4);
677 assert_eq!(KvElem::from_ctk("turbo3"), KvElem::F16);
679 assert_eq!(KvElem::from_ctk(" nonsense "), KvElem::F16);
680 }
681
682 #[test]
683 fn mha_costs_exactly_the_gqa_ratio_more_than_gqa() {
684 let gqa = llama31_8b();
687 let mha = KvShape {
688 layout: KvLayout::Gqa {
689 n_kv_heads: 32,
690 head_dim: 128,
691 },
692 ..gqa
693 };
694 assert_eq!(mha.per_token_kv_bytes(), 4 * gqa.per_token_kv_bytes());
695 assert_eq!(mha.per_token_kv_bytes(), 32 * 2 * 32 * 128 * 4);
696 }
697
698 #[test]
699 fn sliding_window_layers_saturate_and_full_layers_do_not() {
700 let shape = KvShape {
704 n_layers: 32,
705 layout: KvLayout::Gqa {
706 n_kv_heads: 8,
707 head_dim: 128,
708 },
709 elem: KvElem::F16,
710 sliding: Some(SlidingWindow {
711 window: 4096,
712 chunk: 1,
713 pattern: None,
714 }),
715 };
716 assert_eq!(shape.sliding_layers(), 32);
717 assert_eq!(shape.full_attention_layers(), 0);
718 assert_eq!(
720 shape.kv_bytes_for_tokens(1024),
721 shape.per_token_kv_bytes() * 1024
722 );
723 let at_window = shape.kv_bytes_for_tokens(4096);
725 assert_eq!(shape.kv_bytes_for_tokens(32_768), at_window);
726 assert_eq!(shape.kv_bytes_for_tokens(1_000_000), at_window);
727 assert_eq!(shape.marginal_per_token_bytes(), 0);
729 }
730
731 #[test]
732 fn chunked_prefill_widens_the_sliding_cap_by_chunk_minus_one() {
733 let base = SlidingWindow {
734 window: 512,
735 chunk: 1,
736 pattern: None,
737 };
738 assert_eq!(base.resident_positions(100_000), 512);
739 let chunked = SlidingWindow { chunk: 256, ..base };
740 assert_eq!(chunked.resident_positions(100_000), 512 + 256 - 1);
743 assert_eq!(chunked.resident_positions(300), 300);
744 }
745
746 #[test]
747 fn gemma_alternating_pattern_leaves_every_sixth_layer_full_attention() {
748 let shape = KvShape {
751 n_layers: 30,
752 layout: KvLayout::Gqa {
753 n_kv_heads: 4,
754 head_dim: 256,
755 },
756 elem: KvElem::F16,
757 sliding: Some(SlidingWindow {
758 window: 1024,
759 chunk: 1,
760 pattern: Some(6),
761 }),
762 };
763 assert_eq!(shape.full_attention_layers(), 5);
764 assert_eq!(shape.sliding_layers(), 25);
765 let mut cfg = crate::config::test_dense_fixture();
768 cfg.n_layers = 30;
769 cfg.sliding_window = Some(1024);
770 cfg.swa_pattern = Some(6);
771 let per_layer_full = (0..30)
772 .filter(|&il| cfg.layer_sliding_window(il).is_none())
773 .count();
774 assert_eq!(per_layer_full, shape.full_attention_layers());
775
776 let per_layer_token = shape.elem.bytes_for(2 * 4 * 256);
778 assert_eq!(shape.marginal_per_token_bytes(), 5 * per_layer_token);
779 assert_eq!(
782 shape.kv_bytes_for_tokens(8192),
783 5 * shape.elem.bytes_for(2 * 4 * 256 * 8192)
784 + 25 * shape.elem.bytes_for(2 * 4 * 256 * 1024)
785 );
786 }
787
788 #[test]
789 fn mla_latent_is_one_vector_and_far_cheaper_than_the_expanded_form() {
790 let latent = KvShape {
794 n_layers: 60,
795 layout: KvLayout::MlaLatent {
796 kv_lora_rank: 512,
797 qk_rope_head_dim: 64,
798 },
799 elem: KvElem::F32,
800 sliding: None,
801 };
802 assert_eq!(latent.layout.elems_per_token_per_layer(), 576);
805 assert_eq!(latent.per_token_kv_bytes(), 60 * 576 * 4);
806
807 let expanded = KvShape::mla_expanded(60, 128, 128, 64, 128, KvElem::F32);
808 assert_eq!(
810 expanded.layout.elems_per_token_per_layer(),
811 128 * (192 + 128)
812 );
813 assert_eq!(expanded.per_token_kv_bytes(), 60 * 40_960 * 4);
814 assert!(expanded.per_token_kv_bytes() / latent.per_token_kv_bytes() > 70);
817
818 let gqa = KvShape {
820 layout: KvLayout::Gqa {
821 n_kv_heads: 128,
822 head_dim: 128,
823 },
824 ..latent
825 };
826 assert_eq!(gqa.per_token_kv_bytes(), 60 * 2 * 128 * 128 * 4);
827 }
828
829 #[test]
830 fn from_config_reads_layers_heads_and_the_sliding_window() {
831 let mut cfg = crate::config::test_dense_fixture();
832 cfg.n_layers = 12;
833 cfg.n_kv_heads = 2;
834 cfg.head_dim = 64;
835 cfg.sliding_window = None;
836 let shape = KvShape::from_config(&cfg, KvElem::F32, 1);
837 assert_eq!(shape.n_layers, 12);
838 assert_eq!(shape.per_token_kv_bytes(), 12 * 2 * 2 * 64 * 4);
839 assert!(shape.sliding.is_none());
840
841 cfg.sliding_window = Some(256);
842 cfg.swa_pattern = None;
843 let swa = KvShape::from_config(&cfg, KvElem::F32, 64);
844 assert_eq!(
845 swa.sliding,
846 Some(SlidingWindow {
847 window: 256,
848 chunk: 64,
849 pattern: None
850 })
851 );
852 assert_eq!(swa.sliding_layers(), 12);
853 }
854
855 fn budget(weights: u64, device: u64, shape: KvShape) -> KvBudget {
856 KvBudget {
857 weights_bytes: weights,
858 activation_headroom_bytes: 0,
859 device_budget_bytes: device,
860 shape,
861 concurrent_requests: 1,
862 }
863 }
864
865 #[test]
866 fn check_accepts_a_fitting_context_and_names_the_binding_ceiling_otherwise() {
867 let shape = llama31_8b(); let b = budget(1_000_000, 1_000_000 + 262_144 * 10, shape);
869 assert_eq!(b.check(10).unwrap(), 1_000_000 + 262_144 * 10);
870 let err = b.check(11).expect_err("one token past the budget");
871 assert_eq!(err.binding, Ceiling::DeviceMemory);
872 assert_eq!(err.code(), "device_memory_budget_exceeded");
873 assert_eq!(err.estimated_bytes, 1_000_000 + 262_144 * 11);
874 assert_eq!(err.limit_bytes, 1_000_000 + 262_144 * 10);
875 assert_eq!(err.overage_bytes(), 262_144);
876 }
877
878 #[test]
879 fn concurrency_multiplies_kv_but_not_weights() {
880 let shape = llama31_8b();
881 let one = budget(1_000, 1 << 40, shape);
882 let four = KvBudget {
883 concurrent_requests: 4,
884 ..one
885 };
886 assert_eq!(
887 four.estimated_bytes(100) - 1_000,
888 4 * (one.estimated_bytes(100) - 1_000)
889 );
890 }
891
892 #[test]
893 fn max_context_is_the_closed_form_division_floored_to_granularity() {
894 let shape = llama31_8b(); let b = budget(5_000_000, 5_000_000 + 262_144 * 1000, shape);
897 let fit = b.max_context(131_072, 256);
898 assert_eq!(fit.capped_by, ContextCap::DeviceBudget);
899 assert_eq!(fit.tokens, 768);
901 assert_eq!(fit.kv_available_bytes, 262_144 * 1000);
902 assert_eq!(fit.marginal_per_token_bytes, 262_144);
903 assert!(b.check(fit.tokens).is_ok());
905 assert!(b.check(fit.tokens + 256).is_err());
907 }
908
909 #[test]
910 fn max_context_clamps_to_the_models_trained_context_when_memory_is_plentiful() {
911 let b = budget(1_000, 1 << 40, llama31_8b());
912 let fit = b.max_context(8192, 256);
913 assert_eq!(fit.tokens, 8192);
914 assert_eq!(fit.capped_by, ContextCap::ModelContextLength);
915 }
916
917 #[test]
922 fn a_context_under_one_granularity_step_is_reported_exactly_not_floored_away() {
923 let shape = llama31_8b(); let b = budget(1_000, 1_000 + 262_144 * 100, shape);
925 let fit = b.max_context(131_072, 256);
926 assert_eq!(fit.tokens, 100);
927 assert_eq!(fit.capped_by, ContextCap::DeviceBudget);
928 assert!(b.check(fit.tokens).is_ok());
929 assert!(b.check(fit.tokens + 1).is_err());
930 }
931
932 #[test]
933 fn max_context_is_zero_when_the_weights_alone_do_not_fit() {
934 let b = budget(10_000_000, 1_000_000, llama31_8b());
935 let fit = b.max_context(8192, 256);
936 assert_eq!(fit.tokens, 0);
937 assert_eq!(fit.capped_by, ContextCap::DeviceBudget);
938 assert_eq!(fit.kv_available_bytes, 0);
939 assert!(b.check(0).is_err(), "weights alone already overflow");
940 }
941
942 #[test]
943 fn an_all_sliding_model_is_limited_only_by_its_context_length() {
944 let shape = KvShape {
945 n_layers: 32,
946 layout: KvLayout::Gqa {
947 n_kv_heads: 8,
948 head_dim: 128,
949 },
950 elem: KvElem::F16,
951 sliding: Some(SlidingWindow {
952 window: 4096,
953 chunk: 1,
954 pattern: None,
955 }),
956 };
957 let saturated = shape.kv_bytes_for_tokens(4096);
959 let b = budget(1_000, 1_000 + saturated * 2, shape);
960 let fit = b.max_context(1_000_000, 256);
961 assert_eq!(fit.capped_by, ContextCap::ModelContextLength);
962 assert_eq!(fit.tokens, 1_000_000);
963 assert!(b.check(fit.tokens).is_ok());
964 }
965
966 #[test]
967 fn a_mixed_swa_model_prices_the_saturated_sliding_layers_before_dividing() {
968 let shape = KvShape {
970 n_layers: 6,
971 layout: KvLayout::Gqa {
972 n_kv_heads: 1,
973 head_dim: 16,
974 },
975 elem: KvElem::F32,
976 sliding: Some(SlidingWindow {
977 window: 128,
978 chunk: 1,
979 pattern: Some(3),
980 }),
981 };
982 assert_eq!(shape.full_attention_layers(), 2);
983 let per_layer_token = 2 * 16 * 4;
985 let sliding_saturated = 4 * per_layer_token * 128;
986 let full_marginal = 2 * per_layer_token;
987 let b = budget(0, (sliding_saturated + full_marginal * 512) as u64, shape);
990 let fit = b.max_context(4096, 256);
991 assert_eq!(fit.tokens, 512);
992 assert_eq!(fit.capped_by, ContextCap::DeviceBudget);
993 assert!(b.check(512).is_ok());
994 }
995
996 #[test]
997 fn ctx_auto_explanation_names_every_term_it_divided() {
998 let b = budget(5_000_000, 5_000_000 + 262_144 * 1000, llama31_8b());
999 let text = b.max_context(131_072, CTX_AUTO_GRANULARITY).to_string();
1000 assert!(text.contains("ctx auto = 768 tokens"), "{text}");
1001 assert!(text.contains("262144"), "per-token divisor missing: {text}");
1002 assert!(text.contains("5000000"), "weights term missing: {text}");
1003 assert!(text.contains("131072"), "model cap missing: {text}");
1004 }
1005}