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 kv_overridden = snapshot_value(¤t, "FERRUM_KV_MAX_BLOCKS").is_some();
267 let max_seqs_overridden = snapshot_value(¤t, "FERRUM_PAGED_MAX_SEQS").is_some();
268 let max_batched_tokens_overridden =
269 snapshot_value(¤t, "FERRUM_MAX_BATCHED_TOKENS").is_some();
270 let model_hints = model_auto_size_hints(model_dir);
271 let mut entries = Vec::new();
272 if kv_overridden && max_seqs_overridden && max_batched_tokens_overridden {
274 return;
275 }
276 let kv_pool_copies = kv_pool_copies_from_snapshot(¤t);
277 let preliminary_result = auto_size_kv_blocks_with_pool_copies_for_snapshot(
278 model_dir,
279 gpu_util,
280 kv_pool_copies,
281 ¤t,
282 );
283 let model_class =
284 model_auto_size_class_from_hints_and_budget(model_hints, preliminary_result.as_ref());
285 let defaults = model_auto_size_defaults(model_class, profile);
286 if !max_batched_tokens_overridden {
290 let mbt = defaults.max_batched_tokens;
295 entries.push(RuntimeConfigEntry::new(
296 "FERRUM_MAX_BATCHED_TOKENS",
297 mbt.to_string(),
298 RuntimeConfigSource::MemoryProfile,
299 ));
300 eprintln!(
301 "[auto-size] MAX_BATCHED_TOKENS={} (profile={:?} model={:?})",
302 mbt, profile, model_class
303 );
304 }
305 if kv_overridden && max_seqs_overridden {
306 crate::runtime_env::materialize_runtime_env_defaults(&entries);
307 return;
308 }
309 let mut budget_snapshot = current.clone();
310 for entry in &entries {
311 budget_snapshot.upsert_entry(entry.clone());
312 }
313 let result = if entries.is_empty() {
314 preliminary_result
315 } else {
316 auto_size_kv_blocks_with_pool_copies_for_snapshot(
317 model_dir,
318 gpu_util,
319 kv_pool_copies,
320 &budget_snapshot,
321 )
322 };
323 let Some(result) = result else {
324 crate::runtime_env::materialize_runtime_env_defaults(&entries);
325 return;
326 };
327 result.print_summary();
328 let max_blocks = result.max_blocks.max(defaults.kv_block_floor);
329
330 let (max_seqs_clamped, kv_capacity) = select_dynamic_paged_pool_shape(
335 defaults.max_sequences,
336 defaults.max_sequence_tokens,
337 max_blocks,
338 );
339
340 let kv_capacity_overridden = snapshot_value(¤t, "FERRUM_KV_CAPACITY").is_some();
341 if !kv_overridden {
347 entries.push(RuntimeConfigEntry::new(
348 "FERRUM_KV_MAX_BLOCKS",
349 max_blocks.to_string(),
350 RuntimeConfigSource::MemoryProfile,
351 ));
352 }
353 if !max_seqs_overridden {
354 entries.push(RuntimeConfigEntry::new(
355 "FERRUM_PAGED_MAX_SEQS",
356 max_seqs_clamped.to_string(),
357 RuntimeConfigSource::MemoryProfile,
358 ));
359 }
360 if kv_capacity > 0 && !kv_capacity_overridden {
361 entries.push(RuntimeConfigEntry::new(
362 "FERRUM_KV_CAPACITY",
363 kv_capacity.to_string(),
364 RuntimeConfigSource::MemoryProfile,
365 ));
366 }
367 crate::runtime_env::materialize_runtime_env_defaults(&entries);
368 eprintln!(
369 "[auto-size] KV_MAX_BLOCKS={} PAGED_MAX_SEQS={} KV_CAPACITY={}",
370 if kv_overridden {
371 "<user>".to_string()
372 } else {
373 max_blocks.to_string()
374 },
375 if max_seqs_overridden {
376 "<user>".to_string()
377 } else {
378 max_seqs_clamped.to_string()
379 },
380 if kv_capacity_overridden {
381 "<user>".to_string()
382 } else if kv_capacity > 0 {
383 kv_capacity.to_string()
384 } else {
385 "<default>".to_string()
386 },
387 );
388}
389
390const MAX_AUTOSIZED_SEQUENCE_TOKENS: usize = 16_384;
391const DEFAULT_SERVER_MAX_SEQUENCES: usize = 32;
392const TIGHT_RECURRENT_STATE_SERVER_MAX_SEQUENCES: usize = 16;
393const CHAT_MAX_SEQUENCES: usize = 2;
394
395fn model_auto_size_defaults(
396 model_class: ModelAutoSizeClass,
397 profile: AutoSizeProfile,
398) -> ModelAutoSizeDefaults {
399 match (model_class, profile) {
400 (ModelAutoSizeClass::TightRecurrentState, AutoSizeProfile::Server) => {
401 ModelAutoSizeDefaults {
402 max_batched_tokens: TIGHT_RECURRENT_STATE_MAX_BATCHED_TOKENS,
403 max_sequences: TIGHT_RECURRENT_STATE_SERVER_MAX_SEQUENCES,
404 max_sequence_tokens: MAX_AUTOSIZED_SEQUENCE_TOKENS,
405 kv_block_floor: TIGHT_RECURRENT_STATE_KV_BLOCK_FLOOR,
406 }
407 }
408 (ModelAutoSizeClass::TightRecurrentState, AutoSizeProfile::Chat) => ModelAutoSizeDefaults {
409 max_batched_tokens: TIGHT_RECURRENT_STATE_MAX_BATCHED_TOKENS,
410 max_sequences: CHAT_MAX_SEQUENCES,
411 max_sequence_tokens: MAX_AUTOSIZED_SEQUENCE_TOKENS,
412 kv_block_floor: TIGHT_RECURRENT_STATE_KV_BLOCK_FLOOR,
413 },
414 (ModelAutoSizeClass::Generic, AutoSizeProfile::Server) => ModelAutoSizeDefaults {
415 max_batched_tokens: DEFAULT_MAX_BATCHED_TOKENS,
416 max_sequences: DEFAULT_SERVER_MAX_SEQUENCES,
417 max_sequence_tokens: MAX_AUTOSIZED_SEQUENCE_TOKENS,
418 kv_block_floor: 0,
419 },
420 (ModelAutoSizeClass::Generic, AutoSizeProfile::Chat) => ModelAutoSizeDefaults {
421 max_batched_tokens: DEFAULT_MAX_BATCHED_TOKENS,
422 max_sequences: CHAT_MAX_SEQUENCES,
423 max_sequence_tokens: MAX_AUTOSIZED_SEQUENCE_TOKENS,
424 kv_block_floor: 0,
425 },
426 }
427}
428
429fn model_auto_size_hints(model_dir: &Path) -> ModelAutoSizeHints {
430 let Ok(config_text) = std::fs::read_to_string(model_dir.join("config.json")) else {
431 return ModelAutoSizeHints::default();
432 };
433 let Ok(config) = serde_json::from_str::<serde_json::Value>(&config_text) else {
434 return ModelAutoSizeHints::default();
435 };
436 model_auto_size_hints_from_config(&config)
437}
438
439fn model_auto_size_hints_from_config(config: &serde_json::Value) -> ModelAutoSizeHints {
440 ModelAutoSizeHints {
441 has_recurrent_linear_attention_state: has_recurrent_linear_attention_state(config),
442 }
443}
444
445fn model_auto_size_class_from_hints_and_budget(
446 hints: ModelAutoSizeHints,
447 budget: Option<&AutoSizeResult>,
448) -> ModelAutoSizeClass {
449 if !hints.has_recurrent_linear_attention_state {
450 return ModelAutoSizeClass::Generic;
451 }
452 let Some(budget) = budget else {
453 return ModelAutoSizeClass::Generic;
454 };
455 let generic_prefill_blocks =
456 ceil_div_usize(DEFAULT_MAX_BATCHED_TOKENS, PAGED_BLOCK_SIZE as usize);
457 if budget.estimated_budget_blocks < generic_prefill_blocks {
458 ModelAutoSizeClass::TightRecurrentState
459 } else {
460 ModelAutoSizeClass::Generic
461 }
462}
463
464fn has_recurrent_linear_attention_state(config: &serde_json::Value) -> bool {
465 let text = config.get("text_config").unwrap_or(config);
466 let has_linear_layers = text
467 .get("layer_types")
468 .and_then(|value| value.as_array())
469 .is_some_and(|layers| {
470 layers.iter().any(|layer| {
471 layer
472 .as_str()
473 .is_some_and(|name| name.eq_ignore_ascii_case("linear_attention"))
474 })
475 });
476 let has_linear_state_dims = [
477 "linear_conv_kernel_dim",
478 "linear_key_head_dim",
479 "linear_num_key_heads",
480 "linear_num_value_heads",
481 "linear_value_head_dim",
482 ]
483 .iter()
484 .all(|key| text.get(*key).and_then(|value| value.as_u64()).is_some());
485 has_linear_layers && has_linear_state_dims
486}
487
488fn config_or_text_u64(config: &serde_json::Value, key: &str) -> Option<u64> {
489 config
490 .get(key)
491 .and_then(|value| value.as_u64())
492 .or_else(|| {
493 config
494 .get("text_config")
495 .and_then(|text| text.get(key))
496 .and_then(|value| value.as_u64())
497 })
498}
499
500fn ceil_div_u64(value: u64, divisor: u64) -> u64 {
501 if divisor == 0 {
502 return value;
503 }
504 value.div_ceil(divisor)
505}
506
507fn ceil_div_usize(value: usize, divisor: usize) -> usize {
508 if divisor == 0 {
509 return value;
510 }
511 value.div_ceil(divisor)
512}
513
514fn select_dynamic_paged_pool_shape(
515 max_sequences: usize,
516 max_sequence_tokens: usize,
517 max_blocks: usize,
518) -> (usize, usize) {
519 let physical_blocks = max_blocks.max(1);
520 let sequences = max_sequences.max(1).min(physical_blocks);
521 let physical_tokens = physical_blocks.saturating_mul(PAGED_BLOCK_SIZE as usize);
522 let sequence_tokens = max_sequence_tokens
523 .max(PAGED_BLOCK_SIZE as usize)
524 .min(physical_tokens);
525 (sequences, sequence_tokens)
526}
527
528fn weight_budget_shard_count(snapshot: &RuntimeConfigSnapshot) -> u64 {
529 match snapshot_value(
530 snapshot,
531 crate::gpu_devices::SELECTED_DISTRIBUTED_STRATEGY_KEY,
532 ) {
533 Some("layer_split") => selected_gpu_device_count(snapshot).max(1) as u64,
534 _ => 1,
538 }
539}
540
541fn layer_count_for_memory_budget(num_layers: u64, snapshot: &RuntimeConfigSnapshot) -> u64 {
542 match snapshot_value(
543 snapshot,
544 crate::gpu_devices::SELECTED_DISTRIBUTED_STRATEGY_KEY,
545 ) {
546 Some("layer_split") => {
547 let shards = selected_gpu_device_count(snapshot).max(1) as u64;
548 ceil_div_u64(num_layers, shards).max(1)
549 }
550 _ => num_layers,
551 }
552}
553
554fn selected_gpu_device_count(snapshot: &RuntimeConfigSnapshot) -> usize {
555 snapshot_value(snapshot, crate::gpu_devices::SELECTED_GPU_DEVICES_KEY)
556 .map(|value| {
557 value
558 .split(',')
559 .filter(|part| !part.trim().is_empty())
560 .count()
561 })
562 .unwrap_or(1)
563}
564
565fn requested_min_kv_blocks_from_snapshot(snapshot: &RuntimeConfigSnapshot) -> usize {
566 let max_model_len_blocks = snapshot_usize(snapshot, "FERRUM_MAX_MODEL_LEN")
567 .map(|value| ceil_div_usize(value, PAGED_BLOCK_SIZE as usize))
568 .unwrap_or(0);
569 let max_batched_token_blocks = snapshot_usize(snapshot, "FERRUM_MAX_BATCHED_TOKENS")
570 .map(|value| ceil_div_usize(value, PAGED_BLOCK_SIZE as usize))
571 .unwrap_or(0);
572
573 max_model_len_blocks.max(max_batched_token_blocks)
574}
575
576fn snapshot_value<'a>(snapshot: &'a RuntimeConfigSnapshot, key: &str) -> Option<&'a str> {
577 snapshot
578 .entries
579 .iter()
580 .find(|entry| entry.key == key)
581 .map(|entry| entry.effective_value.as_str())
582}
583
584fn snapshot_usize(snapshot: &RuntimeConfigSnapshot, key: &str) -> Option<usize> {
585 snapshot_value(snapshot, key).and_then(|value| value.parse::<usize>().ok())
586}
587
588fn snapshot_bool(snapshot: &RuntimeConfigSnapshot, key: &str) -> Option<bool> {
589 snapshot_value(snapshot, key).map(|value| matches!(value, "1" | "true" | "TRUE" | "on" | "ON"))
590}
591
592fn kv_pool_copies_from_snapshot(snapshot: &RuntimeConfigSnapshot) -> u64 {
593 let fa_layout = snapshot_bool(snapshot, "FERRUM_FA_LAYOUT_VARLEN").unwrap_or(false);
594 let fa2_source = snapshot_bool(snapshot, "FERRUM_FA2_SOURCE").unwrap_or(false);
595 let fa2_direct_ffi = snapshot_bool(snapshot, "FERRUM_FA2_DIRECT_FFI")
596 .unwrap_or_else(|| snapshot_value(snapshot, "FERRUM_FA2_DIRECT_FFI_SHIM").is_some());
597
598 if fa_layout || fa2_source || fa2_direct_ffi {
599 2
600 } else {
601 1
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 fn snapshot(vars: &[(&str, &str)]) -> RuntimeConfigSnapshot {
610 RuntimeConfigSnapshot::from_env_vars(vars.iter().copied())
611 }
612
613 fn budget_with_estimated_blocks(estimated_budget_blocks: usize) -> AutoSizeResult {
614 AutoSizeResult {
615 total_gpu_bytes: 24 * 1024 * 1024 * 1024,
616 free_gpu_bytes: 20 * 1024 * 1024 * 1024,
617 weight_bytes: 18 * 1024 * 1024 * 1024,
618 budgeted_weight_bytes: 18 * 1024 * 1024 * 1024,
619 weight_budget_shards: 1,
620 budgeted_layer_count: 40,
621 kv_block_bytes: 4 * 1024 * 1024,
622 kv_pool_copies: 1,
623 estimated_budget_blocks,
624 requested_min_blocks: 0,
625 max_blocks: estimated_budget_blocks,
626 reserved_for_scratch: SCRATCH_RESERVE_BYTES,
627 }
628 }
629
630 #[test]
631 fn fa_compatible_attention_paths_count_two_kv_pool_copies() {
632 assert_eq!(kv_pool_copies_from_snapshot(&snapshot(&[])), 1);
633 assert_eq!(
634 kv_pool_copies_from_snapshot(&snapshot(&[("FERRUM_FA_LAYOUT_VARLEN", "1")])),
635 2
636 );
637 assert_eq!(
638 kv_pool_copies_from_snapshot(&snapshot(&[("FERRUM_FA2_SOURCE", "1")])),
639 2
640 );
641 assert_eq!(
642 kv_pool_copies_from_snapshot(&snapshot(&[("FERRUM_FA2_DIRECT_FFI_SHIM", "/tmp/x.so")])),
643 2
644 );
645 assert_eq!(
646 kv_pool_copies_from_snapshot(&snapshot(&[
647 ("FERRUM_FA2_DIRECT_FFI", "0"),
648 ("FERRUM_FA2_DIRECT_FFI_SHIM", "/tmp/x.so"),
649 ])),
650 1
651 );
652 }
653
654 #[test]
655 fn layer_split_scopes_weight_and_layer_budget_to_selected_devices() {
656 let snapshot = snapshot(&[
657 (
658 crate::gpu_devices::SELECTED_DISTRIBUTED_STRATEGY_KEY,
659 "layer_split",
660 ),
661 (crate::gpu_devices::SELECTED_GPU_DEVICES_KEY, "0,1"),
662 ]);
663
664 assert_eq!(weight_budget_shard_count(&snapshot), 2);
665 assert_eq!(layer_count_for_memory_budget(80, &snapshot), 40);
666 assert_eq!(ceil_div_u64(37, weight_budget_shard_count(&snapshot)), 19);
667 }
668
669 #[test]
670 fn unknown_multi_gpu_strategy_keeps_single_device_budget_until_wired() {
671 let snapshot = snapshot(&[
672 (
673 crate::gpu_devices::SELECTED_DISTRIBUTED_STRATEGY_KEY,
674 "tensor_parallel",
675 ),
676 (crate::gpu_devices::SELECTED_GPU_DEVICES_KEY, "0,1"),
677 ]);
678
679 assert_eq!(weight_budget_shard_count(&snapshot), 1);
680 assert_eq!(layer_count_for_memory_budget(80, &snapshot), 80);
681 }
682
683 #[test]
684 fn requested_runtime_token_limits_define_kv_block_floor() {
685 let snapshot = snapshot(&[
686 ("FERRUM_MAX_MODEL_LEN", "8192"),
687 ("FERRUM_MAX_BATCHED_TOKENS", "1024"),
688 ("FERRUM_PAGED_MAX_SEQS", "8"),
689 ("FERRUM_KV_CAPACITY", "2048"),
690 ]);
691
692 assert_eq!(requested_min_kv_blocks_from_snapshot(&snapshot), 512);
693 }
694
695 #[test]
696 fn paged_pool_shape_decouples_admission_width_from_sequence_capacity() {
697 assert_eq!(
698 select_dynamic_paged_pool_shape(32, 16_384, 338),
699 (32, 5_408)
700 );
701 assert_eq!(
702 select_dynamic_paged_pool_shape(32, 16_384, 2_048),
703 (32, 16_384)
704 );
705 assert_eq!(select_dynamic_paged_pool_shape(32, 16_384, 8), (8, 128));
706 }
707
708 #[test]
709 fn recurrent_linear_attention_budget_pressure_selects_tight_memory_profile() {
710 let config = serde_json::json!({
711 "architectures": ["SyntheticRecurrentStateModel"],
712 "model_type": "synthetic_recurrent_state",
713 "text_config": {
714 "model_type": "synthetic_recurrent_state_text",
715 "layer_types": ["linear_attention", "full_attention"],
716 "linear_conv_kernel_dim": 4,
717 "mamba_ssm_dtype": "float32",
718 "linear_key_head_dim": 128,
719 "linear_num_key_heads": 16,
720 "linear_num_value_heads": 16,
721 "linear_value_head_dim": 128
722 }
723 });
724
725 let hints = model_auto_size_hints_from_config(&config);
726 assert!(hints.has_recurrent_linear_attention_state);
727 let class = model_auto_size_class_from_hints_and_budget(
728 hints,
729 Some(&budget_with_estimated_blocks(127)),
730 );
731 assert_eq!(class, ModelAutoSizeClass::TightRecurrentState);
732 let server = model_auto_size_defaults(class, AutoSizeProfile::Server);
733 assert_eq!(
734 server.max_batched_tokens,
735 TIGHT_RECURRENT_STATE_MAX_BATCHED_TOKENS
736 );
737 assert_eq!(
738 server
739 .max_batched_tokens
740 .div_ceil(PAGED_BLOCK_SIZE as usize),
741 12
742 );
743 assert!(
744 server
745 .max_batched_tokens
746 .div_ceil(PAGED_BLOCK_SIZE as usize)
747 <= server.kv_block_floor
748 );
749 assert_eq!(server.kv_block_floor, TIGHT_RECURRENT_STATE_KV_BLOCK_FLOOR);
750 assert_eq!(
751 select_dynamic_paged_pool_shape(
752 server.max_sequences,
753 server.max_sequence_tokens,
754 server.kv_block_floor,
755 ),
756 (16, 4096)
757 );
758
759 let chat = model_auto_size_defaults(class, AutoSizeProfile::Chat);
760 assert_eq!(
761 chat.max_batched_tokens,
762 TIGHT_RECURRENT_STATE_MAX_BATCHED_TOKENS
763 );
764 assert_eq!(chat.kv_block_floor, TIGHT_RECURRENT_STATE_KV_BLOCK_FLOOR);
765 assert_eq!(
766 select_dynamic_paged_pool_shape(
767 chat.max_sequences,
768 chat.max_sequence_tokens,
769 chat.kv_block_floor,
770 ),
771 (2, 4096)
772 );
773 }
774
775 #[test]
776 fn recurrent_linear_attention_memory_profile_requires_budget_pressure() {
777 let recurrent = serde_json::json!({
778 "model_type": "synthetic_recurrent_state",
779 "text_config": {
780 "layer_types": ["linear_attention", "full_attention"],
781 "linear_conv_kernel_dim": 4,
782 "mamba_ssm_dtype": "float32",
783 "linear_key_head_dim": 128,
784 "linear_num_key_heads": 16,
785 "linear_num_value_heads": 16,
786 "linear_value_head_dim": 128
787 }
788 });
789 let hints = model_auto_size_hints_from_config(&recurrent);
790 assert_eq!(
791 model_auto_size_class_from_hints_and_budget(
792 hints,
793 Some(&budget_with_estimated_blocks(128)),
794 ),
795 ModelAutoSizeClass::Generic
796 );
797 assert_eq!(
798 model_auto_size_class_from_hints_and_budget(hints, None),
799 ModelAutoSizeClass::Generic
800 );
801
802 let dense = serde_json::json!({
803 "model_type": "dense",
804 "text_config": {
805 "layer_types": ["full_attention", "full_attention"]
806 }
807 });
808 assert_eq!(
809 model_auto_size_class_from_hints_and_budget(
810 model_auto_size_hints_from_config(&dense),
811 Some(&budget_with_estimated_blocks(0)),
812 ),
813 ModelAutoSizeClass::Generic
814 );
815
816 let generic =
817 model_auto_size_defaults(ModelAutoSizeClass::Generic, AutoSizeProfile::Server);
818 assert_eq!(generic.max_batched_tokens, DEFAULT_MAX_BATCHED_TOKENS);
819 assert_eq!(generic.max_sequences, DEFAULT_SERVER_MAX_SEQUENCES);
820 assert_eq!(generic.max_sequence_tokens, MAX_AUTOSIZED_SEQUENCE_TOKENS);
821 assert_eq!(generic.kv_block_floor, 0);
822 }
823
824 #[test]
825 fn autosize_dimension_lookup_falls_back_to_text_config() {
826 let config = serde_json::json!({
827 "model_type": "synthetic_text_wrapped_model",
828 "text_config": {
829 "hidden_size": 2048,
830 "num_hidden_layers": 40,
831 "num_attention_heads": 16,
832 "num_key_value_heads": 2,
833 "head_dim": 256
834 }
835 });
836
837 assert_eq!(config_or_text_u64(&config, "hidden_size"), Some(2048));
838 assert_eq!(config_or_text_u64(&config, "num_hidden_layers"), Some(40));
839 assert_eq!(config_or_text_u64(&config, "num_key_value_heads"), Some(2));
840 assert_eq!(config_or_text_u64(&config, "head_dim"), Some(256));
841 }
842}