libmir-metal 0.2.0

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

use super::layer::EncoderLayer;
use crate::engine::{Array, DenseEmbedding, DenseLinear, LayerNorm, ModelTensors, Result, Stream};

#[derive(Debug)]
pub struct SequenceScoringModel {
    words: DenseEmbedding,
    token_types: Option<DenseEmbedding>,
    embedding_norm: LayerNorm,
    layers: Vec<EncoderLayer>,
    pooler: DenseLinear,
    classifier: DenseLinear,
}

impl SequenceScoringModel {
    pub fn load(tensors: &ModelTensors, config: &EncoderConfig, stream: &Stream) -> Result<Self> {
        if !config.packed_qkv || config.hidden_activation != "gelu" || config.num_labels != 1 {
            return Err(crate::engine::Error::InvalidModel(
                "unsupported sequence-scoring encoder contract".into(),
            ));
        }
        let eps = config.layer_norm_eps.to_string().parse()?;
        let mut layers = Vec::with_capacity(config.num_hidden_layers);
        for index in 0..config.num_hidden_layers {
            layers.push(EncoderLayer::load(tensors, index, config, stream)?);
        }
        Ok(Self {
            words: DenseEmbedding::load(tensors, "new.embeddings.word_embeddings")?,
            token_types: (config.type_vocab_size > 0)
                .then(|| DenseEmbedding::load(tensors, "new.embeddings.token_type_embeddings"))
                .transpose()?,
            embedding_norm: LayerNorm::load(tensors, "new.embeddings.LayerNorm", eps)?,
            layers,
            pooler: DenseLinear::load(tensors, "new.pooler.dense", stream)?,
            classifier: DenseLinear::load(tensors, "classifier", stream)?,
        })
    }

    pub fn score(&self, token_ids: &[u32], stream: &Stream) -> Result<f32> {
        if token_ids.is_empty() {
            return Err(crate::engine::Error::InvalidModel(
                "sequence scoring input is empty".into(),
            ));
        }
        let sequence = i32::try_from(token_ids.len())?;
        let ids = Array::from_u32(token_ids, &[1, sequence])?;
        let mut hidden = self.words.lookup(&ids, stream)?;
        if let Some(token_types) = &self.token_types {
            let zeros = vec![0; token_ids.len()];
            hidden = hidden.add(
                &token_types.lookup(&Array::from_u32(&zeros, &[1, sequence])?, stream)?,
                stream,
            )?;
        }
        hidden = self.embedding_norm.forward(&hidden, stream)?;
        for layer in &self.layers {
            hidden = layer.forward(&hidden, stream)?;
        }
        let cls =
            hidden.slice(&[0, 0, 0], &[1, 1, usize::try_from(hidden.shape()?[2])?], stream)?;
        let pooled = self.pooler.forward(&cls, stream)?.tanh(stream)?;
        let score = self.classifier.forward(&pooled, stream)?.to_vec_f32_on_stream(stream)?;
        score.first().copied().ok_or_else(|| {
            crate::engine::Error::InvalidModel("sequence classifier returned no score".into())
        })
    }
}