libmir-cuda 0.1.0

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

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

const TOP_K: usize = 128;
const THREADS: u32 = 256;
const ITEMS_PER_THREAD: usize = 8;
const CHUNK: usize = THREADS as usize * ITEMS_PER_THREAD;

cuda_export!(CandidateKernel = "libmir_cuda_output_refine_candidates"(
    logits: &DeviceBuffer<bf16>, output: &mut DeviceBuffer<u64>, vocab: u32,
));
cuda_export!(MergeKernel = "libmir_cuda_output_refine_merge"(
    input: &DeviceBuffer<u64>, output: &mut DeviceBuffer<u64>, count: u32,
));
cuda_export!(RefineKernel = "libmir_cuda_output_refine_bf16"(
    input: &DeviceBuffer<bf16>, weight: &DeviceBuffer<bf16>,
    candidates: &DeviceBuffer<u64>, output: &mut DeviceBuffer<bf16>, columns: u32,
));

#[derive(Clone, Debug)]
pub struct Fp8RefinementKernels {
    candidates: TypedKernel<CandidateKernel>,
    merge: TypedKernel<MergeKernel>,
    refine: TypedKernel<RefineKernel>,
    spec: Fp8OutputSpec,
}

impl Fp8RefinementKernels {
    pub fn compile(compiler: &Compiler, spec: Fp8OutputSpec) -> Result<Self> {
        let source = cuda_kernel_file!("../../../kernels/output_fp8_refine.cu");
        let module = compiler.compile(
            source,
            &CompileOptions {
                extra_options: vec![
                    "--std=c++17".into(),
                    format!("--define-macro=LIBMIR_OUTPUT_REFINE_TOP_K={TOP_K}"),
                ],
                ..CompileOptions::default()
            },
        )?;
        Ok(Self {
            candidates: module.kernel()?,
            merge: module.kernel()?,
            refine: module.kernel()?,
            spec,
        })
    }

    pub fn workspace_elements(vocab: usize) -> Result<usize> {
        blocks(vocab)?
            .checked_mul(TOP_K)
            .ok_or(Error::InvalidDecoderKernel("FP8 refinement workspace overflow"))
    }

    #[must_use]
    pub const fn candidate_count() -> usize {
        TOP_K
    }

    pub fn execute(
        &self,
        stream: &Stream,
        input: &DeviceBuffer<bf16>,
        weight: &DeviceBuffer<bf16>,
        logits: &mut DeviceBuffer<bf16>,
        first: &mut DeviceBuffer<u64>,
        second: &mut DeviceBuffer<u64>,
    ) -> Result<()> {
        self.validate(input, weight, logits, first, second)?;
        let initial_blocks = blocks(self.spec.output_features)?;
        self.candidates.launch(
            stream,
            launch(initial_blocks)?,
            (logits, &mut *first, narrow(self.spec.output_features)?),
        )?;
        let mut count = initial_blocks * TOP_K;
        let mut in_first = true;
        while count > TOP_K {
            let next_blocks = blocks(count)?;
            if in_first {
                self.merge.launch(
                    stream,
                    launch(next_blocks)?,
                    (&*first, &mut *second, narrow(count)?),
                )?;
            } else {
                self.merge.launch(
                    stream,
                    launch(next_blocks)?,
                    (&*second, &mut *first, narrow(count)?),
                )?;
            }
            count = next_blocks * TOP_K;
            in_first = !in_first;
        }
        let candidates = if in_first {
            &*first
        } else {
            &*second
        };
        Ok(self.refine.launch(
            stream,
            LaunchConfig {
                grid: (narrow(TOP_K.div_ceil(8))?, 1, 1),
                block: (THREADS, 1, 1),
                shared_memory_bytes: 0,
            },
            (input, weight, candidates, logits, narrow(self.spec.input_features)?),
        )?)
    }

    fn validate(
        &self,
        input: &DeviceBuffer<bf16>,
        weight: &DeviceBuffer<bf16>,
        logits: &DeviceBuffer<bf16>,
        first: &DeviceBuffer<u64>,
        second: &DeviceBuffer<u64>,
    ) -> Result<()> {
        let workspace = Self::workspace_elements(self.spec.output_features)?;
        require("refinement input", self.spec.input_features, input.len())?;
        require("refinement BF16 weight", self.spec.weight_elements()?, weight.len())?;
        require("refinement logits", self.spec.output_features, logits.len())?;
        require("refinement first workspace", workspace, first.len())?;
        require("refinement second workspace", workspace, second.len())
    }
}

fn blocks(elements: usize) -> Result<usize> {
    elements
        .checked_add(CHUNK - 1)
        .map(|padded| padded / CHUNK)
        .ok_or(Error::InvalidDecoderKernel("FP8 refinement grid overflow"))
}

fn launch(block_count: usize) -> Result<LaunchConfig> {
    Ok(LaunchConfig {
        grid: (narrow(block_count)?, 1, 1),
        block: (THREADS, 1, 1),
        shared_memory_bytes: 0,
    })
}