use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::Path;
use std::sync::Arc;
use cudarc::driver::{CudaDevice, CudaSlice};
use haagenti::holotensor::{HoloTensorDecoder, HoloTensorReader, HOLO_MAGIC};
use haagenti::tensor::HctReader;
use haagenti::Lz4Decompressor;
use crate::gpu_holo::cuda::GpuHoloContext;
use crate::hct::HctLoader;
use super::arch::{ModelArch, ModelConfig, WeightNameMap};
use super::tensor::{GpuDType, GpuTensor};
use super::InferenceError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FileFormat {
StandardHct,
HoloTensor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QuantFormat {
F16,
BF16,
Int8,
Int4,
Gptq,
Awq,
}
impl QuantFormat {
pub fn bits(&self) -> usize {
match self {
QuantFormat::F16 | QuantFormat::BF16 => 16,
QuantFormat::Int8 => 8,
QuantFormat::Int4 | QuantFormat::Gptq | QuantFormat::Awq => 4,
}
}
pub fn uses_zero_points(&self) -> bool {
matches!(self, QuantFormat::Gptq | QuantFormat::Awq)
}
pub fn uses_g_idx(&self) -> bool {
matches!(self, QuantFormat::Gptq)
}
}
#[derive(Debug)]
pub struct QuantizedWeight {
pub data: GpuTensor,
pub format: QuantFormat,
pub scales: Option<GpuTensor>,
pub zero_points: Option<GpuTensor>,
pub g_idx: Option<GpuTensor>,
pub shape: (usize, usize),
pub block_size: usize,
pub transposed: bool,
}
impl QuantizedWeight {
pub fn out_features(&self) -> usize {
self.shape.0
}
pub fn in_features(&self) -> usize {
self.shape.1
}
pub fn is_quantized(&self) -> bool {
!matches!(self.format, QuantFormat::F16 | QuantFormat::BF16)
}
}
#[derive(Debug)]
pub struct RMSNormWeights {
pub weight: GpuTensor,
}
#[derive(Debug)]
pub struct LayerWeights {
pub index: usize,
pub q_proj: QuantizedWeight,
pub k_proj: QuantizedWeight,
pub v_proj: QuantizedWeight,
pub o_proj: QuantizedWeight,
pub gate_proj: QuantizedWeight,
pub up_proj: QuantizedWeight,
pub down_proj: QuantizedWeight,
pub input_norm: RMSNormWeights,
pub post_attn_norm: RMSNormWeights,
}
impl LayerWeights {
pub fn size_bytes(&self) -> usize {
self.q_proj.data.size_bytes()
+ self.k_proj.data.size_bytes()
+ self.v_proj.data.size_bytes()
+ self.o_proj.data.size_bytes()
+ self.gate_proj.data.size_bytes()
+ self.up_proj.data.size_bytes()
+ self.down_proj.data.size_bytes()
+ self.input_norm.weight.size_bytes()
+ self.post_attn_norm.weight.size_bytes()
+ self.q_proj.scales.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.q_proj.zero_points.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.k_proj.scales.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.k_proj.zero_points.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.v_proj.scales.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.v_proj.zero_points.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.o_proj.scales.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.o_proj.zero_points.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.gate_proj.scales.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.gate_proj.zero_points.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.up_proj.scales.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.up_proj.zero_points.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.down_proj.scales.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ self.down_proj.zero_points.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
}
}
#[derive(Debug)]
pub struct WeightStore {
pub config: ModelConfig,
device: Arc<CudaDevice>,
pub embed_tokens: GpuTensor,
pub layers: Vec<LayerWeights>,
pub final_norm: RMSNormWeights,
pub lm_head: Option<GpuTensor>,
pub memory_used: usize,
}
impl WeightStore {
pub fn load_hct(
model_dir: impl AsRef<Path>,
arch: Option<ModelArch>,
device_id: usize,
) -> Result<Self, InferenceError> {
let model_dir = model_dir.as_ref();
let device =
CudaDevice::new(device_id).map_err(|e| InferenceError::Device(e.to_string()))?;
let arch = match arch {
Some(a) => a,
None => ModelArch::detect(model_dir)?,
};
let config = Self::load_config(model_dir, arch)?;
let weight_map = arch.weight_map();
tracing::info!(
arch = ?arch,
hidden_size = config.hidden_size,
num_layers = config.num_layers,
"Loading model weights"
);
let hct_files = Self::find_hct_files(model_dir)?;
tracing::info!(num_files = hct_files.len(), "Found HCT weight files");
let raw_weights = Self::load_raw_weights(&hct_files, &device)?;
let weights = Self::map_weights(raw_weights, &weight_map);
Self::build_weight_store(config, weights, device)
}
fn load_config(model_dir: &Path, arch: ModelArch) -> Result<ModelConfig, InferenceError> {
let config_path = model_dir.join("config.json");
if config_path.exists() {
let config_str = std::fs::read_to_string(&config_path)
.map_err(|e| InferenceError::ModelLoad(e.to_string()))?;
ModelConfig::from_json(&config_str, arch)
} else {
let dir_name = model_dir
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_lowercase();
let (hidden_size, num_layers) = if dir_name.contains("7b") {
(4096, 32)
} else if dir_name.contains("13b") {
(5120, 40)
} else if dir_name.contains("70b") {
(8192, 80)
} else if dir_name.contains("1b") || dir_name.contains("1.5b") {
(2048, 24)
} else if dir_name.contains("3b") {
(3200, 26)
} else {
(4096, 32) };
Ok(ModelConfig::default_for_arch(arch, hidden_size, num_layers))
}
}
fn find_hct_files(dir: &Path) -> Result<Vec<std::path::PathBuf>, InferenceError> {
let entries = std::fs::read_dir(dir)
.map_err(|e| InferenceError::ModelLoad(format!("Cannot read directory: {}", e)))?;
let mut files: Vec<_> = entries
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "hct"))
.map(|e| e.path())
.collect();
files.sort();
Ok(files)
}
fn detect_format(path: &Path) -> Result<FileFormat, InferenceError> {
let mut file = File::open(path).map_err(|e| {
InferenceError::ModelLoad(format!("Cannot open {}: {}", path.display(), e))
})?;
let mut magic = [0u8; 4];
file.read_exact(&mut magic)
.map_err(|e| InferenceError::ModelLoad(format!("Cannot read magic: {}", e)))?;
if magic == HOLO_MAGIC {
Ok(FileFormat::HoloTensor)
} else {
Ok(FileFormat::StandardHct)
}
}
fn load_raw_weights(
hct_files: &[std::path::PathBuf],
device: &Arc<CudaDevice>,
) -> Result<HashMap<String, LoadedWeight>, InferenceError> {
let mut weights = HashMap::new();
let mut total_bytes = 0usize;
let format = if let Some(first) = hct_files.first() {
Self::detect_format(first)?
} else {
return Err(InferenceError::ModelLoad("No HCT files found".to_string()));
};
match format {
FileFormat::HoloTensor => {
tracing::info!("Detected HoloTensor format, using GPU holographic reconstruction");
Self::load_holotensor_weights(hct_files, device, &mut weights, &mut total_bytes)?;
},
FileFormat::StandardHct => {
tracing::info!("Detected standard HCT format, using LZ4 decompression");
Self::load_standard_hct_weights(hct_files, device, &mut weights, &mut total_bytes)?;
},
}
tracing::info!(
total_mb = total_bytes as f64 / 1024.0 / 1024.0,
num_weights = weights.len(),
"Loaded raw weights to GPU"
);
Ok(weights)
}
fn load_standard_hct_weights(
hct_files: &[std::path::PathBuf],
device: &Arc<CudaDevice>,
weights: &mut HashMap<String, LoadedWeight>,
total_bytes: &mut usize,
) -> Result<(), InferenceError> {
let decompressor = Lz4Decompressor::new();
for path in hct_files {
let loader = HctLoader::from_file(path).map_err(|e| {
InferenceError::ModelLoad(format!("Failed to load {}: {}", path.display(), e))
})?;
let name = filename_to_hf_name(&loader.metadata().name);
let shape: Vec<usize> = loader
.metadata()
.shape
.iter()
.map(|&d| d as usize)
.collect();
let dtype = loader.metadata().dtype;
let file = File::open(path).map_err(|e| InferenceError::ModelLoad(e.to_string()))?;
let reader = BufReader::new(file);
let mut hct_reader =
HctReader::new(reader).map_err(|e| InferenceError::ModelLoad(e.to_string()))?;
let data = hct_reader
.decompress_all(&decompressor)
.map_err(|e| InferenceError::ModelLoad(format!("Decompress failed: {}", e)))?;
let (gpu_data, format, scales, zero_points) =
Self::process_weight_data(&data, &shape, dtype, device)?;
*total_bytes += gpu_data.size_bytes();
if let Some(ref s) = scales {
*total_bytes += s.size_bytes();
}
if let Some(ref z) = zero_points {
*total_bytes += z.size_bytes();
}
weights.insert(
name,
LoadedWeight {
data: gpu_data,
format,
scales,
zero_points,
shape,
},
);
}
Ok(())
}
fn load_holotensor_weights(
hct_files: &[std::path::PathBuf],
device: &Arc<CudaDevice>,
weights: &mut HashMap<String, LoadedWeight>,
total_bytes: &mut usize,
) -> Result<(), InferenceError> {
let mut gpu_ctx = GpuHoloContext::with_device(Arc::clone(device), 0);
gpu_ctx
.load_all_kernels()
.map_err(|e| InferenceError::ModelLoad(format!("Failed to load GPU kernels: {}", e)))?;
gpu_ctx.load_fused_kernel().map_err(|e| {
InferenceError::ModelLoad(format!("Failed to load fused kernel: {}", e))
})?;
tracing::info!("GPU holographic kernels loaded successfully");
for path in hct_files {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.map(filename_to_hf_name)
.ok_or_else(|| InferenceError::ModelLoad("Invalid filename".to_string()))?;
let file = File::open(path).map_err(|e| {
InferenceError::ModelLoad(format!("Cannot open {}: {}", path.display(), e))
})?;
let reader = BufReader::new(file);
let mut holo_reader = HoloTensorReader::new(reader).map_err(|e| {
InferenceError::ModelLoad(format!(
"Failed to parse HoloTensor {}: {}",
path.display(),
e
))
})?;
let (header, fragments) = holo_reader.read_all().map_err(|e| {
InferenceError::ModelLoad(format!(
"Failed to read fragments from {}: {}",
path.display(),
e
))
})?;
let shape: Vec<usize> = header.shape.iter().map(|&d| d as usize).collect();
let gpu_tensor = if shape.len() == 1 {
let mut decoder = haagenti::holotensor::HoloTensorDecoder::new(header.clone());
for fragment in &fragments {
decoder.add_fragment(fragment.clone()).map_err(|e| {
InferenceError::ModelLoad(format!("Failed to add fragment: {}", e))
})?;
}
let cpu_data = decoder.reconstruct().map_err(|e| {
InferenceError::ModelLoad(format!(
"CPU reconstruction failed for {}: {}",
path.display(),
e
))
})?;
let f16_data: Vec<half::f16> =
cpu_data.iter().map(|&f| half::f16::from_f32(f)).collect();
let bytes: &[u8] = unsafe {
std::slice::from_raw_parts(f16_data.as_ptr() as *const u8, f16_data.len() * 2)
};
let gpu_data: CudaSlice<u8> = device.htod_sync_copy(bytes).map_err(|e| {
InferenceError::Memory(format!("Failed to upload 1D tensor: {}", e))
})?;
GpuTensor::from_cuda_slice(
gpu_data,
shape.clone(),
GpuDType::F16,
Arc::clone(device),
)?
} else {
let gpu_data_f32: CudaSlice<f32> =
gpu_ctx.reconstruct(&header, &fragments).map_err(|e| {
InferenceError::ModelLoad(format!(
"GPU reconstruction failed for {}: {}",
path.display(),
e
))
})?;
let gpu_data_f16 = gpu_ctx.convert_f32_to_f16(&gpu_data_f32).map_err(|e| {
InferenceError::ModelLoad(format!("F32->F16 conversion failed: {}", e))
})?;
GpuTensor::from_cuda_slice_f16(gpu_data_f16, shape.clone(), Arc::clone(device))?
};
*total_bytes += gpu_tensor.size_bytes();
weights.insert(
name,
LoadedWeight {
data: gpu_tensor,
format: QuantFormat::F16,
scales: None,
zero_points: None,
shape,
},
);
}
Ok(())
}
fn process_weight_data(
data: &[u8],
shape: &[usize],
dtype: haagenti::tensor::DType,
device: &Arc<CudaDevice>,
) -> Result<(GpuTensor, QuantFormat, Option<GpuTensor>, Option<GpuTensor>), InferenceError>
{
use haagenti::tensor::DType;
match dtype {
DType::F16 => {
let gpu_data = Self::upload_to_gpu(data, shape.to_vec(), GpuDType::F16, device)?;
Ok((gpu_data, QuantFormat::F16, None, None))
},
DType::BF16 => {
let gpu_data = Self::upload_to_gpu(data, shape.to_vec(), GpuDType::BF16, device)?;
Ok((gpu_data, QuantFormat::BF16, None, None))
},
DType::I4 => {
let num_elements: usize = shape.iter().product();
let block_size = 128; let num_blocks = (num_elements + block_size - 1) / block_size;
let scales_size = num_blocks * 4; let zp_size = num_blocks; let packed_size = (num_elements + 1) / 2;
if data.len() < scales_size + zp_size + packed_size {
return Err(InferenceError::ModelLoad(format!(
"INT4 data too small: {} bytes for {} elements",
data.len(),
num_elements
)));
}
let scales_f16: Vec<u8> = data[..scales_size]
.chunks_exact(4)
.flat_map(|c| {
let f = f32::from_le_bytes([c[0], c[1], c[2], c[3]]);
half::f16::from_f32(f).to_le_bytes()
})
.collect();
let scales_tensor =
Self::upload_to_gpu(&scales_f16, vec![num_blocks], GpuDType::F16, device)?;
let zp_data = &data[scales_size..scales_size + zp_size];
let zp_tensor =
Self::upload_to_gpu(zp_data, vec![num_blocks], GpuDType::I8, device)?;
let packed_data = &data[scales_size + zp_size..];
let packed_tensor =
Self::upload_to_gpu(packed_data, shape.to_vec(), GpuDType::I4, device)?;
Ok((
packed_tensor,
QuantFormat::Int4,
Some(scales_tensor),
Some(zp_tensor),
))
},
DType::I8 => {
let gpu_data = Self::upload_to_gpu(data, shape.to_vec(), GpuDType::I8, device)?;
Ok((gpu_data, QuantFormat::Int8, None, None))
},
other => Err(InferenceError::ModelLoad(format!(
"Unsupported dtype: {:?}",
other
))),
}
}
fn upload_to_gpu(
data: &[u8],
shape: Vec<usize>,
dtype: GpuDType,
device: &Arc<CudaDevice>,
) -> Result<GpuTensor, InferenceError> {
let gpu_data: CudaSlice<u8> = device
.htod_sync_copy(data)
.map_err(|e| InferenceError::Memory(e.to_string()))?;
GpuTensor::from_cuda_slice(gpu_data, shape, dtype, Arc::clone(device))
}
fn map_weights(
raw_weights: HashMap<String, LoadedWeight>,
weight_map: &WeightNameMap,
) -> HashMap<String, LoadedWeight> {
let mut mapped = HashMap::new();
for (hf_name, weight) in raw_weights {
if let Some(internal_name) = weight_map.map_name(&hf_name) {
mapped.insert(internal_name, weight);
} else {
tracing::debug!(name = %hf_name, "No mapping for weight, using original name");
mapped.insert(hf_name, weight);
}
}
mapped
}
fn build_weight_store(
config: ModelConfig,
mut weights: HashMap<String, LoadedWeight>,
device: Arc<CudaDevice>,
) -> Result<Self, InferenceError> {
let embed_tokens = weights
.remove("embed_tokens")
.ok_or_else(|| InferenceError::ModelLoad("Missing embed_tokens".to_string()))?
.data;
let final_norm = RMSNormWeights {
weight: weights
.remove("final_norm")
.ok_or_else(|| InferenceError::ModelLoad("Missing final_norm".to_string()))?
.data,
};
let lm_head = weights.remove("lm_head").map(|w| w.data);
let mut layers = Vec::with_capacity(config.num_layers);
for i in 0..config.num_layers {
let layer = Self::build_layer(&mut weights, i, &config)?;
layers.push(layer);
}
let memory_used = embed_tokens.size_bytes()
+ final_norm.weight.size_bytes()
+ lm_head.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ layers.iter().map(|l| Self::layer_memory(l)).sum::<usize>();
tracing::info!(
memory_mb = memory_used as f64 / 1024.0 / 1024.0,
num_layers = layers.len(),
"Built weight store"
);
Ok(Self {
config,
device,
embed_tokens,
layers,
final_norm,
lm_head,
memory_used,
})
}
fn build_layer(
weights: &mut HashMap<String, LoadedWeight>,
index: usize,
_config: &ModelConfig,
) -> Result<LayerWeights, InferenceError> {
let mut get_weight = |name: &str| -> Result<LoadedWeight, InferenceError> {
let key = format!("layers.{}.{}", index, name);
weights
.remove(&key)
.ok_or_else(|| InferenceError::ModelLoad(format!("Missing weight: {}", key)))
};
let to_quantized = |w: LoadedWeight| -> QuantizedWeight {
let shape = if w.shape.len() >= 2 {
(w.shape[0], w.shape[1])
} else {
(w.shape[0], 1)
};
QuantizedWeight {
data: w.data,
format: w.format,
scales: w.scales,
zero_points: w.zero_points,
g_idx: None, shape,
block_size: 128, transposed: false, }
};
let to_norm = |w: LoadedWeight| -> RMSNormWeights { RMSNormWeights { weight: w.data } };
Ok(LayerWeights {
index,
q_proj: to_quantized(get_weight("q_proj")?),
k_proj: to_quantized(get_weight("k_proj")?),
v_proj: to_quantized(get_weight("v_proj")?),
o_proj: to_quantized(get_weight("o_proj")?),
gate_proj: to_quantized(get_weight("gate_proj")?),
up_proj: to_quantized(get_weight("up_proj")?),
down_proj: to_quantized(get_weight("down_proj")?),
input_norm: to_norm(get_weight("input_norm")?),
post_attn_norm: to_norm(get_weight("post_attn_norm")?),
})
}
fn layer_memory(layer: &LayerWeights) -> usize {
let qw_size = |qw: &QuantizedWeight| {
qw.data.size_bytes()
+ qw.scales.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
+ qw.zero_points.as_ref().map(|t| t.size_bytes()).unwrap_or(0)
};
qw_size(&layer.q_proj)
+ qw_size(&layer.k_proj)
+ qw_size(&layer.v_proj)
+ qw_size(&layer.o_proj)
+ qw_size(&layer.gate_proj)
+ qw_size(&layer.up_proj)
+ qw_size(&layer.down_proj)
+ layer.input_norm.weight.size_bytes()
+ layer.post_attn_norm.weight.size_bytes()
}
pub fn device(&self) -> &Arc<CudaDevice> {
&self.device
}
}
struct LoadedWeight {
data: GpuTensor,
format: QuantFormat,
scales: Option<GpuTensor>,
zero_points: Option<GpuTensor>,
shape: Vec<usize>,
}
fn filename_to_hf_name(filename: &str) -> String {
const UNDERSCORE_COMPONENTS: &[&str] = &[
"embed_tokens",
"input_layernorm",
"post_attention_layernorm",
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
"self_attn",
"lm_head",
];
let mut result = filename.to_string();
let mut protected = Vec::new();
for (i, comp) in UNDERSCORE_COMPONENTS.iter().enumerate() {
let placeholder = format!("XPLACEHOLDER{}X", i);
if result.contains(comp) {
result = result.replace(comp, &placeholder);
protected.push((placeholder, comp.to_string()));
}
}
result = result.replace('_', ".");
for (placeholder, original) in protected {
result = result.replace(&placeholder, &original);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filename_to_hf_name() {
assert_eq!(
filename_to_hf_name("model_embed_tokens_weight"),
"model.embed_tokens.weight"
);
assert_eq!(
filename_to_hf_name("model_layers_0_self_attn_q_proj_weight"),
"model.layers.0.self_attn.q_proj.weight"
);
assert_eq!(
filename_to_hf_name("model_layers_29_mlp_down_proj_weight"),
"model.layers.29.mlp.down_proj.weight"
);
assert_eq!(
filename_to_hf_name("model_layers_0_input_layernorm_weight"),
"model.layers.0.input_layernorm.weight"
);
assert_eq!(
filename_to_hf_name("model_norm_weight"),
"model.norm.weight"
);
}
}