libmir-metal 0.1.0

Metal inference backend for libmir
use super::{HybridMoeLayerConfig, weights::AttentionWeights};
use crate::engine::{
    Array, FusedAttention, FusedKeyValue, KvCache, PagedContextMode, Result, RopeOptions, Stream,
    native_paged_attention_mode, paged_attention_min_context,
};

pub(super) struct DecodeContext<'a> {
    pub(super) cache: Option<&'a mut KvCache>,
    pub(super) position: i32,
    pub(super) causal: bool,
    pub(super) stream: &'a Stream,
}

pub(super) fn forward_decode(
    input: &Array,
    weights: &AttentionWeights,
    config: HybridMoeLayerConfig,
    fused_attention: Option<&FusedAttention>,
    fused_key_value: Option<&FusedKeyValue>,
    context: DecodeContext<'_>,
) -> Result<Array> {
    let DecodeContext { cache, position, causal, stream } = context;
    let sequence = input.shape()?.get(1).copied().ok_or_else(|| {
        crate::engine::Error::InvalidModel("attention input has no sequence axis".into())
    })?;
    let fused_attention = (sequence == 1)
        .then_some(fused_attention)
        .flatten()
        .map(|fused| fused.forward(input, stream))
        .transpose()?;
    let (queries, raw_keys, raw_values) = if let Some(output) = fused_attention {
        (output.query, output.key, output.value)
    } else {
        let fused_key_value = (sequence == 1)
            .then_some(fused_key_value)
            .flatten()
            .map(|fused| fused.forward(input, stream))
            .transpose()?;
        let (raw_keys, raw_values) = match fused_key_value {
            Some((key, value)) => (key, Some(value)),
            None => (
                weights.key.forward(input, stream)?,
                weights.value.as_ref().map(|value| value.forward(input, stream)).transpose()?,
            ),
        };
        (weights.query.forward(input, stream)?, raw_keys, raw_values)
    };
    let queries =
        queries.reshape(&[1, sequence, config.attention_heads, config.head_dim], stream)?;
    let queries = weights.query_norm.apply(&queries, config.rms_norm_eps, stream)?;
    let queries =
        rope_layout(&queries, weights.rope_frequencies.as_ref(), config, position, stream)?;

    let raw_keys = raw_keys.reshape(&[1, sequence, config.kv_heads, config.head_dim], stream)?;
    let values = if config.use_k_eq_v {
        raw_keys
            .rms_norm_unit(config.rms_norm_eps, stream)?
            .transpose(&[0, 2, 1, 3], stream)?
    } else {
        let raw_values = raw_values.ok_or_else(|| {
            crate::engine::Error::InvalidModel("missing hybrid MoE value projection".into())
        })?;
        raw_values
            .reshape(&[1, sequence, config.kv_heads, config.head_dim], stream)?
            .rms_norm_unit(config.rms_norm_eps, stream)?
            .transpose(&[0, 2, 1, 3], stream)?
    };
    let keys = weights.key_norm.apply(&raw_keys, config.rms_norm_eps, stream)?;
    let keys = rope_layout(&keys, weights.rope_frequencies.as_ref(), config, position, stream)?;
    let output = match cache {
        Some(cache) => {
            let mode = if sequence == 1 {
                native_paged_attention_mode(
                    config.head_dim,
                    config.attention_heads,
                    config.kv_heads,
                    usize::try_from(position)? + 1,
                    stream.config().cache.force_native_paged_attention,
                )
            } else {
                PagedContextMode::View
            };
            let context = cache.update_for_attention_mode(
                &keys,
                &values,
                stream,
                paged_attention_min_context(stream),
                mode,
            )?;
            if let Some(paged) = context.paged {
                queries.paged_scaled_dot_product_attention_with_scratch(
                    paged.attention(),
                    paged.scratch(),
                    1.0,
                    stream,
                )?
            } else if let Some(mask) = context.mask.as_ref() {
                queries.masked_scaled_dot_product_attention(
                    &context.keys, &context.values, 1.0, mask, stream,
                )?
            } else {
                queries.scaled_dot_product_attention(
                    &context.keys, &context.values, 1.0, causal, stream,
                )?
            }
        },
        None => queries.scaled_dot_product_attention(&keys, &values, 1.0, causal, stream)?,
    };
    let output = output.transpose(&[0, 2, 1, 3], stream)?;
    let output_width = config.attention_heads * config.head_dim;
    let output = output.reshape(&[1, sequence, output_width], stream)?;
    weights.output.forward(&output, stream)
}

pub(super) fn rope_layout(
    input: &Array,
    frequencies: Option<&Array>,
    config: HybridMoeLayerConfig,
    position: i32,
    stream: &Stream,
) -> Result<Array> {
    let input = input.transpose(&[0, 2, 1, 3], stream)?;
    frequencies.map_or_else(
        || {
            input.rope(
                RopeOptions {
                    dimensions: config.rope_dimensions,
                    traditional: false,
                    base: Some(config.rope_base),
                    scale: 1.0,
                    offset: position,
                },
                stream,
            )
        },
        |frequencies| {
            input.rope_with_frequencies(config.head_dim, false, frequencies, position, stream)
        },
    )
}