libmir-metal 0.3.0

Metal inference backend for libmir
use super::Kernels;
use crate::engine::{Array, Dtype, Error, Result, Stream};

mirtal::metal_kernel! {
    fn nvfp4_gathered_linear {
        name: "mirmir_nvfp4_gathered_linear",
        templates: [
            T: dtype = bf16, INPUT: int = 2880, OUTPUT: int = 2880,
            MATRICES: int = 32, SELECTIONS: int = 1, PER_MATRIX_GLOBAL: bool = false,
        ],
        inputs: [input: T, weight: u8, scales: u8, global_scale: f32, indices: u32],
        outputs: [output: T],
        source: file "kernels/nvfp4_gathered_linear.metal",
        header: inline r"
            inline float mirmir_e4m3_to_float(uchar encoded) {
              uint magnitude = uint(encoded & 0x7fu);
              uint exponent = magnitude >> 3;
              uint mantissa = magnitude & 7u;
              float value = exponent == 0u
                  ? ldexp(float(mantissa), -9)
                  : ldexp(float(8u + mantissa), int(exponent) - 10);
              return (encoded & 0x80u) == 0u ? value : -value;
            }
        ",
        row_contiguous: true,
        atomic_outputs: false,
    }
}

#[derive(Debug)]
pub(super) struct NvFp4GatheredLinearKernel {
    kernel: mirtal::MetalKernel<5, 1>,
}

impl NvFp4GatheredLinearKernel {
    pub(super) fn new() -> Result<Self> {
        Ok(Self { kernel: nvfp4_gathered_linear()? })
    }

    fn execute(
        &self,
        inputs: [&Array; 5],
        input_features: usize,
        output_features: usize,
        matrices: usize,
        per_matrix_global: bool,
        stream: &Stream,
    ) -> Result<Array> {
        let input_shape = inputs[0].shape()?;
        let Some((&physical_input, matrix_shape)) = input_shape.split_last() else {
            return Err(Error::InvalidModel("gathered NVFP4 input shape is empty".into()));
        };
        if usize::try_from(physical_input)? != input_features {
            return Err(Error::InvalidModel("gathered NVFP4 input width differs from plan".into()));
        }
        let Some((&rows, input_prefix)) = matrix_shape.split_last() else {
            return Err(Error::InvalidModel("gathered NVFP4 input must have matrix rank".into()));
        };
        let indices_shape = inputs[4].shape()?;
        let input_rows = product(input_prefix)?;
        let assignments = product(&indices_shape)?;
        if inputs[4].dtype()? != Dtype::Uint32
            || rows != 1
            || input_prefix.len() != indices_shape.len()
            || !input_prefix
                .iter()
                .zip(&indices_shape)
                .all(|(input, selected)| *input == 1 || input == selected)
            || input_rows == 0
            || !assignments.is_multiple_of(input_rows)
        {
            return Err(Error::InvalidModel("gathered NVFP4 indices differ from input".into()));
        }
        let selections = assignments / input_rows;
        let mut output_shape = indices_shape
            .iter()
            .copied()
            .map(usize::try_from)
            .collect::<std::result::Result<Vec<_>, _>>()?;
        output_shape.push(usize::try_from(rows)?);
        output_shape.push(output_features);
        let output =
            mirtal::OutputSpec::new(mirtal::Shape::new(output_shape)?, mirtal::DType::Bfloat16);
        let templates = [
            mirtal::TemplateArg::dtype("T", mirtal::DType::Bfloat16),
            integer("INPUT", input_features)?,
            integer("OUTPUT", output_features)?,
            integer("MATRICES", matrices)?,
            integer("SELECTIONS", selections)?,
            mirtal::TemplateArg::bool("PER_MATRIX_GLOBAL", per_matrix_global),
        ];
        let [output] = self.kernel.dispatch(
            stream.native(),
            inputs.map(Array::native),
            &[output],
            &mirtal::Dispatch::new([output_features * 32, assignments, 1], [32, 1, 1])
                .templates(templates),
        )?;
        Array::from_native(output)
    }
}

impl Kernels {
    pub(crate) fn nvfp4_gathered_linear(
        &self,
        inputs: [&Array; 5],
        input_features: usize,
        output_features: usize,
        matrices: usize,
        per_matrix_global: bool,
        stream: &Stream,
    ) -> Result<Array> {
        self.nvfp4_gathered_linear.execute(
            inputs,
            input_features,
            output_features,
            matrices,
            per_matrix_global,
            stream,
        )
    }
}

fn product(shape: &[i32]) -> Result<usize> {
    shape.iter().try_fold(1_usize, |total, value| {
        total.checked_mul(usize::try_from(*value)?).ok_or(Error::ShapeOverflow)
    })
}

fn integer(name: &'static str, value: usize) -> Result<mirtal::TemplateArg> {
    Ok(mirtal::TemplateArg::int(name, i32::try_from(value)?))
}