You are browsing as a guest. Sign up (or log in) to start making projects!

1h 15m 21s logged

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.

the rest would be explained in another devlog, i hate the char limit :angry-3d-emoji:

(had to re-write like 10 times btw)

0
0

Comments 0

No comments yet. Be the first!