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
110
111
112
113
114
115
116
117
//! Numerically stable softmax.
use kopitiam_core::{DType, Error, Result};
use crate::storage::Storage;
use super::Tensor;
impl Tensor {
/// Softmax along `axis`: for every slice along that axis,
/// `out[i] = exp(x[i] - max(x)) / sum(exp(x - max(x)))`.
///
/// # Why subtract the max before `exp`
///
/// Real transformer logits routinely reach the hundreds or thousands in
/// magnitude. `exp(1000.0)` overflows `f32` to `+inf`, and
/// `inf / inf` is `NaN` — softmax computed the naive way silently
/// poisons the rest of the forward pass on real inputs, not just
/// adversarial ones. Subtracting the row's max first is mathematically
/// a no-op (it cancels between numerator and denominator) but keeps
/// every exponent `<= 0`, so the largest possible input to `exp` is
/// `exp(0) = 1`. This is why the max-subtraction step is not an
/// optional optimization here — see the `large_magnitude_input_does_not_overflow`
/// test below for the exact failure this avoids.
pub fn softmax(&self, axis: usize) -> Result<Tensor> {
self.require_dtype(DType::F32)?;
let rank = self.rank();
if axis >= rank {
return Err(Error::IndexOutOfBounds { dim: axis, index: axis, len: rank });
}
let Storage::F32(data) = self.storage.as_ref() else { unreachable!() };
let contiguous: Vec<f32> = self.logical_offsets().map(|i| data[i]).collect();
let dims = self.shape.dims();
let axis_len = dims[axis];
let outer: usize = dims[..axis].iter().product();
let inner: usize = dims[axis + 1..].iter().product();
let mut out = vec![0f32; contiguous.len()];
for o in 0..outer {
for inn in 0..inner {
let base = o * axis_len * inner + inn;
let at = |a: usize| contiguous[base + a * inner];
let max = (0..axis_len).map(at).fold(f32::NEG_INFINITY, f32::max);
let mut sum = 0f32;
for a in 0..axis_len {
let exp = (at(a) - max).exp();
out[base + a * inner] = exp;
sum += exp;
}
for a in 0..axis_len {
out[base + a * inner] /= sum;
}
}
}
Tensor::from_f32(out, self.shape.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_close(a: f32, b: f32) {
assert!((a - b).abs() < 1e-5, "expected {b}, got {a}");
}
#[test]
fn softmax_rows_sum_to_one() {
let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], [2, 3]).unwrap();
let out = t.softmax(1).unwrap().to_vec_f32().unwrap();
assert_close(out[0] + out[1] + out[2], 1.0);
assert_close(out[3] + out[4] + out[5], 1.0);
}
#[test]
fn softmax_matches_hand_computation_for_a_simple_row() {
// softmax([0, 0]) = [0.5, 0.5]
let t = Tensor::from_f32(vec![0.0, 0.0], [2]).unwrap();
let out = t.softmax(0).unwrap().to_vec_f32().unwrap();
assert_close(out[0], 0.5);
assert_close(out[1], 0.5);
}
#[test]
fn softmax_preserves_relative_order() {
let t = Tensor::from_f32(vec![1.0, 3.0, 2.0], [3]).unwrap();
let out = t.softmax(0).unwrap().to_vec_f32().unwrap();
assert!(out[1] > out[2]);
assert!(out[2] > out[0]);
}
/// The whole point of subtracting the row max first: without it,
/// `exp(1000.0)` and `exp(1001.0)` both overflow `f32` to infinity, and
/// `inf / inf` is `NaN`. With the max-subtraction trick this input is
/// completely unremarkable (it reduces to `softmax([-1.0, 0.0])`).
#[test]
fn large_magnitude_input_does_not_overflow_to_nan() {
let t = Tensor::from_f32(vec![1000.0, 1001.0], [2]).unwrap();
let out = t.softmax(0).unwrap().to_vec_f32().unwrap();
assert!(!out[0].is_nan() && !out[1].is_nan(), "softmax produced NaN: {out:?}");
assert_close(out[0] + out[1], 1.0);
// exp(-1)/(exp(-1)+1) ~= 0.2689414
assert_close(out[0], 0.268_941_4);
}
#[test]
fn softmax_operates_along_an_arbitrary_axis_of_a_3d_tensor() {
// shape [1, 2, 2]; softmax along axis 1 (not the last axis).
let t = Tensor::from_f32(vec![1.0, 2.0, 3.0, 4.0], [1, 2, 2]).unwrap();
let out = t.softmax(1).unwrap().to_vec_f32().unwrap();
// Column 0: [1, 3], column 1: [2, 4], each independently normalized.
assert_close(out[0] + out[2], 1.0);
assert_close(out[1] + out[3], 1.0);
}
}