sim-lib-compute-wgpu 0.2.0

Evidence-based wgpu tensor compute site discovery for SIM.
Documentation
use super::*;

#[test]
fn portable_reductions_match_cpu_and_handle_zero_extent() {
    let mut cx = test_cx();
    let source = tensor(&mut cx, vec![2, 3], &["3", "-1", "5", "7", "0.5", "2"]);
    let dtype = Symbol::qualified("numbers", "f32");

    for symbol in [
        sum_op_symbol(),
        min_op_symbol(),
        max_op_symbol(),
        norm_op_symbol(),
    ] {
        let wgpu = execute_portable(
            &mut cx,
            symbol.clone(),
            vec![source.clone()],
            Vec::new(),
            dtype.clone(),
        );
        let cpu = execute_cpu(
            &mut cx,
            symbol,
            vec![source.clone()],
            Vec::new(),
            dtype.clone(),
        );
        assert_same_f32_cells(&wgpu, &cpu);
    }

    let empty = tensor(&mut cx, vec![0, 3], &[]);
    let zero_sum = execute_portable(
        &mut cx,
        sum_op_symbol(),
        vec![empty.clone()],
        Vec::new(),
        dtype.clone(),
    );
    let zero_norm = execute_portable(&mut cx, norm_op_symbol(), vec![empty], Vec::new(), dtype);
    assert_eq!(f32_cells(&zero_sum), vec![0.0]);
    assert_eq!(f32_cells(&zero_norm), vec![0.0]);

    let non_finite = tensor(&mut cx, vec![4], &["NaN", "-1", "inf", "2"]);
    let sum = execute_portable(
        &mut cx,
        sum_op_symbol(),
        vec![non_finite.clone()],
        Vec::new(),
        Symbol::qualified("numbers", "f32"),
    );
    let norm = execute_portable(
        &mut cx,
        norm_op_symbol(),
        vec![non_finite.clone()],
        Vec::new(),
        Symbol::qualified("numbers", "f32"),
    );
    let min = execute_portable(
        &mut cx,
        min_op_symbol(),
        vec![non_finite.clone()],
        Vec::new(),
        Symbol::qualified("numbers", "f32"),
    );
    let max = execute_portable(
        &mut cx,
        max_op_symbol(),
        vec![non_finite],
        Vec::new(),
        Symbol::qualified("numbers", "f32"),
    );
    assert!(f32_cells(&sum)[0].is_nan());
    assert!(f32_cells(&norm)[0].is_nan());
    assert_eq!(f32_cells(&min), vec![-1.0]);
    assert_eq!(f32_cells(&max), vec![f32::INFINITY]);

    let empty_min_op = TensorOp::without_attributes(&mut cx, min_op_symbol()).unwrap();
    let empty_min_input = tensor(&mut cx, vec![0], &[]);
    let empty_min_request = TensorRequest::new(
        empty_min_op,
        vec![empty_min_input],
        TensorMeta::new(Vec::new(), Symbol::qualified("numbers", "f32")),
    );
    let empty_min = execute_portable_kernel(&mut cx, &empty_min_request, WgpuKernelDType::F32);
    match empty_min {
        Ok(_) => panic!("empty min reduction must be rejected"),
        Err(error) => assert!(error.to_string().contains("at least one")),
    }
}

#[test]
fn portable_transpose_dot_and_matmul_match_cpu() {
    let mut cx = test_cx();
    let dtype = Symbol::qualified("numbers", "f32");
    let matrix = tensor(&mut cx, vec![2, 3], &["1", "2", "3", "4", "5", "6"]);
    let transposed = execute_portable(
        &mut cx,
        transpose_exec_op_symbol(),
        vec![matrix.clone()],
        vec![3, 2],
        dtype.clone(),
    );
    assert_same_f32_cells(
        &transposed,
        &execute_cpu(
            &mut cx,
            transpose_exec_op_symbol(),
            vec![matrix.clone()],
            vec![3, 2],
            dtype.clone(),
        ),
    );

    let left_vec = tensor(&mut cx, vec![3], &["1", "2", "3"]);
    let right_vec = tensor(&mut cx, vec![3], &["4", "5", "6"]);
    let dot = execute_portable(
        &mut cx,
        dot_op_symbol(),
        vec![left_vec.clone(), right_vec.clone()],
        Vec::new(),
        dtype.clone(),
    );
    assert_same_f32_cells(
        &dot,
        &execute_cpu(
            &mut cx,
            dot_op_symbol(),
            vec![left_vec.clone(), right_vec.clone()],
            Vec::new(),
            dtype.clone(),
        ),
    );

    let right_matrix = tensor(&mut cx, vec![3, 2], &["7", "8", "9", "10", "11", "12"]);
    let matmul = execute_portable(
        &mut cx,
        matmul_exec_op_symbol(),
        vec![matrix.clone(), right_matrix.clone()],
        vec![2, 2],
        dtype.clone(),
    );
    assert_same_f32_cells(
        &matmul,
        &execute_cpu(
            &mut cx,
            matmul_exec_op_symbol(),
            vec![matrix, right_matrix],
            vec![2, 2],
            dtype.clone(),
        ),
    );

    let matrix_vector_left = tensor(&mut cx, vec![2, 3], &["1", "2", "3", "4", "5", "6"]);
    let matrix_vector = execute_portable(
        &mut cx,
        matmul_exec_op_symbol(),
        vec![matrix_vector_left.clone(), left_vec.clone()],
        vec![2],
        dtype.clone(),
    );
    assert_same_f32_cells(
        &matrix_vector,
        &execute_cpu(
            &mut cx,
            matmul_exec_op_symbol(),
            vec![matrix_vector_left, left_vec],
            vec![2],
            dtype,
        ),
    );
}

#[test]
fn portable_linalg_handles_aliases_segments_oom_and_half_accumulate() {
    let mut cx = test_cx();
    let dtype = Symbol::qualified("numbers", "f32");
    let alias = tensor(&mut cx, vec![3], &["1", "2", "3"]);
    let alias_dot = execute_portable(
        &mut cx,
        dot_op_symbol(),
        vec![alias.clone(), alias],
        Vec::new(),
        dtype.clone(),
    );
    assert_eq!(f32_cells(&alias_dot), vec![14.0]);

    let f16_dtype = Symbol::qualified("numbers", "f16");
    let half_left = tensor(&mut cx, vec![2], &["1.5", "2.5"]);
    let half_right = tensor(&mut cx, vec![2], &["2", "4"]);
    let half = execute_portable(
        &mut cx,
        dot_op_symbol(),
        vec![half_left, half_right],
        Vec::new(),
        f16_dtype,
    );
    assert_eq!(
        parse_f16_literal_cell(&half.cells().unwrap()[0])
            .unwrap()
            .to_f32(),
        half::f16::from_f32(13.0).to_f32()
    );

    let tiny = WgpuTensorExecutor::new(
        WgpuDiscovery::from_probes(
            vec![adapter_with_limits("tiny", "Vulkan", true, limits(4))],
            Vec::new(),
        )
        .adapters[0]
            .clone(),
    );
    let op = TensorOp::without_attributes(&mut cx, add_op_symbol()).unwrap();
    let tiny_left = tensor(&mut cx, vec![2], &["1", "2"]);
    let tiny_right = tensor(&mut cx, vec![2], &["3", "4"]);
    let result = tiny.execute(
        &mut cx,
        TensorRequest::new(
            op,
            vec![tiny_left, tiny_right],
            TensorMeta::new(vec![2], Symbol::qualified("numbers", "f32")),
        ),
    );
    match result {
        Ok(_) => panic!("tiny wgpu profile must reject the submission"),
        Err(error) => assert!(
            error.to_string().contains("device context is unavailable")
                || error.to_string().contains("byte limit")
        ),
    }
}

#[test]
fn physical_reduction_and_linalg_dispatch_match_cpu_when_opted_in() {
    if std::env::var_os("SIM_COMPUTE_WGPU_PHYSICAL").is_none() {
        return;
    }
    let mut cx = test_cx();
    let mut runtimes = discover_wgpu_adapter_runtimes(&Default::default()).unwrap();
    let runtime = runtimes
        .pop()
        .expect("SIM_COMPUTE_WGPU_PHYSICAL requires a probe-backed adapter");
    assert!(verify_physical(&runtime.probe).is_ok());
    let executor = WgpuTensorExecutor::from_parts(
        runtime.probe,
        Some(WgpuExecutionContext {
            device: Arc::new(runtime.device),
            queue: Arc::new(runtime.queue),
        }),
    );
    let dtype = Symbol::qualified("numbers", "f32");
    let source = tensor(&mut cx, vec![2, 3], &["3", "-1", "5", "7", "0.5", "2"]);

    for symbol in [
        sum_op_symbol(),
        min_op_symbol(),
        max_op_symbol(),
        norm_op_symbol(),
    ] {
        let first = execute_wgpu(
            &mut cx,
            &executor,
            symbol.clone(),
            vec![source.clone()],
            Vec::new(),
            dtype.clone(),
        );
        let second = execute_wgpu(
            &mut cx,
            &executor,
            symbol.clone(),
            vec![source.clone()],
            Vec::new(),
            dtype.clone(),
        );
        assert_same_f32_cells(&first, &second);
        assert_same_f32_cells(
            &first,
            &execute_cpu(
                &mut cx,
                symbol,
                vec![source.clone()],
                Vec::new(),
                dtype.clone(),
            ),
        );
        assert!(matches!(
            first.location(),
            TensorLocation::Resident { site, .. } if site == compute_wgpu_site_symbol(0)
        ));
    }

    let matrix = tensor(&mut cx, vec![2, 3], &["1", "2", "3", "4", "5", "6"]);
    let right_matrix = tensor(&mut cx, vec![3, 2], &["7", "8", "9", "10", "11", "12"]);
    let left_vec = tensor(&mut cx, vec![3], &["1", "2", "3"]);
    let right_vec = tensor(&mut cx, vec![3], &["4", "5", "6"]);
    for (symbol, inputs, shape) in [
        (transpose_exec_op_symbol(), vec![matrix.clone()], vec![3, 2]),
        (
            dot_op_symbol(),
            vec![left_vec.clone(), right_vec.clone()],
            Vec::new(),
        ),
        (
            matmul_exec_op_symbol(),
            vec![matrix.clone(), right_matrix.clone()],
            vec![2, 2],
        ),
        (
            matmul_exec_op_symbol(),
            vec![matrix.clone(), left_vec.clone()],
            vec![2],
        ),
        (
            matmul_exec_op_symbol(),
            vec![left_vec.clone(), right_matrix.clone()],
            vec![2],
        ),
    ] {
        let gpu = execute_wgpu(
            &mut cx,
            &executor,
            symbol.clone(),
            inputs.clone(),
            shape.clone(),
            dtype.clone(),
        );
        let cpu = execute_cpu(&mut cx, symbol, inputs, shape, dtype.clone());
        assert_same_f32_cells(&gpu, &cpu);
        assert!(matches!(
            gpu.location(),
            TensorLocation::Resident { site, .. } if site == compute_wgpu_site_symbol(0)
        ));
    }

    let segment_left = tensor(&mut cx, vec![2, 2], &["1", "2", "3", "4"]);
    let segment_right = tensor(&mut cx, vec![2, 3], &["5", "6", "7", "8", "9", "10"]);
    let segmented = execute_wgpu(
        &mut cx,
        &executor,
        matmul_exec_op_symbol(),
        vec![segment_left, segment_right],
        vec![2, 3],
        dtype,
    );
    assert!(
        resident_storage(&segmented)
            .pipeline()
            .as_qualified_str()
            .contains("matmul")
    );
    assert_eq!(executor.flush().unwrap().accepted, 14);
}