libmir-cuda 0.3.0

CUDA inference backend for libmir
use mircuda::{DeviceBuffer, Stream, bf16};
use models::weights::{BitsAndBytes4BitQuantization, TensorBinding, TensorStorage};

use crate::{
    CudaBackend, CudaTensorSet, Error, Result,
    kernels::{BitsAndBytes4BitLaunch, BitsAndBytes4BitLinear, BitsAndBytes4BitSpec},
};

#[derive(Clone, Debug)]
pub struct BitsAndBytes4BitWeight {
    weight: DeviceBuffer<u8>,
    absmax: DeviceBuffer<u8>,
    quant_map: DeviceBuffer<u8>,
    nested_absmax: DeviceBuffer<u8>,
    nested_quant_map: DeviceBuffer<u8>,
    nested_offset: f32,
    format: BitsAndBytes4BitQuantization,
    input: usize,
    output: usize,
}

impl BitsAndBytes4BitWeight {
    pub(crate) fn load_binding(
        tensors: &CudaTensorSet,
        binding: &TensorBinding,
        input: usize,
        output: usize,
    ) -> Result<Self> {
        let TensorStorage::BitsAndBytes4Bit {
            format,
            absmax,
            quant_map,
            nested_absmax,
            nested_quant_map,
            nested_offset_bits,
            ..
        } = &binding.storage
        else {
            return Err(Error::InvalidQuantizedGemv("binding is not bitsandbytes 4-bit"));
        };
        let weight_tensor = required(tensors, &binding.source)?;
        let absmax_tensor = required(tensors, absmax)?;
        let map_tensor = required(tensors, quant_map)?;
        let nested_absmax_tensor =
            nested_absmax.as_deref().map(|name| required(tensors, name)).transpose()?;
        let nested_map_tensor =
            nested_quant_map.as_deref().map(|name| required(tensors, name)).transpose()?;
        let dummy = absmax_tensor.raw_u8()?;
        let value = Self {
            weight: weight_tensor.raw_u8()?,
            absmax: absmax_tensor.raw_u8()?,
            quant_map: map_tensor.raw_u8()?,
            nested_absmax: nested_absmax_tensor
                .map_or_else(|| Ok(dummy.clone()), crate::CudaTensor::raw_u8)?,
            nested_quant_map: nested_map_tensor
                .map_or_else(|| Ok(dummy), crate::CudaTensor::raw_u8)?,
            nested_offset: nested_offset_bits.map_or(0.0, f32::from_bits),
            format: *format,
            input,
            output,
        };
        value.validate()?;
        Ok(value)
    }

    pub(in crate::backend) fn validate(&self) -> Result<()> {
        let elements = self
            .input
            .checked_mul(self.output)
            .ok_or(Error::InvalidQuantizedGemv("bitsandbytes matrix size overflows"))?;
        require(self.weight.len(), elements.div_ceil(2), "weight")?;
        require(self.quant_map.len(), 16 * 4, "quant map")?;
        let blocks = elements.div_ceil(self.format.block_size);
        if let Some(nested) = self.format.nested_block_size {
            require(self.absmax.len(), blocks, "nested codes")?;
            require(self.nested_absmax.len(), blocks.div_ceil(nested) * 4, "nested absmax")?;
            require(self.nested_quant_map.len(), 256 * 4, "nested quant map")?;
        } else {
            require(self.absmax.len(), blocks * 4, "absmax")?;
        }
        Ok(())
    }
}

#[derive(Clone, Debug)]
pub struct BitsAndBytes4BitBf16Linear {
    operation: BitsAndBytes4BitLinear,
    stream: Stream,
}

impl BitsAndBytes4BitBf16Linear {
    pub(in crate::backend) fn new(
        backend: &CudaBackend,
        tokens: usize,
        weight: &BitsAndBytes4BitWeight,
    ) -> Result<Self> {
        weight.validate()?;
        let spec = BitsAndBytes4BitSpec::new(
            tokens,
            weight.input,
            weight.output,
            weight.format.block_size,
            weight.format.nested_block_size,
        )?;
        Ok(Self {
            operation: BitsAndBytes4BitLinear::compile(&backend.inner.compiler, spec)?,
            stream: backend.inner.stream.clone(),
        })
    }

    pub(in crate::backend) fn execute(
        &self,
        input: &DeviceBuffer<bf16>,
        weight: &BitsAndBytes4BitWeight,
        output: &mut DeviceBuffer<bf16>,
    ) -> Result<()> {
        weight.validate()?;
        self.operation.execute(
            &self.stream,
            &mut BitsAndBytes4BitLaunch {
                input,
                weight: &weight.weight,
                absmax: &weight.absmax,
                quant_map: &weight.quant_map,
                nested_absmax: &weight.nested_absmax,
                nested_quant_map: &weight.nested_quant_map,
                nested_offset: weight.nested_offset,
                output,
            },
        )
    }
}

fn required<'a>(tensors: &'a CudaTensorSet, name: &str) -> Result<&'a crate::CudaTensor> {
    tensors.get(name).ok_or_else(|| Error::MissingTensor(name.into()))
}

fn require(actual: usize, expected: usize, kind: &'static str) -> Result<()> {
    if actual == expected {
        Ok(())
    } else {
        Err(Error::InvalidQuantizedGemv(kind))
    }
}