#[allow(unused_imports)]
use crate::TensorError;
use crate::{Result, Tensor};
use scirs2_core::numeric::{One, Zero};
#[cfg(feature = "gpu")]
pub fn gpu_einsum_matmul<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
use crate::gpu::ops::execute_einsum_matmul;
use crate::tensor::TensorStorage;
match (&a.storage, &b.storage) {
(TensorStorage::Gpu(gpu_a), TensorStorage::Gpu(gpu_b)) => {
if a.shape().dims().len() != 2 || b.shape().dims().len() != 2 {
return Err(TensorError::invalid_shape_simple(
"GPU einsum matrix multiplication requires 2D tensors".to_string(),
));
}
let a_shape = a.shape().dims();
let b_shape = b.shape().dims();
if a_shape[1] != b_shape[0] {
return Err(TensorError::ShapeMismatch {
operation: "einsum_matmul_gpu".to_string(),
expected: format!("({}, K) and (K, {})", a_shape[0], b_shape[1]),
got: format!(
"({}, {}) and ({}, {})",
a_shape[0], a_shape[1], b_shape[0], b_shape[1]
),
context: None,
});
}
let output_shape = crate::Shape::new(vec![a_shape[0], b_shape[1]]);
let result_buffer = execute_einsum_matmul(
gpu_a,
gpu_b,
&a_shape,
&b_shape,
output_shape.dims().iter().product::<usize>(),
)?;
Ok(Tensor::from_gpu_buffer(result_buffer, output_shape))
}
_ => {
crate::ops::matmul(a, b)
}
}
}
#[cfg(feature = "gpu")]
pub fn gpu_einsum_batched_matmul<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
use crate::tensor::TensorStorage;
match (&a.storage, &b.storage) {
(TensorStorage::Gpu(_), TensorStorage::Gpu(_)) => {
if a.shape().dims().len() != 3 || b.shape().dims().len() != 3 {
return Err(TensorError::invalid_shape_simple(
"GPU einsum batched matrix multiplication requires 3D tensors".to_string(),
));
}
let a_shape = a.shape().dims();
let b_shape = b.shape().dims();
if a_shape[0] != b_shape[0] || a_shape[2] != b_shape[1] {
return Err(TensorError::ShapeMismatch {
operation: "einsum_batch_matmul_gpu".to_string(),
expected: "(B, M, K) and (B, K, N)".to_string(),
got: format!(
"({}, {}, {}) and ({}, {}, {})",
a_shape[0], a_shape[1], a_shape[2], b_shape[0], b_shape[1], b_shape[2]
),
context: None,
});
}
let cpu_a = a.to_cpu()?;
let cpu_b = b.to_cpu()?;
crate::ops::einsum::einsum("bij,bjk->bik", &[&cpu_a, &cpu_b])
}
_ => {
crate::ops::matmul(a, b)
}
}
}
#[cfg(feature = "gpu")]
pub fn gpu_einsum_transpose<T>(tensor: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
use crate::tensor::TensorStorage;
match &tensor.storage {
TensorStorage::Gpu(_) => {
if tensor.shape().dims().len() != 2 {
return Err(TensorError::invalid_shape_simple(
"GPU einsum transpose requires 2D tensor".to_string(),
));
}
let cpu_tensor = tensor.to_cpu()?;
crate::ops::einsum::einsum("ij->ji", &[&cpu_tensor])
}
_ => {
tensor.transpose()
}
}
}
#[cfg(feature = "gpu")]
pub fn gpu_einsum_diagonal<T>(tensor: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
if tensor.shape().dims().len() != 2 {
return Err(TensorError::invalid_shape_simple(
"GPU einsum diagonal requires 2D tensor".to_string(),
));
}
let cpu_tensor = tensor.to_cpu()?;
crate::ops::einsum::einsum("ii->i", &[&cpu_tensor])
}
#[cfg(feature = "gpu")]
pub fn gpu_einsum_outer_product<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
if a.shape().dims().len() != 1 || b.shape().dims().len() != 1 {
return Err(TensorError::invalid_shape_simple(
"GPU einsum outer product requires 1D tensors".to_string(),
));
}
let cpu_a = a.to_cpu()?;
let cpu_b = b.to_cpu()?;
crate::ops::einsum::einsum("i,j->ij", &[&cpu_a, &cpu_b])
}
#[cfg(feature = "gpu")]
pub fn gpu_einsum_vector_dot<T>(a: &Tensor<T>, b: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
use crate::gpu::ops::execute_einsum_vector_dot;
use crate::tensor::TensorStorage;
match (&a.storage, &b.storage) {
(TensorStorage::Gpu(gpu_a), TensorStorage::Gpu(gpu_b)) => {
if a.shape().dims().len() != 1 || b.shape().dims().len() != 1 {
return Err(TensorError::invalid_shape_simple(
"GPU einsum vector dot product requires 1D tensors".to_string(),
));
}
let a_shape = a.shape().dims();
let b_shape = b.shape().dims();
if a_shape[0] != b_shape[0] {
return Err(TensorError::ShapeMismatch {
operation: "einsum_dot_gpu".to_string(),
expected: format!("({},) and ({},)", a_shape[0], a_shape[0]),
got: format!("({},) and ({},)", a_shape[0], b_shape[0]),
context: None,
});
}
let output_shape = crate::Shape::new(vec![]);
let result_buffer = execute_einsum_vector_dot(
gpu_a,
gpu_b,
&a_shape,
&b_shape,
output_shape.dims().iter().product::<usize>(),
)?;
Ok(Tensor::from_gpu_buffer(result_buffer, output_shape))
}
_ => {
crate::ops::dot(a, b)
}
}
}
#[cfg(feature = "gpu")]
pub fn gpu_einsum_trace<T>(tensor: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
if tensor.shape().dims().len() != 2 {
return Err(TensorError::invalid_shape_simple(
"GPU einsum trace requires 2D tensor".to_string(),
));
}
let input_shape = tensor.shape().dims();
if input_shape[0] != input_shape[1] {
return Err(TensorError::invalid_shape_simple(
"GPU einsum trace requires square matrix".to_string(),
));
}
let cpu_tensor = tensor.to_cpu()?;
crate::ops::einsum::einsum("ii->", &[&cpu_tensor])
}
#[cfg(not(feature = "gpu"))]
pub fn gpu_einsum_matmul<T>(_a: &Tensor<T>, _b: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
Err(TensorError::unsupported_operation_simple(
"GPU einsum requires the gpu feature".to_string(),
))
}
#[cfg(not(feature = "gpu"))]
pub fn gpu_einsum_batched_matmul<T>(_a: &Tensor<T>, _b: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
Err(TensorError::unsupported_operation_simple(
"GPU einsum requires the gpu feature".to_string(),
))
}
#[cfg(not(feature = "gpu"))]
pub fn gpu_einsum_transpose<T>(_tensor: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
Err(TensorError::unsupported_operation_simple(
"GPU einsum requires the gpu feature".to_string(),
))
}
#[cfg(not(feature = "gpu"))]
pub fn gpu_einsum_diagonal<T>(_tensor: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
Err(TensorError::unsupported_operation_simple(
"GPU einsum requires the gpu feature".to_string(),
))
}
#[cfg(not(feature = "gpu"))]
pub fn gpu_einsum_outer_product<T>(_a: &Tensor<T>, _b: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
Err(TensorError::unsupported_operation_simple(
"GPU einsum requires the gpu feature".to_string(),
))
}
#[cfg(not(feature = "gpu"))]
pub fn gpu_einsum_vector_dot<T>(_a: &Tensor<T>, _b: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
Err(TensorError::unsupported_operation_simple(
"GPU einsum requires the gpu feature".to_string(),
))
}
#[cfg(not(feature = "gpu"))]
pub fn gpu_einsum_trace<T>(_tensor: &Tensor<T>) -> Result<Tensor<T>>
where
T: Clone
+ Default
+ Zero
+ One
+ std::ops::Add<Output = T>
+ std::ops::Mul<Output = T>
+ Send
+ Sync
+ 'static
+ bytemuck::Pod
+ bytemuck::Zeroable,
{
Err(TensorError::unsupported_operation_simple(
"GPU einsum requires the gpu feature".to_string(),
))
}
#[cfg(all(test, not(feature = "gpu")))]
mod tests {
use super::*;
fn assert_requires_gpu_feature(err: TensorError) {
match err {
TensorError::UnsupportedOperation { reason, .. } => {
assert!(
reason.contains("gpu feature"),
"expected an honest 'gpu feature' error, got: {reason}"
);
}
other => panic!("expected UnsupportedOperation, got: {other:?}"),
}
}
#[test]
fn matmul_fallback_returns_honest_error_not_panic() {
let a = Tensor::<f32>::zeros(&[2, 2]);
let b = Tensor::<f32>::zeros(&[2, 2]);
let err = gpu_einsum_matmul(&a, &b).expect_err("must not fabricate a result");
assert_requires_gpu_feature(err);
}
#[test]
fn batched_matmul_fallback_returns_honest_error_not_panic() {
let a = Tensor::<f32>::zeros(&[2, 2, 2]);
let b = Tensor::<f32>::zeros(&[2, 2, 2]);
let err = gpu_einsum_batched_matmul(&a, &b).expect_err("must not fabricate a result");
assert_requires_gpu_feature(err);
}
#[test]
fn transpose_fallback_returns_honest_error_not_panic() {
let t = Tensor::<f32>::zeros(&[2, 3]);
let err = gpu_einsum_transpose(&t).expect_err("must not fabricate a result");
assert_requires_gpu_feature(err);
}
#[test]
fn diagonal_fallback_returns_honest_error_not_panic() {
let t = Tensor::<f32>::zeros(&[3, 3]);
let err = gpu_einsum_diagonal(&t).expect_err("must not fabricate a result");
assert_requires_gpu_feature(err);
}
#[test]
fn outer_product_fallback_returns_honest_error_not_panic() {
let a = Tensor::<f32>::zeros(&[3]);
let b = Tensor::<f32>::zeros(&[4]);
let err = gpu_einsum_outer_product(&a, &b).expect_err("must not fabricate a result");
assert_requires_gpu_feature(err);
}
#[test]
fn vector_dot_fallback_returns_honest_error_not_panic() {
let a = Tensor::<f32>::zeros(&[5]);
let b = Tensor::<f32>::zeros(&[5]);
let err = gpu_einsum_vector_dot(&a, &b).expect_err("must not fabricate a result");
assert_requires_gpu_feature(err);
}
#[test]
fn trace_fallback_returns_honest_error_not_panic() {
let t = Tensor::<f32>::zeros(&[4, 4]);
let err = gpu_einsum_trace(&t).expect_err("must not fabricate a result");
assert_requires_gpu_feature(err);
}
}
#[cfg(all(test, feature = "gpu"))]
mod gpu_delegate_tests {
use super::*;
use crate::Device;
#[test]
fn gpu_batched_matmul_matches_cpu_reference() {
let a_cpu =
Tensor::<f32>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 1.0, 0.0, 0.0, 1.0], &[2, 2, 2])
.expect("test: from_vec should succeed");
let b_cpu =
Tensor::<f32>::from_vec(vec![5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0], &[2, 2, 2])
.expect("test: from_vec should succeed");
let (a_gpu, b_gpu) = match (a_cpu.to(Device::Gpu(0)), b_cpu.to(Device::Gpu(0))) {
(Ok(a), Ok(b)) => (a, b),
_ => return, };
let result = gpu_einsum_batched_matmul(&a_gpu, &b_gpu)
.expect("test: gpu_einsum_batched_matmul should succeed with a real adapter");
assert_eq!(result.shape().dims(), &[2, 2, 2]);
let data = result.to_vec().expect("test: to_vec should succeed");
assert_eq!(data, vec![19.0, 22.0, 43.0, 50.0, 9.0, 10.0, 11.0, 12.0]);
}
#[test]
fn gpu_transpose_matches_cpu_reference() {
let a_cpu = Tensor::<f32>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], &[2, 3])
.expect("test: from_vec should succeed");
let a_gpu = match a_cpu.to(Device::Gpu(0)) {
Ok(t) => t,
Err(_) => return, };
let result = gpu_einsum_transpose(&a_gpu)
.expect("test: gpu_einsum_transpose should succeed with a real adapter");
assert_eq!(result.shape().dims(), &[3, 2]);
let data = result.to_vec().expect("test: to_vec should succeed");
assert_eq!(data, vec![1.0, 4.0, 2.0, 5.0, 3.0, 6.0]);
}
#[test]
fn gpu_diagonal_matches_cpu_reference() {
let a_cpu =
Tensor::<f32>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], &[3, 3])
.expect("test: from_vec should succeed");
let a_gpu = match a_cpu.to(Device::Gpu(0)) {
Ok(t) => t,
Err(_) => return, };
let result = gpu_einsum_diagonal(&a_gpu)
.expect("test: gpu_einsum_diagonal should succeed with a real adapter");
assert_eq!(result.shape().dims(), &[3]);
let data = result.to_vec().expect("test: to_vec should succeed");
assert_eq!(data, vec![1.0, 5.0, 9.0]);
}
#[test]
fn gpu_outer_product_matches_cpu_reference() {
let a_cpu = Tensor::<f32>::from_vec(vec![1.0, 2.0, 3.0], &[3])
.expect("test: from_vec should succeed");
let b_cpu =
Tensor::<f32>::from_vec(vec![10.0, 20.0], &[2]).expect("test: from_vec should succeed");
let (a_gpu, b_gpu) = match (a_cpu.to(Device::Gpu(0)), b_cpu.to(Device::Gpu(0))) {
(Ok(a), Ok(b)) => (a, b),
_ => return, };
let result = gpu_einsum_outer_product(&a_gpu, &b_gpu)
.expect("test: gpu_einsum_outer_product should succeed with a real adapter");
assert_eq!(result.shape().dims(), &[3, 2]);
let data = result.to_vec().expect("test: to_vec should succeed");
assert_eq!(data, vec![10.0, 20.0, 20.0, 40.0, 30.0, 60.0]);
}
#[test]
fn gpu_trace_matches_cpu_reference() {
let a_cpu =
Tensor::<f32>::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0], &[3, 3])
.expect("test: from_vec should succeed");
let a_gpu = match a_cpu.to(Device::Gpu(0)) {
Ok(t) => t,
Err(_) => return, };
let result = gpu_einsum_trace(&a_gpu)
.expect("test: gpu_einsum_trace should succeed with a real adapter");
let data = result.to_vec().expect("test: to_vec should succeed");
assert_eq!(data, vec![15.0]);
}
}