Numerically Stable Softmax with Temperature
Write `softmax(logits, temperature=1.0)` that returns a probability distribution. It must not overflow for logits like `[1000, 1001]`, must treat `-inf` logits as masked (probability 0), must return a one-hot on the first argmax when `temperature == 0` (greedy), and must raise `ValueError` on empty input, negative temperature, or when every logit is masked.
Subtract the max before exponentiating: softmax is shift-invariant, and the shift keeps every exponent at or below zero so nothing overflows. Temperature divides logits, so T -> 0 is a limit, not a value; special-case it as argmax rather than dividing by zero. All-masked rows are a real production bug (a grammar or tool constraint that leaves no legal token) and must fail loudly instead of returning NaNs that silently sample token 0.
Try it first.
tests.py · 7 passing
p = softmax([1.0, 2.0, 3.0])
assert abs(sum(p) - 1) < 1e-12 and p[2] > p[1] > p[0]
assert abs(softmax([1000.0, 1001.0])[1] - 1 / (1 + math.exp(-1))) < 1e-12
assert softmax([3.0, 5.0, 5.0], 0) == [0.0, 1.0, 0.0]
cold = softmax([1.0, 2.0], 0.1)
hot = softmax([1.0, 2.0], 100.0)
assert cold[1] > 0.9999 and abs(hot[0] - 0.5) < 0.01
masked = softmax([0.0, -math.inf, 0.0])
assert masked == [0.5, 0.0, 0.5]
for bad in ([], [-math.inf, -math.inf]):
try:
softmax(bad)
assert False
except ValueError:
pass
try:
softmax([1.0], -1)
assert False
except ValueError:
passOn GPU the same idea becomes the online softmax used by FlashAttention: keep a running max and running sum, rescaling the sum when the max changes, so you never materialise the full row. Also ask what 'temperature 0' means in a given serving stack: batched inference with non-deterministic kernels can still diverge between runs even at argmax.