mircuda 0.1.0

Native, explicit-stream Rust gateway to NVIDIA CUDA
#![cfg(all(target_os = "linux", feature = "cutlass"))]

use mircuda::{
    Context, DenseMatmulPlan, DenseMatmulSpec, DenseVectorPlan, DenseVectorSpec, DeviceBuffer,
    DeviceElement, Driver, MemoryPool, Stream, bf16, f16,
};

const N: usize = 128;
const K: usize = 128;

#[test]
fn dense_bf16_supports_decode_and_prefill() -> mircuda::Result<()> {
    run_bf16(1)?;
    run_bf16(128)
}

#[test]
fn dense_bf16_supports_profiled_wide_decode() -> mircuda::Result<()> {
    const OUTPUTS: usize = 8_192;
    let (context, stream, pool) = environment()?;
    let input = copy_device(&context, &stream, &pool, &[bf16::ONE; K])?;
    let weight = copy_device(&context, &stream, &pool, &vec![bf16::ONE; OUTPUTS * K])?;
    let mut output = copy_device(&context, &stream, &pool, &[bf16::ZERO; OUTPUTS])?;
    DenseMatmulPlan::new(&context, &stream, DenseMatmulSpec::new(1, OUTPUTS, K)?)?
        .execute(&stream, &input, &weight, &mut output, 1.0, 0.0)?;
    let actual = read_device(&context, &stream, &output)?;
    assert!(actual.iter().all(|value| *value == bf16::from_f32(128.0)));
    Ok(())
}

#[test]
fn dense_bf16_retains_fp32_output() -> mircuda::Result<()> {
    let (context, stream, pool) = environment()?;
    let input = copy_device(&context, &stream, &pool, &[bf16::from_f32(0.1); K])?;
    let weight = copy_device(&context, &stream, &pool, &vec![bf16::from_f32(0.1); N * K])?;
    let mut output = copy_device(&context, &stream, &pool, &[f32::NAN; N])?;
    DenseMatmulPlan::<bf16, f32>::new(&context, &stream, DenseMatmulSpec::new(1, N, K)?)?
        .execute(&stream, &input, &weight, &mut output, 1.0, 0.0)?;
    let actual = read_device(&context, &stream, &output)?;
    let rounded = bf16::from_f32(actual[0]).to_f32();
    assert!(
        actual
            .iter()
            .all(|value| value.is_finite() && (*value - actual[0]).abs() < f32::EPSILON)
    );
    assert!((actual[0] - rounded).abs() > f32::EPSILON, "FP32 result was rounded to BF16");
    Ok(())
}

#[test]
fn dense_bf16_supports_unaligned_shapes() -> mircuda::Result<()> {
    let (context, stream, pool) = environment()?;
    let a = copy_device(&context, &stream, &pool, &[bf16::ONE; 6])?;
    let b = copy_device(&context, &stream, &pool, &[bf16::ONE; 12])?;
    let mut c = copy_device(&context, &stream, &pool, &[bf16::ONE; 8])?;
    let mut plan = DenseMatmulPlan::<bf16>::new(&context, &stream, DenseMatmulSpec::new(2, 4, 3)?)?;
    plan.execute(&stream, &a, &b, &mut c, 1.0, 1.0)?;
    drop(plan);
    let actual = read_device(&context, &stream, &c)?;
    assert!(actual.iter().all(|value| *value == bf16::from_f32(4.0)));
    Ok(())
}

#[test]
fn dense_f16_supports_decode() -> mircuda::Result<()> {
    let (context, stream, pool) = environment()?;
    let a = copy_device(&context, &stream, &pool, &vec![f16::ONE; K])?;
    let b = copy_device(&context, &stream, &pool, &vec![f16::ONE; N * K])?;
    let mut c = copy_device(&context, &stream, &pool, &vec![f16::ONE; N])?;
    let mut plan = DenseMatmulPlan::<f16>::new(&context, &stream, DenseMatmulSpec::new(1, N, K)?)?;
    plan.execute(&stream, &a, &b, &mut c, 1.0, 1.0)?;
    drop(plan);
    let actual = read_device(&context, &stream, &c)?;
    let expected = f16::from_f32(129.0);
    assert!(actual.iter().all(|value| *value == expected));
    Ok(())
}

#[test]
fn dense_bf16_vector_matches_tensor_core_gemm() -> mircuda::Result<()> {
    const OUTPUTS: usize = 257;
    const FEATURES: usize = 2_816;
    let (context, stream, pool) = environment()?;
    let input = (0..FEATURES)
        .map(|index| Ok(bf16::from_f32(f32::from(u8::try_from(index % 31)?) / 32.0 - 0.5)))
        .collect::<mircuda::Result<Vec<_>>>()?;
    let weight = (0..OUTPUTS * FEATURES)
        .map(|index| Ok(bf16::from_f32(f32::from(u8::try_from(index % 17)?) / 64.0 - 0.125)))
        .collect::<mircuda::Result<Vec<_>>>()?;
    let input = copy_device(&context, &stream, &pool, &input)?;
    let weight = copy_device(&context, &stream, &pool, &weight)?;
    let mut expected = copy_device(&context, &stream, &pool, &[bf16::ZERO; OUTPUTS])?;
    let mut actual = copy_device(&context, &stream, &pool, &[bf16::NAN; OUTPUTS])?;
    DenseMatmulPlan::new(&context, &stream, DenseMatmulSpec::new(1, OUTPUTS, FEATURES)?)?
        .execute(&stream, &input, &weight, &mut expected, 1.0, 0.0)?;
    DenseVectorPlan::new(&context, &stream, DenseVectorSpec::new(OUTPUTS, FEATURES)?)?
        .execute(&stream, &input, &weight, &mut actual, 1.0, 0.0)?;
    let expected = read_device(&context, &stream, &expected)?;
    let actual = read_device(&context, &stream, &actual)?;
    assert!(actual.iter().all(|value| value.to_f32().is_finite()));
    let maximum = |values: &[bf16]| {
        values
            .iter()
            .enumerate()
            .max_by(|left, right| left.1.to_f32().total_cmp(&right.1.to_f32()))
            .map(|(index, _)| index)
    };
    assert_eq!(maximum(&actual), maximum(&expected));
    let error = expected
        .iter()
        .zip(&actual)
        .map(|(left, right)| (left.to_f32() - right.to_f32()).abs())
        .fold(0.0_f32, f32::max);
    assert!(error <= 0.125, "maximum BF16 GEMV difference: {error}");
    Ok(())
}

fn run_bf16(m: usize) -> mircuda::Result<()> {
    let (context, stream, pool) = environment()?;
    let a = copy_device(&context, &stream, &pool, &vec![bf16::ONE; m * K])?;
    let b = copy_device(&context, &stream, &pool, &vec![bf16::ONE; N * K])?;
    let mut c = copy_device(&context, &stream, &pool, &vec![bf16::ONE; m * N])?;
    let mut plan = DenseMatmulPlan::<bf16>::new(&context, &stream, DenseMatmulSpec::new(m, N, K)?)?;
    plan.execute(&stream, &a, &b, &mut c, 0.5, 1.0)?;
    assert_eq!(plan.spec(), DenseMatmulSpec::new(m, N, K)?);
    drop(plan);
    let actual = read_device(&context, &stream, &c)?;
    let expected = bf16::from_f32(65.0);
    assert!(actual.iter().all(|value| *value == expected));
    Ok(())
}

fn environment() -> mircuda::Result<(Context, Stream, MemoryPool)> {
    let driver = Driver::initialize()?;
    let device = driver.devices()?.into_iter().next().ok_or(mircuda::Error::InvalidLaunch)?;
    let context = driver.create_context(device)?;
    let stream = context.create_stream()?;
    let pool = context.default_memory_pool()?;
    Ok((context, stream, pool))
}

fn copy_device<T: DeviceElement>(
    context: &Context,
    stream: &Stream,
    pool: &MemoryPool,
    values: &[T],
) -> mircuda::Result<DeviceBuffer<T>> {
    let mut host = context.allocate_pinned::<T>(values.len())?;
    host.copy_from_slice(values)?;
    let mut device = pool.allocate::<T>(stream, values.len())?;
    stream.copy_to_device(&mut host, &mut device)?;
    stream.synchronize()?;
    Ok(device)
}

fn read_device<T: DeviceElement>(
    context: &Context,
    stream: &Stream,
    device: &DeviceBuffer<T>,
) -> mircuda::Result<Vec<T>> {
    let mut host = context.allocate_pinned::<T>(device.len())?;
    stream.copy_to_host(device, &mut host)?;
    stream.synchronize()?;
    host.to_vec()
}