1use ferrum_types::{RuntimeConfigEntry, RuntimeConfigSnapshot, RuntimeConfigSource};
14use std::path::Path;
15
16const SCRATCH_RESERVE_BYTES: u64 = 4 * 1024 * 1024 * 1024;
21
22const PAGED_BLOCK_SIZE: u64 = 16;
25const DEFAULT_MAX_BATCHED_TOKENS: usize = 2048;
26const TIGHT_RECURRENT_STATE_MAX_BATCHED_TOKENS: usize = 192;
31const TIGHT_RECURRENT_STATE_KV_BLOCK_FLOOR: usize = 256;
32
33const KV_DTYPE_BYTES: u64 = 2;
36
37#[derive(Debug)]
38pub struct AutoSizeResult {
39 pub total_gpu_bytes: u64,
40 pub free_gpu_bytes: u64,
41 pub weight_bytes: u64,
42 pub budgeted_weight_bytes: u64,
43 pub weight_budget_shards: u64,
44 pub budgeted_layer_count: u64,
45 pub kv_block_bytes: u64,
46 pub kv_pool_copies: u64,
47 pub estimated_budget_blocks: usize,
48 pub requested_min_blocks: usize,
49 pub max_blocks: usize,
50 pub reserved_for_scratch: u64,
51}
52
53impl AutoSizeResult {
54 pub fn print_summary(&self) {
55 let gb = |b: u64| (b as f64) / 1024.0 / 1024.0 / 1024.0;
56 eprintln!(
57 "[auto-size] gpu={:.1} GB total / {:.1} GB free | weights={:.1} GB budget / {:.1} GB total | layers={} budget | scratch reserve={:.1} GB | KV pool budget {:.1} GB → max_blocks={}",
58 gb(self.total_gpu_bytes),
59 gb(self.free_gpu_bytes),
60 gb(self.budgeted_weight_bytes),
61 gb(self.weight_bytes),
62 self.budgeted_layer_count,
63 gb(self.reserved_for_scratch),
64 gb((self.max_blocks as u64) * self.kv_block_bytes * self.kv_pool_copies),
65 self.max_blocks,
66 );
67 if self.weight_budget_shards > 1 {
68 eprintln!(
69 "[auto-size] weight budget shards={} (distributed strategy)",
70 self.weight_budget_shards
71 );
72 }
73 if self.kv_pool_copies > 1 {
74 eprintln!(
75 "[auto-size] KV pool copies={} (FA-compatible attention path)",
76 self.kv_pool_copies
77 );
78 }
79 if self.requested_min_blocks > self.estimated_budget_blocks {
80 eprintln!(
81 "[auto-size] requested runtime token floor requires KV_MAX_BLOCKS={} above estimated budget {}; honoring explicit runtime limits",
82 self.requested_min_blocks, self.estimated_budget_blocks
83 );
84 }
85 }
86}
87
88pub fn auto_size_kv_blocks(model_dir: &Path, gpu_util: f32) -> Option<AutoSizeResult> {
93 auto_size_kv_blocks_with_pool_copies(model_dir, gpu_util, 1)
94}
95
96pub fn auto_size_kv_blocks_with_pool_copies(
97 model_dir: &Path,
98 gpu_util: f32,
99 kv_pool_copies: u64,
100) -> Option<AutoSizeResult> {
101 let current = RuntimeConfigSnapshot::capture_current();
102 auto_size_kv_blocks_with_pool_copies_for_snapshot(model_dir, gpu_util, kv_pool_copies, ¤t)
103}
104
105fn auto_size_kv_blocks_with_pool_copies_for_snapshot(
106 model_dir: &Path,
107 gpu_util: f32,
108 kv_pool_copies: u64,
109 runtime_config: &RuntimeConfigSnapshot,
110) -> Option<AutoSizeResult> {
111 let gpu_util = gpu_util.clamp(0.1, 1.0);
112 let kv_pool_copies = kv_pool_copies.max(1);
113
114 let nvsmi = std::process::Command::new("nvidia-smi")
117 .args([
118 "--query-gpu=memory.total,memory.free",
119 "--format=csv,noheader,nounits",
120 ])
121 .output()
122 .ok()?;
123 if !nvsmi.status.success() {
124 return None;
125 }
126 let s = String::from_utf8(nvsmi.stdout).ok()?;
127 let line = s.lines().next()?.trim();
128 let parts: Vec<&str> = line.split(',').map(str::trim).collect();
129 let total_mb: u64 = parts.first()?.parse().ok()?;
130 let free_mb: u64 = parts.get(1)?.parse().ok()?;
131 let total_bytes = total_mb * 1024 * 1024;
132 let free_bytes = free_mb * 1024 * 1024;
133
134 let config_path = model_dir.join("config.json");
136 let config: serde_json::Value =
137 serde_json::from_str(&std::fs::read_to_string(&config_path).ok()?).ok()?;
138 let num_layers = config_or_text_u64(&config, "num_hidden_layers")
139 .or_else(|| config_or_text_u64(&config, "num_layers"))?;
140 let hidden_size = config_or_text_u64(&config, "hidden_size")?;
141 let num_attn_heads = config_or_text_u64(&config, "num_attention_heads")?;
142 let num_kv_heads = config_or_text_u64(&config, "num_key_value_heads").unwrap_or(num_attn_heads);
143 let head_dim = config_or_text_u64(&config, "head_dim")
144 .unwrap_or_else(|| hidden_size / num_attn_heads.max(1));
145
146 let mut weight_bytes: u64 = 0;
148 if let Ok(entries) = std::fs::read_dir(model_dir) {
149 for entry in entries.flatten() {
150 let p = entry.path();
151 let is_weight = p
152 .extension()
153 .and_then(|s| s.to_str())
154 .map(|ext| ext == "safetensors" || ext == "bin")
155 .unwrap_or(false);
156 if is_weight {
157 if let Ok(meta) = std::fs::metadata(&p) {
161 weight_bytes += meta.len();
162 }
163 }
164 }
165 }
166 if weight_bytes == 0 {
167 return None;
169 }
170 let weight_budget_shards = weight_budget_shard_count(runtime_config);
171 let budgeted_weight_bytes = ceil_div_u64(weight_bytes, weight_budget_shards);
172 let budgeted_layer_count = layer_count_for_memory_budget(num_layers, runtime_config);
173
174 let target_used = (total_bytes as f64 * gpu_util as f64) as u64;
179 let avail_for_kv = target_used
180 .saturating_sub(budgeted_weight_bytes)
181 .saturating_sub(SCRATCH_RESERVE_BYTES);
182
183 let block_bytes =
186 budgeted_layer_count * num_kv_heads * PAGED_BLOCK_SIZE * head_dim * 2 * KV_DTYPE_BYTES;
187 if block_bytes == 0 {
188 return None;
189 }
190 let estimated_budget_blocks = (avail_for_kv / (block_bytes * kv_pool_copies)) as usize;
191 let requested_min_blocks = requested_min_kv_blocks_from_snapshot(runtime_config);
192 let max_blocks = estimated_budget_blocks.max(requested_min_blocks);
193
194 Some(AutoSizeResult {
195 total_gpu_bytes: total_bytes,
196 free_gpu_bytes: free_bytes,
197 weight_bytes,
198 budgeted_weight_bytes,
199 weight_budget_shards,
200 budgeted_layer_count,
201 kv_block_bytes: block_bytes,
202 kv_pool_copies,
203 estimated_budget_blocks,
204 requested_min_blocks,
205 max_blocks,
206 reserved_for_scratch: SCRATCH_RESERVE_BYTES,
207 })
208}
209
210#[derive(Clone, Copy, Debug, PartialEq, Eq)]
212pub enum AutoSizeProfile {
213 Server,
217 Chat,
222}
223
224#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
225enum ModelAutoSizeClass {
226 #[default]
227 Generic,
228 TightRecurrentState,
229}
230
231#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
232struct ModelAutoSizeHints {
233 has_recurrent_linear_attention_state: bool,
234}
235
236#[derive(Clone, Copy, Debug)]
237struct ModelAutoSizeDefaults {
238 max_batched_tokens: usize,
239 max_sequences: usize,
240 max_sequence_tokens: usize,
241 kv_block_floor: usize,
242}
243
244pub fn apply_auto_size(model_dir: &Path, gpu_util: f32) {
257 apply_auto_size_with_profile(model_dir, gpu_util, AutoSizeProfile::Server);
258}
259
260pub fn apply_auto_size_with_profile(model_dir: &Path, gpu_util: f32, profile: AutoSizeProfile) {
265 let current = RuntimeConfigSnapshot::capture_current();
266 let entries = auto_size_runtime_entries(model_dir, gpu_util, profile, ¤t);
267 crate::runtime_env::materialize_runtime_env_defaults(&entries);
268}
269
270pub fn auto_size_runtime_entries(
273 model_dir: &Path,
274 gpu_util: f32,
275 profile: AutoSizeProfile,
276 current: &RuntimeConfigSnapshot,
277) -> Vec<RuntimeConfigEntry> {
278 let kv_overridden = snapshot_value(¤t, "FERRUM_KV_MAX_BLOCKS").is_some();
279 let max_seqs_overridden = snapshot_value(¤t, "FERRUM_PAGED_MAX_SEQS").is_some();
280 let max_batched_tokens_overridden =
281 snapshot_value(¤t, "FERRUM_MAX_BATCHED_TOKENS").is_some();
282 let model_hints = model_auto_size_hints(model_dir);
283 let mut entries = Vec::new();
284 if kv_overridden && max_seqs_overridden && max_batched_tokens_overridden {
286 return entries;
287 }
288 let kv_pool_copies = kv_pool_copies_from_snapshot(¤t);
289 let preliminary_result = auto_size_kv_blocks_with_pool_copies_for_snapshot(
290 model_dir,
291 gpu_util,
292 kv_pool_copies,
293 ¤t,
294 );
295 let model_class =
296 model_auto_size_class_from_hints_and_budget(model_hints, preliminary_result.as_ref());
297 let defaults = model_auto_size_defaults(model_class, profile);
298 if !max_batched_tokens_overridden {
302 let mbt = defaults.max_batched_tokens;
307 entries.push(RuntimeConfigEntry::new(
308 "FERRUM_MAX_BATCHED_TOKENS",
309 mbt.to_string(),
310 RuntimeConfigSource::MemoryProfile,
311 ));
312 eprintln!(
313 "[auto-size] MAX_BATCHED_TOKENS={} (profile={:?} model={:?})",
314 mbt, profile, model_class
315 );
316 }
317 if kv_overridden && max_seqs_overridden {
318 return entries;
319 }
320 let mut budget_snapshot = current.clone();
321 for entry in &entries {
322 budget_snapshot.upsert_entry(entry.clone());
323 }
324 let result = if entries.is_empty() {
325 preliminary_result
326 } else {
327 auto_size_kv_blocks_with_pool_copies_for_snapshot(
328 model_dir,
329 gpu_util,
330 kv_pool_copies,
331 &budget_snapshot,
332 )
333 };
334 let Some(result) = result else {
335 return entries;
336 };
337 result.print_summary();
338 let max_blocks = result.max_blocks.max(defaults.kv_block_floor);
339
340 let (max_seqs_clamped, kv_capacity) = select_dynamic_paged_pool_shape(
345 defaults.max_sequences,
346 defaults.max_sequence_tokens,
347 max_blocks,
348 );
349
350 let kv_capacity_overridden = snapshot_value(¤t, "FERRUM_KV_CAPACITY").is_some();
351 if !kv_overridden {
357 entries.push(RuntimeConfigEntry::new(
358 "FERRUM_KV_MAX_BLOCKS",
359 max_blocks.to_string(),
360 RuntimeConfigSource::MemoryProfile,
361 ));
362 }
363 if !max_seqs_overridden {
364 entries.push(RuntimeConfigEntry::new(
365 "FERRUM_PAGED_MAX_SEQS",
366 max_seqs_clamped.to_string(),
367 RuntimeConfigSource::MemoryProfile,
368 ));
369 }
370 if kv_capacity > 0 && !kv_capacity_overridden {
371 entries.push(RuntimeConfigEntry::new(
372 "FERRUM_KV_CAPACITY",
373 kv_capacity.to_string(),
374 RuntimeConfigSource::MemoryProfile,
375 ));
376 }
377 eprintln!(
378 "[auto-size] KV_MAX_BLOCKS={} PAGED_MAX_SEQS={} KV_CAPACITY={}",
379 if kv_overridden {
380 "<user>".to_string()
381 } else {
382 max_blocks.to_string()
383 },
384 if max_seqs_overridden {
385 "<user>".to_string()
386 } else {
387 max_seqs_clamped.to_string()
388 },
389 if kv_capacity_overridden {
390 "<user>".to_string()
391 } else if kv_capacity > 0 {
392 kv_capacity.to_string()
393 } else {
394 "<default>".to_string()
395 },
396 );
397 entries
398}
399
400const MAX_AUTOSIZED_SEQUENCE_TOKENS: usize = 16_384;
401const DEFAULT_SERVER_MAX_SEQUENCES: usize = 32;
402const TIGHT_RECURRENT_STATE_SERVER_MAX_SEQUENCES: usize = 16;
403const CHAT_MAX_SEQUENCES: usize = 2;
404
405fn model_auto_size_defaults(
406 model_class: ModelAutoSizeClass,
407 profile: AutoSizeProfile,
408) -> ModelAutoSizeDefaults {
409 match (model_class, profile) {
410 (ModelAutoSizeClass::TightRecurrentState, AutoSizeProfile::Server) => {
411 ModelAutoSizeDefaults {
412 max_batched_tokens: TIGHT_RECURRENT_STATE_MAX_BATCHED_TOKENS,
413 max_sequences: TIGHT_RECURRENT_STATE_SERVER_MAX_SEQUENCES,
414 max_sequence_tokens: MAX_AUTOSIZED_SEQUENCE_TOKENS,
415 kv_block_floor: TIGHT_RECURRENT_STATE_KV_BLOCK_FLOOR,
416 }
417 }
418 (ModelAutoSizeClass::TightRecurrentState, AutoSizeProfile::Chat) => ModelAutoSizeDefaults {
419 max_batched_tokens: TIGHT_RECURRENT_STATE_MAX_BATCHED_TOKENS,
420 max_sequences: CHAT_MAX_SEQUENCES,
421 max_sequence_tokens: MAX_AUTOSIZED_SEQUENCE_TOKENS,
422 kv_block_floor: TIGHT_RECURRENT_STATE_KV_BLOCK_FLOOR,
423 },
424 (ModelAutoSizeClass::Generic, AutoSizeProfile::Server) => ModelAutoSizeDefaults {
425 max_batched_tokens: DEFAULT_MAX_BATCHED_TOKENS,
426 max_sequences: DEFAULT_SERVER_MAX_SEQUENCES,
427 max_sequence_tokens: MAX_AUTOSIZED_SEQUENCE_TOKENS,
428 kv_block_floor: 0,
429 },
430 (ModelAutoSizeClass::Generic, AutoSizeProfile::Chat) => ModelAutoSizeDefaults {
431 max_batched_tokens: DEFAULT_MAX_BATCHED_TOKENS,
432 max_sequences: CHAT_MAX_SEQUENCES,
433 max_sequence_tokens: MAX_AUTOSIZED_SEQUENCE_TOKENS,
434 kv_block_floor: 0,
435 },
436 }
437}
438
439fn model_auto_size_hints(model_dir: &Path) -> ModelAutoSizeHints {
440 let Ok(config_text) = std::fs::read_to_string(model_dir.join("config.json")) else {
441 return ModelAutoSizeHints::default();
442 };
443 let Ok(config) = serde_json::from_str::<serde_json::Value>(&config_text) else {
444 return ModelAutoSizeHints::default();
445 };
446 model_auto_size_hints_from_config(&config)
447}
448
449fn model_auto_size_hints_from_config(config: &serde_json::Value) -> ModelAutoSizeHints {
450 ModelAutoSizeHints {
451 has_recurrent_linear_attention_state: has_recurrent_linear_attention_state(config),
452 }
453}
454
455fn model_auto_size_class_from_hints_and_budget(
456 hints: ModelAutoSizeHints,
457 budget: Option<&AutoSizeResult>,
458) -> ModelAutoSizeClass {
459 if !hints.has_recurrent_linear_attention_state {
460 return ModelAutoSizeClass::Generic;
461 }
462 let Some(budget) = budget else {
463 return ModelAutoSizeClass::Generic;
464 };
465 let generic_prefill_blocks =
466 ceil_div_usize(DEFAULT_MAX_BATCHED_TOKENS, PAGED_BLOCK_SIZE as usize);
467 if budget.estimated_budget_blocks < generic_prefill_blocks {
468 ModelAutoSizeClass::TightRecurrentState
469 } else {
470 ModelAutoSizeClass::Generic
471 }
472}
473
474fn has_recurrent_linear_attention_state(config: &serde_json::Value) -> bool {
475 let text = config.get("text_config").unwrap_or(config);
476 let has_linear_layers = text
477 .get("layer_types")
478 .and_then(|value| value.as_array())
479 .is_some_and(|layers| {
480 layers.iter().any(|layer| {
481 layer
482 .as_str()
483 .is_some_and(|name| name.eq_ignore_ascii_case("linear_attention"))
484 })
485 });
486 let has_linear_state_dims = [
487 "linear_conv_kernel_dim",
488 "linear_key_head_dim",
489 "linear_num_key_heads",
490 "linear_num_value_heads",
491 "linear_value_head_dim",
492 ]
493 .iter()
494 .all(|key| text.get(*key).and_then(|value| value.as_u64()).is_some());
495 has_linear_layers && has_linear_state_dims
496}
497
498fn config_or_text_u64(config: &serde_json::Value, key: &str) -> Option<u64> {
499 config
500 .get(key)
501 .and_then(|value| value.as_u64())
502 .or_else(|| {
503 config
504 .get("text_config")
505 .and_then(|text| text.get(key))
506 .and_then(|value| value.as_u64())
507 })
508}
509
510fn ceil_div_u64(value: u64, divisor: u64) -> u64 {
511 if divisor == 0 {
512 return value;
513 }
514 value.div_ceil(divisor)
515}
516
517fn ceil_div_usize(value: usize, divisor: usize) -> usize {
518 if divisor == 0 {
519 return value;
520 }
521 value.div_ceil(divisor)
522}
523
524fn select_dynamic_paged_pool_shape(
525 max_sequences: usize,
526 max_sequence_tokens: usize,
527 max_blocks: usize,
528) -> (usize, usize) {
529 let physical_blocks = max_blocks.max(1);
530 let sequences = max_sequences.max(1).min(physical_blocks);
531 let physical_tokens = physical_blocks.saturating_mul(PAGED_BLOCK_SIZE as usize);
532 let sequence_tokens = max_sequence_tokens
533 .max(PAGED_BLOCK_SIZE as usize)
534 .min(physical_tokens);
535 (sequences, sequence_tokens)
536}
537
538fn weight_budget_shard_count(snapshot: &RuntimeConfigSnapshot) -> u64 {
539 match snapshot_value(
540 snapshot,
541 crate::gpu_devices::SELECTED_DISTRIBUTED_STRATEGY_KEY,
542 ) {
543 Some("layer_split") => selected_gpu_device_count(snapshot).max(1) as u64,
544 _ => 1,
548 }
549}
550
551fn layer_count_for_memory_budget(num_layers: u64, snapshot: &RuntimeConfigSnapshot) -> u64 {
552 match snapshot_value(
553 snapshot,
554 crate::gpu_devices::SELECTED_DISTRIBUTED_STRATEGY_KEY,
555 ) {
556 Some("layer_split") => {
557 let shards = selected_gpu_device_count(snapshot).max(1) as u64;
558 ceil_div_u64(num_layers, shards).max(1)
559 }
560 _ => num_layers,
561 }
562}
563
564fn selected_gpu_device_count(snapshot: &RuntimeConfigSnapshot) -> usize {
565 snapshot_value(snapshot, crate::gpu_devices::SELECTED_GPU_DEVICES_KEY)
566 .map(|value| {
567 value
568 .split(',')
569 .filter(|part| !part.trim().is_empty())
570 .count()
571 })
572 .unwrap_or(1)
573}
574
575fn requested_min_kv_blocks_from_snapshot(snapshot: &RuntimeConfigSnapshot) -> usize {
576 let max_model_len_blocks = snapshot_usize(snapshot, "FERRUM_MAX_MODEL_LEN")
577 .map(|value| ceil_div_usize(value, PAGED_BLOCK_SIZE as usize))
578 .unwrap_or(0);
579 let max_batched_token_blocks = snapshot_usize(snapshot, "FERRUM_MAX_BATCHED_TOKENS")
580 .map(|value| ceil_div_usize(value, PAGED_BLOCK_SIZE as usize))
581 .unwrap_or(0);
582
583 max_model_len_blocks.max(max_batched_token_blocks)
584}
585
586fn snapshot_value<'a>(snapshot: &'a RuntimeConfigSnapshot, key: &str) -> Option<&'a str> {
587 snapshot
588 .entries
589 .iter()
590 .find(|entry| entry.key == key)
591 .map(|entry| entry.effective_value.as_str())
592}
593
594fn snapshot_usize(snapshot: &RuntimeConfigSnapshot, key: &str) -> Option<usize> {
595 snapshot_value(snapshot, key).and_then(|value| value.parse::<usize>().ok())
596}
597
598fn snapshot_bool(snapshot: &RuntimeConfigSnapshot, key: &str) -> Option<bool> {
599 snapshot_value(snapshot, key).map(|value| matches!(value, "1" | "true" | "TRUE" | "on" | "ON"))
600}
601
602fn kv_pool_copies_from_snapshot(snapshot: &RuntimeConfigSnapshot) -> u64 {
603 let fa_layout = snapshot_bool(snapshot, "FERRUM_FA_LAYOUT_VARLEN").unwrap_or(false);
604 let fa2_source = snapshot_bool(snapshot, "FERRUM_FA2_SOURCE").unwrap_or(false);
605 let fa2_direct_ffi = snapshot_bool(snapshot, "FERRUM_FA2_DIRECT_FFI")
606 .unwrap_or_else(|| snapshot_value(snapshot, "FERRUM_FA2_DIRECT_FFI_SHIM").is_some());
607
608 if fa_layout || fa2_source || fa2_direct_ffi {
609 2
610 } else {
611 1
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 fn snapshot(vars: &[(&str, &str)]) -> RuntimeConfigSnapshot {
620 RuntimeConfigSnapshot::from_env_vars(vars.iter().copied())
621 }
622
623 fn budget_with_estimated_blocks(estimated_budget_blocks: usize) -> AutoSizeResult {
624 AutoSizeResult {
625 total_gpu_bytes: 24 * 1024 * 1024 * 1024,
626 free_gpu_bytes: 20 * 1024 * 1024 * 1024,
627 weight_bytes: 18 * 1024 * 1024 * 1024,
628 budgeted_weight_bytes: 18 * 1024 * 1024 * 1024,
629 weight_budget_shards: 1,
630 budgeted_layer_count: 40,
631 kv_block_bytes: 4 * 1024 * 1024,
632 kv_pool_copies: 1,
633 estimated_budget_blocks,
634 requested_min_blocks: 0,
635 max_blocks: estimated_budget_blocks,
636 reserved_for_scratch: SCRATCH_RESERVE_BYTES,
637 }
638 }
639
640 #[test]
641 fn fa_compatible_attention_paths_count_two_kv_pool_copies() {
642 assert_eq!(kv_pool_copies_from_snapshot(&snapshot(&[])), 1);
643 assert_eq!(
644 kv_pool_copies_from_snapshot(&snapshot(&[("FERRUM_FA_LAYOUT_VARLEN", "1")])),
645 2
646 );
647 assert_eq!(
648 kv_pool_copies_from_snapshot(&snapshot(&[("FERRUM_FA2_SOURCE", "1")])),
649 2
650 );
651 assert_eq!(
652 kv_pool_copies_from_snapshot(&snapshot(&[("FERRUM_FA2_DIRECT_FFI_SHIM", "/tmp/x.so")])),
653 2
654 );
655 assert_eq!(
656 kv_pool_copies_from_snapshot(&snapshot(&[
657 ("FERRUM_FA2_DIRECT_FFI", "0"),
658 ("FERRUM_FA2_DIRECT_FFI_SHIM", "/tmp/x.so"),
659 ])),
660 1
661 );
662 }
663
664 #[test]
665 fn layer_split_scopes_weight_and_layer_budget_to_selected_devices() {
666 let snapshot = snapshot(&[
667 (
668 crate::gpu_devices::SELECTED_DISTRIBUTED_STRATEGY_KEY,
669 "layer_split",
670 ),
671 (crate::gpu_devices::SELECTED_GPU_DEVICES_KEY, "0,1"),
672 ]);
673
674 assert_eq!(weight_budget_shard_count(&snapshot), 2);
675 assert_eq!(layer_count_for_memory_budget(80, &snapshot), 40);
676 assert_eq!(ceil_div_u64(37, weight_budget_shard_count(&snapshot)), 19);
677 }
678
679 #[test]
680 fn unknown_multi_gpu_strategy_keeps_single_device_budget_until_wired() {
681 let snapshot = snapshot(&[
682 (
683 crate::gpu_devices::SELECTED_DISTRIBUTED_STRATEGY_KEY,
684 "tensor_parallel",
685 ),
686 (crate::gpu_devices::SELECTED_GPU_DEVICES_KEY, "0,1"),
687 ]);
688
689 assert_eq!(weight_budget_shard_count(&snapshot), 1);
690 assert_eq!(layer_count_for_memory_budget(80, &snapshot), 80);
691 }
692
693 #[test]
694 fn requested_runtime_token_limits_define_kv_block_floor() {
695 let snapshot = snapshot(&[
696 ("FERRUM_MAX_MODEL_LEN", "8192"),
697 ("FERRUM_MAX_BATCHED_TOKENS", "1024"),
698 ("FERRUM_PAGED_MAX_SEQS", "8"),
699 ("FERRUM_KV_CAPACITY", "2048"),
700 ]);
701
702 assert_eq!(requested_min_kv_blocks_from_snapshot(&snapshot), 512);
703 }
704
705 #[test]
706 fn paged_pool_shape_decouples_admission_width_from_sequence_capacity() {
707 assert_eq!(
708 select_dynamic_paged_pool_shape(32, 16_384, 338),
709 (32, 5_408)
710 );
711 assert_eq!(
712 select_dynamic_paged_pool_shape(32, 16_384, 2_048),
713 (32, 16_384)
714 );
715 assert_eq!(select_dynamic_paged_pool_shape(32, 16_384, 8), (8, 128));
716 }
717
718 #[test]
719 fn recurrent_linear_attention_budget_pressure_selects_tight_memory_profile() {
720 let config = serde_json::json!({
721 "architectures": ["SyntheticRecurrentStateModel"],
722 "model_type": "synthetic_recurrent_state",
723 "text_config": {
724 "model_type": "synthetic_recurrent_state_text",
725 "layer_types": ["linear_attention", "full_attention"],
726 "linear_conv_kernel_dim": 4,
727 "mamba_ssm_dtype": "float32",
728 "linear_key_head_dim": 128,
729 "linear_num_key_heads": 16,
730 "linear_num_value_heads": 16,
731 "linear_value_head_dim": 128
732 }
733 });
734
735 let hints = model_auto_size_hints_from_config(&config);
736 assert!(hints.has_recurrent_linear_attention_state);
737 let class = model_auto_size_class_from_hints_and_budget(
738 hints,
739 Some(&budget_with_estimated_blocks(127)),
740 );
741 assert_eq!(class, ModelAutoSizeClass::TightRecurrentState);
742 let server = model_auto_size_defaults(class, AutoSizeProfile::Server);
743 assert_eq!(
744 server.max_batched_tokens,
745 TIGHT_RECURRENT_STATE_MAX_BATCHED_TOKENS
746 );
747 assert_eq!(
748 server
749 .max_batched_tokens
750 .div_ceil(PAGED_BLOCK_SIZE as usize),
751 12
752 );
753 assert!(
754 server
755 .max_batched_tokens
756 .div_ceil(PAGED_BLOCK_SIZE as usize)
757 <= server.kv_block_floor
758 );
759 assert_eq!(server.kv_block_floor, TIGHT_RECURRENT_STATE_KV_BLOCK_FLOOR);
760 assert_eq!(
761 select_dynamic_paged_pool_shape(
762 server.max_sequences,
763 server.max_sequence_tokens,
764 server.kv_block_floor,
765 ),
766 (16, 4096)
767 );
768
769 let chat = model_auto_size_defaults(class, AutoSizeProfile::Chat);
770 assert_eq!(
771 chat.max_batched_tokens,
772 TIGHT_RECURRENT_STATE_MAX_BATCHED_TOKENS
773 );
774 assert_eq!(chat.kv_block_floor, TIGHT_RECURRENT_STATE_KV_BLOCK_FLOOR);
775 assert_eq!(
776 select_dynamic_paged_pool_shape(
777 chat.max_sequences,
778 chat.max_sequence_tokens,
779 chat.kv_block_floor,
780 ),
781 (2, 4096)
782 );
783 }
784
785 #[test]
786 fn recurrent_linear_attention_memory_profile_requires_budget_pressure() {
787 let recurrent = serde_json::json!({
788 "model_type": "synthetic_recurrent_state",
789 "text_config": {
790 "layer_types": ["linear_attention", "full_attention"],
791 "linear_conv_kernel_dim": 4,
792 "mamba_ssm_dtype": "float32",
793 "linear_key_head_dim": 128,
794 "linear_num_key_heads": 16,
795 "linear_num_value_heads": 16,
796 "linear_value_head_dim": 128
797 }
798 });
799 let hints = model_auto_size_hints_from_config(&recurrent);
800 assert_eq!(
801 model_auto_size_class_from_hints_and_budget(
802 hints,
803 Some(&budget_with_estimated_blocks(128)),
804 ),
805 ModelAutoSizeClass::Generic
806 );
807 assert_eq!(
808 model_auto_size_class_from_hints_and_budget(hints, None),
809 ModelAutoSizeClass::Generic
810 );
811
812 let dense = serde_json::json!({
813 "model_type": "dense",
814 "text_config": {
815 "layer_types": ["full_attention", "full_attention"]
816 }
817 });
818 assert_eq!(
819 model_auto_size_class_from_hints_and_budget(
820 model_auto_size_hints_from_config(&dense),
821 Some(&budget_with_estimated_blocks(0)),
822 ),
823 ModelAutoSizeClass::Generic
824 );
825
826 let generic =
827 model_auto_size_defaults(ModelAutoSizeClass::Generic, AutoSizeProfile::Server);
828 assert_eq!(generic.max_batched_tokens, DEFAULT_MAX_BATCHED_TOKENS);
829 assert_eq!(generic.max_sequences, DEFAULT_SERVER_MAX_SEQUENCES);
830 assert_eq!(generic.max_sequence_tokens, MAX_AUTOSIZED_SEQUENCE_TOKENS);
831 assert_eq!(generic.kv_block_floor, 0);
832 }
833
834 #[test]
835 fn autosize_dimension_lookup_falls_back_to_text_config() {
836 let config = serde_json::json!({
837 "model_type": "synthetic_text_wrapped_model",
838 "text_config": {
839 "hidden_size": 2048,
840 "num_hidden_layers": 40,
841 "num_attention_heads": 16,
842 "num_key_value_heads": 2,
843 "head_dim": 256
844 }
845 });
846
847 assert_eq!(config_or_text_u64(&config, "hidden_size"), Some(2048));
848 assert_eq!(config_or_text_u64(&config, "num_hidden_layers"), Some(40));
849 assert_eq!(config_or_text_u64(&config, "num_key_value_heads"), Some(2));
850 assert_eq!(config_or_text_u64(&config, "head_dim"), Some(256));
851 }
852}