libmir-metal 0.2.0

Metal inference backend for libmir
use super::*;
use crate::engine::kernels::MxFp4Shape;

#[test]
fn executes_native_mxfp4_clamped_experts() -> Result<()> {
    let stream = Stream::new_gpu()?;
    let shape = MxFp4Shape {
        tokens: 1,
        top_k: 1,
        hidden: 32,
        intermediate: 32,
    };
    let input = Array::from_f32(&[1.0; 32], &[1, 32])?;
    let gate_blocks = u8_array(&vec![0x11; 64 * 16], &[1, 64, 1, 16], &stream)?;
    let gate_scales = u8_array(&vec![127; 64], &[1, 64, 1], &stream)?;
    let gate_bias = Array::from_f32(&vec![0.0; 64], &[1, 64])?;
    let down_blocks = u8_array(&vec![0x11; 32 * 16], &[1, 32, 1, 16], &stream)?;
    let down_scales = u8_array(&[127; 32], &[1, 32, 1], &stream)?;
    let down_bias = Array::from_f32(&[0.0; 32], &[1, 32])?;
    let indices = Array::from_u32(&[0], &[1, 1])?;
    let routing = Array::from_f32(&[1.0], &[1, 1])?;
    let limit = Array::from_f32(&[7.0], &[])?;

    let activated = stream
        .mxfp4_gate_up([&input, &gate_blocks, &gate_scales, &gate_bias, &indices, &limit], shape)?;
    let output = stream.mxfp4_down(
        [&activated, &down_blocks, &down_scales, &down_bias, &indices, &routing],
        shape,
    )?;
    output.async_eval()?;
    stream.synchronize()?;

    let expected_activation = 7.0 / (1.0 + (-1.702_f32 * 7.0).exp()) * 8.0;
    let expected = expected_activation * 16.0;
    assert!(output.to_vec_f32()?.iter().all(|value| (value - expected).abs() < 0.1));
    Ok(())
}

#[test]
fn executes_mlx_u32_split_mxfp4_experts() -> Result<()> {
    let stream = Stream::new_gpu()?;
    let shape = MxFp4Shape {
        tokens: 1,
        top_k: 1,
        hidden: 32,
        intermediate: 32,
    };
    let input = Array::from_f32(&[1.0; 32], &[1, 32])?;
    let blocks = Array::from_u32(&[0x1111_1111; 32 * 4], &[1, 32, 4])?;
    let scales = u8_array(&[127; 32], &[1, 32, 1], &stream)?;
    let bias = Array::from_f32(&[0.0; 32], &[1, 32])?;
    let indices = Array::from_u32(&[0], &[1, 1])?;
    let routing = Array::from_f32(&[1.0], &[1, 1])?;
    let limit = Array::from_f32(&[7.0], &[])?;

    let activated = stream.mxfp4_split_gate_up(
        [&input, &blocks, &scales, &bias, &blocks, &scales, &bias, &indices, &limit],
        shape,
    )?;
    let output =
        stream.mxfp4_u32_down([&activated, &blocks, &scales, &bias, &indices, &routing], shape)?;
    output.async_eval()?;
    stream.synchronize()?;

    let expected_activation = 7.0 / (1.0 + (-1.702_f32 * 7.0).exp()) * 8.0;
    let expected = expected_activation * 16.0;
    assert!(output.to_vec_f32()?.iter().all(|value| (value - expected).abs() < 0.1));
    Ok(())
}

#[test]
fn learned_sink_participates_in_attention_normalization() -> Result<()> {
    let stream = Stream::new_gpu()?;
    let query = Array::from_f32(&[0.0], &[1, 1, 1, 1])?;
    let key = Array::from_f32(&[0.0], &[1, 1, 1, 1])?;
    let value = Array::from_f32(&[2.0], &[1, 1, 1, 1])?;
    let sink = Array::from_f32(&[0.0], &[1])?;
    let output =
        query.scaled_dot_product_attention_with_sinks(&key, &value, 1.0, false, &sink, &stream)?;
    output.async_eval()?;
    stream.synchronize()?;

    assert!((output.to_vec_f32()?[0] - 1.0).abs() < 1.0e-5);
    Ok(())
}

fn u8_array(values: &[u32], shape: &[i32], stream: &Stream) -> Result<Array> {
    Array::from_u32(values, shape)?.astype(Dtype::Uint8, stream)
}