libmir-metal 0.1.0

Metal inference backend for libmir
use std::sync::{Mutex, MutexGuard};

use super::super::{Error, Result};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScratchSpec {
    pub(crate) query_heads: usize,
    pub(crate) kv_heads: usize,
    pub(crate) page_capacity: usize,
    pub(crate) blocks: usize,
    pub(crate) head_dim: usize,
    pub(crate) page_size: usize,
    pub(crate) scale_bits: u32,
    pub(crate) dtype: mirtal::DType,
}

#[derive(Debug, Default)]
pub struct PagedAttentionScratch {
    state: Mutex<ScratchState>,
}

#[derive(Debug, Default)]
pub struct ScratchState {
    spec: Option<ScratchSpec>,
    partials: Option<mirtal::Array>,
    sums: Option<mirtal::Array>,
    maximums: Option<mirtal::Array>,
    barrier: Option<mirtal::Array>,
    partial_kernel: Option<mirtal::PreparedAliasing<9, 3>>,
    reduce_dispatch: Option<mirtal::Dispatch>,
    reduce_output: Option<[mirtal::OutputSpec; 1]>,
}

impl PagedAttentionScratch {
    pub(crate) fn lock(&self) -> Result<MutexGuard<'_, ScratchState>> {
        self.state.lock().map_or_else(
            |_| Err(Error::InvalidModel("paged attention scratch lock was poisoned".into())),
            Ok,
        )
    }
}

impl ScratchState {
    pub(crate) fn prepare(
        &mut self,
        spec: ScratchSpec,
        stream: &mirtal::Stream,
        library: &mirtal::MetalLibrary,
        function: &'static str,
    ) -> Result<()> {
        if self.spec == Some(spec) {
            return Ok(());
        }
        let graph = stream.graph();
        let partial_shape =
            mirtal::Shape::new([1, spec.query_heads, 1, spec.blocks, spec.head_dim])?;
        let statistics_shape = mirtal::Shape::new([1, spec.query_heads, 1, spec.blocks])?;
        self.partials = Some(graph.full(&partial_shape, 0.0, spec.dtype)?);
        self.sums = Some(graph.full(&statistics_shape, 0.0, mirtal::DType::Float32)?);
        self.maximums = Some(graph.full(&statistics_shape, 0.0, mirtal::DType::Float32)?);
        self.barrier = None;
        let dispatch = mirtal::AliasingDispatch::new([5, 6, 7])
            .constants([
                u32::try_from(spec.query_heads)?,
                u32::try_from(spec.kv_heads)?,
                u32::try_from(spec.page_capacity)?,
                u32::try_from(spec.blocks)?,
                u32::try_from(spec.page_size)?,
                spec.scale_bits,
            ])
            .grid([32, spec.query_heads, spec.blocks])
            .threadgroup([32, spec.query_heads / spec.kv_heads, 1]);
        self.partial_kernel = Some(library.export(function)?.prepare_aliasing(dispatch)?);
        self.reduce_dispatch =
            Some(mirtal::Dispatch::new([1_024, spec.query_heads, 1], [1_024, 1, 1]).templates([
                mirtal::TemplateArg::dtype("T", spec.dtype),
                mirtal::TemplateArg::int("HEAD_DIM", i32::try_from(spec.head_dim)?),
                mirtal::TemplateArg::int("BLOCKS", i32::try_from(spec.blocks)?),
            ]));
        self.reduce_output = Some([mirtal::OutputSpec::new(
            mirtal::Shape::new([1, spec.query_heads, 1, spec.head_dim])?,
            spec.dtype,
        )]);
        self.spec = Some(spec);
        Ok(())
    }

    pub(crate) fn reduce(&self) -> Result<(&mirtal::Dispatch, &[mirtal::OutputSpec; 1])> {
        Ok((
            self.reduce_dispatch
                .as_ref()
                .ok_or(Error::NullHandle("paged attention reduce dispatch"))?,
            self.reduce_output
                .as_ref()
                .ok_or(Error::NullHandle("paged attention reduce output"))?,
        ))
    }

    pub(crate) fn partial<'a>(
        &'a mut self,
        initial_barrier: &'a mirtal::Array,
    ) -> Result<(&'a mut mirtal::PreparedAliasing<9, 3>, [&'a mirtal::Array; 4])> {
        let kernel = self
            .partial_kernel
            .as_mut()
            .ok_or(Error::NullHandle("paged attention partial kernel"))?;
        let arrays = [
            self.partials.as_ref().ok_or(Error::NullHandle("paged attention partials"))?,
            self.sums.as_ref().ok_or(Error::NullHandle("paged attention sums"))?,
            self.maximums.as_ref().ok_or(Error::NullHandle("paged attention maximums"))?,
            self.barrier.as_ref().unwrap_or(initial_barrier),
        ];
        Ok((kernel, arrays))
    }

    pub(crate) fn update(
        &mut self,
        [partials, sums, maximums]: [mirtal::Array; 3],
        barrier: mirtal::Array,
    ) {
        self.partials = Some(partials);
        self.sums = Some(sums);
        self.maximums = Some(maximums);
        self.barrier = Some(barrier);
    }
}