libmir-cuda 0.3.0

CUDA inference backend for libmir
use mircuda::{DeviceBuffer, DeviceElement, bf16};

use super::AutoClampedExperts;
use crate::{
    CudaBackend, CudaConfig, CudaTensor, ExecutionPhase, PlanSource, Result,
    backend::{
        clamped_routed::{
            ClampedRoutedConfig,
            weights::{ClampedRoutedExpertWeights, NativeExpertWeights},
        },
        tuning::{ClampedMoeStorage, MoeProfileExecution, MoeProfileRequest},
    },
    kernels::{ClampedRoutedKernels, ClampedRoutedSpec},
};

const EXPERTS: usize = 4;
const TOP_K: usize = 2;
const WIDTH: usize = 32;

#[test]
fn synthetic_clamped_experts_autotune_decode_and_prefill() -> Result<()> {
    let backend = CudaBackend::new(CudaConfig::default())?;
    let weights = native_weights(&backend)?;
    run(&backend, &weights, ExecutionPhase::Decode, 1)?;
    run(&backend, &weights, ExecutionPhase::Prefill, 2)
}

fn run(
    backend: &CudaBackend,
    weights: &ClampedRoutedExpertWeights,
    phase: ExecutionPhase,
    tokens: usize,
) -> Result<()> {
    let config = config();
    let kernels = ClampedRoutedKernels::compile(backend.compiler(), kernel_spec(config, tokens))?;
    let mut experts = AutoClampedExperts::new(backend, config, tokens, phase, weights, kernels)
        .ok_or(crate::Error::InvalidExecutionPlan("missing synthetic clamped experts"))?;
    let input = upload(backend, &vec![bf16::from_f32(1.0); tokens * WIDTH])?;
    let selections = u32::try_from(tokens * TOP_K)?;
    let expert_count = u32::try_from(EXPERTS)?;
    let selected =
        upload(backend, &(0..selections).map(|value| value % expert_count).collect::<Vec<_>>())?;
    let routing = upload(backend, &vec![bf16::from_f32(0.5); tokens * TOP_K])?;
    let mut activated = backend.pool().allocate(backend.stream(), tokens * TOP_K * WIDTH)?;
    let mut partial = backend.pool().allocate(backend.stream(), tokens * TOP_K * WIDTH)?;
    let mut output = backend.pool().allocate(backend.stream(), tokens * WIDTH)?;

    experts
        .execute(weights, &input, &selected, &routing, &mut activated, &mut partial, &mut output)?;
    backend.synchronize()?;
    let profile = MoeProfileRequest::clamped(
        phase,
        tokens,
        EXPERTS,
        TOP_K,
        WIDTH,
        WIDTH,
        ClampedMoeStorage::Native,
    );
    assert!(matches!(
        backend.auto_tuner().lookup_moe(profile),
        Some((MoeProfileExecution::Clamped(_), PlanSource::MeasuredStartup))
    ));
    Ok(())
}

fn native_weights(backend: &CudaBackend) -> Result<ClampedRoutedExpertWeights> {
    let gate_rows = 2 * WIDTH;
    Ok(ClampedRoutedExpertWeights::Native(Box::new(NativeExpertWeights {
        gate_up_blocks: u8_tensor(
            backend,
            "gate-up-blocks",
            vec![EXPERTS, gate_rows, 1, 16],
            0x11,
        )?,
        gate_up_scales: u8_tensor(backend, "gate-up-scales", vec![EXPERTS, gate_rows, 1], 127)?,
        gate_up_bias: bf16_tensor(backend, "gate-up-bias", EXPERTS * gate_rows)?,
        down_blocks: u8_tensor(backend, "down-blocks", vec![EXPERTS, WIDTH, 1, 16], 0x11)?,
        down_scales: u8_tensor(backend, "down-scales", vec![EXPERTS, WIDTH, 1], 127)?,
        down_bias: bf16_tensor(backend, "down-bias", EXPERTS * WIDTH)?,
    })))
}

fn u8_tensor(
    backend: &CudaBackend,
    name: &str,
    shape: Vec<usize>,
    value: u8,
) -> Result<CudaTensor> {
    let elements = shape.iter().product();
    Ok(CudaTensor::from_u8(
        name.into(),
        shape,
        upload(backend, &vec![value; elements])?,
    ))
}

fn bf16_tensor(backend: &CudaBackend, name: &str, elements: usize) -> Result<CudaTensor> {
    Ok(CudaTensor::from_bf16(
        name.into(),
        vec![elements],
        upload(backend, &vec![bf16::from_f32(0.0); elements])?,
    ))
}

fn upload<T: DeviceElement>(backend: &CudaBackend, values: &[T]) -> Result<DeviceBuffer<T>> {
    let mut host = backend.context().allocate_pinned(values.len())?;
    host.copy_from_slice(values)?;
    let mut device = backend.pool().allocate(backend.stream(), values.len())?;
    backend.stream().copy_to_device(&mut host, &mut device)?;
    Ok(device)
}

fn config() -> ClampedRoutedConfig {
    ClampedRoutedConfig {
        vocab: 32,
        hidden: WIDTH,
        intermediate: WIDTH,
        query_heads: 1,
        kv_heads: 1,
        head_dim: WIDTH,
        experts: EXPERTS,
        top_k: TOP_K,
        epsilon: 1.0e-5,
        scale: 1.0,
        theta: 150_000.0,
        factor: 32.0,
        initial_context: 4_096.0,
        beta_fast: 32.0,
        beta_slow: 1.0,
        swiglu_limit: 7.0,
    }
}

fn kernel_spec(config: ClampedRoutedConfig, tokens: usize) -> ClampedRoutedSpec {
    ClampedRoutedSpec {
        tokens,
        hidden: config.hidden,
        intermediate: config.intermediate,
        query_heads: config.query_heads,
        kv_heads: config.kv_heads,
        head_dim: config.head_dim,
        top_k: config.top_k,
        theta: config.theta,
        factor: config.factor,
        initial_context: config.initial_context,
        beta_fast: config.beta_fast,
        beta_slow: config.beta_slow,
        swiglu_limit: config.swiglu_limit,
    }
}