nu_plugin_vulkan_matrix 0.1.2

GPU matrix operations for Nushell using Vulkan
use cubecl::{std::tensor::TensorHandle, wgpu::WgpuDeviceKind};
use ndarray::{ArrayD, IxDyn, s};
use nu_plugin_vulkan_matrix::gpu::{Gpu, download_array, open_device, upload_array, upload_view};
use std::sync::OnceLock;

fn gpu() -> &'static Gpu {
    static GPU: OnceLock<Gpu> = OnceLock::new();
    GPU.get_or_init(|| {
        let kind = match std::env::var("VM_GPU").as_deref().unwrap_or("d0") {
            "d0" => WgpuDeviceKind::DiscreteGpu(0),
            "d1" => WgpuDeviceKind::DiscreteGpu(1),
            "i0" => WgpuDeviceKind::IntegratedGpu(0),
            other => panic!("unknown selector {other}"),
        };
        open_device(kind).unwrap()
    })
}

fn check_f64(tensor: &TensorHandle, expected: &ArrayD<f64>) {
    let actual = download_array::<f64>(gpu(), tensor).unwrap();
    assert_eq!(actual.shape(), expected.shape());
    assert_eq!(
        actual.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
        expected.iter().map(|v| v.to_bits()).collect::<Vec<_>>()
    );
}

#[test]
fn strided_nd_views_and_owned_arrays_keep_shape_and_values() {
    let input =
        ArrayD::from_shape_vec(IxDyn(&[3, 4, 5]), (0..60).map(|n| n as f64).collect()).unwrap();
    for view in [
        input.view(),
        input.view().permuted_axes(IxDyn(&[2, 0, 1])),
        input.slice(s![..;-1, 1..4;2, ..;2]).into_dyn(),
    ] {
        check_f64(&upload_view(gpu(), view.clone()).unwrap(), &view.to_owned());
    }
    for owned in [
        input.clone(),
        input.clone().permuted_axes(IxDyn(&[2, 0, 1])),
        input.clone().slice_move(s![1..3, .., ..]).into_dyn(),
        input.slice_move(s![..;-1, .., ..;2]).into_dyn(),
    ] {
        let expected = owned.clone();
        let tensor = upload_array(gpu(), owned).unwrap();
        check_f64(&tensor, &expected);
    }
}

#[test]
fn scalar_vector_and_empty_axes_round_trip() {
    for shape in [&[][..], &[4], &[0, 2, 3], &[2, 0, 3], &[2, 3, 0]] {
        let input = ArrayD::from_elem(IxDyn(shape), -0.0_f64);
        check_f64(&upload_view(gpu(), input.view()).unwrap(), &input);
        check_f64(&upload_array(gpu(), input.clone()).unwrap(), &input);
    }
}

#[test]
fn f32_bits_are_preserved_and_mismatched_types_are_rejected() {
    let values = [
        0, 0x80000000, 1, 0x007fffff, 0x00800000, 0x7f800000, 0xff800000, 0x7fc01234,
    ];
    let input =
        ArrayD::from_shape_vec(IxDyn(&[2, 4]), values.map(f32::from_bits).to_vec()).unwrap();
    let tensor = upload_array(gpu(), input).unwrap();
    assert_eq!(
        download_array::<f32>(gpu(), &tensor)
            .unwrap()
            .iter()
            .map(|v| v.to_bits())
            .collect::<Vec<_>>(),
        values
    );
    assert!(download_array::<f64>(gpu(), &tensor).is_err());
    assert!(upload_array(gpu(), ArrayD::from_elem(IxDyn(&[1]), 1_u32)).is_err());
}

#[test]
fn unsupported_precision_is_rejected_before_transfer() {
    let mut limited = gpu().clone();
    limited.f64 = false;
    limited.denorm32 = true;
    limited.denorm64 = true;
    let doubles = ArrayD::from_elem(IxDyn(&[1]), 1.0_f64);
    assert!(upload_view(&limited, doubles.view()).is_err());
    assert!(upload_array(&limited, doubles).is_err());
    limited.f64 = true;
    limited.denorm64 = false;
    assert!(upload_array(&limited, ArrayD::from_elem(IxDyn(&[1]), 1.0_f64)).is_err());
    limited.denorm32 = false;
    assert!(upload_array(&limited, ArrayD::from_elem(IxDyn(&[1]), 1.0_f32)).is_err());
    let tensor = upload_array(gpu(), ArrayD::from_elem(IxDyn(&[1]), 1.0_f32)).unwrap();
    assert!(download_array::<f32>(&limited, &tensor).is_err());
}

#[test]
fn a_different_device_cannot_read_the_tensor() {
    let other_kind = if std::env::var("VM_GPU").as_deref() == Ok("i0") {
        WgpuDeviceKind::DiscreteGpu(0)
    } else {
        WgpuDeviceKind::IntegratedGpu(0)
    };
    let other = open_device(other_kind).unwrap();
    let tensor = upload_array(gpu(), ArrayD::from_elem(IxDyn(&[3]), 1.0_f64)).unwrap();
    assert!(download_array::<f64>(&other, &tensor).is_err());
}