Js got to know that devlogs longer than 10hrs are stripped to 10 ðŸ˜, i cant have more than 4k char so bear with me theres a LOT to tell that im not able to fit, but ill try my best anyways
Dev Log #2 — Converting SafeTensors Into .thin
(had to cut so much explaination bcs i HATE THE CHAR LIMIT, id add them in another devlog)
The main conversion flow is:
pub fn convert_hf(options: ConvertHfOptions) -> Result<ConvertHfResult> {
let config: Value = serde_json::from_reader(
File::open(options.hf_dir.join("config.json"))?
)?;
let safetensor_paths = collect_safetensors(&options.hf_dir)?;
let tensors = collect_tensors(&safetensor_paths)?;
let layers = required_u32(&config, "num_hidden_layers")?;
let model = build_model_spec(&config, layers, &tensors, &options, &mut warnings)?;
let pages = build_pages(&tensors, &config)?;
let execution_tape = build_execution_tape(layers, &tensors, &mut warnings)?;
let manifest = Manifest {
format: FORMAT_NAME.to_string(),
version: FORMAT_VERSION,
model,
execution_tape,
pages,
memory_plan: build_memory_plan(&config, &tensors),
};
write_archive_pages(&options.out_path, &manifest, &archive_pages)?;
let archive = Archive::open(&options.out_path)?;
verify_archive(&archive)?;
Ok(ConvertHfResult { archive, warnings })
}
The converter memory-maps each SafeTensors file read-only:
let mmap = unsafe { Mmap::map(&file)? };
let safetensors = SafeTensors::deserialize(&mmap)?;
let base = mmap.as_ptr() as usize;
Then every tensor becomes a page candidate. The converter records where the tensor bytes live inside the mapped file:
let data = tensor.data();
let offset = (data.as_ptr() as usize).checked_sub(base)?;
and stores:
struct TensorPage {
id: String,
path: PathBuf,
offset: u64,
size: u64,
checksum: [u8; 32],
dtype: String,
shape: Vec<u64>,
}
This avoids rebuilding every tensor in memory. The archive can later copy the original byte range directly:
ArchivePageSource::FileRange {
path: tensor.path.clone(),
offset: tensor.offset,
}
After collecting raw tensor data, the converter builds the model spec. It extracts or infers things like hidden size, layer count, attention heads, KV heads, head dimension, RoPE settings, vocab size, activation type, quantization config, and required runtime operators.
One important field is attention type:
let attention_kind = if kv_heads == heads {
"mha"
} else if kv_heads == 1 {
"mqa"
} else {
"gqa"
};
This matters because many modern decoder models use grouped-query attention. If the runtime only sees shapes but does not understand Q heads vs KV heads, causal attention can be wrong while still looking structurally valid.
The converter also records required operators:
let mut required_operators = vec![
"embedding".to_string(),
"rms_norm".to_string(),
"qkv_projection".to_string(),
"rope".to_string(),
format!("{attention_kind}_attention"),
"o_projection".to_string(),
"lm_head".to_string(),
];
For dense MLPs it adds:
gated_activation
down_projection
For MoE models it records router and expert stages instead.
Comments 0
No comments yet. Be the first!
Sign in to join the conversation.