use crate::error::Error;
use crate::neural_network::Tensor;
use crate::neural_network::layers::upsampling::Interpolation;
use ndarray::IxDyn;
use rayon::iter::{IndexedParallelIterator, ParallelIterator};
use rayon::slice::ParallelSliceMut;
tunable_gate! {
pub(crate) UPSAMPLE_PARALLEL_MIN_OPS
=> upsample_parallel_min_ops / set_upsample_parallel_min_ops = 2_000_000
}
const MAX_TAPS: usize = 11;
const MIN_WEIGHT_SUM: f64 = 1000.0 * f32::EPSILON as f64;
type Kernel = fn(f64) -> f64;
struct Band {
out_len: usize,
taps: usize,
starts: Vec<usize>,
weights: Option<Vec<f32>>,
}
fn triangle(x: f64) -> f64 {
(1.0 - x).max(0.0)
}
fn keys_cubic(x: f64) -> f64 {
if x < 1.0 {
((1.5 * x - 2.5) * x) * x + 1.0
} else if x < 2.0 {
((-0.5 * x + 2.5) * x - 4.0) * x + 2.0
} else {
0.0
}
}
fn lanczos(radius: f64, x: f64) -> f64 {
if x <= 1e-3 {
return 1.0;
}
if x > radius {
return 0.0;
}
let numerator =
radius * (std::f64::consts::PI * x).sin() * (std::f64::consts::PI * x / radius).sin();
numerator / (std::f64::consts::PI * std::f64::consts::PI * x * x)
}
fn resample_kernel(interpolation: Interpolation) -> Option<(usize, Kernel)> {
match interpolation {
Interpolation::Nearest => None,
Interpolation::Bilinear => Some((1, triangle)),
Interpolation::Bicubic => Some((2, keys_cubic)),
Interpolation::Lanczos3 => Some((3, |x| lanczos(3.0, x))),
Interpolation::Lanczos5 => Some((5, |x| lanczos(5.0, x))),
}
}
fn resample_weights(
in_len: usize,
factor: usize,
radius: usize,
kernel: Kernel,
) -> (usize, Vec<usize>, Vec<f32>) {
let out_len = in_len * factor;
let taps = (2 * radius + 1).min(in_len);
let inv_scale = 1.0 / factor as f64;
let mut starts = Vec::with_capacity(out_len);
let mut weights = vec![0.0f32; out_len * taps];
for (position, row) in weights.chunks_exact_mut(taps).enumerate() {
let center = (position as f64 + 0.5) * inv_scale - 0.5;
let lowest = (center - radius as f64).ceil().max(0.0) as usize;
let start = lowest.min(in_len - taps);
let mut raw = [0.0f64; MAX_TAPS];
let mut total = 0.0;
for (tap, value) in raw[..taps].iter_mut().enumerate() {
*value = kernel((center - (start + tap) as f64).abs());
total += *value;
}
if total.abs() > MIN_WEIGHT_SUM {
for (weight, value) in row.iter_mut().zip(&raw[..taps]) {
*weight = (value / total) as f32;
}
}
starts.push(start);
}
(taps, starts, weights)
}
fn transpose_weights(src_len: usize, taps: usize, starts: &[usize], weights: &[f32]) -> Band {
let out_len = starts.len();
let mut firsts = vec![0usize; src_len];
let mut counts = vec![0usize; src_len];
let (mut low, mut high) = (0usize, 0usize);
for (i, (first, count)) in firsts.iter_mut().zip(counts.iter_mut()).enumerate() {
while low < out_len && starts[low] + taps <= i {
low += 1;
}
while high < out_len && starts[high] <= i {
high += 1;
}
*first = low;
*count = high - low;
}
let back_taps = counts.iter().copied().max().unwrap_or(1).max(1);
let mut back_starts = Vec::with_capacity(src_len);
let mut back_weights = vec![0.0f32; src_len * back_taps];
let rows = back_weights.chunks_exact_mut(back_taps);
for ((i, row), (&first, &count)) in rows.enumerate().zip(firsts.iter().zip(&counts)) {
let start = first.min(out_len - back_taps);
for position in first..first + count {
row[position - start] = weights[position * taps + (i - starts[position])];
}
back_starts.push(start);
}
Band {
out_len: src_len,
taps: back_taps,
starts: back_starts,
weights: Some(back_weights),
}
}
fn axis_band(
in_len: usize,
factor: usize,
interpolation: Interpolation,
backward: bool,
) -> Option<Band> {
if factor == 1 {
return None;
}
let out_len = in_len * factor;
let Some((radius, kernel)) = resample_kernel(interpolation) else {
return Some(if backward {
Band {
out_len: in_len,
taps: factor,
starts: (0..in_len).map(|i| i * factor).collect(),
weights: None,
}
} else {
Band {
out_len,
taps: 1,
starts: (0..out_len).map(|j| j / factor).collect(),
weights: None,
}
});
};
let (taps, starts, weights) = resample_weights(in_len, factor, radius, kernel);
Some(if backward {
transpose_weights(in_len, taps, &starts, &weights)
} else {
Band {
out_len,
taps,
starts,
weights: Some(weights),
}
})
}
const TASK_ELEMENTS: usize = 16_384;
fn apply_band(src: &[f32], src_len: usize, inner: usize, band: &Band, dst: &mut [f32]) {
let rows_per_task = (TASK_ELEMENTS / inner).max(1);
let task = |(index, dst_task): (usize, &mut [f32])| {
let first_row = index * rows_per_task;
let mut lane = first_row / band.out_len;
let mut position = first_row % band.out_len;
for dst_row in dst_task.chunks_mut(inner) {
let base = lane * src_len * inner + band.starts[position] * inner;
match &band.weights {
None => {
dst_row.copy_from_slice(&src[base..base + inner]);
for tap in 1..band.taps {
let from = base + tap * inner;
for (d, &s) in dst_row.iter_mut().zip(&src[from..from + inner]) {
*d += s;
}
}
}
Some(weights) => {
let row_weights = &weights[position * band.taps..][..band.taps];
for (d, &s) in dst_row.iter_mut().zip(&src[base..base + inner]) {
*d = row_weights[0] * s;
}
for (tap, &weight) in row_weights.iter().enumerate().skip(1) {
let from = base + tap * inner;
for (d, &s) in dst_row.iter_mut().zip(&src[from..from + inner]) {
*d += weight * s;
}
}
}
}
position += 1;
if position == band.out_len {
position = 0;
lane += 1;
}
}
};
let chunk = rows_per_task * inner;
if dst.len() * band.taps >= upsample_parallel_min_ops() {
dst.par_chunks_mut(chunk).enumerate().for_each(task);
} else {
dst.chunks_mut(chunk).enumerate().for_each(task);
}
}
fn run_bands(source: &Tensor, bands: &[Option<Band>]) -> Tensor {
let mut shape = source.shape().to_vec();
let mut current = source.as_standard_layout().into_owned();
for (spatial, band) in bands.iter().enumerate() {
let Some(band) = band else { continue };
let axis = spatial + 1;
let inner: usize = shape[axis + 1..].iter().product();
let outer: usize = shape[..axis].iter().product();
let mut next = vec![0.0f32; outer * band.out_len * inner];
apply_band(
current.as_slice().expect("the buffer is kept in C order"),
shape[axis],
inner,
band,
&mut next,
);
shape[axis] = band.out_len;
current = Tensor::from_shape_vec(IxDyn(&shape), next).expect("the shape matches the data");
}
current
}
fn upsampled_shape(input_shape: &[usize], factors: &[usize]) -> Vec<usize> {
let mut shape = input_shape.to_vec();
for (spatial, &factor) in factors.iter().enumerate() {
shape[spatial + 1] *= factor;
}
shape
}
pub(super) fn upsample_forward(
input: &Tensor,
factors: &[usize],
interpolation: Interpolation,
rank: usize,
layer: &'static str,
) -> Result<Tensor, Error> {
if input.ndim() != rank {
return Err(Error::invalid_input(format!(
"{} layer expects a {}D input, got a {}D tensor",
layer,
rank,
input.ndim()
)));
}
if input.is_empty() {
return Err(Error::empty_input("input tensor"));
}
let mut elements = input.len();
for (spatial, &factor) in factors.iter().enumerate() {
elements = elements.checked_mul(factor).ok_or_else(|| {
Error::invalid_input(format!(
"{} layer grows axis {} of a {:?} input by {}, and the output does not fit in memory",
layer,
spatial + 1,
input.shape(),
factor
))
})?;
}
let bands: Vec<Option<Band>> = factors
.iter()
.enumerate()
.map(|(spatial, &factor)| {
axis_band(input.shape()[spatial + 1], factor, interpolation, false)
})
.collect();
Ok(run_bands(input, &bands))
}
pub(super) fn upsample_backward(
grad_output: &Tensor,
input_shape: Option<&[usize]>,
factors: &[usize],
interpolation: Interpolation,
layer: &'static str,
) -> Result<Tensor, Error> {
let Some(input_shape) = input_shape else {
return Err(Error::forward_pass_not_run(layer));
};
let expected = upsampled_shape(input_shape, factors);
if grad_output.shape() != expected.as_slice() {
return Err(Error::shape_mismatch(expected, grad_output.shape()));
}
let bands: Vec<Option<Band>> = factors
.iter()
.enumerate()
.map(|(spatial, &factor)| axis_band(input_shape[spatial + 1], factor, interpolation, true))
.collect();
Ok(run_bands(grad_output, &bands))
}
pub(super) fn upsample_summary(input_shape: Option<&[usize]>, factors: &[usize]) -> String {
match input_shape {
Some(shape) => {
let axes: Vec<String> = upsampled_shape(shape, factors)[1..]
.iter()
.map(|extent| extent.to_string())
.collect();
format!("(None, {})", axes.join(", "))
}
None => "Unknown".to_string(),
}
}
pub(super) fn validate_factors(factors: &[usize]) -> Result<(), Error> {
if factors.contains(&0) {
return Err(Error::invalid_parameter(
"size",
"holds a factor of 0, and every factor must be at least 1",
));
}
Ok(())
}