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
use smallvec::smallvec;
use crate::{Element, Recordable, Shape, Tensor};
use super::{Cotangents, Operation, Reads, unary};
/// The explicit broadcast of a single-value payload across a target
/// shape carried by the operation itself.
///
/// It is the only shape-changing expansion in the engine, and it is
/// deliberately explicit: the target shape is a recorded parameter,
/// never an alignment rule — and never an operand, because a shape is
/// static record-time data, not dataflow. Broadcasting and summation
/// are adjoint, so the operand's gradient is the sum of the incoming
/// gradient, restored to the operand's own single-value shape.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Broadcast {
pub(crate) shape: Shape,
}
impl Broadcast {
/// Returns the arity: one operand.
pub(crate) fn arity(&self) -> usize {
1
}
/// Returns the read set of the derivative rule below.
/// It reads its operand for shape only, which a placeholder answers.
pub(crate) fn reads(&self) -> Reads {
Reads::NOTHING
}
/// Infers the shape of the result: the carried target shape,
/// reachable only from a single-value operand.
pub(crate) fn infer_shape(&self, operands: &[Shape]) -> Shape {
let operand = unary(operands);
assert_eq!(
operand.volume(),
1,
"broadcast requires a single-element operand, got {operand}"
);
self.shape.clone()
}
}
impl Broadcast {
pub(crate) fn forward<E: Element>(&self, operands: &[&Tensor<E>]) -> Tensor<E> {
unary(operands).broadcast(self.shape.clone())
}
}
impl<Rule: Recordable> Operation<Rule> for Broadcast {
fn backward(&self, operands: &[&Rule], _output: &Rule, gradient: &Rule) -> Cotangents<Rule> {
let &operand = unary(operands);
// The reduced gradient is rank 0, but the operand may be any
// volume-1 shape (such as `[1]`); broadcasting the sum back to
// the operand's own shape keeps the accumulation well-formed.
smallvec![Some(gradient.sum().broadcast(operand.shape()))]
}
}