libmir-cuda 0.2.0

CUDA inference backend for libmir
use mircuda::{
    CompileOptions, Compiler, DeviceBuffer, LaunchConfig, Stream, TypedKernel, bf16, cuda_export,
    cuda_kernel_file,
};

use super::super::geometry::{narrow, product, require};
use crate::{Error, Result};

cuda_export!(
    Int4Kernel = "libmir_cuda_affine_embedding_bf16_int4"(
        weight: &DeviceBuffer<u32>, scales: &DeviceBuffer<bf16>, biases: &DeviceBuffer<bf16>,
        selected: &DeviceBuffer<u32>, output: &mut DeviceBuffer<bf16>, selected_start: u32,
        tokens: u32, vocab: u32, hidden: u32, group_size: u32, output_scale: f32,
    )
);
cuda_export!(
    Int8Kernel = "libmir_cuda_affine_embedding_bf16_int8"(
        weight: &DeviceBuffer<u32>, scales: &DeviceBuffer<bf16>, biases: &DeviceBuffer<bf16>,
        selected: &DeviceBuffer<u32>, output: &mut DeviceBuffer<bf16>, selected_start: u32,
        tokens: u32, vocab: u32, hidden: u32, group_size: u32, output_scale: f32,
    )
);

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AffineEmbeddingSpec {
    pub vocab: usize,
    pub hidden: usize,
    pub group_size: usize,
    pub bits: usize,
    pub output_scale: f32,
}

#[derive(Clone, Debug)]
pub struct AffineEmbedding {
    kernel: Kernel,
    spec: AffineEmbeddingSpec,
}

#[derive(Clone, Debug)]
enum Kernel {
    Int4(TypedKernel<Int4Kernel>),
    Int8(TypedKernel<Int8Kernel>),
}

impl AffineEmbedding {
    pub fn compile(compiler: &Compiler, spec: AffineEmbeddingSpec) -> Result<Self> {
        validate(spec)?;
        let source = cuda_kernel_file!("../../../kernels/affine_embedding_bf16.cu");
        let module = compiler.compile(source, &CompileOptions::default())?;
        let kernel = match spec.bits {
            4 => Kernel::Int4(module.kernel()?),
            8 => Kernel::Int8(module.kernel()?),
            _ => return Err(Error::InvalidQuantizedGemv("unsupported embedding precision")),
        };
        Ok(Self { kernel, spec })
    }

    #[allow(clippy::too_many_arguments)]
    pub fn execute(
        &self,
        stream: &Stream,
        weight: &DeviceBuffer<u32>,
        scales: &DeviceBuffer<bf16>,
        biases: &DeviceBuffer<bf16>,
        selected: &DeviceBuffer<u32>,
        selected_start: usize,
        tokens: usize,
        output: &mut DeviceBuffer<bf16>,
    ) -> Result<()> {
        if tokens == 0 {
            return Err(Error::InvalidQuantizedGemv("embedding token batch is empty"));
        }
        let values_per_word = 32 / self.spec.bits;
        require(
            "affine embedding weight",
            product(self.spec.vocab, self.spec.hidden / values_per_word)?,
            weight.len(),
        )?;
        let groups = product(self.spec.vocab, self.spec.hidden / self.spec.group_size)?;
        require("affine embedding scales", groups, scales.len())?;
        require("affine embedding biases", groups, biases.len())?;
        require(
            "affine embedding selected",
            selected_start
                .checked_add(tokens)
                .ok_or(Error::InvalidQuantizedGemv("embedding token index overflow"))?,
            selected.len(),
        )?;
        let elements = product(tokens, self.spec.hidden)?;
        require("affine embedding output", elements, output.len())?;
        let launch = LaunchConfig {
            grid: (narrow(elements.div_ceil(256))?, 1, 1),
            block: (256, 1, 1),
            shared_memory_bytes: 0,
        };
        let arguments = (
            weight,
            scales,
            biases,
            selected,
            output,
            narrow(selected_start)?,
            narrow(tokens)?,
            narrow(self.spec.vocab)?,
            narrow(self.spec.hidden)?,
            narrow(self.spec.group_size)?,
            self.spec.output_scale,
        );
        Ok(match &self.kernel {
            Kernel::Int4(kernel) => kernel.launch(stream, launch, arguments),
            Kernel::Int8(kernel) => kernel.launch(stream, launch, arguments),
        }?)
    }
}

fn validate(spec: AffineEmbeddingSpec) -> Result<()> {
    let values_per_word = match spec.bits {
        4 => 8,
        8 => 4,
        _ => return Err(Error::InvalidQuantizedGemv("unsupported embedding precision")),
    };
    if spec.vocab == 0
        || spec.hidden == 0
        || spec.group_size == 0
        || !spec.hidden.is_multiple_of(spec.group_size)
        || !spec.hidden.is_multiple_of(values_per_word)
        || !spec.output_scale.is_finite()
        || spec.output_scale <= 0.0
    {
        return Err(Error::InvalidQuantizedGemv("invalid affine embedding geometry"));
    }
    Ok(())
}