use crate::{
array::Array,
error::{Error, OutOfRangePayload, Result, check},
shape::dim_ptr,
stream::default_stream,
};
use smol_str::format_smolstr;
fn require_positive(context: &'static str, value: i32) -> Result<()> {
if value < 1 {
return Err(Error::OutOfRange(OutOfRangePayload::new(
context,
"must be >= 1",
format_smolstr!("{value}"),
)));
}
Ok(())
}
fn conv_overflow_err(context: &'static str) -> Error {
Error::OutOfRange(OutOfRangePayload::new(
context,
"parameters overflow MLX int32 convolution shape arithmetic",
"overflow",
))
}
#[allow(clippy::too_many_arguments)]
fn check_conv_no_overflow(
context: &'static str,
input: &Array,
weight: &Array,
stride: &[i32],
pad_lo: &[i32],
pad_hi: &[i32],
kernel_dilation: &[i32],
input_dilation: &[i32],
output_padding: Option<&[i32]>,
groups: i32,
) -> Result<()> {
let in_shape = input.shape();
let wt_shape = weight.shape();
let nd = in_shape.len();
if !(3..=5).contains(&nd) {
return Err(Error::OutOfRange(OutOfRangePayload::new(
context,
"input rank must be 3, 4, or 5 (1 to 3 spatial dims)",
format_smolstr!("rank {nd}"),
)));
}
if wt_shape.len() != nd {
return Err(Error::OutOfRange(OutOfRangePayload::new(
context,
"weight rank does not match the input rank",
format_smolstr!("input rank {nd}"),
)));
}
let spatial_rank = nd - 2;
if [stride, pad_lo, pad_hi, kernel_dilation, input_dilation]
.into_iter()
.chain(output_padding)
.any(|s| !(s.is_empty() || s.len() == 1 || s.len() == spatial_rank))
{
return Err(Error::OutOfRange(OutOfRangePayload::new(
context,
"input spatial rank does not match the spatial parameters",
format_smolstr!("spatial rank {spatial_rank}"),
)));
}
let max_dim = in_shape
.iter()
.chain(wt_shape.iter())
.map(|&d| d as i128)
.max()
.unwrap_or(0);
let max_param = [stride, pad_lo, pad_hi, kernel_dilation, input_dilation]
.into_iter()
.chain(output_padding)
.flatten()
.chain(std::iter::once(&groups))
.map(|&p| i128::from(p).abs())
.max()
.unwrap_or(0);
if 6 * max_param * max_param.max(max_dim) + 32 * max_param + 64 > i128::from(i32::MAX) {
return Err(conv_overflow_err(context));
}
Ok(())
}
pub fn conv1d(
input: &Array,
weight: &Array,
stride: i32,
padding: i32,
dilation: i32,
groups: i32,
) -> Result<Array> {
require_positive("conv1d stride", stride)?;
require_positive("conv1d groups", groups)?;
check_conv_no_overflow(
"conv1d",
input,
weight,
&[stride],
&[padding],
&[padding],
&[dilation],
&[],
None,
groups,
)?;
let mut out = Array(unsafe { mlxrs_sys::mlx_array_new() });
check(unsafe {
mlxrs_sys::mlx_conv1d(
&mut out.0,
input.0,
weight.0,
stride,
padding,
dilation,
groups,
default_stream(),
)
})?;
Ok(out)
}
pub fn conv2d(
input: &Array,
weight: &Array,
stride: (i32, i32),
padding: (i32, i32),
dilation: (i32, i32),
groups: i32,
) -> Result<Array> {
require_positive("conv2d stride", stride.0)?;
require_positive("conv2d stride", stride.1)?;
require_positive("conv2d groups", groups)?;
check_conv_no_overflow(
"conv2d",
input,
weight,
&[stride.0, stride.1],
&[padding.0, padding.1],
&[padding.0, padding.1],
&[dilation.0, dilation.1],
&[],
None,
groups,
)?;
let mut out = Array(unsafe { mlxrs_sys::mlx_array_new() });
check(unsafe {
mlxrs_sys::mlx_conv2d(
&mut out.0,
input.0,
weight.0,
stride.0,
stride.1,
padding.0,
padding.1,
dilation.0,
dilation.1,
groups,
default_stream(),
)
})?;
Ok(out)
}
pub fn conv3d(
input: &Array,
weight: &Array,
stride: (i32, i32, i32),
padding: (i32, i32, i32),
dilation: (i32, i32, i32),
groups: i32,
) -> Result<Array> {
require_positive("conv3d stride", stride.0)?;
require_positive("conv3d stride", stride.1)?;
require_positive("conv3d stride", stride.2)?;
require_positive("conv3d groups", groups)?;
check_conv_no_overflow(
"conv3d",
input,
weight,
&[stride.0, stride.1, stride.2],
&[padding.0, padding.1, padding.2],
&[padding.0, padding.1, padding.2],
&[dilation.0, dilation.1, dilation.2],
&[],
None,
groups,
)?;
let mut out = Array(unsafe { mlxrs_sys::mlx_array_new() });
check(unsafe {
mlxrs_sys::mlx_conv3d(
&mut out.0,
input.0,
weight.0,
stride.0,
stride.1,
stride.2,
padding.0,
padding.1,
padding.2,
dilation.0,
dilation.1,
dilation.2,
groups,
default_stream(),
)
})?;
Ok(out)
}
pub fn conv_transpose1d(
input: &Array,
weight: &Array,
stride: i32,
padding: i32,
dilation: i32,
output_padding: i32,
groups: i32,
) -> Result<Array> {
require_positive("conv_transpose1d groups", groups)?;
check_conv_no_overflow(
"conv_transpose1d",
input,
weight,
&[stride],
&[padding],
&[padding],
&[dilation],
&[],
Some(&[output_padding]),
groups,
)?;
let mut out = Array(unsafe { mlxrs_sys::mlx_array_new() });
check(unsafe {
mlxrs_sys::mlx_conv_transpose1d(
&mut out.0,
input.0,
weight.0,
stride,
padding,
dilation,
output_padding,
groups,
default_stream(),
)
})?;
Ok(out)
}
pub fn conv_transpose2d(
input: &Array,
weight: &Array,
stride: (i32, i32),
padding: (i32, i32),
dilation: (i32, i32),
output_padding: (i32, i32),
groups: i32,
) -> Result<Array> {
require_positive("conv_transpose2d groups", groups)?;
check_conv_no_overflow(
"conv_transpose2d",
input,
weight,
&[stride.0, stride.1],
&[padding.0, padding.1],
&[padding.0, padding.1],
&[dilation.0, dilation.1],
&[],
Some(&[output_padding.0, output_padding.1]),
groups,
)?;
let mut out = Array(unsafe { mlxrs_sys::mlx_array_new() });
check(unsafe {
mlxrs_sys::mlx_conv_transpose2d(
&mut out.0,
input.0,
weight.0,
stride.0,
stride.1,
padding.0,
padding.1,
dilation.0,
dilation.1,
output_padding.0,
output_padding.1,
groups,
default_stream(),
)
})?;
Ok(out)
}
pub fn conv_transpose3d(
input: &Array,
weight: &Array,
stride: (i32, i32, i32),
padding: (i32, i32, i32),
dilation: (i32, i32, i32),
output_padding: (i32, i32, i32),
groups: i32,
) -> Result<Array> {
require_positive("conv_transpose3d groups", groups)?;
check_conv_no_overflow(
"conv_transpose3d",
input,
weight,
&[stride.0, stride.1, stride.2],
&[padding.0, padding.1, padding.2],
&[padding.0, padding.1, padding.2],
&[dilation.0, dilation.1, dilation.2],
&[],
Some(&[output_padding.0, output_padding.1, output_padding.2]),
groups,
)?;
let mut out = Array(unsafe { mlxrs_sys::mlx_array_new() });
check(unsafe {
mlxrs_sys::mlx_conv_transpose3d(
&mut out.0,
input.0,
weight.0,
stride.0,
stride.1,
stride.2,
padding.0,
padding.1,
padding.2,
dilation.0,
dilation.1,
dilation.2,
output_padding.0,
output_padding.1,
output_padding.2,
groups,
default_stream(),
)
})?;
Ok(out)
}
#[allow(clippy::too_many_arguments)]
pub fn conv_general(
input: &Array,
weight: &Array,
stride: &[i32],
padding_lo: &[i32],
padding_hi: &[i32],
kernel_dilation: &[i32],
input_dilation: &[i32],
groups: i32,
flip: bool,
) -> Result<Array> {
require_positive("conv_general groups", groups)?;
let spatial_rank = input.shape().len().saturating_sub(2);
for (context, slice) in [
("conv_general stride", stride),
("conv_general padding_lo", padding_lo),
("conv_general padding_hi", padding_hi),
("conv_general kernel_dilation", kernel_dilation),
("conv_general input_dilation", input_dilation),
] {
if !(slice.is_empty() || slice.len() == 1 || slice.len() == spatial_rank) {
return Err(Error::OutOfRange(OutOfRangePayload::new(
context,
"length must be 0, 1, or the input spatial rank",
format_smolstr!("{}", slice.len()),
)));
}
}
for &s in stride {
require_positive("conv_general stride", s)?;
}
check_conv_no_overflow(
"conv_general",
input,
weight,
stride,
padding_lo,
padding_hi,
kernel_dilation,
input_dilation,
None,
groups,
)?;
let mut out = Array(unsafe { mlxrs_sys::mlx_array_new() });
check(unsafe {
mlxrs_sys::mlx_conv_general(
&mut out.0,
input.0,
weight.0,
dim_ptr(stride),
stride.len(),
dim_ptr(padding_lo),
padding_lo.len(),
dim_ptr(padding_hi),
padding_hi.len(),
dim_ptr(kernel_dilation),
kernel_dilation.len(),
dim_ptr(input_dilation),
input_dilation.len(),
groups,
flip,
default_stream(),
)
})?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Array, Result, error::Error};
#[test]
fn conv1d_cross_correlation_closed_form() -> Result<()> {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 4, 1])?;
let weight = Array::from_slice::<f32>(&[1.0, 0.0, -1.0], &[1, 3, 1])?;
let mut out = conv1d(&input, &weight, 1, 0, 1, 1)?;
assert_eq!(out.shape(), vec![1, 2, 1]);
assert_eq!(out.to_vec::<f32>()?, vec![-2.0, -2.0]);
Ok(())
}
#[test]
fn conv2d_cross_correlation_closed_form() -> Result<()> {
let input = Array::from_slice::<f32>(
&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
&[1, 3, 3, 1],
)?;
let weight = Array::from_slice::<f32>(&[1.0, 0.0, 0.0, -1.0], &[1, 2, 2, 1])?;
let mut out = conv2d(&input, &weight, (1, 1), (0, 0), (1, 1), 1)?;
assert_eq!(out.shape(), vec![1, 2, 2, 1]);
assert_eq!(out.to_vec::<f32>()?, vec![-4.0, -4.0, -4.0, -4.0]);
Ok(())
}
#[test]
fn conv3d_all_ones_sums_kernel_volume() -> Result<()> {
let input = Array::from_slice::<f32>(&[1.0; 8], &[1, 2, 2, 2, 1])?;
let weight = Array::from_slice::<f32>(&[1.0; 8], &[1, 2, 2, 2, 1])?;
let mut out = conv3d(&input, &weight, (1, 1, 1), (0, 0, 0), (1, 1, 1), 1)?;
assert_eq!(out.shape(), vec![1, 1, 1, 1, 1]);
assert_eq!(out.to_vec::<f32>()?, vec![8.0]);
Ok(())
}
#[test]
fn conv_general_matches_conv1d() -> Result<()> {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 4, 1])?;
let weight = Array::from_slice::<f32>(&[1.0, 0.0, -1.0], &[1, 3, 1])?;
let mut general = conv_general(&input, &weight, &[1], &[0], &[0], &[1], &[1], 1, false)?;
assert_eq!(general.shape(), vec![1, 2, 1]);
assert_eq!(general.to_vec::<f32>()?, vec![-2.0, -2.0]);
Ok(())
}
#[test]
fn conv_transpose1d_expands_length() -> Result<()> {
let input = Array::from_slice::<f32>(&[1.0, 2.0], &[1, 2, 1])?;
let weight = Array::from_slice::<f32>(&[1.0, 1.0], &[1, 2, 1])?;
let out = conv_transpose1d(&input, &weight, 1, 0, 1, 0, 1)?;
assert_eq!(out.shape(), vec![1, 3, 1]);
Ok(())
}
#[test]
fn conv1d_rejects_non_positive_groups() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 4, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 0.0, -1.0], &[1, 3, 1]).unwrap();
let zero = conv1d(&input, &weight, 1, 0, 1, 0).expect_err("groups=0 must be rejected");
assert!(matches!(zero, Error::OutOfRange(_)), "got {zero:?}");
let neg = conv1d(&input, &weight, 1, 0, 1, -1).expect_err("negative groups must be rejected");
assert!(matches!(neg, Error::OutOfRange(_)), "got {neg:?}");
}
#[test]
fn conv1d_rejects_zero_stride() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 4, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 0.0, -1.0], &[1, 3, 1]).unwrap();
let err = conv1d(&input, &weight, 0, 0, 1, 1).expect_err("stride=0 must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_general_rejects_mismatched_slice_length() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 4, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 0.0, -1.0], &[1, 3, 1]).unwrap();
let err = conv_general(&input, &weight, &[1, 1], &[0], &[0], &[1], &[1], 1, false)
.expect_err("len-2 spatial slice on a 1-D conv must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_general_rejects_non_positive_groups() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 4, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 0.0, -1.0], &[1, 3, 1]).unwrap();
let err = conv_general(&input, &weight, &[1], &[0], &[0], &[1], &[1], 0, false)
.expect_err("groups=0 must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv1d_rejects_overflowing_dilation() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 4, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 0.0, -1.0], &[1, 3, 1]).unwrap();
let err = conv1d(&input, &weight, 1, 0, i32::MAX, 1)
.expect_err("overflowing dilation must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv1d_rejects_overflowing_padding() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 4, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 0.0, -1.0], &[1, 3, 1]).unwrap();
let err =
conv1d(&input, &weight, 1, i32::MAX, 1, 1).expect_err("overflowing padding must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv1d_rejects_overflowing_groups() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 2, 2]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 1.0], &[1, 1, 2]).unwrap();
let err =
conv1d(&input, &weight, 1, 0, 1, i32::MAX).expect_err("overflowing groups must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_transpose1d_rejects_overflowing_output_padding() {
let input = Array::from_slice::<f32>(&[1.0, 2.0], &[1, 2, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 1.0], &[1, 2, 1]).unwrap();
let err = conv_transpose1d(&input, &weight, 1, 0, 1, i32::MAX, 1)
.expect_err("overflowing output_padding must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_general_rejects_overflowing_dilation() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 4, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 0.0, -1.0], &[1, 3, 1]).unwrap();
let err = conv_general(
&input,
&weight,
&[1],
&[0],
&[0],
&[i32::MAX],
&[1],
1,
false,
)
.expect_err("overflowing kernel_dilation must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_general_rejects_canceling_padding() {
let input = Array::from_slice::<f32>(&[1.0, 2.0], &[1, 2, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0], &[1, 1, 1]).unwrap();
let err = conv_general(
&input,
&weight,
&[1],
&[i32::MIN],
&[i32::MAX],
&[1],
&[1],
1,
false,
)
.expect_err("canceling i32::MIN / i32::MAX padding must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_transpose1d_rejects_overflowing_padding() {
let input = Array::from_slice::<f32>(&[1.0], &[1, 1, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0], &[1, 1, 1]).unwrap();
let err = conv_transpose1d(&input, &weight, 1, 1_500_000_000, 1, 0, 1)
.expect_err("transpose 2*padding overflow must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_general_all_empty_slices_uses_defaults() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0, 4.0], &[1, 4, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 0.0, -1.0], &[1, 3, 1]).unwrap();
let mut out = conv_general(&input, &weight, &[], &[], &[], &[], &[], 1, false)
.expect("all-empty conv_general must use defaults");
assert_eq!(out.shape(), vec![1, 2, 1]);
assert_eq!(out.to_vec::<f32>().unwrap(), vec![-2.0, -2.0]);
}
#[test]
fn conv_general_allows_negative_padding_crop() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0], &[1, 3, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0], &[1, 1, 1]).unwrap();
let out = conv_general(&input, &weight, &[1], &[0], &[-1], &[1], &[1], 1, false)
.expect("a normal negative-padding crop must not be rejected");
assert_eq!(out.shape(), vec![1, 2, 1]);
}
#[test]
fn conv_transpose1d_rejects_rank_invalid_prelude_overflow() {
let input = Array::from_slice::<f32>(&[1.0, 2.0, 3.0], &[1, 3]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0, 1.0, 1.0], &[1, 3]).unwrap();
let err = conv_transpose1d(&input, &weight, 1, 0, i32::MAX, 0, 1)
.expect_err("rank-invalid transpose prelude overflow must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_general_rejects_slice_overflow_on_huge_dim() {
let input = Array::zeros::<f32>(&[i32::MAX, 2, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0], &[1, 1, 1]).unwrap();
let err = conv_general(&input, &weight, &[1], &[0], &[-1], &[1], &[1], 1, false)
.expect_err("i32::MAX dim + negative-padding slice overflow must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_general_rejects_huge_spatial_axis_conservatively() {
let input = Array::zeros::<f32>(&[1, i32::MAX, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0], &[1, 1, 1]).unwrap();
let err = conv_general(&input, &weight, &[1], &[0], &[-1], &[1], &[1], 1, false)
.expect_err("an axis near i32::MAX is conservatively rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_transpose1d_rejects_4d_broadcast_overflow() {
let input = Array::zeros::<f32>(&[1, 1, 1, 1]).unwrap();
let weight = Array::zeros::<f32>(&[1, 1, 3, 1]).unwrap();
let err = conv_transpose1d(&input, &weight, 1, 0, i32::MAX, 0, 1)
.expect_err("4D transpose broadcast overflow must be rejected");
assert!(matches!(err, Error::OutOfRange(_)), "got {err:?}");
}
#[test]
fn conv_general_accepts_large_unit_parameter_dim() {
let input = Array::zeros::<f32>(&[1, 134_217_728, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0], &[1, 1, 1]).unwrap();
assert!(
conv_general(&input, &weight, &[1], &[0], &[0], &[1], &[1], 1, false).is_ok(),
"a large unit-parameter convolution must not be falsely rejected"
);
}
#[test]
fn conv1d_handles_zero_length_axis_without_panic() {
let input = Array::zeros::<f32>(&[1, 0, 1]).unwrap();
let weight = Array::from_slice::<f32>(&[1.0], &[1, 1, 1]).unwrap();
let _ = conv1d(&input, &weight, 1, 0, 1, 1);
}
#[test]
fn conv2d_rejects_mismatched_input_rank_without_panic() {
let input = Array::zeros::<f32>(&[1, 2, 2, 2, 1]).unwrap();
let weight = Array::zeros::<f32>(&[1, 2, 2, 2, 1]).unwrap();
assert!(conv2d(&input, &weight, (1, 1), (0, 0), (1, 1), 1).is_err());
}
#[test]
fn conv3d_rejects_mismatched_input_rank_without_panic() {
let input = Array::zeros::<f32>(&[1, 2, 2, 2, 2, 1]).unwrap();
let weight = Array::zeros::<f32>(&[1, 2, 2, 2, 2, 1]).unwrap();
assert!(conv3d(&input, &weight, (1, 1, 1), (0, 0, 0), (1, 1, 1), 1).is_err());
}
#[test]
fn conv_transpose2d_rejects_mismatched_input_rank_without_panic() {
let input = Array::zeros::<f32>(&[1, 2, 2, 2, 1]).unwrap();
let weight = Array::zeros::<f32>(&[1, 2, 2, 2, 1]).unwrap();
assert!(conv_transpose2d(&input, &weight, (1, 1), (1, 1), (1, 1), (0, 0), 1).is_err());
}
#[test]
fn conv_transpose3d_rejects_mismatched_input_rank_without_panic() {
let input = Array::zeros::<f32>(&[1, 2, 2, 2, 2, 1]).unwrap();
let weight = Array::zeros::<f32>(&[1, 2, 2, 2, 2, 1]).unwrap();
assert!(
conv_transpose3d(
&input,
&weight,
(1, 1, 1),
(1, 1, 1),
(1, 1, 1),
(0, 0, 0),
1
)
.is_err()
);
}
#[test]
fn conv2d_rejects_mismatched_weight_rank_without_panic() {
let input = Array::zeros::<f32>(&[1, 4, 4, 1]).unwrap();
let weight = Array::zeros::<f32>(&[2, 1]).unwrap();
assert!(conv2d(&input, &weight, (1, 1), (0, 0), (1, 1), 1).is_err());
}
#[test]
fn conv_transpose1d_rejects_low_rank_inputs_without_overflow() {
let weight = Array::zeros::<f32>(&[1, 3, 1]).unwrap();
let scalar_shape: [i32; 0] = [];
let rank0 = Array::zeros::<f32>(&scalar_shape).unwrap();
assert!(conv_transpose1d(&rank0, &weight, 1, 0, i32::MAX, 0, 1).is_err());
let rank1 = Array::zeros::<f32>(&[1]).unwrap();
assert!(conv_transpose1d(&rank1, &weight, 1, 0, i32::MAX, 0, 1).is_err());
}
}