use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ReconstructionPath {
GpuFast,
CpuDirect,
DirectLoad,
}
impl ReconstructionPath {
const GPU_ELEMENT_THRESHOLD: usize = 4096;
pub fn classify(shape: &[u64], is_safetensor: bool) -> Self {
if is_safetensor {
return ReconstructionPath::DirectLoad;
}
if shape.len() < 2 {
return ReconstructionPath::CpuDirect;
}
let total_elements: u64 = shape.iter().product();
if total_elements < Self::GPU_ELEMENT_THRESHOLD as u64 {
return ReconstructionPath::CpuDirect;
}
ReconstructionPath::GpuFast
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LoadingBackend {
Eager,
EagerWithRamCache,
Progressive,
}
impl LoadingBackend {
pub fn select(plan: &AllocationPlan) -> Self {
if plan.nvme_usage == 0 {
LoadingBackend::Eager
}
else {
LoadingBackend::Progressive
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MemoryTier {
Vram,
Ram,
Nvme,
}
impl MemoryTier {
pub fn latency_factor(&self) -> f32 {
match self {
MemoryTier::Vram => 1.0,
MemoryTier::Ram => 10.0,
MemoryTier::Nvme => 100.0,
}
}
pub fn priority(&self) -> u8 {
match self {
MemoryTier::Vram => 3,
MemoryTier::Ram => 2,
MemoryTier::Nvme => 1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TensorPrecision {
BF16,
FP8,
INT8,
INT4,
}
impl TensorPrecision {
pub fn bits(&self) -> u32 {
match self {
TensorPrecision::BF16 => 16,
TensorPrecision::FP8 => 8,
TensorPrecision::INT8 => 8,
TensorPrecision::INT4 => 4,
}
}
pub fn size_divisor(&self) -> f32 {
16.0 / self.bits() as f32
}
pub fn storage_size(&self, bf16_size: u64) -> u64 {
(bf16_size as f64 / self.size_divisor() as f64).ceil() as u64
}
pub fn quality_factor(&self) -> f32 {
match self {
TensorPrecision::BF16 => 1.0,
TensorPrecision::FP8 => 0.99,
TensorPrecision::INT8 => 0.98,
TensorPrecision::INT4 => 0.95,
}
}
pub fn all_by_quality() -> &'static [TensorPrecision] {
&[
TensorPrecision::BF16,
TensorPrecision::FP8,
TensorPrecision::INT8,
TensorPrecision::INT4,
]
}
}
#[derive(Debug, Clone)]
pub struct TensorAllocation {
pub tier: MemoryTier,
pub precision: TensorPrecision,
pub priority: f32,
pub prefetch: bool,
pub storage_size: u64,
}
#[derive(Debug, Clone)]
pub struct AllocationPlan {
pub allocations: HashMap<String, TensorAllocation>,
pub vram_usage: u64,
pub ram_usage: u64,
pub nvme_usage: u64,
pub swap_count: usize,
pub quality_score: f32,
}
impl AllocationPlan {
pub fn new() -> Self {
Self {
allocations: HashMap::new(),
vram_usage: 0,
ram_usage: 0,
nvme_usage: 0,
swap_count: 0,
quality_score: 1.0,
}
}
pub fn total_usage(&self) -> u64 {
self.vram_usage + self.ram_usage + self.nvme_usage
}
pub fn requires_swapping(&self) -> bool {
self.swap_count > 0
}
pub fn tensors_in_tier(
&self,
tier: MemoryTier,
) -> impl Iterator<Item = (&String, &TensorAllocation)> {
self.allocations.iter().filter(move |(_, a)| a.tier == tier)
}
}
impl Default for AllocationPlan {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct TensorInfo {
pub name: String,
pub size_bytes: u64,
pub shape: Vec<u64>,
pub layer_index: Option<usize>,
pub tensor_type: TensorType,
pub reconstruction_path: ReconstructionPath,
}
impl TensorInfo {
pub fn from_name(name: impl Into<String>, size_bytes: u64) -> Self {
Self::from_name_with_shape(name, size_bytes, Vec::new())
}
pub fn from_name_with_shape(name: impl Into<String>, size_bytes: u64, shape: Vec<u64>) -> Self {
let name = name.into();
let tensor_type = TensorType::from_name(&name);
let layer_index = Self::parse_layer_index(&name);
let reconstruction_path = ReconstructionPath::classify(&shape, false);
Self {
name,
size_bytes,
shape,
layer_index,
tensor_type,
reconstruction_path,
}
}
fn parse_layer_index(name: &str) -> Option<usize> {
let patterns = ["layers.", "layer.", "blocks.", "block."];
for pattern in patterns {
if let Some(start) = name.find(pattern) {
let after_pattern = &name[start + pattern.len()..];
if let Some(end) = after_pattern.find('.') {
if let Ok(idx) = after_pattern[..end].parse::<usize>() {
return Some(idx);
}
}
}
}
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TensorType {
Embedding,
LmHead,
Attention,
LayerNorm,
MlpGate,
MlpUp,
MlpDown,
Other,
}
impl TensorType {
pub fn from_name(name: &str) -> Self {
let name_lower = name.to_lowercase();
if name_lower.contains("embed") || name_lower.contains("wte") || name_lower.contains("wpe")
{
TensorType::Embedding
} else if name_lower.contains("lm_head")
|| name_lower.contains("output")
&& name_lower.contains("weight")
&& !name_lower.contains("layer")
{
TensorType::LmHead
} else if name_lower.contains("q_proj")
|| name_lower.contains("k_proj")
|| name_lower.contains("v_proj")
|| name_lower.contains("o_proj")
|| name_lower.contains("self_attn")
|| name_lower.contains("attention")
{
TensorType::Attention
} else if name_lower.contains("layernorm")
|| name_lower.contains("ln_")
|| name_lower.contains("_norm")
|| name_lower.contains("rmsnorm")
{
TensorType::LayerNorm
} else if name_lower.contains("gate_proj") || name_lower.contains("w1") {
TensorType::MlpGate
} else if name_lower.contains("up_proj") || name_lower.contains("w3") {
TensorType::MlpUp
} else if name_lower.contains("down_proj") || name_lower.contains("w2") {
TensorType::MlpDown
} else {
TensorType::Other
}
}
pub fn base_importance(&self) -> f32 {
match self {
TensorType::Embedding => 1.0,
TensorType::LmHead => 1.0,
TensorType::Attention => 0.9,
TensorType::LayerNorm => 0.85,
TensorType::MlpDown => 0.7,
TensorType::MlpGate => 0.6,
TensorType::MlpUp => 0.6,
TensorType::Other => 0.5,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArchitectureType {
Llama,
Gpt,
Unknown,
}
impl ArchitectureType {
pub fn detect(tensor_names: &[String]) -> Self {
let has_gate = tensor_names.iter().any(|n| n.contains("gate_proj"));
let has_rotary = tensor_names.iter().any(|n| n.contains("rotary"));
let has_mlp_c = tensor_names.iter().any(|n| n.contains("mlp.c_"));
if has_gate || has_rotary {
ArchitectureType::Llama
} else if has_mlp_c {
ArchitectureType::Gpt
} else {
ArchitectureType::Unknown
}
}
}
#[derive(Debug, Clone)]
pub struct ModelProfile {
pub tensors: Vec<TensorInfo>,
pub num_layers: usize,
pub architecture: ArchitectureType,
pub total_size_bytes: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum ProfileError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("no tensors found in directory: {0}")]
NoTensors(String),
#[error("HCT error: {0}")]
Hct(String),
}
impl ModelProfile {
pub fn new(tensors: Vec<TensorInfo>) -> Self {
let num_layers = tensors
.iter()
.filter_map(|t| t.layer_index)
.max()
.map(|m| m + 1)
.unwrap_or(0);
let tensor_names: Vec<_> = tensors.iter().map(|t| t.name.clone()).collect();
let architecture = ArchitectureType::detect(&tensor_names);
let total_size_bytes = tensors.iter().map(|t| t.size_bytes).sum();
Self {
tensors,
num_layers,
architecture,
total_size_bytes,
}
}
pub fn from_hct_directory(
directory: impl AsRef<std::path::Path>,
) -> Result<Self, ProfileError> {
use crate::hct::{filename_to_tensor_name, HctLoader};
let directory = directory.as_ref();
let mut tensors = Vec::new();
for entry in std::fs::read_dir(directory)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("hct") {
continue;
}
let filename = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
let tensor_name = filename_to_tensor_name(filename);
match HctLoader::from_file(&path) {
Ok(loader) => {
let metadata = loader.metadata();
tensors.push(TensorInfo::from_name_with_shape(
&tensor_name,
metadata.original_size,
metadata.shape.clone(),
));
},
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"Failed to load HCT metadata, skipping tensor"
);
},
}
}
if tensors.is_empty() {
return Err(ProfileError::NoTensors(directory.display().to_string()));
}
tracing::info!(
tensor_count = tensors.len(),
directory = %directory.display(),
"Built model profile from HCT directory"
);
Ok(Self::new(tensors))
}
pub fn size_gb(&self) -> f64 {
self.total_size_bytes as f64 / (1024.0 * 1024.0 * 1024.0)
}
pub fn reconstruction_summary(&self) -> ReconstructionSummary {
let mut gpu_count = 0;
let mut cpu_count = 0;
let mut direct_count = 0;
for tensor in &self.tensors {
match tensor.reconstruction_path {
ReconstructionPath::GpuFast => gpu_count += 1,
ReconstructionPath::CpuDirect => cpu_count += 1,
ReconstructionPath::DirectLoad => direct_count += 1,
}
}
ReconstructionSummary {
gpu_fast_count: gpu_count,
cpu_direct_count: cpu_count,
direct_load_count: direct_count,
total_count: self.tensors.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct ReconstructionSummary {
pub gpu_fast_count: usize,
pub cpu_direct_count: usize,
pub direct_load_count: usize,
pub total_count: usize,
}
impl std::fmt::Display for ReconstructionSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Reconstruction paths: {} GPU, {} CPU, {} direct ({} total)",
self.gpu_fast_count, self.cpu_direct_count, self.direct_load_count, self.total_count
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tensor_precision_size_divisor() {
assert!((TensorPrecision::BF16.size_divisor() - 1.0).abs() < f32::EPSILON);
assert!((TensorPrecision::INT8.size_divisor() - 2.0).abs() < f32::EPSILON);
assert!((TensorPrecision::INT4.size_divisor() - 4.0).abs() < f32::EPSILON);
}
#[test]
fn test_tensor_precision_storage_size() {
let bf16_size = 1000u64;
assert_eq!(TensorPrecision::BF16.storage_size(bf16_size), 1000);
assert_eq!(TensorPrecision::INT8.storage_size(bf16_size), 500);
assert_eq!(TensorPrecision::INT4.storage_size(bf16_size), 250);
}
#[test]
fn test_tensor_type_from_name() {
assert_eq!(
TensorType::from_name("model.embed_tokens.weight"),
TensorType::Embedding
);
assert_eq!(TensorType::from_name("lm_head.weight"), TensorType::LmHead);
assert_eq!(
TensorType::from_name("model.layers.0.self_attn.q_proj.weight"),
TensorType::Attention
);
assert_eq!(
TensorType::from_name("model.layers.0.mlp.gate_proj.weight"),
TensorType::MlpGate
);
assert_eq!(
TensorType::from_name("model.layers.0.input_layernorm.weight"),
TensorType::LayerNorm
);
}
#[test]
fn test_tensor_info_parse_layer() {
let info = TensorInfo::from_name("model.layers.5.self_attn.q_proj.weight", 1000);
assert_eq!(info.layer_index, Some(5));
assert_eq!(info.tensor_type, TensorType::Attention);
let embed = TensorInfo::from_name("model.embed_tokens.weight", 1000);
assert_eq!(embed.layer_index, None);
assert_eq!(embed.tensor_type, TensorType::Embedding);
}
#[test]
fn test_model_profile_layer_count() {
let tensors = vec![
TensorInfo::from_name("model.embed_tokens.weight", 1000),
TensorInfo::from_name("model.layers.0.self_attn.q_proj.weight", 1000),
TensorInfo::from_name("model.layers.1.self_attn.q_proj.weight", 1000),
TensorInfo::from_name("model.layers.2.self_attn.q_proj.weight", 1000),
TensorInfo::from_name("lm_head.weight", 1000),
];
let profile = ModelProfile::new(tensors);
assert_eq!(profile.num_layers, 3);
assert_eq!(profile.total_size_bytes, 5000);
}
#[test]
fn test_memory_tier_priority() {
assert!(MemoryTier::Vram.priority() > MemoryTier::Ram.priority());
assert!(MemoryTier::Ram.priority() > MemoryTier::Nvme.priority());
}
#[test]
fn test_reconstruction_path_classify_1d() {
assert_eq!(
ReconstructionPath::classify(&[4096], false),
ReconstructionPath::CpuDirect
);
assert_eq!(
ReconstructionPath::classify(&[128], false),
ReconstructionPath::CpuDirect
);
}
#[test]
fn test_reconstruction_path_classify_small_2d() {
assert_eq!(
ReconstructionPath::classify(&[63, 64], false), ReconstructionPath::CpuDirect
);
assert_eq!(
ReconstructionPath::classify(&[32, 32], false), ReconstructionPath::CpuDirect
);
assert_eq!(
ReconstructionPath::classify(&[64, 64], false), ReconstructionPath::GpuFast
);
}
#[test]
fn test_reconstruction_path_classify_large_2d() {
assert_eq!(
ReconstructionPath::classify(&[4096, 4096], false),
ReconstructionPath::GpuFast
);
assert_eq!(
ReconstructionPath::classify(&[128, 4096], false),
ReconstructionPath::GpuFast
);
}
#[test]
fn test_reconstruction_path_safetensor() {
assert_eq!(
ReconstructionPath::classify(&[4096, 4096], true),
ReconstructionPath::DirectLoad
);
}
#[test]
fn test_loading_backend_eager_no_swap() {
let plan = AllocationPlan {
allocations: HashMap::from([(
"t1".to_string(),
TensorAllocation {
tier: MemoryTier::Vram,
precision: TensorPrecision::BF16,
priority: 1.0,
prefetch: false,
storage_size: 1000,
},
)]),
vram_usage: 1000,
ram_usage: 0,
nvme_usage: 0,
swap_count: 0,
quality_score: 1.0,
};
assert_eq!(LoadingBackend::select(&plan), LoadingBackend::Eager);
}
#[test]
fn test_loading_backend_progressive() {
let mut allocations = HashMap::new();
for i in 0..100 {
allocations.insert(
format!("t{i}"),
TensorAllocation {
tier: MemoryTier::Nvme,
precision: TensorPrecision::BF16,
priority: 0.5,
prefetch: false,
storage_size: 1000,
},
);
}
let plan = AllocationPlan {
allocations,
vram_usage: 1000,
ram_usage: 1000,
nvme_usage: 100000,
swap_count: 50, quality_score: 0.9,
};
assert_eq!(LoadingBackend::select(&plan), LoadingBackend::Progressive);
}
#[test]
fn test_tensor_info_with_shape() {
let info = TensorInfo::from_name_with_shape(
"model.layers.0.self_attn.q_proj.weight",
1000,
vec![4096, 4096],
);
assert_eq!(info.shape, vec![4096, 4096]);
assert_eq!(info.reconstruction_path, ReconstructionPath::GpuFast);
}
#[test]
fn test_tensor_info_1d_shape() {
let info = TensorInfo::from_name_with_shape(
"model.layers.0.input_layernorm.weight",
1000,
vec![4096],
);
assert_eq!(info.shape, vec![4096]);
assert_eq!(info.reconstruction_path, ReconstructionPath::CpuDirect);
}
#[test]
fn test_reconstruction_summary() {
let tensors = vec![
TensorInfo::from_name_with_shape("t1", 1000, vec![4096, 4096]), TensorInfo::from_name_with_shape("t2", 1000, vec![4096, 4096]), TensorInfo::from_name_with_shape("t3", 1000, vec![4096]), TensorInfo::from_name_with_shape("t4", 1000, vec![32, 32]), ];
let profile = ModelProfile::new(tensors);
let summary = profile.reconstruction_summary();
assert_eq!(summary.gpu_fast_count, 2);
assert_eq!(summary.cpu_direct_count, 2);
assert_eq!(summary.direct_load_count, 0);
assert_eq!(summary.total_count, 4);
}
}