libmir-metal 0.3.0

Metal inference backend for libmir
use std::{hint::black_box, time::Instant};

use super::{Result, Stream, mlp, values, weights};
use crate::engine::Array;

const ITERATIONS: u32 = 20;

#[test]
#[ignore = "synthetic GPU benchmark"]
#[allow(clippy::print_stdout)]
fn benchmarks_fused_restore_reduction() -> Result<()> {
    let stream = Stream::new_gpu()?;
    let projections = weights(8, 256, 512, &stream)?;
    for tokens in [16_usize, 64, 256] {
        let input = Array::from_f32(&values(tokens * 256), &[1, i32::try_from(tokens)?, 256])?;
        let routes = tokens * 4;
        let indices = (0..routes)
            .map(|index| Ok(u32::try_from(index % 8)?))
            .collect::<Result<Vec<_>>>()?;
        let indices = Array::from_u32(&indices, &[1, i32::try_from(tokens)?, 4])?;
        let routing = Array::from_f32(&vec![0.25; routes], &[1, i32::try_from(tokens)?, 4])?;
        let sorted = input.sort_expert_inputs(&indices, &stream)?;
        let output = mlp(&projections, &sorted.input, &sorted.indices, true, &stream)?;
        let graph = measure(&stream, || {
            sorted.restore(&output, &stream)?.weighted_sum(&routing, -2, &stream)
        })?;
        let fused = measure(&stream, || sorted.restore_weighted(&output, &routing, &stream))?;
        println!(
            "metal_expert_reduce_profile tokens={tokens} routes={routes} graph_us={graph:.3} fused_us={fused:.3}"
        );
    }
    Ok(())
}

#[test]
#[ignore = "synthetic GPU benchmark"]
#[allow(clippy::print_stdout)]
fn benchmarks_kernel_grouping_against_argsort() -> Result<()> {
    let stream = Stream::new_gpu()?;
    for experts in [8_usize, 32, 128] {
        for tokens in [16_usize, 64, 256] {
            let input = Array::from_f32(&values(tokens * 64), &[1, i32::try_from(tokens)?, 64])?;
            let routes = tokens * 4;
            for skewed in [false, true] {
                let indices = (0..routes)
                    .map(|route| {
                        let expert = if skewed && route % 4 != 0 {
                            0
                        } else {
                            route % experts
                        };
                        Ok(u32::try_from(expert)?)
                    })
                    .collect::<Result<Vec<_>>>()?;
                let indices = Array::from_u32(&indices, &[1, i32::try_from(tokens)?, 4])?;
                let sorted = measure_pair(&stream, || {
                    let grouped = input.sort_expert_inputs(&indices, &stream)?;
                    Ok((grouped.input, grouped.indices))
                })?;
                let kernel = measure_pair(&stream, || {
                    let grouped = input.group_expert_inputs(&indices, experts, &stream)?;
                    Ok((grouped.input, grouped.indices))
                })?;
                println!(
                    "metal_expert_group_profile experts={experts} tokens={tokens} routes={routes} \
                     skewed={skewed} argsort_us={sorted:.3} kernel_us={kernel:.3}"
                );
            }
        }
    }
    Ok(())
}

#[test]
#[ignore = "synthetic GPU benchmark"]
#[allow(clippy::print_stdout)]
fn benchmarks_kernel_grouping_in_full_expert_path() -> Result<()> {
    let stream = Stream::new_gpu()?;
    let projections = weights(8, 256, 512, &stream)?;
    for tokens in [16_usize, 64, 256] {
        let input = Array::from_f32(&values(tokens * 256), &[1, i32::try_from(tokens)?, 256])?;
        let routes = tokens * 4;
        let routing = Array::from_f32(&vec![0.25; routes], &[1, i32::try_from(tokens)?, 4])?;
        for skewed in [false, true] {
            let indices = (0..routes)
                .map(|route| {
                    let expert = if skewed && route % 4 != 0 {
                        0
                    } else {
                        route % 8
                    };
                    Ok(u32::try_from(expert)?)
                })
                .collect::<Result<Vec<_>>>()?;
            let indices = Array::from_u32(&indices, &[1, i32::try_from(tokens)?, 4])?;
            let argsort = measure(&stream, || {
                let grouped = input.sort_expert_inputs(&indices, &stream)?;
                let output = mlp(&projections, &grouped.input, &grouped.indices, true, &stream)?;
                grouped.restore_weighted(&output, &routing, &stream)
            })?;
            let kernel = measure(&stream, || {
                let grouped = input.group_expert_inputs(&indices, 8, &stream)?;
                let output = mlp(&projections, &grouped.input, &grouped.indices, true, &stream)?;
                grouped.restore_weighted(&output, &routing, &stream)
            })?;
            println!(
                "metal_expert_kernel_path_profile tokens={tokens} routes={routes} skewed={skewed} \
                 argsort_us={argsort:.3} kernel_us={kernel:.3}"
            );
        }
    }
    Ok(())
}

fn measure(stream: &Stream, operation: impl Fn() -> Result<Array>) -> Result<f64> {
    for _ in 0..3 {
        operation()?.async_eval()?;
    }
    stream.synchronize()?;
    let started = Instant::now();
    for _ in 0..ITERATIONS {
        black_box(operation()?).async_eval()?;
    }
    stream.synchronize()?;
    Ok(started.elapsed().as_secs_f64() * 1_000_000.0 / f64::from(ITERATIONS))
}

fn measure_pair(stream: &Stream, operation: impl Fn() -> Result<(Array, Array)>) -> Result<f64> {
    for _ in 0..3 {
        let (first, second) = operation()?;
        first.async_eval()?;
        second.async_eval()?;
    }
    stream.synchronize()?;
    let started = Instant::now();
    for _ in 0..ITERATIONS {
        let (first, second) = black_box(operation()?);
        first.async_eval()?;
        second.async_eval()?;
    }
    stream.synchronize()?;
    Ok(started.elapsed().as_secs_f64() * 1_000_000.0 / f64::from(ITERATIONS))
}