QuiddityML

By · 22 October 2026 · 5 min read

The log-sum-exp trick explained: overflow, underflow, and adding logs

Why exp(1000) returns inf, why a product of 500 small probabilities returns 0, and how factoring out the largest value lets you add numbers stored as logs in PyTorch.

The log-sum-exp trick is a way to compute $\log \sum_j e^{x_j}$ without ever exponentiating a large number, by pulling the largest value out of the sum first. You need it whenever numbers are stored as logarithms, often because they are tiny probabilities, and you have to add them back together without the result turning into inf or -inf.

What overflow and underflow are

A float32, the default number type in PyTorch, covers a fixed range. The largest float32 is about $3.4 \times 10^{38}$. A result bigger than that has no representation, so it becomes inf, which is called overflow. On the other end, the smallest positive float32 is about $1.4 \times 10^{-45}$, and anything closer to zero rounds to exactly 0.0, which is called underflow.

The exponential function $e^x$, written exp(x) in code, runs into both edges quickly. Each step of 1 in $x$ multiplies the result by $e \approx 2.718$, so $e^2 \approx 7.39$ and $e^4 \approx 54.6$. In the other direction $e^{-2} \approx 0.135$, and it keeps shrinking toward zero.

1import torch
2 
3print(torch.exp(torch.tensor(1000.0)))
4print(torch.exp(torch.tensor(-1000.0)))
5# tensor(inf)
6# tensor(0.)

The curve of exp(x) from x = -4 to 4, with exp(-2) = 0.135, exp(0) = 1, exp(2) = 7.39 and exp(4) = 54.6 marked, and below it torch.exp of 1000 returning inf and torch.exp of -1000 returning 0.

Neither case raises an error. PyTorch hands back inf or 0.0, and the next line of code carries on with it.

Where exp(x) overflows

$e^{88}$ is still a valid float32, and $e^{89}$ is already too large:

1print(torch.exp(torch.tensor(88.0)), torch.exp(torch.tensor(89.0)))
2print(torch.exp(torch.tensor(-103.0)), torch.exp(torch.tensor(-104.0)))
3# tensor(1.6516e+38) tensor(inf)
4# tensor(1.4013e-45) tensor(0.)

So in float32, exp overflows past about $x = 88.7$, and on the negative side it rounds to exactly 0 once $x$ drops below about $-104$. In float64 the same limits are about $710$ and $-745$. That means $e^{10}$ is safe in either format, while $e^{100}$ already overflows float32.

A log-scale plot of exp(x) in float32 drawn as a straight line, with the overflow point at x = 88.7 and the point below x = -104 where exp returns exactly 0.0, plus a table of the overflow and zero points for float32 and float64.

Multiplying many small probabilities

Underflow shows up whenever many small numbers get multiplied together. Take 500 independent events, each with probability 0.01. The chance that all of them happen is $0.01^{500} = 10^{-1000}$, far below anything float32 can hold:

1p = torch.full((500,), 0.01)
2print(p.prod())
3print(torch.log(p).sum())
4# tensor(0.)
5# tensor(-2302.5850)

The product is exactly 0.0, so two different products that both underflow can no longer be told apart. The fix is to add logarithms instead of multiplying probabilities. Here $\prod$ means multiply all the terms and $\sum$ means add them:

$$\log \prod_i p_i = \sum_i \log p_i$$

The log of a product equals the sum of the logs. Each $\log 0.01 \approx -4.605$ is a modest negative number, and 500 of them add up to about $-2303$, nowhere near either edge of float32.

A table of the running float32 product of 0.01 values for 500 independent events, reaching 9.8e-45 after 22 events and exactly 0.0 from event 23, next to the log version where 500 times -4.605 gives about -2303.

Adding numbers stored as logs

Storing probabilities as logs keeps tiny values representable, but it makes adding them harder. Say two probabilities $p$ and $q$ are stored only as their logs, $a = \log p$ and $b = \log q$, and you need $\log(p + q)$. The direct route converts back, adds, and takes the log again:

$$\log(p + q) = \log(e^a + e^b)$$

With $a = -1000$ and $b = -1001$, both $e^a$ and $e^b$ underflow to 0, so the code takes $\log(0)$:

1a, b = torch.tensor(-1000.0), torch.tensor(-1001.0)
2print(torch.log(torch.exp(a) + torch.exp(b)))
3# tensor(-inf)

The true answer is about $-999.69$, a perfectly ordinary number. Large values break it the other way: with $a = 1000$, $e^a$ overflows to inf.

The direct way of computing log(e^a + e^b) with a = -1000 and b = -1001: each exponential underflows to 0.0, the sum is 0.0 and the log is -inf, while the true answer is about -999.69, and a = 1000 overflowing to inf.

Written for any number of values, the expression $\log \sum_j e^{x_j}$ is called log-sum-exp, or LSE.

Factoring out the max

The fix is to pull the largest value out before exponentiating. For any constant $c$, $e^{x_j} = e^{c} \cdot e^{x_j - c}$, so the $e^c$ can come out of the sum and out of the log:

$$\log \sum_j e^{x_j} = c + \log \sum_j e^{x_j - c}$$

Choosing $c = \max_j x_j$, the largest value, makes the largest shifted value exactly 0, and $e^0 = 1$. Every other shifted value is negative, so every other term sits between 0 and 1. Nothing can overflow, and the sum is at least 1, so its log never hits $\log(0)$.

For example, take $x = [-1000, -1001, -1003]$. The max is $-1000$, so the shifted values are $[0, -1, -3]$, their exponentials are $[1, 0.368, 0.050]$, and the sum is $1.418$:

$$\text{LSE}(x) = -1000 + \log(1.418) \approx -999.651$$

The identity e^x = e^c times e^(x - c), then a table for x = -1000, -1001 and -1003 with x - max equal to 0, -1 and -3 and e to that equal to 1.000, 0.368 and 0.050, summing to 1.418, and the log-sum-exp trick written as max(x) plus the log of the shifted sum.

This rewrite is the log-sum-exp trick:

$$\text{LSE}(x) = \max(x) + \log \sum_j e^{x_j - \max(x)}$$

Both sides equal the same number, and only the right side is safe to compute. In code it takes three lines, and PyTorch has it built in as torch.logsumexp:

1def log_sum_exp(x):
2    m = x.max()
3    return m + torch.log(torch.exp(x - m).sum())
4 
5x = torch.tensor([-1000.0, -1001.0, -1003.0])
6print(log_sum_exp(x), torch.logsumexp(x, dim=0))
7# tensor(-999.6510) tensor(-999.6510)

Both failures, fixed

The trick handles the underflow side and the overflow side the same way. For $x = [-1000, -1001]$ the shifted values are $[0, -1]$:

$$\text{LSE}(x) = -1000 + \log(e^{0} + e^{-1}) = -1000 + \log(1.368) \approx -999.69$$

For $x = [1000, 0, 0]$ the max is 1000 and the shifted values are $[0, -1000, -1000]$:

$$\text{LSE}(x) = 1000 + \log(1 + 2e^{-1000}) \approx 1000 + \log(1) = 1000$$

$e^{-1000}$ still underflows to 0 in the second case, which does no harm because it was far too small to change the sum.

1for values in ([-1000.0, -1001.0], [1000.0, 0.0, 0.0]):
2    x = torch.tensor(values)
3    print(torch.log(torch.exp(x).sum()), torch.logsumexp(x, dim=0))
4# tensor(-inf) tensor(-999.6868)
5# tensor(inf) tensor(1000.)

Two columns of steps for x = [-1000, -1001] and x = [1000, 0, 0]: take the max, subtract it, exponentiate, sum, take the log and add the max back, ending at LSE of about -999.69 and LSE = 1000.

Common mistakes

Floating point for deep learning covers how floats store numbers and why they round. Softmax uses the same max shift to keep its probabilities finite. Exponents and logarithms for machine learning covers the log rules this trick relies on.

QuiddityML teaches overflow and the log-sum-exp trick as two concepts in the Math track's numerical computation unit. The exercises on them include writing a log_prob function that adds logs instead of multiplying probabilities, ordering the lines of a stable log_sum_exp, and spotting the bug in a version that shifts by the smallest value instead of the largest.