libmir-metal 0.3.0

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

use super::{ClampedRoutedConfig, ExpertLayout};
use crate::engine::{Array, Dtype, Error, ModelTensors, Result};

pub(super) fn native_layout(
    tensors: &ModelTensors,
    gate_up: &TensorBinding,
    down: &TensorBinding,
    config: ClampedRoutedConfig,
) -> Result<ExpertLayout> {
    let Some(BlockProjectionLayout::FusedGateUpBank { experts, interleaved: true }) =
        gate_up.block_projection_layout()
    else {
        return Err(invalid(gate_up, "requires an interleaved gate/up expert bank"));
    };
    let (gate_up_scales, gate_up_bias) =
        companions(gate_up, BlockProjectionLayout::FusedGateUpBank { experts, interleaved: true })?;
    let (down_scales, down_bias) =
        companions(down, BlockProjectionLayout::MatrixBank { matrices: experts })?;
    let hidden = usize::try_from(config.hidden)?;
    let intermediate = usize::try_from(config.intermediate)?;
    let gate_rows = intermediate.checked_mul(2).ok_or(Error::ShapeOverflow)?;
    let layout = ExpertLayout::Native {
        gate_up_blocks: tensors.get(&gate_up.source)?,
        gate_up_scales: tensors.get(gate_up_scales)?,
        gate_up_bias: tensors.get(gate_up_bias)?,
        down_blocks: tensors.get(&down.source)?,
        down_scales: tensors.get(down_scales)?,
        down_bias: tensors.get(down_bias)?,
    };
    validate(&layout, experts, hidden, intermediate, gate_rows, gate_up, down)?;
    Ok(layout)
}

fn companions(binding: &TensorBinding, expected: BlockProjectionLayout) -> Result<(&str, &str)> {
    let TensorStorage::BlockQuantized {
        format: BlockQuantization::MXFP4,
        scales,
        bias: Some(bias),
        ..
    } = &binding.storage
    else {
        return Err(invalid(binding, "requires MXFP4 scales and BF16 bias"));
    };
    if binding.block_projection_layout() != Some(expected) {
        return Err(invalid(binding, "matrix-bank layout differs from routed role"));
    }
    Ok((scales, bias))
}

#[allow(clippy::too_many_arguments)]
fn validate(
    layout: &ExpertLayout,
    experts: usize,
    hidden: usize,
    intermediate: usize,
    gate_rows: usize,
    gate: &TensorBinding,
    down: &TensorBinding,
) -> Result<()> {
    let ExpertLayout::Native {
        gate_up_blocks,
        gate_up_scales,
        gate_up_bias,
        down_blocks,
        down_scales,
        down_bias,
    } = layout
    else {
        return Err(Error::InvalidModel("expected native MXFP4 experts".into()));
    };
    require(gate_up_blocks, Dtype::Uint8, &[experts, gate_rows, hidden / 32, 16], gate)?;
    require(gate_up_scales, Dtype::Uint8, &[experts, gate_rows, hidden / 32], gate)?;
    require(gate_up_bias, Dtype::Bfloat16, &[experts, gate_rows], gate)?;
    require(down_blocks, Dtype::Uint8, &[experts, hidden, intermediate / 32, 16], down)?;
    require(down_scales, Dtype::Uint8, &[experts, hidden, intermediate / 32], down)?;
    require(down_bias, Dtype::Bfloat16, &[experts, hidden], down)
}

fn require(array: &Array, dtype: Dtype, shape: &[usize], binding: &TensorBinding) -> 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, "MXFP4 expert companion dtype or shape differs"))
    }
}

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

#[cfg(test)]
mod tests {
    use models::weights::{
        BindingTransform, ExpertProjectionRole, LayerTensorRole, LogicalTensorRole, TensorPacking,
    };

    use super::*;

    #[test]
    fn requires_typed_interleaved_expert_bank() -> Result<()> {
        let valid = binding(TensorPacking::InterleavedGateUp);
        let expected = BlockProjectionLayout::FusedGateUpBank { experts: 8, interleaved: true };
        assert_eq!(companions(&valid, expected)?, ("scales", "bias"));

        let invalid = binding(TensorPacking::Separate);
        assert!(companions(&invalid, expected).is_err());
        Ok(())
    }

    fn binding(packing: TensorPacking) -> TensorBinding {
        TensorBinding {
            role: LogicalTensorRole::Layer {
                index: 0,
                tensor: LayerTensorRole::ExpertProjection {
                    expert: None,
                    projection: ExpertProjectionRole::GateUp,
                },
            },
            source: "blocks".into(),
            shape: vec![8, 64, 1, 16],
            logical_shape: Some(vec![8, 64, 32]),
            transforms: vec![
                BindingTransform::StackedExperts { count: 8 },
                BindingTransform::FusedGateUp { interleaved: true },
            ],
            storage: TensorStorage::BlockQuantized {
                format: BlockQuantization::MXFP4,
                scales: "scales".into(),
                global_scale: None,
                input_scale: None,
                bias: Some("bias".into()),
                packing,
            },
        }
    }
}