1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Validated entry for model inputs (GAP-326).
//!
//! Models check rank with the existing `fuel_ir` helpers:
//! `Shape::dims3()` and `Shape::dims4()` return a typed
//! `Error::UnexpectedNumberOfDims`, and `Error::context` names the model:
//!
//! ```text
//! let (n, c, h, w) = image.shape().dims4().map_err(|e| e.context("ResNet::forward: image"))?;
//! ```
//!
//! This module adds only what `fuel_ir` lacks: a fixed channel count on an
//! `[N, C, H, W]` image.
//!
//! Deliberately NOT here, because other rows own them: batch == 1 guards,
//! fixed spatial sizes, square or divisible-by-N inputs, codebook counts,
//! tile counts (GAP-328), and config-vs-config agreement (GAP-314). If any of
//! them is ruled in, it gets its own helper, not an option on this one.
use fuel_ir::{error::Error, error::Result, shape::Shape};
use fuel_tensor::lazy::Tensor;
/// The `(n, c, h, w)` of an `[N, C, H, W]` image whose channel count is
/// `channels`.
///
/// Never panics. A rank other than 4 is `Error::UnexpectedNumberOfDims`,
/// wrapped with `what`. A channel count other than `channels` is
/// `Error::UnexpectedShape`, whose message names `what`.
pub(crate) fn image_nchw(
x: &Tensor,
channels: usize,
what: &'static str,
) -> Result<(usize, usize, usize, usize)> {
let shape = x.shape();
let (n, c, h, w) = shape.dims4().map_err(|e| e.context(what))?;
if c != channels {
return Err(Error::UnexpectedShape {
msg: format!("{what}: expected {channels} input channels, got {c}"),
expected: Box::new(Shape::from_dims(&[n, channels, h, w])),
got: Box::new(shape),
}
.bt());
}
Ok((n, c, h, w))
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_tensor::Device;
fn tensor(dims: &[usize]) -> Tensor {
let count = dims.iter().product();
Tensor::from_f32(vec![0.0; count], Shape::from_dims(dims), &Device::cpu()).unwrap()
}
/// The error under any `Context` / `WithBacktrace` / `WithPath` wrapping.
fn root(e: &Error) -> &Error {
match e {
Error::Context { inner, .. }
| Error::WithBacktrace { inner, .. }
| Error::WithPath { inner, .. } => root(inner),
other => other,
}
}
#[test]
fn accepts_the_requested_channel_count() {
assert_eq!(
image_nchw(&tensor(&[2, 3, 4, 5]), 3, "m").unwrap(),
(2, 3, 4, 5)
);
// The count is a parameter, not a hard-coded 3.
assert_eq!(
image_nchw(&tensor(&[1, 1, 2, 2]), 1, "m").unwrap(),
(1, 1, 2, 2)
);
}
#[test]
fn a_wrong_rank_is_a_typed_rank_error_naming_the_model() {
let e = image_nchw(&tensor(&[3, 4, 5]), 3, "ResNet::forward: image").unwrap_err();
assert!(
matches!(
root(&e),
Error::UnexpectedNumberOfDims {
expected: 4,
got: 3,
..
}
),
"{e}"
);
assert!(e.to_string().contains("ResNet::forward: image"), "{e}");
}
#[test]
fn a_wrong_channel_count_is_a_typed_shape_error_naming_the_model() {
let e = image_nchw(&tensor(&[1, 4, 2, 2]), 3, "ResNet::forward: image").unwrap_err();
match root(&e) {
Error::UnexpectedShape { msg, expected, got } => {
assert!(msg.contains("ResNet::forward: image"), "{msg}");
assert_eq!(expected.dims(), &[1, 3, 2, 2]);
assert_eq!(got.dims(), &[1, 4, 2, 2]);
}
other => panic!("expected UnexpectedShape, got {other}"),
}
}
}