use crate::{
backend::{validate_axpby_read_into_accum, validate_dot_general_read_into},
AllocationDomainId, AllocationId, BackendStorage, CompareDir, ContractionScalar, DType,
DotGeneralAccumulation, DotGeneralConfig, ElementwiseReadOp, Engine, GatherConfig, PadConfig,
Placement, ScatterConfig, SessionCachedDot, SliceConfig, StorageBuffer, Tensor, TensorAnalytic,
TensorBuffer, TensorDeviceTransfer, TensorDot, TensorElementwise, TensorFusion, TensorIndexing,
TensorRead, TensorReduction, TensorScalar, TensorStructural, TensorView, TensorViewMut,
TensorWrite, TypedTensor, TypedTensorView, TypedTensorViewMut,
};
use num_complex::{Complex32, Complex64};
mod cached_dot_defaults;
pub(crate) struct DefaultReadBackend {
calls: Vec<&'static str>,
dot_result: Option<Tensor>,
gather_indices: Option<Tensor>,
gather_config: Option<GatherConfig>,
reshape_shapes: Vec<Vec<usize>>,
concatenate_axis: Option<usize>,
structural_runtime_enabled: bool,
contiguous_reads: usize,
}
impl Default for DefaultReadBackend {
fn default() -> Self {
Self {
calls: Vec::new(),
dot_result: None,
gather_indices: None,
gather_config: None,
reshape_shapes: Vec::new(),
concatenate_axis: None,
structural_runtime_enabled: true,
contiguous_reads: 0,
}
}
}
fn marker() -> Tensor {
Tensor::from_vec_col_major(vec![1], vec![42.0_f64]).unwrap()
}
fn for_each_index_col_major(shape: &[usize], mut visit: impl FnMut(&[usize])) {
if shape.contains(&0) {
return;
}
let mut index = vec![0; shape.len()];
loop {
visit(&index);
let Some(axis) = (0..shape.len()).find(|&axis| {
index[axis] += 1;
if index[axis] < shape[axis] {
true
} else {
index[axis] = 0;
false
}
}) else {
break;
};
let _ = axis;
}
}
fn materialize_host_view<T: TensorScalar>(view: TypedTensorView<'_, T>) -> crate::Result<Tensor> {
let shape = view.shape().to_vec();
let mut data = Vec::with_capacity(shape.iter().product());
let mut error = None;
for_each_index_col_major(&shape, |index| match view.get(index) {
Some(value) => data.push(*value),
None => {
error = Some(crate::Error::backend_failure(
"to_contiguous_read",
"test backend could not read a host view element",
))
}
});
if let Some(error) = error {
return Err(error);
}
Tensor::from_vec_col_major(shape, data)
}
fn copy_host_view<T: TensorScalar>(
src: &TypedTensor<T>,
mut dst: TypedTensorViewMut<'_, T>,
) -> crate::Result<()> {
let shape = src.shape().to_vec();
let mut error = None;
for_each_index_col_major(&shape, |index| {
let value = src.get(index).cloned();
match (value, dst.get_mut(index)) {
(Ok(value), Some(slot)) => *slot = value,
_ => {
error = Some(crate::Error::backend_failure(
"copy_read_into",
"test backend could not copy a host view element",
));
}
}
});
error.map_or(Ok(()), Err)
}
impl TensorElementwise for DefaultReadBackend {
fn elementwise_read_into(
&mut self,
op: ElementwiseReadOp,
inputs: &[TensorRead<'_>],
out: TensorWrite<'_>,
) -> crate::Result<()> {
crate::backend::validate_read_into_destination(op.label(), inputs, &out)?;
let expected = op.arity();
if inputs.len() != expected {
return Err(crate::Error::invalid_argument(
op.label(),
"inputs",
format!("expected {expected} inputs, got {}", inputs.len()),
));
}
let direct = out.dtype() == DType::F64
&& out.as_read().backend_family().is_none()
&& inputs.iter().all(|input| {
matches!(input, TensorRead::Tensor(tensor)
if tensor.dtype() == DType::F64
&& tensor
.as_typed::<f64>()
.expect("the dtype guard selects f64")
.backend_buffer()
.is_none())
});
if !direct {
return crate::backend::elementwise_read_into_via_allocating_ops(self, op, inputs, out);
}
if inputs.iter().any(|input| input.shape() != out.shape()) {
return Err(crate::Error::shape_mismatch(
op.label(),
inputs[0].shape().to_vec(),
out.shape().to_vec(),
));
}
let values = inputs
.iter()
.map(|input| input.as_slice::<f64>())
.collect::<crate::Result<Vec<_>>>()?;
let value_at = |index: usize| match op {
ElementwiseReadOp::Add => values[0][index] + values[1][index],
ElementwiseReadOp::Subtract => values[0][index] - values[1][index],
ElementwiseReadOp::Multiply => values[0][index] * values[1][index],
ElementwiseReadOp::Negate => -values[0][index],
ElementwiseReadOp::Conj => values[0][index],
ElementwiseReadOp::Divide => values[0][index] / values[1][index],
};
match out {
TensorWrite::Tensor(output) => {
for (index, value) in <f64 as TensorScalar>::as_slice_mut(output)?
.iter_mut()
.enumerate()
{
*value = value_at(index);
}
}
TensorWrite::View(TensorViewMut::F64(mut output)) => {
let shape = output.shape().to_vec();
let mut linear = 0;
let mut error = None;
for_each_index_col_major(&shape, |index| {
if let Some(slot) = output.get_mut(index) {
*slot = value_at(linear);
} else {
error = Some(crate::Error::backend_failure(
op.label(),
"test backend could not write a host view element",
));
}
linear += 1;
});
if let Some(error) = error {
return Err(error);
}
}
_ => unreachable!("dtype and placement were validated before direct replay"),
}
Ok(())
}
fn add_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("add", lhs)?;
let _ = crate::backend::read_owned_tensor("add", rhs)?;
self.calls.push("add");
Ok(marker())
}
fn sub_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("sub", lhs)?;
let _ = crate::backend::read_owned_tensor("sub", rhs)?;
self.calls.push("sub");
Ok(marker())
}
fn mul_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("mul", lhs)?;
let _ = crate::backend::read_owned_tensor("mul", rhs)?;
self.calls.push("mul");
Ok(marker())
}
fn neg_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("neg", input)?;
self.calls.push("neg");
Ok(marker())
}
fn conj_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("conj", input)?;
self.calls.push("conj");
Ok(marker())
}
fn div_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("div", lhs)?;
let _ = crate::backend::read_owned_tensor("div", rhs)?;
self.calls.push("div");
Ok(marker())
}
fn abs_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("abs", input)?;
self.calls.push("abs");
Ok(marker())
}
fn sign_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("sign", input)?;
self.calls.push("sign");
Ok(marker())
}
fn maximum_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("maximum", lhs)?;
let _ = crate::backend::read_owned_tensor("maximum", rhs)?;
self.calls.push("maximum");
Ok(marker())
}
fn minimum_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("minimum", lhs)?;
let _ = crate::backend::read_owned_tensor("minimum", rhs)?;
self.calls.push("minimum");
Ok(marker())
}
fn compare_read(
&mut self,
lhs: TensorRead<'_>,
rhs: TensorRead<'_>,
dir: &CompareDir,
) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("compare", lhs)?;
let _ = crate::backend::read_owned_tensor("compare", rhs)?;
let _ = dir;
self.calls.push("compare");
Ok(marker())
}
fn select_read(
&mut self,
pred: TensorRead<'_>,
on_true: TensorRead<'_>,
on_false: TensorRead<'_>,
) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("select", pred)?;
let _ = crate::backend::read_owned_tensor("select", on_true)?;
let _ = crate::backend::read_owned_tensor("select", on_false)?;
self.calls.push("select");
Ok(marker())
}
fn clamp_read(
&mut self,
input: TensorRead<'_>,
lower: TensorRead<'_>,
upper: TensorRead<'_>,
) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("clamp", input)?;
let _ = crate::backend::read_owned_tensor("clamp", lower)?;
let _ = crate::backend::read_owned_tensor("clamp", upper)?;
self.calls.push("clamp");
Ok(marker())
}
}
impl TensorAnalytic for DefaultReadBackend {
fn exp_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("exp", input)?;
self.calls.push("exp");
Ok(marker())
}
fn log_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("log", input)?;
self.calls.push("log");
Ok(marker())
}
fn sin_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("sin", input)?;
self.calls.push("sin");
Ok(marker())
}
fn cos_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("cos", input)?;
self.calls.push("cos");
Ok(marker())
}
fn tanh_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("tanh", input)?;
self.calls.push("tanh");
Ok(marker())
}
fn sqrt_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("sqrt", input)?;
self.calls.push("sqrt");
Ok(marker())
}
fn rsqrt_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("rsqrt", input)?;
self.calls.push("rsqrt");
Ok(marker())
}
fn pow_read(&mut self, lhs: TensorRead<'_>, rhs: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("pow", lhs)?;
let _ = crate::backend::read_owned_tensor("pow", rhs)?;
self.calls.push("pow");
Ok(marker())
}
fn expm1_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("expm1", input)?;
self.calls.push("expm1");
Ok(marker())
}
fn log1p_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("log1p", input)?;
self.calls.push("log1p");
Ok(marker())
}
fn erf_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("erf", input)?;
self.calls.push("erf");
Ok(marker())
}
}
impl TensorStructural for DefaultReadBackend {
fn to_contiguous_read(&mut self, input: TensorRead<'_>) -> crate::Result<Tensor> {
self.contiguous_reads += 1;
if !self.structural_runtime_enabled {
let TensorRead::Tensor(tensor) = input else {
return Err(crate::Error::unsupported(
"to_contiguous_read",
"backend does not accept borrowed tensor views at this execution boundary",
));
};
if tensor.is_backend_buffer()
|| !matches!(
tensor.placement().memory_kind,
crate::MemoryKind::PinnedHost | crate::MemoryKind::UnpinnedHost
)
{
return Err(crate::Error::runtime_state(
"to_contiguous_read",
"default materialization accepts only host-owned tensors; use the storage's owning backend",
));
}
return tensor.duplicate();
}
match input {
TensorRead::Tensor(tensor) => tensor.duplicate(),
TensorRead::View(TensorView::F32(view)) => materialize_host_view(view),
TensorRead::View(TensorView::F64(view)) => materialize_host_view(view),
TensorRead::View(TensorView::I32(view)) => materialize_host_view(view),
TensorRead::View(TensorView::I64(view)) => materialize_host_view(view),
TensorRead::View(TensorView::Bool(view)) => materialize_host_view(view),
TensorRead::View(TensorView::C32(view)) => materialize_host_view(view),
TensorRead::View(TensorView::C64(view)) => materialize_host_view(view),
}
}
fn copy_read_into(&mut self, src: TensorRead<'_>, dst: TensorWrite<'_>) -> crate::Result<()> {
if !self.structural_runtime_enabled {
return Err(crate::Error::unsupported(
"copy_read_into",
"backend-owned runtime copy is unsupported by this backend",
));
}
if src.dtype() != dst.dtype() {
return Err(crate::Error::validation(
"copy_read_into",
crate::ValidationError::DTypeMismatch {
expected: dst.dtype(),
actual: src.dtype(),
},
));
}
if src.shape() != dst.shape() {
return Err(crate::Error::validation(
"copy_read_into",
crate::ShapeMismatch::IncompatibleShapes {
lhs: src.shape().iter().copied().collect(),
rhs: dst.shape().iter().copied().collect(),
}
.into(),
));
}
let src = self.to_contiguous_read(src)?;
macro_rules! copy_typed {
($src:expr, $dst:expr, $variant:ident, $ty:ty) => {
match $dst {
TensorWrite::Tensor(dst) => *dst = Tensor::from_typed::<$ty>($src),
TensorWrite::View(TensorViewMut::$variant(dst)) => copy_host_view(&$src, dst)?,
_ => unreachable!("dtype was validated before copy dispatch"),
}
};
}
match src.dtype() {
crate::DType::F32 => copy_typed!(src.into_typed::<f32>().unwrap(), dst, F32, f32),
crate::DType::F64 => copy_typed!(src.into_typed::<f64>().unwrap(), dst, F64, f64),
crate::DType::I32 => copy_typed!(src.into_typed::<i32>().unwrap(), dst, I32, i32),
crate::DType::I64 => copy_typed!(src.into_typed::<i64>().unwrap(), dst, I64, i64),
crate::DType::Bool => copy_typed!(src.into_typed::<bool>().unwrap(), dst, Bool, bool),
crate::DType::C32 => {
copy_typed!(
src.into_typed::<num_complex::Complex32>().unwrap(),
dst,
C32,
num_complex::Complex32
)
}
crate::DType::C64 => {
copy_typed!(
src.into_typed::<num_complex::Complex64>().unwrap(),
dst,
C64,
num_complex::Complex64
)
}
_ => unreachable!("the fixture covers the preset dtypes"),
}
Ok(())
}
fn transpose_read(&mut self, input: TensorRead<'_>, perm: &[usize]) -> crate::Result<Tensor> {
let _ = crate::backend::read_owned_tensor("transpose", input)?;
let _ = perm;
self.calls.push("transpose");
Ok(marker())
}
fn reshape_read(&mut self, input: TensorRead<'_>, shape: &[usize]) -> crate::Result<Tensor> {
let _input = crate::backend::read_owned_tensor("reshape", input)?;
let _shape = shape;
self.calls.push("reshape");
self.reshape_shapes.push(_shape.to_vec());
Ok(marker())
}
fn broadcast_in_dim_read(
&mut self,
input: TensorRead<'_>,
shape: &[usize],
dims: &[usize],
) -> crate::Result<Tensor> {
let _input = crate::backend::read_owned_tensor("broadcast_in_dim", input)?;
let _shape = shape;
let _dims = dims;
self.calls.push("broadcast_in_dim");
Ok(marker())
}
fn cast(&mut self, _input: &Tensor, _to: DType) -> crate::Result<Tensor> {
self.calls.push("cast");
Ok(marker())
}
fn extract_diagonal(
&mut self,
_input: &Tensor,
_axis_a: usize,
_axis_b: usize,
) -> crate::Result<Tensor> {
self.calls.push("extract_diagonal");
Ok(marker())
}
fn embed_diagonal(
&mut self,
_input: &Tensor,
_axis_a: usize,
_axis_b: usize,
) -> crate::Result<Tensor> {
self.calls.push("embed_diagonal");
Ok(marker())
}
fn tril(&mut self, _input: &Tensor, _k: i64) -> crate::Result<Tensor> {
self.calls.push("tril");
Ok(marker())
}
fn triu(&mut self, _input: &Tensor, _k: i64) -> crate::Result<Tensor> {
self.calls.push("triu");
Ok(marker())
}
}
impl TensorReduction for DefaultReadBackend {
fn reduce_sum_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
let _input = crate::backend::read_owned_tensor("reduce_sum", input)?;
let _axes = axes;
self.calls.push("reduce_sum");
Ok(marker())
}
fn reduce_prod_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
let _input = crate::backend::read_owned_tensor("reduce_prod", input)?;
let _axes = axes;
self.calls.push("reduce_prod");
Ok(marker())
}
fn reduce_max_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
let _input = crate::backend::read_owned_tensor("reduce_max", input)?;
let _axes = axes;
self.calls.push("reduce_max");
Ok(marker())
}
fn reduce_min_read(&mut self, input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
let _input = crate::backend::read_owned_tensor("reduce_min", input)?;
let _axes = axes;
self.calls.push("reduce_min");
Ok(marker())
}
}
#[test]
fn reduce_sum_squares_default_requires_an_explicit_backend_override() {
let input = Tensor::from_vec_col_major(vec![1], vec![3.0_f64]).unwrap();
let mut backend = DefaultReadBackend::default();
let error = backend
.reduce_sum_squares_read(TensorRead::from_tensor(&input), &[0])
.unwrap_err();
assert!(matches!(
error,
crate::Error::Unsupported {
op: "reduce_sum_squares",
..
}
));
assert!(backend.calls.is_empty());
}
impl TensorIndexing for DefaultReadBackend {
fn gather(
&mut self,
operand: &Tensor,
start_indices: &Tensor,
config: &GatherConfig,
) -> crate::Result<Tensor> {
let _ = operand;
self.calls.push("gather");
self.gather_indices = Some(start_indices.duplicate().unwrap());
self.gather_config = Some(config.clone());
Ok(marker())
}
fn scatter(
&mut self,
_operand: &Tensor,
_scatter_indices: &Tensor,
_updates: &Tensor,
_config: &ScatterConfig,
) -> crate::Result<Tensor> {
Ok(marker())
}
fn slice(&mut self, _input: &Tensor, _config: &SliceConfig) -> crate::Result<Tensor> {
Ok(marker())
}
fn dynamic_slice(
&mut self,
_input: &Tensor,
_starts: &Tensor,
_slice_sizes: &[usize],
) -> crate::Result<Tensor> {
Ok(marker())
}
fn dynamic_update_slice(
&mut self,
_operand: &Tensor,
_update: &Tensor,
_starts: &Tensor,
) -> crate::Result<Tensor> {
Ok(marker())
}
fn pad(&mut self, _input: &Tensor, _config: &PadConfig) -> crate::Result<Tensor> {
Ok(marker())
}
fn concatenate(&mut self, _inputs: &[&Tensor], axis: usize) -> crate::Result<Tensor> {
self.calls.push("concatenate");
self.concatenate_axis = Some(axis);
Ok(marker())
}
fn reverse(&mut self, _input: &Tensor, _axes: &[usize]) -> crate::Result<Tensor> {
Ok(marker())
}
}
impl TensorDot for DefaultReadBackend {
fn dot_general_read(
&mut self,
lhs: TensorRead<'_>,
rhs: TensorRead<'_>,
_config: &DotGeneralConfig,
) -> crate::Result<Tensor> {
if lhs.as_tensor().is_some() && rhs.as_tensor().is_some() {
return record_dot_general(self);
}
let lhs = self.to_contiguous_read(lhs)?;
let rhs = self.to_contiguous_read(rhs)?;
let _ = (&lhs, &rhs);
record_dot_general(self)
}
}
fn record_dot_general(backend: &mut DefaultReadBackend) -> crate::Result<Tensor> {
backend.calls.push("dot_general");
backend
.dot_result
.as_ref()
.map(Tensor::duplicate)
.transpose()
.map(|result| result.unwrap_or_else(marker))
}
impl TensorFusion for DefaultReadBackend {}
impl TensorBuffer for DefaultReadBackend {}
impl TensorDeviceTransfer for DefaultReadBackend {
fn download_to_host(&mut self, _tensor: TensorRead<'_>) -> crate::Result<Tensor> {
Err(crate::Error::unsupported(
"DefaultReadBackend::download_to_host",
"test backend does not transfer tensors",
))
}
fn upload_host_tensor(&mut self, _tensor: TensorRead<'_>) -> crate::Result<Tensor> {
Err(crate::Error::unsupported(
"DefaultReadBackend::upload_host_tensor",
"test backend does not transfer tensors",
))
}
}
impl SessionCachedDot for DefaultReadBackend {}
impl Engine for DefaultReadBackend {}
#[test]
fn blas1_session_defaults_are_unsupported_without_transfer_or_fallback() {
let input = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
let mut output = Tensor::from_vec_col_major(vec![2], vec![10.0_f64, 20.0]).unwrap();
let mut backend = DefaultReadBackend::default();
let vdot = backend.vdot_read(
TensorRead::from_tensor(&input),
TensorRead::from_tensor(&input),
);
let norm = backend.norm_squared_read(TensorRead::from_tensor(&input));
let axpby = backend.axpby_read_into_accum(
ContractionScalar::F64(1.0),
TensorRead::from_tensor(&input),
ContractionScalar::F64(1.0),
TensorWrite::from_tensor(&mut output),
);
for result in [vdot.map(|_| ()), norm.map(|_| ()), axpby] {
assert!(matches!(result, Err(crate::Error::Unsupported { .. })));
}
assert!(backend.calls.is_empty());
assert_eq!(output.as_slice::<f64>().unwrap(), &[10.0, 20.0]);
}
#[derive(Debug)]
struct AliasedBackendStorage {
domain: AllocationDomainId,
allocation: AllocationId,
len: usize,
}
impl BackendStorage<f64> for AliasedBackendStorage {
fn backend_family(&self) -> &'static str {
"blas1-alias-test"
}
fn len(&self) -> usize {
self.len
}
fn allocation_domain(&self) -> Option<AllocationDomainId> {
Some(self.domain)
}
fn allocation_id(&self) -> Option<AllocationId> {
Some(self.allocation)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[test]
fn blas1_axpby_rejects_backend_alias_before_execution() {
let domain = AllocationDomainId::fresh();
let allocation = AllocationId::from_backend_id(1713);
let storage = || {
StorageBuffer::Backend(Box::new(AliasedBackendStorage {
domain,
allocation,
len: 2,
}))
};
let placement = Placement {
memory_kind: crate::MemoryKind::Device,
device: None,
cpu_affinity: None,
};
let x = Tensor::from_typed::<f64>(
TypedTensor::from_buffer_col_major(vec![2], storage(), placement.clone()).unwrap(),
);
let mut y = Tensor::from_typed::<f64>(
TypedTensor::from_buffer_col_major(vec![2], storage(), placement).unwrap(),
);
let error = validate_axpby_read_into_accum(
ContractionScalar::F64(1.0),
&TensorRead::from_tensor(&x),
ContractionScalar::F64(1.0),
&TensorWrite::from_tensor(&mut y),
)
.unwrap_err();
assert!(matches!(
error,
crate::Error::Validation {
source: crate::ValidationError::InvalidArgument {
argument: "out",
..
},
..
}
));
}
#[test]
fn materializing_read_defaults_stage_views_only() {
let owned = Tensor::from_vec_col_major(vec![2, 2], vec![1.0_f64, 2.0, 3.0, 4.0]).unwrap();
let index = Tensor::from_vec_col_major(vec![1, 1], vec![0_i64]).unwrap();
let data = [1.0_f64, 2.0, 3.0, 4.0];
let view = || {
TensorRead::from_view(TensorView::F64(
TypedTensorView::from_slice(vec![2, 2], vec![2, 1], 0, &data).unwrap(),
))
};
let gather = GatherConfig {
offset_dims: vec![1],
collapsed_slice_dims: vec![0],
start_index_map: vec![0],
index_vector_dim: 1,
slice_sizes: vec![1, 2],
};
let scatter = ScatterConfig {
update_window_dims: vec![1],
inserted_window_dims: vec![0],
scatter_dims_to_operand_dims: vec![0],
index_vector_dim: 1,
};
let slice = SliceConfig {
starts: vec![0, 0],
limits: vec![1, 2],
strides: vec![1, 1],
};
let pad = PadConfig {
edge_padding_low: vec![1, 0],
edge_padding_high: vec![0, 0],
interior_padding: vec![0, 0],
};
macro_rules! run_all {
($backend:expr, $read:expr) => {{
let backend: &mut DefaultReadBackend = $backend;
let idx = || TensorRead::from_tensor(&index);
backend.gather_read($read, idx(), &gather).unwrap();
backend.scatter_read($read, idx(), $read, &scatter).unwrap();
backend.slice_read($read, &slice).unwrap();
backend.dynamic_slice_read($read, idx(), &[1, 2]).unwrap();
backend
.dynamic_update_slice_read($read, $read, idx())
.unwrap();
backend.pad_read($read, &pad).unwrap();
backend.concatenate_read(&[$read, $read], 0).unwrap();
backend.reverse_read($read, &[0]).unwrap();
backend.tril_read($read, 0).unwrap();
backend.triu_read($read, 0).unwrap();
backend.extract_diagonal_read($read, 0, 1).unwrap();
backend.embed_diagonal_read($read, 0, 1).unwrap();
15
}};
}
let mut backend = DefaultReadBackend::default();
run_all!(&mut backend, TensorRead::from_tensor(&owned));
assert_eq!(
backend.contiguous_reads, 0,
"an owned tensor is never staged"
);
let mut backend = DefaultReadBackend::default();
let views = run_all!(&mut backend, view());
assert_eq!(backend.contiguous_reads, views, "each view is staged once");
}
#[test]
fn default_read_methods_delegate_owned_tensors_and_reject_views() {
let a = Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
let b = Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
let pred = Tensor::from_vec_col_major(vec![1], vec![true]).unwrap();
let view_source = TypedTensor::<f64>::from_vec_col_major(vec![1], vec![3.0]).unwrap();
let mut backend = DefaultReadBackend::default();
backend
.add_read(TensorRead::from_tensor(&a), TensorRead::from_tensor(&b))
.unwrap();
backend
.mul_read(TensorRead::from_tensor(&a), TensorRead::from_tensor(&b))
.unwrap();
backend.neg_read(TensorRead::from_tensor(&a)).unwrap();
backend.conj_read(TensorRead::from_tensor(&a)).unwrap();
backend
.div_read(TensorRead::from_tensor(&a), TensorRead::from_tensor(&b))
.unwrap();
backend.abs_read(TensorRead::from_tensor(&a)).unwrap();
backend.sign_read(TensorRead::from_tensor(&a)).unwrap();
backend
.maximum_read(TensorRead::from_tensor(&a), TensorRead::from_tensor(&b))
.unwrap();
backend
.minimum_read(TensorRead::from_tensor(&a), TensorRead::from_tensor(&b))
.unwrap();
backend
.compare_read(
TensorRead::from_tensor(&a),
TensorRead::from_tensor(&b),
&CompareDir::Lt,
)
.unwrap();
backend
.select_read(
TensorRead::from_tensor(&pred),
TensorRead::from_tensor(&a),
TensorRead::from_tensor(&b),
)
.unwrap();
backend
.clamp_read(
TensorRead::from_tensor(&a),
TensorRead::from_tensor(&a),
TensorRead::from_tensor(&b),
)
.unwrap();
backend.exp_read(TensorRead::from_tensor(&a)).unwrap();
backend.log_read(TensorRead::from_tensor(&a)).unwrap();
backend.sin_read(TensorRead::from_tensor(&a)).unwrap();
backend.cos_read(TensorRead::from_tensor(&a)).unwrap();
backend.tanh_read(TensorRead::from_tensor(&a)).unwrap();
backend.sqrt_read(TensorRead::from_tensor(&a)).unwrap();
backend.rsqrt_read(TensorRead::from_tensor(&a)).unwrap();
backend
.pow_read(TensorRead::from_tensor(&a), TensorRead::from_tensor(&b))
.unwrap();
backend.expm1_read(TensorRead::from_tensor(&a)).unwrap();
backend.log1p_read(TensorRead::from_tensor(&a)).unwrap();
backend
.reshape_read(TensorRead::from_tensor(&a), &[1])
.unwrap();
backend
.broadcast_in_dim_read(TensorRead::from_tensor(&a), &[1], &[0])
.unwrap();
backend
.reduce_sum_read(TensorRead::from_tensor(&a), &[0])
.unwrap();
backend
.reduce_prod_read(TensorRead::from_tensor(&a), &[0])
.unwrap();
backend
.reduce_max_read(TensorRead::from_tensor(&a), &[0])
.unwrap();
backend
.reduce_min_read(TensorRead::from_tensor(&a), &[0])
.unwrap();
let config = DotGeneralConfig {
lhs_contracting_dims: [0].as_slice().into(),
rhs_contracting_dims: [0].as_slice().into(),
lhs_batch_dims: [].as_slice().into(),
rhs_batch_dims: [].as_slice().into(),
};
backend
.dot_general_read(
TensorRead::from_tensor(&a),
TensorRead::from_tensor(&b),
&config,
)
.unwrap();
backend
.dot_general_read(
TensorRead::from_view(TensorView::F64(view_source.as_view())),
TensorRead::from_tensor(&b),
&config,
)
.unwrap();
backend
.dot_general_with_conj(&a, &b, &config, true, false)
.unwrap();
backend
.dot_general_read_cached(
Some(0),
TensorRead::from_tensor(&a),
TensorRead::from_tensor(&b),
&config,
)
.unwrap();
backend
.dot_general_read_cached(
Some(1),
TensorRead::from_view(TensorView::F64(view_source.as_view())),
TensorRead::from_tensor(&b),
&config,
)
.unwrap();
backend
.dot_general_with_conj_read_cached(
Some(2),
TensorRead::from_tensor(&a),
TensorRead::from_tensor(&b),
&config,
true,
false,
)
.unwrap();
let err = backend
.add_read(
TensorRead::from_view(TensorView::F64(view_source.as_view())),
TensorRead::from_tensor(&b),
)
.unwrap_err();
assert!(err.to_string().contains("borrowed tensor views"));
assert!(backend.calls.contains(&"add"));
assert!(backend.calls.contains(&"dot_general"));
}
#[test]
fn elementwise_into_defaults_overwrite_outputs_and_validate_output() {
let a = Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
let b = Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
let mut backend = DefaultReadBackend::default();
let mut out = Tensor::from_vec_col_major(vec![1], vec![0.0_f64]).unwrap();
backend
.add_into(&a, &b, TensorWrite::from_tensor(&mut out))
.unwrap();
assert_eq!(out.as_slice::<f64>().unwrap(), &[3.0]);
let mut out = Tensor::from_vec_col_major(vec![1], vec![0.0_f64]).unwrap();
backend
.sub_into(&a, &b, TensorWrite::from_tensor(&mut out))
.unwrap();
assert_eq!(out.as_slice::<f64>().unwrap(), &[-1.0]);
let mut out = Tensor::from_vec_col_major(vec![1], vec![0.0_f64]).unwrap();
backend
.mul_into(&a, &b, TensorWrite::from_tensor(&mut out))
.unwrap();
assert_eq!(out.as_slice::<f64>().unwrap(), &[2.0]);
let mut out = Tensor::from_vec_col_major(vec![1], vec![0.0_f64]).unwrap();
backend
.neg_into(&a, TensorWrite::from_tensor(&mut out))
.unwrap();
assert_eq!(out.as_slice::<f64>().unwrap(), &[-1.0]);
let mut out = Tensor::from_vec_col_major(vec![1], vec![0.0_f64]).unwrap();
backend
.conj_into(&a, TensorWrite::from_tensor(&mut out))
.unwrap();
assert_eq!(out.as_slice::<f64>().unwrap(), &[1.0]);
let mut out = Tensor::from_vec_col_major(vec![1], vec![0.0_f64]).unwrap();
backend
.div_read_into(
TensorRead::from_tensor(&a),
TensorRead::from_tensor(&b),
TensorWrite::from_tensor(&mut out),
)
.unwrap();
assert_eq!(out.as_slice::<f64>().unwrap(), &[0.5]);
assert!(
backend.calls.is_empty(),
"host elementwise into defaults must not allocate through the backend"
);
let mut wrong_shape = Tensor::from_vec_col_major(vec![2], vec![0.0_f64, 0.0]).unwrap();
let err = backend
.add_into(&a, &b, TensorWrite::from_tensor(&mut wrong_shape))
.unwrap_err();
assert!(matches!(
err,
crate::Error::Validation {
source: crate::ValidationError::ShapeMismatch(_),
..
}
));
let mut wrong_dtype = Tensor::from_vec_col_major(vec![1], vec![0_i32]).unwrap();
let err = backend
.add_into(&a, &b, TensorWrite::from_tensor(&mut wrong_dtype))
.unwrap_err();
assert!(matches!(
err,
crate::Error::Validation {
source: crate::ValidationError::DTypeMismatch { .. },
..
}
));
}
#[test]
fn elementwise_into_accepts_independent_backend_destinations() {
let placement = crate::Placement {
memory_kind: crate::MemoryKind::Device,
device: Some(crate::DeviceId {
kind: crate::DeviceKind::Gpu(crate::GpuBackendKind::Cuda),
ordinal: 0,
}),
cpu_affinity: None,
};
let lhs = Tensor::from_typed::<f64>(
TypedTensor::from_buffer_col_major(
vec![1],
crate::StorageBuffer::Backend(Box::new(
crate::BackendStorageHandle::<f64>::new_with_len(17, 1),
)),
placement.clone(),
)
.unwrap(),
);
let rhs = Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
let mut out = Tensor::from_typed::<f64>(
TypedTensor::from_buffer_col_major(
vec![1],
crate::StorageBuffer::Backend(Box::new(
crate::BackendStorageHandle::<f64>::new_with_len(18, 1),
)),
placement,
)
.unwrap(),
);
DefaultReadBackend::default()
.add_into(&lhs, &rhs, TensorWrite::from_tensor(&mut out))
.unwrap();
}
#[test]
fn elementwise_into_updates_caller_view_storage_after_handoff() {
let lhs = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
let rhs = Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
let mut storage = vec![-1.0_f64, 0.0, 0.0, -1.0];
{
let view = TypedTensorViewMut::from_slice(vec![2], vec![1], 1, &mut storage).unwrap();
DefaultReadBackend::default()
.add_into(&lhs, &rhs, TensorWrite::from_view(TensorViewMut::F64(view)))
.unwrap();
}
assert_eq!(storage, vec![-1.0, 4.0, 6.0, -1.0]);
storage[2] = 9.0;
assert_eq!(storage, vec![-1.0, 4.0, 9.0, -1.0]);
}
#[test]
fn contraction_scalar_helpers_cover_supported_and_rejected_dtypes() {
assert_eq!(ContractionScalar::F32(1.0).dtype(), DType::F32);
assert_eq!(ContractionScalar::F64(1.0).dtype(), DType::F64);
assert_eq!(
ContractionScalar::C32(Complex32::new(1.0, 0.0)).dtype(),
DType::C32
);
assert_eq!(
ContractionScalar::C64(Complex64::new(1.0, 0.0)).dtype(),
DType::C64
);
assert_eq!(
ContractionScalar::one(DType::C32).unwrap(),
ContractionScalar::C32(Complex32::new(1.0, 0.0))
);
assert_eq!(
ContractionScalar::zero(DType::C64).unwrap(),
ContractionScalar::C64(Complex64::new(0.0, 0.0))
);
assert!(ContractionScalar::one(DType::I64).is_err());
assert!(ContractionScalar::zero(DType::Bool).is_err());
let overwrite = DotGeneralAccumulation::overwrite(DType::F32).unwrap();
assert_eq!(overwrite.alpha, ContractionScalar::F32(1.0));
assert_eq!(overwrite.beta, ContractionScalar::F32(0.0));
let add_to = DotGeneralAccumulation::add_to(DType::F64).unwrap();
assert_eq!(add_to.alpha, ContractionScalar::F64(1.0));
assert_eq!(add_to.beta, ContractionScalar::F64(1.0));
let scaled =
DotGeneralAccumulation::scaled(ContractionScalar::F64(0.5), ContractionScalar::F64(2.0))
.unwrap();
assert_eq!(scaled.alpha, ContractionScalar::F64(0.5));
assert_eq!(scaled.beta, ContractionScalar::F64(2.0));
assert!(DotGeneralAccumulation::scaled(
ContractionScalar::F32(1.0),
ContractionScalar::F64(1.0)
)
.is_err());
assert!(DotGeneralAccumulation::overwrite(DType::I32).is_err());
}
fn vector_contract_config() -> DotGeneralConfig {
DotGeneralConfig {
lhs_contracting_dims: [0].as_slice().into(),
rhs_contracting_dims: [0].as_slice().into(),
lhs_batch_dims: [].as_slice().into(),
rhs_batch_dims: [].as_slice().into(),
}
}
#[test]
fn dot_general_read_into_dtype_error_reports_output_as_actual() {
let lhs = Tensor::from_vec_col_major(vec![1], vec![1.0_f32]).unwrap();
let rhs = Tensor::from_vec_col_major(vec![1], vec![2.0_f32]).unwrap();
let mut out = Tensor::from_vec_col_major(vec![], vec![0.0_f64]).unwrap();
let err = validate_dot_general_read_into(
&TensorRead::from_tensor(&lhs),
&TensorRead::from_tensor(&rhs),
&vector_contract_config(),
&TensorWrite::from_tensor(&mut out),
"test_dot_general",
)
.unwrap_err();
match err {
crate::Error::Validation {
source: crate::ValidationError::DTypeMismatch { expected, actual },
..
} => {
assert_eq!(expected, DType::F32);
assert_eq!(actual, DType::F64);
}
other => panic!("expected dtype mismatch, got {other:?}"),
}
}
#[test]
fn dot_general_accum_default_fallback_updates_tensor_and_view_outputs() {
let lhs = Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
let rhs = Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
let config = vector_contract_config();
let mut backend = DefaultReadBackend {
dot_result: Some(Tensor::from_vec_col_major(vec![], vec![42.0_f64]).unwrap()),
..Default::default()
};
let accumulation = DotGeneralAccumulation {
lhs_conj: true,
rhs_conj: false,
alpha: ContractionScalar::F64(2.0),
beta: ContractionScalar::F64(0.5),
};
let mut out = Tensor::from_vec_col_major(vec![], vec![10.0_f64]).unwrap();
backend
.dot_general_read_into_accum(
TensorRead::from_tensor(&lhs),
TensorRead::from_tensor(&rhs),
&config,
accumulation,
TensorWrite::from_tensor(&mut out),
)
.unwrap();
assert_eq!(out.as_slice::<f64>().unwrap(), &[89.0]);
assert!(backend.calls.contains(&"conj"));
assert!(backend.calls.contains(&"dot_general"));
let mut out_view = TypedTensor::<f64>::from_vec_col_major(vec![], vec![1.0]).unwrap();
SessionCachedDot::dot_general_read_into_accum_cached(
&mut backend,
Some(7),
TensorRead::from_tensor(&lhs),
TensorRead::from_tensor(&rhs),
&config,
DotGeneralAccumulation {
lhs_conj: false,
rhs_conj: false,
alpha: ContractionScalar::F64(1.0),
beta: ContractionScalar::F64(1.0),
},
TensorWrite::from_view(TensorViewMut::F64(out_view.as_view_mut())),
)
.unwrap();
assert_eq!(out_view.as_slice().unwrap(), &[43.0]);
}
#[test]
fn dot_general_accum_beta_zero_does_not_read_compact_output() {
let dot = Tensor::from_vec_col_major([2], vec![2.0_f64, -3.0]).unwrap();
let mut out = Tensor::from_vec_col_major([2], vec![f64::NAN, f64::NAN]).unwrap();
let mut write = TensorWrite::from_tensor(&mut out);
crate::backend::accumulate_dot_result_into(
&dot,
DotGeneralAccumulation {
lhs_conj: false,
rhs_conj: false,
alpha: ContractionScalar::F64(2.0),
beta: ContractionScalar::F64(0.0),
},
&mut write,
)
.unwrap();
assert_eq!(out.as_slice::<f64>().unwrap(), &[4.0, -6.0]);
}
#[test]
fn dot_general_accum_keeps_strided_output_fallback() {
let dot = Tensor::from_vec_col_major([2], vec![2.0_f64, 3.0]).unwrap();
let mut storage = [10.0_f64, 99.0, 20.0];
let view = TypedTensorViewMut::from_slice([2], [2], 0, &mut storage).unwrap();
let mut write = TensorWrite::from_view(TensorViewMut::F64(view));
crate::backend::accumulate_dot_result_into(
&dot,
DotGeneralAccumulation {
lhs_conj: false,
rhs_conj: false,
alpha: ContractionScalar::F64(1.0),
beta: ContractionScalar::F64(1.0),
},
&mut write,
)
.unwrap();
assert_eq!(storage, [12.0, 99.0, 23.0]);
}
#[test]
fn dot_general_accum_default_fallback_covers_supported_scalar_dtypes() {
let config = vector_contract_config();
let lhs = Tensor::from_vec_col_major(vec![1], vec![1.0_f32]).unwrap();
let rhs = Tensor::from_vec_col_major(vec![1], vec![2.0_f32]).unwrap();
let mut out = Tensor::from_vec_col_major(vec![], vec![10.0_f32]).unwrap();
let mut backend = DefaultReadBackend {
dot_result: Some(Tensor::from_vec_col_major(vec![], vec![4.0_f32]).unwrap()),
..Default::default()
};
backend
.dot_general_read_into_accum(
TensorRead::from_tensor(&lhs),
TensorRead::from_tensor(&rhs),
&config,
DotGeneralAccumulation {
lhs_conj: false,
rhs_conj: false,
alpha: ContractionScalar::F32(2.0),
beta: ContractionScalar::F32(0.5),
},
TensorWrite::from_tensor(&mut out),
)
.unwrap();
assert_eq!(out.as_slice::<f32>().unwrap(), &[13.0]);
let lhs = Tensor::from_vec_col_major(vec![1], vec![Complex32::new(1.0, 0.0)]).unwrap();
let rhs = Tensor::from_vec_col_major(vec![1], vec![Complex32::new(2.0, 0.0)]).unwrap();
let mut out = Tensor::from_vec_col_major(vec![], vec![Complex32::new(3.0, -1.0)]).unwrap();
let mut backend = DefaultReadBackend {
dot_result: Some(
Tensor::from_vec_col_major(vec![], vec![Complex32::new(1.0, 2.0)]).unwrap(),
),
..Default::default()
};
backend
.dot_general_read_into_accum(
TensorRead::from_tensor(&lhs),
TensorRead::from_tensor(&rhs),
&config,
DotGeneralAccumulation {
lhs_conj: false,
rhs_conj: false,
alpha: ContractionScalar::C32(Complex32::new(2.0, 0.0)),
beta: ContractionScalar::C32(Complex32::new(0.0, 1.0)),
},
TensorWrite::from_tensor(&mut out),
)
.unwrap();
assert_eq!(
out.as_slice::<Complex32>().unwrap(),
&[Complex32::new(3.0, 7.0)]
);
let lhs = Tensor::from_vec_col_major(vec![1], vec![Complex64::new(1.0, 0.0)]).unwrap();
let rhs = Tensor::from_vec_col_major(vec![1], vec![Complex64::new(2.0, 0.0)]).unwrap();
let mut out = Tensor::from_vec_col_major(vec![], vec![Complex64::new(5.0, 0.0)]).unwrap();
let mut backend = DefaultReadBackend {
dot_result: Some(
Tensor::from_vec_col_major(vec![], vec![Complex64::new(4.0, -2.0)]).unwrap(),
),
..Default::default()
};
backend
.dot_general_read_into_accum(
TensorRead::from_tensor(&lhs),
TensorRead::from_tensor(&rhs),
&config,
DotGeneralAccumulation {
lhs_conj: false,
rhs_conj: false,
alpha: ContractionScalar::C64(Complex64::new(1.0, 0.0)),
beta: ContractionScalar::C64(Complex64::new(0.0, 0.0)),
},
TensorWrite::from_tensor(&mut out),
)
.unwrap();
assert_eq!(
out.as_slice::<Complex64>().unwrap(),
&[Complex64::new(4.0, -2.0)]
);
}
#[test]
fn dot_general_accum_default_fallback_rejects_scalar_dtype_mismatch() {
let lhs = Tensor::from_vec_col_major(vec![1], vec![1.0_f64]).unwrap();
let rhs = Tensor::from_vec_col_major(vec![1], vec![2.0_f64]).unwrap();
let mut out = Tensor::from_vec_col_major(vec![], vec![0.0_f64]).unwrap();
let config = vector_contract_config();
let mut backend = DefaultReadBackend::default();
let err = backend
.dot_general_read_into_accum(
TensorRead::from_tensor(&lhs),
TensorRead::from_tensor(&rhs),
&config,
DotGeneralAccumulation {
lhs_conj: false,
rhs_conj: false,
alpha: ContractionScalar::F32(1.0),
beta: ContractionScalar::F64(0.0),
},
TensorWrite::from_tensor(&mut out),
)
.unwrap_err();
assert!(err.to_string().contains("dtype mismatch"));
}
#[test]
fn tensor_index_select_builds_gather_config_and_validates_inputs() {
let input = Tensor::from_vec_col_major(vec![2, 3], vec![0.0_f64; 6]).unwrap();
let mut backend = DefaultReadBackend::default();
input.index_select(-1, &[2, 0], &mut backend).unwrap();
let indices = backend.gather_indices.as_ref().unwrap();
assert_eq!(indices.shape(), &[2, 1]);
assert_eq!(indices.as_slice::<i64>().unwrap(), &[2, 0]);
let config = backend.gather_config.as_ref().unwrap();
assert_eq!(config.offset_dims, vec![0]);
assert_eq!(config.collapsed_slice_dims, vec![1]);
assert_eq!(config.start_index_map, vec![1]);
assert_eq!(config.index_vector_dim, 1);
assert_eq!(config.slice_sizes, vec![2, 1]);
let axis_err = input.index_select(2, &[0], &mut backend).unwrap_err();
assert!(axis_err.to_string().contains("axis 2"));
let position_err = input.index_select(1, &[3], &mut backend).unwrap_err();
assert!(position_err
.to_string()
.contains("position 3 out of bounds"));
}
#[test]
fn tensor_stack_reshapes_then_concatenates_and_validates_inputs() {
let a = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
let b = Tensor::from_vec_col_major(vec![2], vec![3.0_f64, 4.0]).unwrap();
let mut backend = DefaultReadBackend::default();
Tensor::stack(&[&a, &b], -1, &mut backend).unwrap();
assert_eq!(backend.reshape_shapes, vec![vec![2, 1], vec![2, 1]]);
assert_eq!(backend.concatenate_axis, Some(1));
let empty: [&Tensor; 0] = [];
let empty_err = Tensor::stack(&empty, 0, &mut backend).unwrap_err();
assert!(empty_err.to_string().contains("at least one input"));
let c = Tensor::from_vec_col_major(vec![3], vec![0.0_f64; 3]).unwrap();
let shape_err = Tensor::stack(&[&a, &c], 0, &mut backend).unwrap_err();
assert!(matches!(
shape_err,
crate::Error::Validation {
source: crate::ValidationError::ShapeMismatch(_),
..
}
));
let axis_err = Tensor::stack(&[&a], 2, &mut backend).unwrap_err();
assert!(axis_err.to_string().contains("axis 2"));
}
#[test]
fn structural_runtime_materialization_is_object_safe_and_clones_owned_input_by_default() {
let input = Tensor::from_vec_col_major(vec![2], vec![1.0_f64, 2.0]).unwrap();
let mut backend = DefaultReadBackend {
structural_runtime_enabled: false,
..Default::default()
};
let session: &mut dyn Engine = &mut backend;
let output = session
.to_contiguous_read(TensorRead::from_tensor(&input))
.unwrap();
assert_eq!(output.as_slice::<f64>().unwrap(), &[1.0, 2.0]);
assert_eq!(input.as_slice::<f64>().unwrap(), &[1.0, 2.0]);
}
#[test]
fn structural_runtime_materialization_rejects_views_by_default() {
let data = [1.0_f64, 2.0];
let view = TensorView::f64(&[2], &data).unwrap();
let mut backend = DefaultReadBackend {
structural_runtime_enabled: false,
..Default::default()
};
let session: &mut dyn Engine = &mut backend;
let err = session
.to_contiguous_read(TensorRead::from_view(view))
.unwrap_err();
assert!(matches!(
err,
crate::Error::Unsupported {
op: "to_contiguous_read",
ref message,
} if message.contains("borrowed tensor views")
));
}
#[test]
fn structural_runtime_materialization_rejects_foreign_backend_storage_by_default() {
let input = Tensor::from_typed::<f64>(
TypedTensor::from_buffer_col_major(
vec![2],
crate::StorageBuffer::Backend(Box::new(
crate::BackendStorageHandle::<f64>::new_with_len(41, 2),
)),
crate::Placement {
memory_kind: crate::MemoryKind::Device,
device: Some(crate::DeviceId {
kind: crate::DeviceKind::Gpu(crate::GpuBackendKind::Cuda),
ordinal: 0,
}),
cpu_affinity: None,
},
)
.unwrap(),
);
let mut backend = DefaultReadBackend {
structural_runtime_enabled: false,
..Default::default()
};
let session: &mut dyn Engine = &mut backend;
let err = session
.to_contiguous_read(TensorRead::from_tensor(&input))
.unwrap_err();
assert!(matches!(
err,
crate::Error::RuntimeState {
op: "to_contiguous_read",
ref message,
} if message.contains("host-owned") && message.contains("owning backend")
));
}
#[test]
fn structural_runtime_copy_is_explicitly_unsupported_by_default() {
let src = Tensor::from_vec_col_major(vec![2], vec![1_i32, 2]).unwrap();
let mut dst = Tensor::from_vec_col_major(vec![2], vec![0_i32, 0]).unwrap();
let mut backend = DefaultReadBackend {
structural_runtime_enabled: false,
..Default::default()
};
let session: &mut dyn Engine = &mut backend;
let err = session
.copy_read_into(
TensorRead::from_tensor(&src),
TensorWrite::from_tensor(&mut dst),
)
.unwrap_err();
assert!(matches!(
err,
crate::Error::Unsupported {
op: "copy_read_into",
ref message,
} if message.contains("unsupported")
));
assert_eq!(dst.as_slice::<i32>().unwrap(), &[0, 0]);
}