libmir-metal 0.3.0

Metal inference backend for libmir
use super::{
    Array, Error, QuantizedArrays, Result, Stream,
    fused_gate_up::{concatenate, split_last},
};

#[derive(Debug)]
pub struct FusedExpertGateUp {
    arrays: QuantizedArrays,
    experts: usize,
    input_width: usize,
    gate_width: usize,
    up_width: usize,
    group_size: i32,
    bits: i32,
}

impl FusedExpertGateUp {
    pub(crate) fn new(
        gate: &QuantizedArrays,
        up: &QuantizedArrays,
        group_size: i32,
        bits: i32,
        stream: &Stream,
    ) -> Result<Self> {
        let gate_shape = gate.weight.native().shape()?;
        let up_shape = up.weight.native().shape()?;
        let gate_shape = gate_shape.dimensions();
        let up_shape = up_shape.dimensions();
        if gate_shape.len() != 3
            || up_shape.len() != 3
            || gate_shape[0] != up_shape[0]
            || gate_shape[2] != up_shape[2]
        {
            return Err(Error::InvalidQuantization(
                "fused expert gate/up weights are incompatible".into(),
            ));
        }
        let input_width = logical_input_width(gate, group_size)?;
        if logical_input_width(up, group_size)? != input_width {
            return Err(Error::InvalidQuantization(
                "fused expert gate/up logical input widths are incompatible".into(),
            ));
        }
        Ok(Self {
            arrays: concatenate(gate, up, 1, group_size, bits, stream)?,
            experts: gate_shape[0],
            input_width,
            gate_width: gate_shape[1],
            up_width: up_shape[1],
            group_size,
            bits,
        })
    }

    pub(crate) fn warm(&self) -> Result<()> {
        self.arrays.weight.async_eval()?;
        self.arrays.scales.async_eval()?;
        self.arrays.biases.async_eval()
    }

    pub(crate) fn forward(
        &self,
        input: &Array,
        indices: &Array,
        stream: &Stream,
    ) -> Result<(Array, Array)> {
        let output = input.gather_qmm(
            &self.arrays,
            indices,
            mirtal::GatherQmmOptions { transpose: true, sorted_indices: false },
            stream,
        )?;
        let (gate, up) = split_last(&output, self.gate_width, stream)?;
        Ok((gate.astype_like(input, stream)?, up.astype_like(input, stream)?))
    }

    pub(crate) const fn tuning_geometry(&self) -> (usize, usize, usize, usize, i32, i32) {
        (
            self.experts, self.input_width, self.gate_width, self.up_width, self.group_size,
            self.bits,
        )
    }
}

fn logical_input_width(arrays: &QuantizedArrays, group_size: i32) -> Result<usize> {
    let shape = arrays.scales.native().shape()?;
    let groups = shape.dimensions().last().copied().ok_or(Error::ShapeOverflow)?;
    groups.checked_mul(usize::try_from(group_size)?).ok_or(Error::ShapeOverflow)
}