libmir-metal 0.3.0

Metal inference backend for libmir
use models::weights::{BitsAndBytesStorageDType, TensorBinding, TensorStorage};

use crate::engine::{Array, Dtype, Error, ModelTensors, Result, Stream};

#[derive(Debug)]
pub(in crate::engine) struct BitsAndBytes4BitLinear {
    weight: Array,
    absmax: Array,
    quant_map: Array,
    nested_absmax: Array,
    nested_quant_map: Array,
    input: usize,
    output: usize,
    block_size: usize,
    nested_block_size: Option<usize>,
    nested_offset_bits: u32,
}

pub(super) fn linear(
    tensors: &ModelTensors,
    binding: &TensorBinding,
) -> Result<BitsAndBytes4BitLinear> {
    let TensorStorage::BitsAndBytes4Bit {
        format,
        absmax,
        quant_map,
        nested_absmax,
        nested_quant_map,
        nested_offset_bits,
        ..
    } = &binding.storage
    else {
        return Err(invalid(binding, "requires bitsandbytes 4-bit storage"));
    };
    let Some([output, input]) = binding.logical_shape.as_deref() else {
        return Err(invalid(binding, "requires a logical matrix"));
    };
    let (output, input) = (*output, *input);
    let weight = tensors.get(&binding.source)?;
    require_bytes(&weight, output * input / 2, binding, "weight")?;
    require_dtype(&weight, storage_dtype(format.storage_dtype), binding, "weight")?;
    let absmax_array = tensors.get(absmax)?;
    let quant_map_array = tensors.get(quant_map)?;
    require(&quant_map_array, Dtype::Float32, &[16], binding, "quant map")?;
    let blocks = (output * input).div_ceil(format.block_size);
    let (nested_absmax_array, nested_quant_map_array) =
        if let Some(nested) = format.nested_block_size {
            require(&absmax_array, Dtype::Uint8, &[blocks], binding, "nested codes")?;
            let nested_absmax = tensors.get(
                nested_absmax
                    .as_deref()
                    .ok_or_else(|| invalid(binding, "nested absmax is missing"))?,
            )?;
            let nested_map = tensors.get(
                nested_quant_map
                    .as_deref()
                    .ok_or_else(|| invalid(binding, "nested map is missing"))?,
            )?;
            require(
                &nested_absmax,
                Dtype::Float32,
                &[blocks.div_ceil(nested)],
                binding,
                "nested absmax",
            )?;
            require(&nested_map, Dtype::Float32, &[256], binding, "nested map")?;
            (nested_absmax, nested_map)
        } else {
            require(&absmax_array, Dtype::Float32, &[blocks], binding, "absmax")?;
            (tensors.get(quant_map)?, tensors.get(quant_map)?)
        };
    Ok(BitsAndBytes4BitLinear {
        weight,
        absmax: absmax_array,
        quant_map: quant_map_array,
        nested_absmax: nested_absmax_array,
        nested_quant_map: nested_quant_map_array,
        input,
        output,
        block_size: format.block_size,
        nested_block_size: format.nested_block_size,
        nested_offset_bits: nested_offset_bits.unwrap_or(0),
    })
}

impl BitsAndBytes4BitLinear {
    pub(in crate::engine) fn forward(&self, input: &Array, stream: &Stream) -> Result<Array> {
        if !matches!(input.dtype()?, Dtype::Float16 | Dtype::Bfloat16) {
            return Err(Error::InvalidQuantization(
                "bitsandbytes input must be F16 or BF16".into(),
            ));
        }
        stream.kernels().bitsandbytes_4bit_linear(
            [
                input,
                &self.weight,
                &self.absmax,
                &self.quant_map,
                &self.nested_absmax,
                &self.nested_quant_map,
            ],
            self.input,
            self.output,
            self.block_size,
            self.nested_block_size,
            self.nested_offset_bits,
            stream,
        )
    }
}

fn storage_dtype(dtype: BitsAndBytesStorageDType) -> Dtype {
    match dtype {
        BitsAndBytesStorageDType::U8 => Dtype::Uint8,
        BitsAndBytesStorageDType::F16 => Dtype::Float16,
        BitsAndBytesStorageDType::Bf16 => Dtype::Bfloat16,
        BitsAndBytesStorageDType::F32 => Dtype::Float32,
    }
}

fn require(
    array: &Array,
    dtype: Dtype,
    shape: &[usize],
    binding: &TensorBinding,
    kind: &str,
) -> Result<()> {
    let expected = shape
        .iter()
        .copied()
        .map(i32::try_from)
        .collect::<std::result::Result<Vec<_>, _>>()?;
    if array.dtype()? == dtype && array.shape()? == expected {
        Ok(())
    } else {
        Err(invalid(binding, &format!("{kind} dtype or shape differs")))
    }
}

fn require_dtype(array: &Array, dtype: Dtype, binding: &TensorBinding, kind: &str) -> Result<()> {
    if array.dtype()? == dtype {
        Ok(())
    } else {
        Err(invalid(binding, &format!("{kind} dtype differs")))
    }
}

fn require_bytes(array: &Array, bytes: usize, binding: &TensorBinding, kind: &str) -> Result<()> {
    if array.byte_len()? == bytes {
        Ok(())
    } else {
        Err(invalid(binding, &format!("{kind} byte size differs")))
    }
}

fn invalid(binding: &TensorBinding, reason: &str) -> Error {
    Error::InvalidQuantization(format!("{}: {reason}", binding.source))
}