use crate::array::Array;
use crate::cast::Canon;
use crate::indexing::{put_along_axis, take_along_axis};
use crate::ops::imp::{
self as ops, broadcast_to, conjugate, cos, divide, equal, exp, greater, greater_equal, less,
less_equal, log, matmul, multiply, negative, not_equal, reshape, sin, stop_gradient, subtract,
sum, transpose, where_,
};
use crate::primitive::{BinaryOp, Primitive, ReduceOp, ScanOp, ScatterReduce, UnaryOp};
use crate::quantized::{dequantize, quantized_matmul};
use crate::sorting::{argsort, cummax, cummin, cumprod, cumsum, logcumsumexp};
use crate::{DType, Error, Result};
fn record(primitive: Primitive, inputs: Vec<Array>) -> Result<Array> {
Ok(Array::from_op(primitive, inputs)?.swap_remove(0))
}
fn lowest(dtype: DType) -> f64 {
match dtype {
DType::F16 => -65504.0,
DType::BF16 => f64::from(crate::bf16::MIN),
DType::F64 => f64::MIN,
_ => f64::from(f32::MIN),
}
}
fn log_halves(v: &Array) -> Result<(Array, Array)> {
let low = lowest(v.dtype());
let log_abs = log(&ops::abs(v)?)?;
Ok((
where_(&greater(v, 0.0)?, &log_abs, low)?,
where_(&less(v, 0.0)?, &log_abs, low)?,
))
}
fn window(a: &Array, axis: usize, start: usize, stop: usize) -> Result<Array> {
let mut args = vec![crate::SliceArg::full(); a.ndim()];
args[axis] = crate::SliceArg::new(Some(start as isize), Some(stop as isize), 1);
ops::slice(a, &args)
}
fn scan_owner(
x: &Array,
out: &Array,
axis: usize,
reverse: bool,
inclusive: bool,
) -> Result<(Array, (usize, usize))> {
let n = x.dims()[axis];
debug_assert!(n > 0, "scan_owner over an empty axis");
let (in_window, extreme_window, out_range, (first, last)) = if inclusive {
(x.clone(), out.clone(), (0, n), (0, n))
} else if reverse {
let (i, e) = (window(x, axis, 1, n)?, window(out, axis, 0, n - 1)?);
(i, e, (0, n - 1), (1, n))
} else {
let (i, e) = (window(x, axis, 0, n - 1)?, window(out, axis, 1, n)?);
(i, e, (1, n), (0, n - 1))
};
let mut index_shape = vec![1isize; x.ndim()];
index_shape[axis] = (last - first) as isize;
let iota = reshape(
&ops::arange(first as f64, last as f64, 1.0, DType::I32)?,
&index_shape,
)?;
let fill = if reverse { n as f64 } else { -1.0 };
let masked = where_(&equal(&in_window, &extreme_window)?, &iota, fill)?;
let a = Some(axis as isize);
let owner = if reverse {
cummin(&masked, a, true, true)?
} else {
cummax(&masked, a, false, true)?
};
Ok((stop_gradient(&owner.try_astype(DType::U32)?)?, out_range))
}
fn first_zero(
x: &Array,
out: &Array,
axis: usize,
reverse: bool,
inclusive: bool,
) -> Result<(Array, Array, Array)> {
let a = Some(axis as isize);
let (mut exclusive, mut inclusive_prod) = (cumprod(x, a, reverse, !inclusive)?, out.clone());
if !inclusive {
std::mem::swap(&mut exclusive, &mut inclusive_prod);
}
let first = multiply(equal(&inclusive_prod, 0.0)?, not_equal(&exclusive, 0.0)?)?;
let with_one = cumprod(&where_(&first, 1.0, x)?, a, reverse, inclusive)?;
Ok((first, with_one, equal(out, 0.0)?))
}
fn scan_vjp(
op: ScanOp,
x: &Array,
out: &Array,
g: &Array,
axis: usize,
reverse: bool,
inclusive: bool,
) -> Result<Array> {
let a = Some(axis as isize);
match op {
ScanOp::Sum => cumsum(g, a, !reverse, inclusive),
ScanOp::LogAddExp => {
let (pos, neg) = log_halves(g)?;
let half = |h: &Array| -> Result<Array> {
exp(&ops::add(
logcumsumexp(&subtract(h, out)?, a, !reverse, inclusive)?,
x,
)?)
};
subtract(half(&pos)?, half(&neg)?)
}
ScanOp::Prod => {
let (first, with_one, zero) = first_zero(x, out, axis, reverse, inclusive)?;
let partial = |p: &Array| cumsum(&multiply(p, g)?, a, !reverse, inclusive);
let regular = divide(partial(out)?, x)?;
where_(&first, &partial(&with_one)?, &where_(&zero, 0.0, ®ular)?)
}
ScanOp::Max | ScanOp::Min => {
if x.dims()[axis] == 0 {
return zeros_like(x); }
let (owner, (start, stop)) = scan_owner(x, out, axis, reverse, inclusive)?;
let g = if inclusive {
g.clone()
} else {
window(g, axis, start, stop)?
};
crate::indexing::scatter_add_along_axis(&zeros_like(x)?, &owner, &g, axis as isize)
}
}
}
fn scan_jvp(
op: ScanOp,
x: &Array,
out: &Array,
t: &Array,
axis: usize,
reverse: bool,
inclusive: bool,
) -> Result<Array> {
let a = Some(axis as isize);
match op {
ScanOp::Sum => cumsum(t, a, reverse, inclusive),
ScanOp::LogAddExp => {
let (pos, neg) = log_halves(t)?;
let half = |h: &Array| -> Result<Array> {
exp(&subtract(
logcumsumexp(&ops::add(h, x)?, a, reverse, inclusive)?,
out,
)?)
};
let neg_inf = equal(out, f64::NEG_INFINITY)?;
where_(&neg_inf, 0.0, &subtract(half(&pos)?, half(&neg)?)?)
}
ScanOp::Prod => {
let (first, with_one, zero) = first_zero(x, out, axis, reverse, inclusive)?;
let safe = where_(&equal(x, 0.0)?, 1.0, x)?;
let regular = multiply(out, cumsum(÷(t, &safe)?, a, reverse, inclusive)?)?;
let t_zero = sum(&where_(&first, t, 0.0)?, &[axis as isize], true)?;
where_(&zero, &multiply(&with_one, &t_zero)?, ®ular)
}
ScanOp::Max | ScanOp::Min => {
if x.dims()[axis] == 0 {
return zeros_like(out);
}
let (owner, (start, _)) = scan_owner(x, out, axis, reverse, inclusive)?;
let moved = take_along_axis(t, &owner, axis as isize)?;
if inclusive {
return Ok(moved);
}
let edge = zeros_like(&window(out, axis, 0, 1)?)?;
let parts = if start == 0 {
[moved, edge]
} else {
[edge, moved]
};
crate::indexing::concatenate(&parts, axis as isize)
}
}
}
pub(crate) fn zeros_like(a: &Array) -> Result<Array> {
let zero = ops::filled(&[], a.dtype(), |_| Canon::Int(0))?;
broadcast_to(&zero, a.dims())
}
fn ones_like(a: &Array) -> Result<Array> {
let one = ops::filled(&[], a.dtype(), |_| Canon::Int(1))?;
broadcast_to(&one, a.dims())
}
fn grouped(a: &Array, size: usize) -> Result<Array> {
let mut shape = isize_shape(a.dims());
let last = shape.pop().expect("at least 1-D");
shape.extend([last / size as isize, size as isize]);
reshape(a, &shape)
}
fn flatten_front(a: &Array, ndim: usize) -> Result<Array> {
let mut shape = vec![-1];
shape.extend(isize_shape(&a.dims()[a.ndim() + 1 - ndim..]));
reshape(a, &shape)
}
fn scale_bias_grads(
dw: &Array,
inputs: &[Array],
quant: crate::Quant,
wanted: [bool; 2],
) -> Result<[Option<Array>; 2]> {
let (w, scales, biases) = (&inputs[0], &inputs[1], &inputs[2]);
let group_size = quant.group_size as usize;
let dw = grouped(dw, group_size)?;
let ds = match wanted[0] {
true => {
let codes = dequantize(
w,
&ones_like(scales)?,
Some(&zeros_like(biases)?),
quant,
None,
)?;
Some(sum(
&multiply(&dw, grouped(&codes, group_size)?)?,
&[-1],
false,
)?)
}
false => None,
};
let db = match wanted[1] {
true => Some(sum(&dw, &[-1], false)?),
false => None,
};
Ok([ds, db])
}
fn no_weight_gradient(op: &str) -> Error {
Error::InvalidArgument(format!(
"{op}: there is no gradient with respect to the packed (quantized) weights"
))
}
fn no_scale_gradient(op: &str, quant: crate::Quant) -> Error {
Error::InvalidArgument(format!(
"{op}: there is no gradient with respect to {} scales (uint8 codes)",
quant.mode
))
}
fn isize_shape(shape: &[usize]) -> Vec<isize> {
shape.iter().map(|&n| n as isize).collect()
}
fn swap_last(a: &Array) -> Result<Array> {
let n = a.ndim();
let mut axes: Vec<usize> = (0..n).collect();
axes.swap(n - 2, n - 1);
transpose(a, &axes)
}
fn sign(x: &Array) -> Result<Array> {
subtract(
greater(x, 0)?.try_astype(x.dtype())?,
less(x, 0)?.try_astype(x.dtype())?,
)
}
fn sum_to(g: &Array, shape: &[usize]) -> Result<Array> {
let extra = g.ndim() - shape.len();
let axes: Vec<isize> = (0..g.ndim())
.filter(|&i| i < extra || (shape[i - extra] == 1 && g.dims()[i] != 1))
.map(|i| i as isize)
.collect();
let summed = if axes.is_empty() {
g.clone()
} else {
sum(g, &axes, true)?
};
reshape(&summed, &isize_shape(shape))
}
fn prod_weights(x: &Array, out: &Array, axes: &[usize]) -> Result<Array> {
let axes: Vec<isize> = axes.iter().map(|&a| a as isize).collect();
let zero = equal(x, 0)?;
let zeros = sum(&zero.try_astype(x.dtype())?, &axes, true)?;
let others = ops::prod(&where_(&zero, 1, x)?, &axes, true)?;
let only_zero = multiply(&zero, equal(&zeros, 1)?)?;
where_(
equal(&zeros, 0)?,
divide(out, x)?,
where_(&only_zero, &others, 0)?,
)
}
fn first_extreme(x: &Array, axes: &[usize], op: ReduceOp) -> Result<Array> {
let kept: Vec<usize> = (0..x.ndim()).filter(|a| !axes.contains(a)).collect();
let perm: Vec<usize> = kept.iter().chain(axes).copied().collect();
let moved = transpose(x, &perm)?;
let mut flat = isize_shape(&moved.dims()[..kept.len()]);
let reduced: usize = axes.iter().map(|&a| x.dims()[a]).product();
flat.push(reduced as isize);
let rows = reshape(&moved, &flat)?;
let index = match op {
ReduceOp::Max => ops::argmax(&rows, Some(-1), true)?,
_ => ops::argmin(&rows, Some(-1), true)?,
};
let positions = ops::arange(0.0, reduced as f64, 1.0, DType::U32)?;
let onehot = equal(&positions, &index)?.try_astype(x.dtype())?;
let mut inverse = vec![0; perm.len()];
for (i, &a) in perm.iter().enumerate() {
inverse[a] = i;
}
transpose(&reshape(&onehot, &isize_shape(moved.dims()))?, &inverse)
}
fn complex_sign(z: &Array, magnitude: &Array) -> Result<Array> {
let unit = divide(z, magnitude.try_astype(DType::Complex64)?)?;
where_(equal(magnitude, 0)?, 0, &unit)
}
fn extreme_weights(x: &Array, out: &Array, axes: &[usize]) -> Result<Array> {
let axes: Vec<isize> = axes.iter().map(|&a| a as isize).collect();
let mask = equal(x, out)?.try_astype(x.dtype())?;
let count = sum(&mask, &axes, true)?;
divide(&mask, &count)
}
impl Primitive {
fn elementwise_partial(
&self,
inputs: &[Array],
out: &Array,
i: usize,
v: &Array,
) -> Result<Option<Array>> {
let x = &inputs[0];
Ok(Some(match self {
Self::Unary(op) => match op {
UnaryOp::Negative => negative(v)?,
UnaryOp::Abs => multiply(v, sign(x)?)?,
UnaryOp::Exp => multiply(v, out)?,
UnaryOp::Log => divide(v, x)?,
UnaryOp::Sqrt => divide(v, multiply(2, out)?)?,
UnaryOp::Sin => multiply(v, cos(x)?)?,
UnaryOp::Cos => multiply(v, negative(&sin(x)?)?)?,
UnaryOp::Tanh => multiply(v, subtract(1, multiply(out, out)?)?)?,
UnaryOp::Sigmoid => multiply(v, multiply(out, subtract(1, out)?)?)?,
UnaryOp::Conjugate => conjugate(v)?,
UnaryOp::ErfInv => multiply(
multiply(v, std::f64::consts::PI.sqrt() / 2.0)?,
ops::exp(&multiply(out, out)?)?,
)?,
},
Self::Binary(op) => {
let (a, b) = (&inputs[0], &inputs[1]);
match (op, i) {
(BinaryOp::Add, _) => v.clone(),
(BinaryOp::Subtract, 0) => v.clone(),
(BinaryOp::Subtract, _) => negative(v)?,
(BinaryOp::Multiply, 0) => multiply(v, b)?,
(BinaryOp::Multiply, _) => multiply(v, a)?,
(BinaryOp::Divide, 0) => divide(v, b)?,
(BinaryOp::Divide, _) => negative(÷(multiply(v, a)?, multiply(b, b)?)?)?,
(BinaryOp::Maximum, 0) => multiply(v, greater(a, b)?.try_astype(a.dtype())?)?,
(BinaryOp::Maximum, _) => {
multiply(v, less_equal(a, b)?.try_astype(a.dtype())?)?
}
(BinaryOp::Minimum, 0) => multiply(v, less(a, b)?.try_astype(a.dtype())?)?,
(BinaryOp::Minimum, _) => {
multiply(v, greater_equal(a, b)?.try_astype(a.dtype())?)?
}
(BinaryOp::LogAddExp, 0) => multiply(v, ops::sigmoid(&subtract(a, b)?)?)?,
(BinaryOp::LogAddExp, _) => {
multiply(v, subtract(1, ops::sigmoid(&subtract(a, b)?)?)?)?
}
(BinaryOp::Power, 0) => multiply(
v,
multiply(
b,
crate::ops::imp::binary(BinaryOp::Power, a, &subtract(b, 1)?)?,
)?,
)?,
(BinaryOp::Power, _) => {
let positive = greater(a, 0)?;
let log_a = ops::log(&where_(&positive, a, 1)?)?;
multiply(v, multiply(log_a, out)?)?
}
_ => return Ok(None),
}
}
Self::Select => match i {
0 => return Ok(None),
1 => where_(x, v, 0)?,
_ => where_(x, 0, v)?,
},
_ => unreachable!("not element-wise: {}", self.name()),
}))
}
fn is_elementwise(&self) -> bool {
matches!(self, Self::Unary(_) | Self::Binary(_) | Self::Select)
}
pub(crate) fn vjp(
&self,
inputs: &[Array],
outputs: &[Array],
cotangents: &[Array],
wanted: &[bool],
) -> Result<Vec<Option<Array>>> {
let g = &cotangents[0];
if let (Self::Unary(UnaryOp::Abs), DType::Complex64) = (self, inputs[0].dtype()) {
return Ok(vec![Some(multiply(
g.try_astype(DType::Complex64)?,
complex_sign(&inputs[0], &outputs[0])?,
)?)]);
}
if self.is_elementwise() {
let inputs = inputs.iter().map(conjugate).collect::<Result<Vec<_>>>()?;
let out = conjugate(&outputs[0])?;
return (0..inputs.len())
.map(|i| match wanted[i] {
true => self.elementwise_partial(&inputs, &out, i, g),
false => Ok(None),
})
.collect();
}
if inputs.is_empty() {
return Ok(Vec::new()); }
let x = &inputs[0];
let one = |a: Result<Array>| a.map(|a| vec![Some(a)]);
match self {
Self::Copy => one(Ok(g.clone())),
Self::Collective { op, group } => match op {
crate::distributed::CollectiveOp::Gather => {
let rank = crate::distributed::group_rank(*group)?;
let n = x.dims().first().copied().unwrap_or(1);
let mut start = vec![0; g.ndim()];
start[0] = rank * n;
let mut len = g.dims().to_vec();
len[0] = n;
let rows = record(
Self::Slice {
start,
step: vec![1; g.ndim()],
len,
},
vec![g.clone()],
)?;
match x.ndim() {
0 => one(record(Self::Squeeze(vec![0]), vec![rows])),
_ => one(Ok(rows)),
}
}
_ => one(Ok(g.clone())),
},
Self::Depends { .. } => {
let mut grads = vec![None; inputs.len()];
grads[0] = wanted[0].then(|| g.clone());
Ok(grads)
}
Self::AsType(_) => one(g.try_astype(x.dtype())),
Self::Reshape(_) => one(reshape(g, &isize_shape(x.dims()))),
Self::Squeeze(axes) => one(record(Self::ExpandDims(axes.clone()), vec![g.clone()])),
Self::ExpandDims(axes) => one(record(Self::Squeeze(axes.clone()), vec![g.clone()])),
Self::Transpose(axes) => {
let mut inverse = vec![0; axes.len()];
for (i, &a) in axes.iter().enumerate() {
inverse[a] = i;
}
one(transpose(g, &inverse))
}
Self::Slice { start, step, len } => {
let update = Self::SliceUpdate {
start: start.clone(),
step: step.clone(),
len: len.clone(),
};
one(record(update, vec![zeros_like(x)?, g.clone()]))
}
Self::Sort { axis } => {
let idx = stop_gradient(&argsort(x, Some(*axis as isize))?)?;
one(put_along_axis(&zeros_like(x)?, &idx, g, *axis as isize))
}
Self::ArgSort { .. } => Ok(vec![None]),
Self::Scan {
op,
axis,
reverse,
inclusive,
} => one(scan_vjp(
*op,
x,
&outputs[0],
g,
*axis,
*reverse,
*inclusive,
)),
Self::GatherAxis { axis } => {
let scatter = Self::ScatterAxis {
axis: *axis,
reduce: ScatterReduce::Sum,
};
let src = match wanted[0] {
true => Some(record(
scatter,
vec![zeros_like(x)?, inputs[1].clone(), g.clone()],
)?),
false => None,
};
Ok(vec![src, None])
}
Self::ScatterAxis { axis, reduce } => {
let (indices, updates) = (&inputs[1], &inputs[2]);
let dst = match (wanted[0], reduce) {
(false, _) => None,
(true, ScatterReduce::None) => Some(record(
self.clone(),
vec![g.clone(), indices.clone(), zeros_like(updates)?],
)?),
(true, _) => Some(g.clone()),
};
let gathered = match wanted[2] {
true => Some(record(
Self::GatherAxis { axis: *axis },
vec![g.clone(), indices.clone()],
)?),
false => None,
};
Ok(vec![dst, None, gathered])
}
Self::Concatenate { axis } => {
let mut start = 0;
inputs
.iter()
.zip(wanted)
.map(|(a, &w)| {
let first = start;
start += a.dims()[*axis];
if !w {
return Ok(None);
}
let mut begin = vec![0; a.ndim()];
begin[*axis] = first;
let part = Self::Slice {
start: begin,
step: vec![1; a.ndim()],
len: a.dims().to_vec(),
};
Ok(Some(record(part, vec![g.clone()])?))
})
.collect()
}
Self::SliceUpdate { start, step, len } => {
let update = &inputs[1];
let base = match wanted[0] {
true => Some(record(self.clone(), vec![g.clone(), zeros_like(update)?])?),
false => None,
};
let slice = Self::Slice {
start: start.clone(),
step: step.clone(),
len: len.clone(),
};
let update = match wanted[1] {
true => Some(record(slice, vec![g.clone()])?),
false => None,
};
Ok(vec![base, update])
}
Self::Broadcast(_) => one(sum_to(g, x.dims())),
Self::StopGradient
| Self::ArgReduce { .. }
| Self::RandomBits { .. }
| Self::SortedOffsets { .. } => Ok(vec![None]),
Self::View(_) | Self::ToFp8 | Self::FromFp8(_) => match wanted[0] {
true => Err(Error::InvalidArgument(format!(
"{}: there is no derivative through a bit reinterpretation or an fp8 code",
self.name()
))),
false => Ok(vec![None]),
},
Self::Reduce { op, axes } => match op {
ReduceOp::Sum => one(broadcast_to(g, x.dims())),
ReduceOp::Prod => one(multiply(
g,
conjugate(&prod_weights(x, &outputs[0], axes)?)?,
)),
ReduceOp::Max | ReduceOp::Min => {
one(multiply(g, extreme_weights(x, &outputs[0], axes)?))
}
ReduceOp::And | ReduceOp::Or => Ok(vec![None]),
},
Self::Matmul => {
let (a, b) = (&inputs[0], &inputs[1]);
let da = match wanted[0] {
true => Some(matmul(g, &conjugate(&swap_last(b)?)?)?),
false => None,
};
let db = match wanted[1] {
true => Some(matmul(&conjugate(&swap_last(a)?)?, g)?),
false => None,
};
Ok(vec![da, db])
}
Self::AddMM { .. } => {
let (alpha, beta) = self.addmm_factors();
let (a, b) = (&inputs[0], &inputs[1]);
let scaled = multiply(g, f64::from(alpha))?;
let da = match wanted[0] {
true => Some(matmul(&scaled, &conjugate(&swap_last(b)?)?)?),
false => None,
};
let db = match wanted[1] {
true => Some(matmul(&conjugate(&swap_last(a)?)?, &scaled)?),
false => None,
};
let dc = match wanted[2] {
true => Some(multiply(g, f64::from(beta))?),
false => None,
};
Ok(vec![da, db, dc])
}
Self::Fast(op) => op.vjp(inputs, outputs, cotangents, wanted),
Self::MetalKernel(_) => Err(crate::Error::InvalidArgument("metal_kernel: no derivative or batching rule (wrap the kernel in `custom_function`)".into())),
Self::Compiled(fused) => fused.vjp(inputs, cotangents, wanted),
Self::Quantize { .. } => Ok(vec![None]),
Self::Dequantize { quant, .. } => {
if wanted[0] {
return Err(no_weight_gradient("dequantize"));
}
if quant.mode != crate::QuantMode::Affine {
return match wanted[1] {
true => Err(no_scale_gradient("dequantize", *quant)),
false => Ok(vec![None, None]),
};
}
let [ds, db] = scale_bias_grads(g, inputs, *quant, [wanted[1], wanted[2]])?;
Ok(vec![None, ds, db])
}
Self::QuantizedMatmul { quant, transpose } => {
if wanted[1] {
return Err(no_weight_gradient("quantized_matmul"));
}
let (x, w, scales) = (&inputs[0], &inputs[1], &inputs[2]);
let biases = inputs.get(3);
let dx = match wanted[0] {
true => Some(quantized_matmul(g, w, scales, biases, !transpose, *quant)?),
false => None,
};
let Some(biases) = biases else {
if wanted[2] {
return Err(no_scale_gradient("quantized_matmul", *quant));
}
return Ok(vec![dx, None, None]);
};
let [ds, db] = match wanted[2] || wanted[3] {
true => {
let keep = w.ndim();
let (fg, fx) = (flatten_front(g, keep)?, flatten_front(x, keep)?);
let dw = match transpose {
true => matmul(&swap_last(&fg)?, &fx)?,
false => matmul(&swap_last(&fx)?, &fg)?,
};
let rest = [w.clone(), scales.clone(), biases.clone()];
scale_bias_grads(&dw, &rest, *quant, [wanted[2], wanted[3]])?
}
false => [None, None],
};
Ok(vec![dx, None, ds, db])
}
Self::Custom(call) => {
let n = call.primals;
let primals = &inputs[..n];
let grads = match &call.rules.vjp {
Some(rule) => rule(primals, outputs, cotangents)?,
None => {
let remat = primals
.iter()
.map(|p| crate::primitive::depends(p, cotangents))
.collect::<Result<Vec<_>>>()?;
crate::vjp_flat(|xs| (call.rules.body)(xs), &remat, cotangents)?.1
}
};
if grads.len() != n {
return Err(Error::InvalidArgument(format!(
"custom_function: the vjp rule returned {} cotangents for {n} primals",
grads.len()
)));
}
Ok(grads
.into_iter()
.zip(wanted)
.map(|(g, &w)| w.then_some(g))
.chain(std::iter::repeat_n(None, inputs.len() - n))
.collect())
}
#[cfg(test)]
Self::AddMultiply => {
let (a, b) = (&inputs[0], &inputs[1]);
let h = &cotangents[1];
Ok(vec![
Some(ops::add(g, multiply(h, b)?)?),
Some(ops::add(g, multiply(h, a)?)?),
])
}
Self::Unary(_) | Self::Binary(_) | Self::Select => unreachable!(),
}
}
pub(crate) fn jvp(
&self,
inputs: &[Array],
outputs: &[Array],
tangents: &[Option<Array>],
) -> Result<Vec<Array>> {
if let (Self::Unary(UnaryOp::Abs), DType::Complex64) = (self, inputs[0].dtype()) {
let (z, out) = (&inputs[0], &outputs[0]);
let t = tangents[0].clone().map_or_else(|| zeros_like(z), Ok)?;
let re = multiply(conjugate(z)?, &t)?.try_astype(DType::F32)?;
return Ok(vec![where_(equal(out, 0)?, 0, divide(&re, out)?)?]);
}
let tangent = |i: usize| -> Result<Array> {
match &tangents[i] {
Some(t) => Ok(t.clone()),
None => zeros_like(&inputs[i]),
}
};
let out = &outputs[0];
if self.is_elementwise() {
let mut total: Option<Array> = None;
for (i, t) in tangents.iter().enumerate() {
let Some(t) = t else { continue };
if let Some(d) = self.elementwise_partial(inputs, out, i, t)? {
total = Some(match total {
Some(sum) => ops::add(&sum, &d)?,
None => d,
});
}
}
return Ok(vec![match total {
Some(t) => t,
None => zeros_like(out)?,
}]);
}
if inputs.is_empty() {
return Ok(vec![zeros_like(out)?]); }
let x = &inputs[0];
let one = |a: Result<Array>| a.map(|a| vec![a]);
match self {
Self::Collective { op, .. } => match op {
crate::distributed::CollectiveOp::Send { .. } => one(tangent(0)),
_ => one(record(self.clone(), vec![tangent(0)?])),
},
Self::Copy | Self::Depends { .. } => one(tangent(0)),
Self::AsType(dtype) => one(tangent(0)?.try_astype(*dtype)),
Self::Reshape(_) => one(reshape(&tangent(0)?, &isize_shape(out.dims()))),
Self::Squeeze(_) | Self::ExpandDims(_) => one(record(self.clone(), vec![tangent(0)?])),
Self::Transpose(axes) => one(transpose(&tangent(0)?, axes)),
Self::Slice { .. } | Self::Broadcast(_) => one(record(self.clone(), vec![tangent(0)?])),
Self::SliceUpdate { .. } => one(record(self.clone(), vec![tangent(0)?, tangent(1)?])),
Self::GatherAxis { .. } => {
one(record(self.clone(), vec![tangent(0)?, inputs[1].clone()]))
}
Self::Sort { axis } => {
let idx = stop_gradient(&argsort(x, Some(*axis as isize))?)?;
one(take_along_axis(&tangent(0)?, &idx, *axis as isize))
}
Self::ArgSort { .. } => one(zeros_like(out)),
Self::Scan {
op,
axis,
reverse,
inclusive,
} => one(scan_jvp(
*op,
x,
out,
&tangent(0)?,
*axis,
*reverse,
*inclusive,
)),
Self::ScatterAxis { .. } => one(record(
self.clone(),
vec![tangent(0)?, inputs[1].clone(), tangent(2)?],
)),
Self::Concatenate { .. } => one(record(
self.clone(),
(0..inputs.len()).map(tangent).collect::<Result<_>>()?,
)),
Self::StopGradient
| Self::ArgReduce { .. }
| Self::RandomBits { .. }
| Self::SortedOffsets { .. } => {
one(zeros_like(out))
}
Self::View(_) | Self::ToFp8 | Self::FromFp8(_) => match tangents[0] {
Some(_) => Err(Error::InvalidArgument(format!(
"{}: there is no derivative through a bit reinterpretation or an fp8 code",
self.name()
))),
None => one(zeros_like(out)),
},
Self::Reduce { op, axes } => {
let summed = |t: Array| {
record(
Self::Reduce {
op: ReduceOp::Sum,
axes: axes.clone(),
},
vec![t],
)
};
match op {
ReduceOp::Sum => one(summed(tangent(0)?)),
ReduceOp::Prod => {
one(summed(multiply(tangent(0)?, prod_weights(x, out, axes)?)?))
}
ReduceOp::Max | ReduceOp::Min => {
one(summed(multiply(tangent(0)?, first_extreme(x, axes, *op)?)?))
}
ReduceOp::And | ReduceOp::Or => one(zeros_like(out)),
}
}
Self::Matmul => {
let (a, b) = (&inputs[0], &inputs[1]);
let total = match (&tangents[0], &tangents[1]) {
(Some(ta), Some(tb)) => ops::add(matmul(ta, b)?, matmul(a, tb)?)?,
(Some(ta), None) => matmul(ta, b)?,
(None, Some(tb)) => matmul(a, tb)?,
(None, None) => zeros_like(out)?,
};
one(Ok(total))
}
Self::AddMM { .. } => {
let (alpha, beta) = self.addmm_factors();
let (a, b) = (&inputs[0], &inputs[1]);
let mut total = match (&tangents[0], &tangents[1]) {
(Some(ta), Some(tb)) => ops::add(matmul(ta, b)?, matmul(a, tb)?)?,
(Some(ta), None) => matmul(ta, b)?,
(None, Some(tb)) => matmul(a, tb)?,
(None, None) => zeros_like(out)?,
};
total = multiply(&total, f64::from(alpha))?;
if let Some(tc) = &tangents[2] {
total = ops::add(&total, multiply(tc, f64::from(beta))?)?;
}
one(Ok(total))
}
Self::Fast(op) => op.jvp(inputs, tangents),
Self::MetalKernel(_) => Err(crate::Error::InvalidArgument("metal_kernel: no derivative or batching rule (wrap the kernel in `custom_function`)".into())),
Self::Compiled(fused) => fused.jvp(inputs, tangents),
Self::Quantize { .. } => outputs.iter().map(zeros_like).collect(),
Self::Dequantize { quant, .. } if quant.mode == crate::QuantMode::Affine => {
one(dequantize(
&inputs[0],
&tangent(1)?,
Some(&tangent(2)?),
*quant,
None,
))
}
Self::Dequantize { quant, .. } => match tangents[1] {
Some(_) => Err(no_scale_gradient("dequantize", *quant)),
None => outputs.iter().map(zeros_like).collect(),
},
Self::QuantizedMatmul { quant, transpose } => {
let (x, w) = (&inputs[0], &inputs[1]);
let biases = inputs.get(3);
let mut total = match &tangents[0] {
Some(t) => quantized_matmul(t, w, &inputs[2], biases, *transpose, *quant)?,
None => zeros_like(out)?,
};
if biases.is_none() && tangents[2].is_some() {
return Err(no_scale_gradient("quantized_matmul", *quant));
}
if biases.is_some() && (tangents[2].is_some() || tangents[3].is_some()) {
let dw = dequantize(w, &tangent(2)?, Some(&tangent(3)?), *quant, None)?;
let dw = if *transpose { swap_last(&dw)? } else { dw };
total = ops::add(&total, matmul(x, &dw)?)?;
}
one(Ok(total))
}
Self::Custom(call) => {
let n = call.primals;
let primals = &inputs[..n];
let tangents = (0..n).map(tangent).collect::<Result<Vec<_>>>()?;
let result = match &call.rules.jvp {
Some(rule) => rule(primals, &tangents)?,
None => crate::jvp_flat(|xs| (call.rules.body)(xs), primals, &tangents)?.1,
};
if result.len() != outputs.len() {
return Err(Error::InvalidArgument(format!(
"custom_function: the jvp rule returned {} tangents for {} outputs",
result.len(),
outputs.len()
)));
}
Ok(result)
}
#[cfg(test)]
Self::AddMultiply => {
let (a, b) = (&inputs[0], &inputs[1]);
let (ta, tb) = (tangent(0)?, tangent(1)?);
Ok(vec![
ops::add(&ta, &tb)?,
ops::add(multiply(&ta, b)?, multiply(a, &tb)?)?,
])
}
Self::Unary(_) | Self::Binary(_) | Self::Select => unreachable!(),
}
}
}