use super::{safe_add, safe_where, simple_eval};
use crate::onnx::{AttrKind, Attribute, Graph, Model, Node, ValueInfo};
use candle_core::{DType, Device, Result, Tensor};
use std::collections::HashMap;
fn node(op: &str, inputs: &[&str], outputs: &[&str]) -> Node {
Node {
op_type: op.to_string(),
name: String::new(),
inputs: inputs
.iter()
.map(std::string::ToString::to_string)
.collect(),
outputs: outputs
.iter()
.map(std::string::ToString::to_string)
.collect(),
attributes: vec![],
}
}
fn node_attrs(op: &str, inputs: &[&str], outputs: &[&str], attributes: Vec<Attribute>) -> Node {
Node {
op_type: op.to_string(),
name: String::new(),
inputs: inputs
.iter()
.map(std::string::ToString::to_string)
.collect(),
outputs: outputs
.iter()
.map(std::string::ToString::to_string)
.collect(),
attributes,
}
}
fn attr(name: &str, kind: AttrKind) -> Attribute {
Attribute {
name: name.to_string(),
kind,
}
}
fn graph(nodes: Vec<Node>, outputs: &[&str]) -> Graph {
Graph {
nodes,
initializers: vec![],
inputs: vec![],
outputs: outputs
.iter()
.map(|n| ValueInfo {
name: n.to_string(),
elem_type: None,
})
.collect(),
}
}
fn model(graph: Graph) -> Model {
Model { graph }
}
fn run(model: &Model, inputs: Vec<(&str, Tensor)>) -> Result<HashMap<String, Tensor>> {
let map = inputs
.into_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
simple_eval(model, map)
}
fn get<'a>(m: &'a HashMap<String, Tensor>, name: &str) -> &'a Tensor {
m.get(name).expect("output not found")
}
#[test]
fn test_dtype_promotion_add() -> Result<()> {
let manual_graph = model(graph(vec![node("Add", &["x", "y"], &["z"])], &["z"]));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[2.0f32, 4.0f32], &Device::Cpu)?),
("y", Tensor::new(&[1i64, 2i64], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dtype(), DType::F32, "mixed add must promote to F32");
assert_eq!(z.to_vec1::<f32>()?, vec![3.0f32, 6.0]);
Ok(())
}
#[test]
fn test_dtype_promotion_mul() -> Result<()> {
let manual_graph = model(graph(vec![node("Mul", &["x", "y"], &["z"])], &["z"]));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[2.0f32, 4.0f32], &Device::Cpu)?),
("y", Tensor::new(&[3i64, 5i64], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dtype(), DType::F32);
assert_eq!(z.to_vec1::<f32>()?, vec![6.0f32, 20.0]);
Ok(())
}
#[test]
fn test_softplus_reciprocal_chain() -> Result<()> {
let manual_graph = model(graph(
vec![
node("Softplus", &["x"], &["s"]),
node("Reciprocal", &["s"], &["r"]),
],
&["r"],
));
let eval = run(
&manual_graph,
vec![("x", Tensor::new(&[1.0f32, 0.0f32], &Device::Cpu)?)],
)?;
let z = get(&eval, "r");
let vals = z.to_vec1::<f32>()?;
let expected = [0.761_467_f32, std::f32::consts::LOG2_E];
for (got, want) in vals.iter().zip(expected) {
assert!((got - want).abs() < 1e-4, "got {got}, expected {want}");
}
Ok(())
}
#[test]
fn test_layer_normalization_graph() -> Result<()> {
let manual_graph = model(graph(
vec![node("LayerNormalization", &["x", "gamma", "beta"], &["z"])],
&["z"],
));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[1.0f32, 2.0, 3.0], &Device::Cpu)?),
("gamma", Tensor::new(&[1.0f32, 1.0, 1.0], &Device::Cpu)?),
("beta", Tensor::new(&[0.0f32, 0.0, 0.0], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec1::<f32>()?;
let expected = [-1.224_744_f32, 0.0, 1.224_744];
for (got, want) in vals.iter().zip(expected) {
assert!((got - want).abs() < 1e-4, "got {got}, expected {want}");
}
Ok(())
}
#[test]
fn test_layer_normalization_explicit_epsilon() -> Result<()> {
let ln = node_attrs(
"LayerNormalization",
&["x", "gamma", "beta"],
&["z"],
vec![
attr("axis", AttrKind::Int(-1)),
attr("epsilon", AttrKind::Float(1e-6)),
],
);
let manual_graph = model(graph(vec![ln], &["z"]));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[0.0f32, 0.0, 0.0], &Device::Cpu)?),
("gamma", Tensor::new(&[1.0f32, 1.0, 1.0], &Device::Cpu)?),
("beta", Tensor::new(&[0.0f32, 0.0, 0.0], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec1::<f32>()?;
assert!(vals.iter().all(|v| v.abs() < 1e-6), "got {vals:?}");
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[0.0f32, 0.0, 1e-3], &Device::Cpu)?),
("gamma", Tensor::new(&[1.0f32, 1.0, 1.0], &Device::Cpu)?),
("beta", Tensor::new(&[0.0f32, 0.0, 0.0], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec1::<f32>()?;
let m = 1e-3_f64 / 3.0;
let var = 2.0 * m * m;
let expected = 2.0 * m / (var + 1e-6).sqrt();
assert!(
(f64::from(vals[2]) - expected).abs() < 1e-6,
"epsilon=1e-6 not honored: got {}, expected {expected}",
vals[2]
);
Ok(())
}
#[test]
fn test_pad_edge_mode_graph() -> Result<()> {
let pad_node = node_attrs(
"Pad",
&["data", "pads"],
&["z"],
vec![attr("mode", AttrKind::Bytes(b"edge".to_vec()))],
);
let manual_graph = model(graph(vec![pad_node], &["z"]));
let eval = run(
&manual_graph,
vec![
(
"data",
Tensor::new(&[[1.0f32, 2.0], [3.0, 4.0]], &Device::Cpu)?,
),
("pads", Tensor::new(&[1i64, 0, 1, 0], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
let expected = vec![
vec![1.0f32, 2.0],
vec![1.0, 2.0],
vec![3.0, 4.0],
vec![3.0, 4.0],
];
assert_eq!(vals, expected);
Ok(())
}
#[test]
fn test_pad_constant_empty_third_input() -> Result<()> {
let pad_node = node_attrs(
"Pad",
&["data", "pads", ""],
&["z"],
vec![attr("mode", AttrKind::Bytes(b"constant".to_vec()))],
);
let manual_graph = model(graph(vec![pad_node], &["z"]));
let eval = run(
&manual_graph,
vec![
(
"data",
Tensor::new(&[[1.0f32, 2.0], [3.0, 4.0]], &Device::Cpu)?,
),
("pads", Tensor::new(&[1i64, 1, 0, 0], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
let expected = vec![
vec![0.0f32, 0.0, 0.0],
vec![0.0, 1.0, 2.0],
vec![0.0, 3.0, 4.0],
];
assert_eq!(vals, expected);
Ok(())
}
#[test]
fn test_pad_constant_provided_value() -> Result<()> {
let pad_node = node_attrs(
"Pad",
&["data", "pads", "value"],
&["z"],
vec![attr("mode", AttrKind::Bytes(b"constant".to_vec()))],
);
let manual_graph = model(graph(vec![pad_node], &["z"]));
let eval = run(
&manual_graph,
vec![
("data", Tensor::new(&[1.0f32, 2.0], &Device::Cpu)?),
("pads", Tensor::new(&[1i64, 1], &Device::Cpu)?),
("value", Tensor::new(&[7.0f32], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec1::<f32>()?;
assert_eq!(vals, vec![7.0f32, 1.0, 2.0, 7.0]);
Ok(())
}
#[test]
fn test_prelu_scalar_slope_graph() -> Result<()> {
let manual_graph = model(graph(vec![node("PRelu", &["x", "slope"], &["z"])], &["z"]));
let eval = run(
&manual_graph,
vec![
(
"x",
Tensor::new(&[[-1.0f32, 2.0], [3.0, -4.0]], &Device::Cpu)?,
),
("slope", Tensor::new(&[0.25f32], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
let expected = vec![vec![-0.25f32, 2.0], vec![3.0, -1.0]];
assert_eq!(vals, expected);
Ok(())
}
#[test]
fn test_operator_reciprocal() -> Result<()> {
let dev = &Device::Cpu;
let input = Tensor::new(&[2.0f32, 4.0, 0.5, -1.0], dev)?;
let result = input.recip()?;
let vals: Vec<f32> = result.to_vec1()?;
assert!(
(vals[0] - 0.5).abs() < 1e-6,
"1/2 should be 0.5, got {}",
vals[0]
);
assert!(
(vals[1] - 0.25).abs() < 1e-6,
"1/4 should be 0.25, got {}",
vals[1]
);
assert!(
(vals[2] - 2.0).abs() < 1e-6,
"1/0.5 should be 2.0, got {}",
vals[2]
);
assert!(
(vals[3] - (-1.0)).abs() < 1e-6,
"1/-1 should be -1.0, got {}",
vals[3]
);
let zero = Tensor::new(&[0.0f32], dev)?;
let inf_result = zero.recip()?;
let inf_vals: Vec<f32> = inf_result.to_vec1()?;
assert!(
inf_vals[0].is_infinite(),
"1/0 should be inf, got {}",
inf_vals[0]
);
Ok(())
}
#[test]
fn test_operator_softplus() -> Result<()> {
let dev = &Device::Cpu;
let input = Tensor::new(&[-100.0f32, -10.0, 0.0, 5.0, 20.0, 25.0, 100.0], dev)?;
let mask = input.gt(20.0f64)?;
let ones = Tensor::ones(input.dims(), input.dtype(), input.device())?;
let exp_add_one = safe_add(&input.exp()?, &ones)?;
let stable = exp_add_one.log()?;
let output = safe_where(&mask, &input, &stable)?;
let vals: Vec<f32> = output.to_vec1()?;
assert!(
vals[0] < 1e-40,
"softplus(-100) should be ~0, got {}",
vals[0]
);
assert!(
(vals[2] - std::f32::consts::LN_2).abs() < 1e-5,
"softplus(0) should be ~0.693147, got {}",
vals[2]
);
assert!(
(vals[3] - 5.0067).abs() < 1e-3,
"softplus(5) should be ~5.0067, got {}",
vals[3]
);
assert!(
(vals[5] - 25.0).abs() < 1e-5,
"softplus(25) should be 25.0 (stable branch), got {}",
vals[5]
);
assert!(
(vals[6] - 100.0).abs() < 1e-5,
"softplus(100) should be 100.0 (stable branch), got {}",
vals[6]
);
Ok(())
}
#[test]
fn test_slice_negative_step() -> Result<()> {
let manual_graph = model(graph(
vec![node("Slice", &["x", "s", "e", "a", "st"], &["z"])],
&["z"],
));
let eval = run(
&manual_graph,
vec![
(
"x",
Tensor::new(&[[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]], &Device::Cpu)?,
),
("s", Tensor::new(&[-1i64], &Device::Cpu)?),
("e", Tensor::new(&[-4i64], &Device::Cpu)?),
("a", Tensor::new(&[1i64], &Device::Cpu)?),
("st", Tensor::new(&[-1i64], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
assert_eq!(vals, vec![vec![3.0f32, 2.0, 1.0], vec![6.0, 5.0, 4.0]]);
Ok(())
}
#[test]
fn test_slice_4_input_form() -> Result<()> {
let manual_graph = model(graph(
vec![node("Slice", &["x", "s", "e", "a"], &["z"])],
&["z"],
));
let eval = run(
&manual_graph,
vec![
(
"x",
Tensor::new(&[[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]], &Device::Cpu)?,
),
("s", Tensor::new(&[1i64], &Device::Cpu)?),
("e", Tensor::new(&[2i64], &Device::Cpu)?),
("a", Tensor::new(&[1i64], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
assert_eq!(vals, vec![vec![2.0f32], vec![5.0]]);
Ok(())
}
#[test]
fn test_gemm_transb() -> Result<()> {
let gemm = node_attrs(
"Gemm",
&["a", "b", "c"],
&["z"],
vec![
attr("alpha", AttrKind::Float(1.0)),
attr("beta", AttrKind::Float(1.0)),
attr("transB", AttrKind::Int(1)),
],
);
let manual_graph = model(graph(vec![gemm], &["z"]));
let eval = run(
&manual_graph,
vec![
("a", Tensor::new(&[[1.0f32, 2.0]], &Device::Cpu)?),
(
"b",
Tensor::new(&[[1.0f32, 0.0], [0.0, 1.0]], &Device::Cpu)?,
),
("c", Tensor::new(&[0.5f32, 0.5], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.flatten_all()?.to_vec1::<f32>()?;
assert_eq!(vals, vec![1.5f32, 2.5]);
Ok(())
}
#[test]
fn test_unsqueeze_negative_axis() -> Result<()> {
let manual_graph = model(graph(
vec![node("Unsqueeze", &["x", "axes"], &["z"])],
&["z"],
));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[1.0f32, 2.0, 3.0], &Device::Cpu)?),
("axes", Tensor::new(&[-1i64], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dims(), &[3, 1]);
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[1.0f32, 2.0, 3.0], &Device::Cpu)?),
("axes", Tensor::new(&[0i64], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dims(), &[1, 3]);
Ok(())
}
#[test]
fn test_concat_majority_dtype_promotion() -> Result<()> {
let cat = node_attrs(
"Concat",
&["a", "b", "c"],
&["z"],
vec![attr("axis", AttrKind::Int(0))],
);
let manual_graph = model(graph(vec![cat], &["z"]));
let eval = run(
&manual_graph,
vec![
("a", Tensor::new(&[[1.0f32]], &Device::Cpu)?),
("b", Tensor::new(&[[2i64]], &Device::Cpu)?),
("c", Tensor::new(&[[3.0f32]], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dtype(), DType::F32, "majority F32 must win");
assert_eq!(
z.to_vec2::<f32>()?,
vec![vec![1.0f32], vec![2.0], vec![3.0]]
);
Ok(())
}
#[test]
fn test_concat_trailing_singleton_squeeze() -> Result<()> {
let cat = node_attrs(
"Concat",
&["a", "b"],
&["z"],
vec![attr("axis", AttrKind::Int(0))],
);
let manual_graph = model(graph(vec![cat], &["z"]));
let eval = run(
&manual_graph,
vec![
("a", Tensor::new(&[[1.0f32], [2.0]], &Device::Cpu)?),
("b", Tensor::new(&[3.0f32, 4.0], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dims(), &[4]);
assert_eq!(z.to_vec1::<f32>()?, vec![1.0f32, 2.0, 3.0, 4.0]);
Ok(())
}
#[test]
fn test_pow_negative_base_f32_i64_exp() -> Result<()> {
let manual_graph = model(graph(
vec![
node("Pow", &["x", "exp2"], &["z2"]),
node("Pow", &["x", "exp3"], &["z3"]),
],
&["z2", "z3"],
));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[-2.0f32, -3.0], &Device::Cpu)?),
("exp2", Tensor::new(&[2i64], &Device::Cpu)?),
("exp3", Tensor::new(&[3i64], &Device::Cpu)?),
],
)?;
assert_eq!(get(&eval, "z2").to_vec1::<f32>()?, vec![4.0f32, 9.0]);
assert_eq!(get(&eval, "z3").to_vec1::<f32>()?, vec![-8.0f32, -27.0]);
Ok(())
}
#[test]
fn test_cast_bool_to_u8() -> Result<()> {
let cast = node_attrs("Cast", &["x"], &["z"], vec![attr("to", AttrKind::Int(9))]);
let manual_graph = model(graph(vec![cast], &["z"]));
let eval = run(
&manual_graph,
vec![("x", Tensor::new(&[0u8, 1u8, 1u8], &Device::Cpu)?)],
)?;
let z = get(&eval, "z");
assert_eq!(z.dtype(), DType::U8);
assert_eq!(z.to_vec1::<u8>()?, vec![0, 1, 1]);
Ok(())
}
#[test]
fn test_cast_int32_to_i64() -> Result<()> {
let cast = node_attrs("Cast", &["x"], &["z"], vec![attr("to", AttrKind::Int(6))]);
let manual_graph = model(graph(vec![cast], &["z"]));
let eval = run(
&manual_graph,
vec![("x", Tensor::new(&[1i64, 2i64], &Device::Cpu)?)],
)?;
let z = get(&eval, "z");
assert_eq!(z.dtype(), DType::I64);
Ok(())
}
#[test]
fn test_reshape_zero_keeps_input_dim() -> Result<()> {
let manual_graph = model(graph(
vec![node("Reshape", &["x", "shape"], &["z"])],
&["z"],
));
let eval = run(
&manual_graph,
vec![
(
"x",
Tensor::new(&[[1.0f32, 2.0], [3.0, 4.0]], &Device::Cpu)?,
),
("shape", Tensor::new(&[0i64, 2], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dims(), &[2, 2]);
assert_eq!(z.to_vec2::<f32>()?, vec![vec![1.0f32, 2.0], vec![3.0, 4.0]]);
Ok(())
}
#[test]
fn test_reshape_minus_one_infers() -> Result<()> {
let manual_graph = model(graph(
vec![node("Reshape", &["x", "shape"], &["z"])],
&["z"],
));
let eval = run(
&manual_graph,
vec![
(
"x",
Tensor::new(&[[1.0f32, 2.0], [3.0, 4.0]], &Device::Cpu)?,
),
("shape", Tensor::new(&[-1i64, 2], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dims(), &[2, 2]);
assert_eq!(z.to_vec2::<f32>()?, vec![vec![1.0f32, 2.0], vec![3.0, 4.0]]);
Ok(())
}
#[test]
fn test_where_broadcast() -> Result<()> {
let manual_graph = model(graph(
vec![node("Where", &["cond", "a", "b"], &["z"])],
&["z"],
));
let eval = run(
&manual_graph,
vec![
("cond", Tensor::new(&[[1u8], [0u8]], &Device::Cpu)?),
("a", Tensor::new(&[10.0f32, 11.0, 12.0], &Device::Cpu)?),
("b", Tensor::new(&[20.0f32, 21.0, 22.0], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
assert_eq!(
vals,
vec![vec![10.0f32, 11.0, 12.0], vec![20.0, 21.0, 22.0]]
);
Ok(())
}
#[test]
fn test_gather_negative_indices() -> Result<()> {
let gather = node_attrs(
"Gather",
&["x", "idx"],
&["z"],
vec![attr("axis", AttrKind::Int(0))],
);
let manual_graph = model(graph(vec![gather], &["z"]));
let eval = run(
&manual_graph,
vec![
(
"x",
Tensor::new(&[[1.0f32, 2.0], [3.0, 4.0], [5.0, 6.0]], &Device::Cpu)?,
),
("idx", Tensor::new(&[0i64, -1], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
assert_eq!(vals, vec![vec![1.0f32, 2.0], vec![5.0, 6.0]]);
Ok(())
}
#[test]
fn test_conv1d_grouped_with_bias() -> Result<()> {
let conv = node_attrs(
"Conv",
&["x", "w", "b"],
&["z"],
vec![
attr("group", AttrKind::Int(4)),
attr("kernel_shape", AttrKind::Ints(vec![3])),
attr("pads", AttrKind::Ints(vec![0, 0])),
attr("strides", AttrKind::Ints(vec![1])),
attr("dilations", AttrKind::Ints(vec![1])),
],
);
let manual_graph = model(graph(vec![conv], &["z"]));
let w = vec![
1.0f32, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, ];
let eval = run(
&manual_graph,
vec![
(
"x",
Tensor::new(
&[[
[1.0f32, 2.0, 3.0, 4.0, 5.0],
[1.0, 2.0, 3.0, 4.0, 5.0],
[1.0, 2.0, 3.0, 4.0, 5.0],
[1.0, 2.0, 3.0, 4.0, 5.0],
]],
&Device::Cpu,
)?,
),
("w", Tensor::from_vec(w, (4, 1, 3), &Device::Cpu)?),
("b", Tensor::new(&[0.0f32, 0.0, 0.0, 0.0], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec3::<f32>()?;
assert_eq!(
vals,
vec![vec![
vec![1.0f32, 2.0, 3.0],
vec![2.0, 3.0, 4.0],
vec![3.0, 4.0, 5.0],
vec![3.0, 5.0, 7.0]
]]
);
Ok(())
}
#[test]
fn test_constant_of_shape_with_value() -> Result<()> {
let cos = node_attrs(
"ConstantOfShape",
&["shape"],
&["z"],
vec![attr(
"value",
AttrKind::Tensor(Tensor::new(&[2.0f32], &Device::Cpu).unwrap()),
)],
);
let manual_graph = model(graph(vec![cos], &["z"]));
let eval = run(
&manual_graph,
vec![("shape", Tensor::new(&[2i64, 3], &Device::Cpu)?)],
)?;
let z = get(&eval, "z");
assert_eq!(z.dims(), &[2, 3]);
let vals = z.to_vec2::<f32>()?;
assert_eq!(vals, vec![vec![2.0f32; 3], vec![2.0; 3]]);
Ok(())
}
#[test]
fn test_shape_start_end() -> Result<()> {
let shape = node_attrs(
"Shape",
&["x"],
&["z"],
vec![
attr("start", AttrKind::Int(0)),
attr("end", AttrKind::Int(-1)),
],
);
let manual_graph = model(graph(vec![shape], &["z"]));
let eval = run(
&manual_graph,
vec![(
"x",
Tensor::new(&[[1.0f32, 2.0], [3.0, 4.0]], &Device::Cpu)?,
)],
)?;
let z = get(&eval, "z");
assert_eq!(z.to_vec1::<i64>()?, vec![2, 2]);
Ok(())
}
#[test]
fn test_split_remainder_to_last() -> Result<()> {
let split = node_attrs(
"Split",
&["x"],
&["a", "b"],
vec![attr("axis", AttrKind::Int(0))],
);
let manual_graph = model(graph(vec![split], &["a", "b"]));
let eval = run(
&manual_graph,
vec![("x", Tensor::new(&[1.0f32, 2.0, 3.0], &Device::Cpu)?)],
)?;
assert_eq!(get(&eval, "a").to_vec1::<f32>()?, vec![1.0f32]);
assert_eq!(get(&eval, "b").to_vec1::<f32>()?, vec![2.0f32, 3.0]);
Ok(())
}
#[test]
fn test_equal_bool_output() -> Result<()> {
let manual_graph = model(graph(vec![node("Equal", &["x", "y"], &["z"])], &["z"]));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[1i64, 2, 3], &Device::Cpu)?),
("y", Tensor::new(&[1i64, 0, 3], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dtype(), DType::U8);
assert_eq!(z.to_vec1::<u8>()?, vec![1, 0, 1]);
Ok(())
}
#[test]
fn test_softmax_axis() -> Result<()> {
let softmax = node_attrs(
"Softmax",
&["x"],
&["z"],
vec![attr("axis", AttrKind::Int(-1))],
);
let manual_graph = model(graph(vec![softmax], &["z"]));
let eval = run(
&manual_graph,
vec![("x", Tensor::new(&[[1.0f32, 1.0, 1.0]], &Device::Cpu)?)],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
let expected = 1.0f32 / 3.0;
for v in &vals[0] {
assert!((v - expected).abs() < 1e-6, "got {v}, expected {expected}");
}
Ok(())
}
#[test]
fn test_reduce_sum_axes_keepdims() -> Result<()> {
let rs = node_attrs(
"ReduceSum",
&["x", "axes"],
&["z"],
vec![attr("keepdims", AttrKind::Int(0))],
);
let manual_graph = model(graph(vec![rs], &["z"]));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[[1.0f32, 2.0, 3.0]], &Device::Cpu)?),
("axes", Tensor::new(&[1i64], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dims(), &[1]);
assert_eq!(z.to_vec1::<f32>()?, vec![6.0f32]);
Ok(())
}
#[test]
fn test_transpose_perm() -> Result<()> {
let tr = node_attrs(
"Transpose",
&["x"],
&["z"],
vec![attr("perm", AttrKind::Ints(vec![0, 2, 1]))],
);
let manual_graph = model(graph(vec![tr], &["z"]));
let eval = run(
&manual_graph,
vec![(
"x",
Tensor::new(&[[[1.0f32, 2.0], [3.0, 4.0]]], &Device::Cpu)?,
)],
)?;
let z = get(&eval, "z");
assert_eq!(z.dims(), &[1, 2, 2]);
let vals = z.to_vec3::<f32>()?;
assert_eq!(vals, vec![vec![vec![1.0f32, 3.0], vec![2.0, 4.0]]]);
Ok(())
}
#[test]
fn test_tile() -> Result<()> {
let manual_graph = model(graph(vec![node("Tile", &["x", "repeats"], &["z"])], &["z"]));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[[1.0f32, 2.0]], &Device::Cpu)?),
("repeats", Tensor::new(&[2i64, 1], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
assert_eq!(vals, vec![vec![1.0f32, 2.0], vec![1.0, 2.0]]);
Ok(())
}
#[test]
fn test_expand() -> Result<()> {
let manual_graph = model(graph(vec![node("Expand", &["x", "shape"], &["z"])], &["z"]));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[[1.0f32, 2.0]], &Device::Cpu)?),
("shape", Tensor::new(&[3i64, 2], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
assert_eq!(
vals,
vec![vec![1.0f32, 2.0], vec![1.0, 2.0], vec![1.0, 2.0]]
);
Ok(())
}
#[test]
fn test_clip() -> Result<()> {
let manual_graph = model(graph(
vec![node("Clip", &["x", "min", "max"], &["z"])],
&["z"],
));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[-2.0f32, 0.5, 3.0], &Device::Cpu)?),
("min", Tensor::new(&[-1.0f32], &Device::Cpu)?),
("max", Tensor::new(&[1.0f32], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.to_vec1::<f32>()?, vec![-1.0f32, 0.5, 1.0]);
Ok(())
}
#[test]
fn test_batch_normalization() -> Result<()> {
let bn = node_attrs(
"BatchNormalization",
&["x", "w", "b", "mean", "var"],
&["z"],
vec![
attr("epsilon", AttrKind::Float(1e-5)),
attr("training_mode", AttrKind::Int(0)),
],
);
let manual_graph = model(graph(vec![bn], &["z"]));
let eval = run(
&manual_graph,
vec![
(
"x",
Tensor::new(&[[[1.0f32, 2.0], [3.0, 4.0]]], &Device::Cpu)?,
),
("w", Tensor::new(&[1.0f32, 1.0], &Device::Cpu)?),
("b", Tensor::new(&[0.0f32, 0.0], &Device::Cpu)?),
("mean", Tensor::new(&[2.0f32, 3.5], &Device::Cpu)?),
("var", Tensor::new(&[1.0f32, 1.0], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec3::<f32>()?;
assert!((vals[0][0][0] - (-1.0)).abs() < 1e-4);
assert!(vals[0][0][1].abs() < 1e-6);
assert!((vals[0][1][0] + 0.5).abs() < 1e-4);
assert!((vals[0][1][1] - 0.5).abs() < 1e-4);
Ok(())
}
#[test]
fn test_elementary_ops() -> Result<()> {
let manual_graph = model(graph(
vec![
node("Cos", &["x"], &["c"]),
node("Sin", &["x"], &["s"]),
node("Tanh", &["x"], &["t"]),
node("Exp", &["x"], &["e"]),
node("Erf", &["x"], &["r"]),
],
&["c", "s", "t", "e", "r"],
));
let eval = run(
&manual_graph,
vec![("x", Tensor::new(&[0.0f32, 1.0], &Device::Cpu)?)],
)?;
let c = get(&eval, "c").to_vec1::<f32>()?;
let s = get(&eval, "s").to_vec1::<f32>()?;
let t = get(&eval, "t").to_vec1::<f32>()?;
let e = get(&eval, "e").to_vec1::<f32>()?;
let r = get(&eval, "r").to_vec1::<f32>()?;
assert!((c[0] - 1.0).abs() < 1e-6);
assert!(s[0].abs() < 1e-6);
assert!(t[0].abs() < 1e-6);
assert!((e[0] - 1.0).abs() < 1e-6);
assert!(r[0].abs() < 1e-6);
assert!((c[1] - 0.5403).abs() < 1e-4);
assert!((s[1] - 0.8414).abs() < 1e-4);
assert!((t[1] - 0.7615).abs() < 1e-4);
assert!((e[1] - std::f32::consts::E).abs() < 1e-6);
assert!((r[1] - 0.8427).abs() < 1e-4);
Ok(())
}
#[test]
fn test_div_sub_mixed_dtype() -> Result<()> {
let manual_graph = model(graph(
vec![
node("Div", &["a", "b"], &["d"]),
node("Sub", &["a", "c"], &["s"]),
],
&["d", "s"],
));
let eval = run(
&manual_graph,
vec![
("a", Tensor::new(&[4.0f32, 9.0], &Device::Cpu)?),
("b", Tensor::new(&[2i64, 3i64], &Device::Cpu)?),
("c", Tensor::new(&[1i64, 1i64], &Device::Cpu)?),
],
)?;
assert_eq!(get(&eval, "d").to_vec1::<f32>()?, vec![2.0f32, 3.0]);
assert_eq!(get(&eval, "s").to_vec1::<f32>()?, vec![3.0f32, 8.0]);
Ok(())
}
#[test]
fn test_squeeze_axes_input() -> Result<()> {
let manual_graph = model(graph(vec![node("Squeeze", &["x", "axes"], &["z"])], &["z"]));
let eval = run(
&manual_graph,
vec![
("x", Tensor::new(&[[[1.0f32], [2.0]]], &Device::Cpu)?),
("axes", Tensor::new(&[2i64], &Device::Cpu)?),
],
)?;
let z = get(&eval, "z");
assert_eq!(z.dims(), &[1, 2]);
Ok(())
}
#[test]
fn test_relu() -> Result<()> {
let manual_graph = model(graph(vec![node("Relu", &["x"], &["z"])], &["z"]));
let eval = run(
&manual_graph,
vec![("x", Tensor::new(&[-1.0f32, 0.0, 2.0], &Device::Cpu)?)],
)?;
assert_eq!(get(&eval, "z").to_vec1::<f32>()?, vec![0.0f32, 0.0, 2.0]);
Ok(())
}
#[test]
fn test_matmul() -> Result<()> {
let manual_graph = model(graph(vec![node("MatMul", &["a", "b"], &["z"])], &["z"]));
let eval = run(
&manual_graph,
vec![
(
"a",
Tensor::new(&[[1.0f32, 2.0], [3.0, 4.0]], &Device::Cpu)?,
),
(
"b",
Tensor::new(&[[1.0f32, 0.0], [0.0, 1.0]], &Device::Cpu)?,
),
],
)?;
let z = get(&eval, "z");
let vals = z.to_vec2::<f32>()?;
assert_eq!(vals, vec![vec![1.0f32, 2.0], vec![3.0, 4.0]]);
Ok(())
}
#[test]
fn test_constant() -> Result<()> {
let c = node_attrs(
"Constant",
&[],
&["z"],
vec![attr(
"value",
AttrKind::Tensor(Tensor::new(&[[1.0f32, 2.0]], &Device::Cpu).unwrap()),
)],
);
let manual_graph = model(graph(vec![c], &["z"]));
let eval = run(&manual_graph, vec![])?;
let z = get(&eval, "z");
assert_eq!(z.to_vec2::<f32>()?, vec![vec![1.0f32, 2.0]]);
Ok(())
}
#[test]
fn test_cast_to_int64() -> Result<()> {
let cast = node_attrs("Cast", &["x"], &["z"], vec![attr("to", AttrKind::Int(7))]);
let manual_graph = model(graph(vec![cast], &["z"]));
let eval = run(
&manual_graph,
vec![("x", Tensor::new(&[1u8, 0u8], &Device::Cpu)?)],
)?;
let z = get(&eval, "z");
assert_eq!(z.dtype(), DType::I64);
assert_eq!(z.to_vec1::<i64>()?, vec![1, 0]);
Ok(())
}