use std::path::{Path, PathBuf};
use memmap2::Mmap;
use safetensors::{Dtype, SafeTensors};
use crate::te::error::{TeError, TeResult};
use crate::te::weights::Tensor;
const GROUP_SIZE: usize = 64;
const NIBBLES_PER_U32: usize = 8;
const SUFFIX_WEIGHT: &str = ".weight";
const SUFFIX_SCALES: &str = ".scales";
const SUFFIX_BIASES: &str = ".biases";
#[inline]
pub fn bf16_to_f32(bits: u16) -> f32 {
f32::from_bits((bits as u32) << 16)
}
pub fn dequantize_mlx_4bit_affine(
weight: &[u32],
scales: &[u16],
biases: &[u16],
out_features: usize,
in_features: usize,
) -> TeResult<Vec<f32>> {
if in_features == 0 || in_features % GROUP_SIZE != 0 {
return Err(TeError::Shape(format!(
"mlx4bit dequant: in_features ({in_features}) must be a positive multiple of {GROUP_SIZE}"
)));
}
let weight_cols = in_features / NIBBLES_PER_U32; let group_cols = in_features / GROUP_SIZE;
let expect_weight = out_features * weight_cols;
if weight.len() != expect_weight {
return Err(TeError::Shape(format!(
"mlx4bit dequant: weight len {} != expected {expect_weight} (out={out_features}, in/8={weight_cols})",
weight.len()
)));
}
let expect_groups = out_features * group_cols;
if scales.len() != expect_groups {
return Err(TeError::Shape(format!(
"mlx4bit dequant: scales len {} != expected {expect_groups} (out={out_features}, in/64={group_cols})",
scales.len()
)));
}
if biases.len() != expect_groups {
return Err(TeError::Shape(format!(
"mlx4bit dequant: biases len {} != expected {expect_groups} (out={out_features}, in/64={group_cols})",
biases.len()
)));
}
let mut out = vec![0.0f32; out_features * in_features];
let dequant_row = |o: usize, row: &mut [f32]| {
let weight_row = o * weight_cols;
let group_row = o * group_cols;
for col in 0..weight_cols {
let word = weight[weight_row + col];
let i_base = col * NIBBLES_PER_U32;
let group = i_base / GROUP_SIZE;
let scale = bf16_to_f32(scales[group_row + group]);
let bias = bf16_to_f32(biases[group_row + group]);
for nibble in 0..NIBBLES_PER_U32 {
let code = ((word >> (4 * nibble)) & 0xF) as f32;
row[i_base + nibble] = code * scale + bias;
}
}
};
let threads = std::thread::available_parallelism()
.map(|t| t.get())
.unwrap_or(1)
.min(out_features.max(1));
if threads <= 1 || out_features < 4 {
for (o, row) in out.chunks_mut(in_features).enumerate() {
dequant_row(o, row);
}
} else {
let rows_per = out_features.div_ceil(threads);
let dequant_ref = &dequant_row;
std::thread::scope(|scope| {
let mut base = 0usize;
for block in out.chunks_mut(rows_per * in_features) {
let start = base;
base += block.len() / in_features;
scope.spawn(move || {
for (r, row) in block.chunks_mut(in_features).enumerate() {
dequant_ref(start + r, row);
}
});
}
});
}
Ok(out)
}
pub struct Mlx4bitModel {
path: PathBuf,
mmap: Mmap,
}
impl Mlx4bitModel {
pub fn open(path: &Path) -> TeResult<Self> {
let file = std::fs::File::open(path).map_err(|source| TeError::Io {
path: path.display().to_string(),
source,
})?;
let mmap = unsafe { Mmap::map(&file) }.map_err(|source| TeError::Io {
path: path.display().to_string(),
source,
})?;
SafeTensors::deserialize(&mmap).map_err(|e| TeError::Npy {
path: path.display().to_string(),
reason: format!("safetensors header: {e}"),
})?;
Ok(Self {
path: path.to_path_buf(),
mmap,
})
}
fn view(&self) -> TeResult<SafeTensors<'_>> {
SafeTensors::deserialize(&self.mmap).map_err(|e| TeError::Npy {
path: self.path.display().to_string(),
reason: format!("safetensors header: {e}"),
})
}
pub fn load_tensor(&self, base: &str) -> TeResult<Tensor> {
let st = self.view()?;
let prefixed = format!("model.{base}");
let weight_name = format!("{prefixed}{SUFFIX_WEIGHT}");
let scales_name = format!("{prefixed}{SUFFIX_SCALES}");
let has_scales = st.tensor(&scales_name).is_ok();
if has_scales {
return self.load_quantized(&st, base, &prefixed);
}
if st.tensor(&weight_name).is_err() {
return Err(TeError::MissingWeight {
name: base.to_string(),
});
}
self.load_plain_bf16(&st, base, &weight_name)
}
fn load_quantized(&self, st: &SafeTensors<'_>, base: &str, prefixed: &str) -> TeResult<Tensor> {
let weight_name = format!("{prefixed}{SUFFIX_WEIGHT}");
let scales_name = format!("{prefixed}{SUFFIX_SCALES}");
let biases_name = format!("{prefixed}{SUFFIX_BIASES}");
let w_view = st.tensor(&weight_name).map_err(|e| self.parse_err(&e))?;
let s_view = st.tensor(&scales_name).map_err(|e| self.parse_err(&e))?;
let b_view = st.tensor(&biases_name).map_err(|e| self.parse_err(&e))?;
if w_view.dtype() != Dtype::U32 {
return Err(TeError::Npy {
path: self.path.display().to_string(),
reason: format!("{base}.weight dtype {:?}, expected U32", w_view.dtype()),
});
}
if s_view.dtype() != Dtype::BF16 {
return Err(TeError::Npy {
path: self.path.display().to_string(),
reason: format!("{base}.scales dtype {:?}, expected BF16", s_view.dtype()),
});
}
if b_view.dtype() != Dtype::BF16 {
return Err(TeError::Npy {
path: self.path.display().to_string(),
reason: format!("{base}.biases dtype {:?}, expected BF16", b_view.dtype()),
});
}
let w_shape = w_view.shape();
if w_shape.len() != 2 {
return Err(TeError::Shape(format!(
"{base}.weight rank {}, expected 2",
w_shape.len()
)));
}
let out_features = w_shape[0];
let in_features = w_shape[1] * NIBBLES_PER_U32;
let weight = u32_from_le_bytes(w_view.data());
let scales = u16_from_le_bytes(s_view.data());
let biases = u16_from_le_bytes(b_view.data());
let data =
dequantize_mlx_4bit_affine(&weight, &scales, &biases, out_features, in_features)?;
Ok(Tensor {
data,
shape: vec![out_features, in_features],
})
}
pub fn gather_quant_rows(&self, base: &str, rows: &[usize], cols: usize) -> TeResult<Vec<f32>> {
if cols == 0 || cols % GROUP_SIZE != 0 {
return Err(TeError::Shape(format!(
"mlx4bit gather: cols ({cols}) must be a positive multiple of {GROUP_SIZE}"
)));
}
let st = self.view()?;
let prefixed = format!("model.{base}");
let weight_name = format!("{prefixed}{SUFFIX_WEIGHT}");
let scales_name = format!("{prefixed}{SUFFIX_SCALES}");
let biases_name = format!("{prefixed}{SUFFIX_BIASES}");
if st.tensor(&scales_name).is_err() {
return Err(TeError::MissingWeight {
name: base.to_string(),
});
}
let w_view = st.tensor(&weight_name).map_err(|e| self.parse_err(&e))?;
let s_view = st.tensor(&scales_name).map_err(|e| self.parse_err(&e))?;
let b_view = st.tensor(&biases_name).map_err(|e| self.parse_err(&e))?;
if w_view.dtype() != Dtype::U32 {
return Err(TeError::Npy {
path: self.path.display().to_string(),
reason: format!("{base}.weight dtype {:?}, expected U32", w_view.dtype()),
});
}
if s_view.dtype() != Dtype::BF16 {
return Err(TeError::Npy {
path: self.path.display().to_string(),
reason: format!("{base}.scales dtype {:?}, expected BF16", s_view.dtype()),
});
}
if b_view.dtype() != Dtype::BF16 {
return Err(TeError::Npy {
path: self.path.display().to_string(),
reason: format!("{base}.biases dtype {:?}, expected BF16", b_view.dtype()),
});
}
let w_shape = w_view.shape();
if w_shape.len() != 2 {
return Err(TeError::Shape(format!(
"{base}.weight rank {}, expected 2",
w_shape.len()
)));
}
let out_features = w_shape[0];
let in_features = w_shape[1] * NIBBLES_PER_U32;
if in_features != cols {
return Err(TeError::Shape(format!(
"{base}: on-disk in_features {in_features} != requested cols {cols}"
)));
}
let weight = u32_from_le_bytes(w_view.data());
let scales = u16_from_le_bytes(s_view.data());
let biases = u16_from_le_bytes(b_view.data());
let weight_cols = cols / NIBBLES_PER_U32; let group_cols = cols / GROUP_SIZE; if weight.len() != out_features * weight_cols {
return Err(TeError::Shape(format!(
"{base}.weight len {} != expected {} (out={out_features}, cols/8={weight_cols})",
weight.len(),
out_features * weight_cols
)));
}
if scales.len() != out_features * group_cols || biases.len() != out_features * group_cols {
return Err(TeError::Shape(format!(
"{base}.scales/biases len {}/{} != expected {} (out={out_features}, cols/64={group_cols})",
scales.len(),
biases.len(),
out_features * group_cols
)));
}
let mut out = vec![0.0f32; rows.len() * cols];
for (dst_row, &r) in rows.iter().enumerate() {
if r >= out_features {
return Err(TeError::Shape(format!(
"{base}: gather row {r} >= out_features {out_features}"
)));
}
let weight_row = r * weight_cols;
let group_row = r * group_cols;
let out_base = dst_row * cols;
for col in 0..weight_cols {
let word = weight[weight_row + col];
let i_base = col * NIBBLES_PER_U32;
let group = i_base / GROUP_SIZE;
let scale = bf16_to_f32(scales[group_row + group]);
let bias = bf16_to_f32(biases[group_row + group]);
let dst = out_base + i_base;
for nibble in 0..NIBBLES_PER_U32 {
let code = ((word >> (4 * nibble)) & 0xF) as f32;
out[dst + nibble] = code * scale + bias;
}
}
}
Ok(out)
}
fn load_plain_bf16(
&self,
st: &SafeTensors<'_>,
base: &str,
weight_name: &str,
) -> TeResult<Tensor> {
let view = st.tensor(weight_name).map_err(|e| self.parse_err(&e))?;
if view.dtype() != Dtype::BF16 {
return Err(TeError::Npy {
path: self.path.display().to_string(),
reason: format!(
"{base}.weight (plain) dtype {:?}, expected BF16",
view.dtype()
),
});
}
let bits = u16_from_le_bytes(view.data());
let numel: usize = view.shape().iter().product();
if bits.len() < numel {
return Err(TeError::Shape(format!(
"{base}.weight: bf16 payload short ({} < {numel})",
bits.len()
)));
}
let data: Vec<f32> = bits[..numel].iter().map(|&b| bf16_to_f32(b)).collect();
Ok(Tensor {
data,
shape: view.shape().to_vec(),
})
}
fn parse_err(&self, e: &safetensors::SafeTensorError) -> TeError {
TeError::Npy {
path: self.path.display().to_string(),
reason: e.to_string(),
}
}
}
fn u32_from_le_bytes(bytes: &[u8]) -> Vec<u32> {
bytes
.chunks_exact(4)
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
}
fn u16_from_le_bytes(bytes: &[u8]) -> Vec<u16> {
bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn f32_to_bf16(value: f32) -> u16 {
half::bf16::from_f32(value).to_bits()
}
fn pack_word(codes: &[u8; 8]) -> u32 {
let mut w = 0u32;
for (n, &c) in codes.iter().enumerate() {
w |= ((c & 0xF) as u32) << (4 * n);
}
w
}
#[test]
fn bf16_to_f32_exact_for_representable() {
for &v in &[0.0f32, 1.0, -1.0, 0.5, -0.0625, 2000.0, 0.125] {
assert_eq!(bf16_to_f32(f32_to_bf16(v)), v, "value {v}");
}
}
#[test]
fn single_group_affine_matches_formula() {
let in_features = 64usize;
let scale = 0.125f32; let bias = -1.0f32; let mut weight = Vec::new();
let mut expected = Vec::new();
for w in 0..(in_features / 8) {
let mut codes = [0u8; 8];
for (n, c) in codes.iter_mut().enumerate() {
let code = ((w * 8 + n) % 16) as u8;
*c = code;
expected.push(code as f32 * scale + bias);
}
weight.push(pack_word(&codes));
}
let scales = vec![f32_to_bf16(scale)];
let biases = vec![f32_to_bf16(bias)];
let out =
dequantize_mlx_4bit_affine(&weight, &scales, &biases, 1, in_features).expect("dequant");
assert_eq!(out, expected);
}
#[test]
fn multi_row_multi_group_distinct_scales() {
let out = 3usize;
let in_features = 128usize;
let words_per_row = in_features / 8; let groups_per_row = in_features / 64;
let mut weight = vec![0u32; out * words_per_row];
let mut scales = vec![0u16; out * groups_per_row];
let mut biases = vec![0u16; out * groups_per_row];
let mut expected = vec![0.0f32; out * in_features];
for o in 0..out {
for g in 0..groups_per_row {
let scale = 1.0f32 / ((1 << (o + g + 1)) as f32); let bias = -(1.0f32 / ((1 << (o + 1)) as f32)); scales[o * groups_per_row + g] = f32_to_bf16(scale);
biases[o * groups_per_row + g] = f32_to_bf16(bias);
for wlocal in 0..(64 / 8) {
let w_idx = g * (64 / 8) + wlocal;
let mut codes = [0u8; 8];
for (n, c) in codes.iter_mut().enumerate() {
let i = g * 64 + wlocal * 8 + n;
let code = ((o + i) % 16) as u8;
*c = code;
expected[o * in_features + i] = code as f32 * scale + bias;
}
weight[o * words_per_row + w_idx] = pack_word(&codes);
}
}
}
let out_buf = dequantize_mlx_4bit_affine(&weight, &scales, &biases, out, in_features)
.expect("dequant");
assert_eq!(out_buf, expected);
}
#[test]
fn nibble_order_is_lsb_first() {
let codes = [0u8, 1, 2, 3, 4, 5, 6, 7];
let weight = vec![pack_word(&codes)];
let scales = vec![f32_to_bf16(1.0)];
let biases = vec![f32_to_bf16(0.0)];
let mut full = weight;
full.extend(std::iter::repeat_n(0u32, 7));
let out = dequantize_mlx_4bit_affine(&full, &scales, &biases, 1, 64).expect("dequant");
for (i, &c) in codes.iter().enumerate() {
assert_eq!(out[i], c as f32, "input {i}");
}
}
#[test]
fn rejects_unaligned_in_features() {
let err = dequantize_mlx_4bit_affine(&[0u32; 1], &[0u16; 1], &[0u16; 1], 1, 60)
.expect_err("in not multiple of 64 must error");
assert!(matches!(err, TeError::Shape(_)));
}
#[test]
fn rejects_wrong_weight_len() {
let err = dequantize_mlx_4bit_affine(&[0u32; 4], &[0u16; 1], &[0u16; 1], 1, 64)
.expect_err("short weight must error");
assert!(matches!(err, TeError::Shape(_)));
}
struct RawTensor {
dtype: Dtype,
shape: Vec<usize>,
bytes: Vec<u8>,
}
impl safetensors::View for RawTensor {
fn dtype(&self) -> Dtype {
self.dtype
}
fn shape(&self) -> &[usize] {
&self.shape
}
fn data(&self) -> std::borrow::Cow<'_, [u8]> {
std::borrow::Cow::Borrowed(&self.bytes)
}
fn data_len(&self) -> usize {
self.bytes.len()
}
}
fn u32_le_bytes(words: &[u32]) -> Vec<u8> {
let mut b = Vec::with_capacity(words.len() * 4);
for &w in words {
b.extend_from_slice(&w.to_le_bytes());
}
b
}
fn u16_le_bytes(words: &[u16]) -> Vec<u8> {
let mut b = Vec::with_capacity(words.len() * 2);
for &w in words {
b.extend_from_slice(&w.to_le_bytes());
}
b
}
#[test]
fn gather_rows_bit_equal_to_full_dequant() {
let out = 5usize;
let cols = 128usize;
let words_per_row = cols / NIBBLES_PER_U32; let groups_per_row = cols / GROUP_SIZE;
let mut weight = vec![0u32; out * words_per_row];
let mut scales = vec![0u16; out * groups_per_row];
let mut biases = vec![0u16; out * groups_per_row];
for o in 0..out {
for g in 0..groups_per_row {
let scale = 1.0f32 / ((1 << (o + g + 1)) as f32);
let bias = -(1.0f32 / ((1 << (o + 1)) as f32));
scales[o * groups_per_row + g] = f32_to_bf16(scale);
biases[o * groups_per_row + g] = f32_to_bf16(bias);
}
for col in 0..words_per_row {
let mut codes = [0u8; 8];
for (n, c) in codes.iter_mut().enumerate() {
*c = (((o * 31 + col * 7 + n * 3) ^ (o + col)) % 16) as u8;
}
weight[o * words_per_row + col] = pack_word(&codes);
}
}
let full =
dequantize_mlx_4bit_affine(&weight, &scales, &biases, out, cols).expect("full dequant");
let base = "layers.0.self_attn.q_proj";
let prefixed = format!("model.{base}");
let tensors = vec![
(
format!("{prefixed}{SUFFIX_WEIGHT}"),
RawTensor {
dtype: Dtype::U32,
shape: vec![out, words_per_row],
bytes: u32_le_bytes(&weight),
},
),
(
format!("{prefixed}{SUFFIX_SCALES}"),
RawTensor {
dtype: Dtype::BF16,
shape: vec![out, groups_per_row],
bytes: u16_le_bytes(&scales),
},
),
(
format!("{prefixed}{SUFFIX_BIASES}"),
RawTensor {
dtype: Dtype::BF16,
shape: vec![out, groups_per_row],
bytes: u16_le_bytes(&biases),
},
),
];
let blob = safetensors::serialize(tensors, None).expect("serialize");
let mut path = std::env::temp_dir();
path.push(format!(
"oxibonsai_te_gather_test_{}.safetensors",
std::process::id()
));
std::fs::write(&path, &blob).expect("write temp safetensors");
let model = Mlx4bitModel::open(&path).expect("open synthetic model");
let rows = [3usize, 0, 4, 0, 2];
let gathered = model
.gather_quant_rows(base, &rows, cols)
.expect("gather rows");
assert_eq!(gathered.len(), rows.len() * cols);
for (dst_row, &r) in rows.iter().enumerate() {
let got = &gathered[dst_row * cols..(dst_row + 1) * cols];
let want = &full[r * cols..(r + 1) * cols];
assert_eq!(
got.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
want.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
"row {r} (dst {dst_row}) differs from full dequant"
);
}
let err = model
.gather_quant_rows(base, &[out], cols)
.expect_err("row >= out_features must error");
assert!(matches!(err, TeError::Shape(_)));
let _ = std::fs::remove_file(&path);
}
}