libmir-metal 0.1.0

Metal inference backend for libmir
use models::layout::AttentionLayerType;

use super::*;

#[test]
fn keeps_recurrent_gated_delta_state_on_the_explicit_gpu_stream() -> Result<()> {
    let stream = Stream::new_gpu()?;
    let mut state = GatedDeltaState::new()?;
    let inputs = inputs(&[2.0, 4.0, 3.0, 5.0], 2)?;
    let output = state.update(inputs.as_borrowed(), &stream)?;
    output.async_eval()?;
    stream.synchronize()?;

    assert_close(&output.to_vec_f32()?, &[1.0, 2.0, 1.75, 3.0]);
    assert_eq!(state.offset()?, 2);
    assert_close(&state.values()?.to_vec_f32_on_stream(&stream)?, &[1.75, 3.0]);
    Ok(())
}

#[test]
fn uses_the_metal_recurrence_kernel_for_supported_key_dimensions() -> Result<()> {
    let stream = Stream::new_gpu()?;
    let mut state = GatedDeltaState::new()?;
    let mut query_and_key = vec![0.0; 64];
    query_and_key[0] = 1.0;
    query_and_key[32] = 1.0;
    let inputs = GatedDeltaInputs {
        query: &Array::from_f32(&query_and_key, &[1, 2, 1, 32])?,
        key: &Array::from_f32(&query_and_key, &[1, 2, 1, 32])?,
        value: &Array::from_f32(&[2.0, 4.0], &[1, 2, 1, 1])?,
        alpha: &Array::from_f32(&[0.0, 0.0], &[1, 2, 1])?,
        beta: &Array::from_f32(&[0.0, 0.0], &[1, 2, 1])?,
        a_log: &Array::from_f32(&[0.0], &[1])?,
        dt_bias: &Array::from_f32(&[0.0], &[1])?,
    };
    let output = state.update(inputs, &stream)?;

    output.async_eval()?;
    stream.synchronize()?;
    assert_close(&output.to_vec_f32()?, &[1.0, 2.25]);
    assert_close(&state.values()?.to_vec_f32_on_stream(&stream)?[..1], &[2.25]);
    Ok(())
}

#[test]
fn fused_decode_matches_the_general_recurrence_step() -> Result<()> {
    let stream = Stream::new_gpu()?;
    let mut general = GatedDeltaState::new()?;
    let mut fused = GatedDeltaState::new()?;
    for step in 0..2 {
        let general_inputs = decode_inputs(step)?;
        let fused_inputs = decode_inputs(step)?;
        let general_output = general.update(general_inputs.as_borrowed(), &stream)?;
        let fused_output = fused.update_fused(fused_inputs.as_borrowed(), &stream)?;
        general_output.async_eval()?;
        fused_output.async_eval()?;
        stream.synchronize()?;
        assert_eq!(general_output.to_vec_f32()?, fused_output.to_vec_f32()?);
        assert_eq!(
            general.values()?.to_vec_f32_on_stream(&stream)?,
            fused.values()?.to_vec_f32_on_stream(&stream)?,
        );
    }
    Ok(())
}

#[test]
fn snapshots_recurrent_state_without_a_host_round_trip() -> Result<()> {
    let stream = Stream::new_gpu()?;
    let mut original = GatedDeltaState::new()?;
    let first = inputs(&[2.0, 4.0], 1)?;
    let _first_output = original.update(first.as_borrowed(), &stream)?;
    let mut snapshot = original.snapshot()?;
    let second = inputs(&[3.0, 5.0], 1)?;
    let original_output = original.update(second.as_borrowed(), &stream)?;
    let snapshot_inputs = inputs(&[3.0, 5.0], 1)?;
    let snapshot_output = snapshot.update(snapshot_inputs.as_borrowed(), &stream)?;

    original_output.async_eval()?;
    snapshot_output.async_eval()?;
    stream.synchronize()?;
    assert_close(&original_output.to_vec_f32()?, &snapshot_output.to_vec_f32()?);
    assert_eq!(original.offset()?, 2);
    assert_eq!(snapshot.offset()?, 2);
    Ok(())
}

#[test]
fn snapshots_mixed_linear_and_key_value_cache_at_a_common_offset() -> Result<()> {
    let mut cache = DecoderCache::new_hybrid_linear(
        &[AttentionLayerType::Linear, AttentionLayerType::Full],
        16,
    )?;

    assert_eq!(cache.cached_tokens()?, 0);
    assert_eq!(cache.snapshot_at(0)?.cached_tokens()?, 0);
    assert!(cache.snapshot_at(1).is_err());
    cache.reset()?;
    Ok(())
}

#[test]
fn keeps_depthwise_convolution_history_in_a_state_snapshot() -> Result<()> {
    let stream = Stream::new_gpu()?;
    let mut original = GatedDeltaState::new()?;
    let weight = Array::from_f32(&[1.0, 1.0], &[1, 2, 1])?;
    let first = Array::from_f32(&[1.0, 2.0], &[1, 2, 1])?;
    let first_output = original.convolve_silu(&first, &weight, &stream)?;
    let mut snapshot = original.snapshot()?;
    let next = Array::from_f32(&[3.0], &[1, 1, 1])?;
    let original_output = original.convolve_silu(&next, &weight, &stream)?;
    let snapshot_output = snapshot.convolve_silu(&next, &weight, &stream)?;

    first_output.async_eval()?;
    original_output.async_eval()?;
    snapshot_output.async_eval()?;
    stream.synchronize()?;
    assert_eq!(first_output.shape()?, vec![1, 2, 1]);
    assert_close(&original_output.to_vec_f32()?, &snapshot_output.to_vec_f32()?);
    Ok(())
}

struct Inputs {
    query: Array,
    key: Array,
    value: Array,
    alpha: Array,
    beta: Array,
    a_log: Array,
    dt_bias: Array,
}

impl Inputs {
    fn as_borrowed(&self) -> GatedDeltaInputs<'_> {
        GatedDeltaInputs {
            query: &self.query,
            key: &self.key,
            value: &self.value,
            alpha: &self.alpha,
            beta: &self.beta,
            a_log: &self.a_log,
            dt_bias: &self.dt_bias,
        }
    }
}

fn inputs(values: &[f32], tokens: i32) -> Result<Inputs> {
    let heads = 2;
    Ok(Inputs {
        query: Array::from_f32(
            &vec![1.0; usize::try_from(tokens * heads)? / 2],
            &[1, tokens, 1, 1],
        )?,
        key: Array::from_f32(&vec![1.0; usize::try_from(tokens * heads)? / 2], &[1, tokens, 1, 1])?,
        value: Array::from_f32(values, &[1, tokens, heads, 1])?,
        alpha: Array::from_f32(&vec![0.0; usize::try_from(tokens * heads)?], &[1, tokens, heads])?,
        beta: Array::from_f32(&vec![0.0; usize::try_from(tokens * heads)?], &[1, tokens, heads])?,
        a_log: Array::from_f32(&[0.0, 0.0], &[heads])?,
        dt_bias: Array::from_f32(&[0.0, 0.0], &[heads])?,
    })
}

fn decode_inputs(step: usize) -> Result<Inputs> {
    let scale = f32::from(u16::try_from(step)?) + 1.0;
    let mut query = vec![0.0; 32];
    let mut key = vec![0.0; 32];
    for index in 0..32 {
        query[index] = scale * f32::from(u16::try_from(index + 1)?) / 64.0;
        key[index] = f32::from(u16::try_from(33 - index)?) / 64.0;
    }
    Ok(Inputs {
        query: Array::from_f32(&query, &[1, 1, 1, 32])?,
        key: Array::from_f32(&key, &[1, 1, 1, 32])?,
        value: Array::from_f32(&[0.25, -0.5, 0.75, 1.25], &[1, 1, 2, 2])?,
        alpha: Array::from_f32(&[-0.3, 0.2], &[1, 1, 2])?,
        beta: Array::from_f32(&[0.4, -0.7], &[1, 1, 2])?,
        a_log: Array::from_f32(&[0.1, -0.2], &[2])?,
        dt_bias: Array::from_f32(&[0.05, -0.1], &[2])?,
    })
}

fn assert_close(actual: &[f32], expected: &[f32]) {
    assert_eq!(actual.len(), expected.len());
    for (actual, expected) in actual.iter().zip(expected) {
        assert!((actual - expected).abs() < 1.0e-5, "{actual} != {expected}");
    }
}