use crate::checkpoint::MoshiCheckpoint;
#[cfg(feature = "gpu-lm")]
use crate::mlx_dequant::{bf16_to_f32, dequantize_affine};
use anyhow::{Context, Result, bail};
use safetensors::SafeTensors;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[cfg(feature = "gpu-lm")]
use candle::{DType, Device, Tensor};
pub struct MlxSafetensorsFile {
data: Vec<u8>,
}
impl MlxSafetensorsFile {
pub fn open(path: &Path) -> Result<Self> {
let data = fs::read(path).with_context(|| format!("read {}", path.display()))?;
Ok(Self { data })
}
pub fn tensor_names(&self) -> Result<Vec<String>> {
let st = SafeTensors::deserialize(&self.data)?;
Ok(st.names().iter().map(|s| (*s).clone()).collect())
}
}
pub fn mlx_to_candle_key(mlx_key: &str) -> Option<String> {
if mlx_key.ends_with(".scales") || mlx_key.ends_with(".biases") {
return None;
}
let mut key = mlx_key.to_string();
key = key.replace("audio_embs.", "emb.");
key = key.replace("depformer.slices.", "depformer.");
key = key.replace(".norm1.weight", ".norm1.alpha");
key = key.replace(".norm2.weight", ".norm2.alpha");
key = key.replace(".self_attn.in_proj.weight", ".self_attn.in_proj_weight");
key = key.replace("out_norm.weight", "out_norm.alpha");
Some(key)
}
#[cfg(feature = "gpu-lm")]
pub fn load_candle_tensors(
path: &Path,
checkpoint: MoshiCheckpoint,
device: &Device,
dtype: DType,
) -> Result<HashMap<String, Tensor>> {
let (bits, group_size) = checkpoint.mlx_quant_params();
let file = MlxSafetensorsFile::open(path)?;
let st = SafeTensors::deserialize(&file.data)?;
let mut out: HashMap<String, Tensor> = HashMap::new();
for name in st.names() {
if name.ends_with(".scales") || name.ends_with(".biases") {
continue;
}
let Some(candle_key) = mlx_to_candle_key(name) else {
continue;
};
let tensor = st.tensor(name)?;
let shape: Vec<usize> = tensor.shape().to_vec();
let t = if checkpoint.is_mlx_quantized()
&& name.ends_with(".weight")
&& tensor.dtype() == safetensors::Dtype::U32
{
let base = &name[..name.len() - ".weight".len()];
let scales_name = format!("{base}.scales");
let biases_name = format!("{base}.biases");
let scales = st
.tensor(&scales_name)
.with_context(|| format!("missing {scales_name}"))?;
let biases = st
.tensor(&biases_name)
.with_context(|| format!("missing {biases_name}"))?;
let packed: Vec<u32> = tensor
.data()
.chunks_exact(4)
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
let scales_bf16: Vec<u16> = scales
.data()
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let biases_bf16: Vec<u16> = biases
.data()
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
let rows = shape[0];
let packed_cols = shape[1];
let out_cols = packed_cols * (32 / bits as usize);
let flat = dequantize_affine(
&packed,
packed_cols,
&scales_bf16,
&biases_bf16,
rows,
out_cols,
group_size,
bits,
)?;
Tensor::from_vec(flat, (rows, out_cols), device)?.to_dtype(dtype)?
} else if tensor.dtype() == safetensors::Dtype::BF16 {
let flat: Vec<f32> = tensor
.data()
.chunks_exact(2)
.map(|c| bf16_to_f32(u16::from_le_bytes([c[0], c[1]])))
.collect();
Tensor::from_vec(flat, shape.as_slice(), device)?.to_dtype(dtype)?
} else if tensor.dtype() == safetensors::Dtype::F32 {
let flat: Vec<f32> = tensor
.data()
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect();
Tensor::from_vec(flat, shape.as_slice(), device)?.to_dtype(dtype)?
} else {
bail!("unsupported mlx dtype {:?} for {name}", tensor.dtype());
};
let t = if candle_key.ends_with(".alpha") && t.dims().len() == 1 {
let d = t.dims()[0];
t.reshape((1, 1, d))?
} else {
t
};
out.insert(candle_key, t);
}
Ok(out)
}
#[cfg(not(feature = "gpu-lm"))]
pub fn load_eager_weight_map(
_path: &Path,
_checkpoint: MoshiCheckpoint,
_cfg: &crate::config::LmConfig,
) -> Result<HashMap<String, (Vec<f32>, Vec<usize>)>> {
bail!("mlx checkpoints on CPU require `gpu-lm` for dequant or use --device metal")
}
#[cfg(feature = "gpu-lm")]
pub fn load_eager_weight_map(
path: &Path,
checkpoint: MoshiCheckpoint,
_cfg: &crate::config::LmConfig,
) -> Result<HashMap<String, (Vec<f32>, Vec<usize>)>> {
use candle::Device;
let dev = Device::Cpu;
let tensors = load_candle_tensors(path, checkpoint, &dev, candle::DType::F32)?;
let mut map = HashMap::new();
for (k, t) in tensors {
let shape: Vec<usize> = t.dims().to_vec();
let data = if shape.len() == 2 {
t.to_vec2::<f32>()?.into_iter().flatten().collect()
} else {
t.flatten_all()?.to_vec1()?
};
map.insert(k, (data, shape));
}
Ok(map)
}
#[cfg(feature = "gpu-lm")]
pub fn load_lm_model_mlx<P: AsRef<Path>>(
cfg: moshi::lm::Config,
path: P,
checkpoint: MoshiCheckpoint,
dtype: DType,
dev: &Device,
) -> Result<moshi::lm::LmModel> {
use moshi::nn::MaybeQuantizedVarBuilder;
let tensors = load_candle_tensors(path.as_ref(), checkpoint, dev, dtype)?;
let vb =
MaybeQuantizedVarBuilder::Real(candle_nn::VarBuilder::from_tensors(tensors, dtype, dev));
Ok(moshi::lm::LmModel::new(&cfg, vb)?)
}