Numerical Reasoning Failures
5 min read · updated August 3, 2026
A model that writes a correct proof sketch will get a four-digit multiplication wrong. That combination looks paradoxical until you look at what the architecture is being asked to do when it multiplies, at which point it becomes the expected result.
Numbers are not numbers
A byte-pair tokeniser learns frequent substrings, and numeric substrings are frequent in ways that have nothing to do with arithmetic. Depending on the vocabulary, 1234 may be one token, or 12 + 34, or 123 + 4. Common years and round numbers often get their own token; the number one greater than them does not. So two numbers of the same magnitude can have different token structures, and the digit in the hundreds column is not at a fixed, addressable place in the sequence.
Every column-based algorithm you learned in school depends on aligning digits by position. The model has to reconstruct that alignment from attention over a segmentation that was chosen for text compression. Nogueira, Jiang and Lin (2021) showed how much this matters by making the digit positions explicit and watching arithmetic generalisation improve; McLeish et al.’s Transformers Can Do Arithmetic with the Right Embeddings (2024) took the idea further with embeddings that encode each digit’s index within its number, reporting generalisation to addition problems far longer than any seen in training. The lesson is that the deficiency is largely representational. Newer models with digit-aware tokenisation are measurably better at this for exactly that reason — which is why this page is marked for refresh.
There is no carry
The deeper issue is that a transformer computing an answer in one forward pass has a fixed depth. Multi-digit multiplication is an iterative algorithm whose step count grows with the input; a fixed-depth network cannot execute an unbounded number of steps, so whatever it is doing is not the algorithm.
Dziri et al.’s Faith and Fate: Limits of Transformers on Compositionality (NeurIPS 2023) established the empirical shape. They studied multi-digit multiplication and similar compositional tasks and reported that accuracy collapses rapidly as the problem’s computation graph grows — even for models fine-tuned on the task, and even when they perform well on smaller instances. Their interpretation: the models are performing approximate subgraph matching against patterns seen in training rather than executing a procedure, which predicts exactly the observed cliff at the point where memorised patterns run out.
Chain of thought helps precisely because it converts depth into length. Writing the intermediate products moves each step into the context window, where the next forward pass can read it — trading a computation the architecture cannot perform for a sequence of ones it can. It helps a great deal and it does not close the gap, because each written step is itself sampled and errors compound multiplicatively along the chain.
What the benchmarks actually show
High GSM8K scores made it tempting to declare grade-school arithmetic solved. Mirzadeh et al.’s GSM-Symbolic (Apple, 2024) complicated that considerably, and it is the result to know.
They built templates from GSM8K problems so they could regenerate each one with different names and different numeric values while keeping the reasoning identical. Two findings. First, performance varies noticeably across instantiations of the same template, and degrades as the numeric values change — behaviour you would not expect from a system executing a procedure. Second, and more striking, their GSM-NoOp variant adds a single clause that is topically related but mathematically irrelevant, and reported performance drops of tens of percent, reaching around 65% for some models. The model incorporates the irrelevant number into its arithmetic.
That second finding is the same phenomenon as Shi et al.’s distraction result on the context poisoning page, and it is the one with the most direct product consequence: real user inputs are full of irrelevant numbers.
Where it breaks in production
- Long chains of small steps. Unit conversions, tax calculations, multi-line invoices. Each step is easy; the product of ten per-step accuracies is not.
- Summing more than a handful of numbers. Totalling a column from a document is a classic silent failure — the answer is close, which is worse than being wrong, because nobody checks a plausible total.
- Decimal comparison. Whether 9.11 is greater than 9.9 became a widely reproduced failure across model families in 2024, and it is a clean illustration of the mechanism: as text, the longer string with the larger trailing digits looks bigger, and version strings in the training data behave exactly that way.
- Percentages of percentages. Compounding, margins, discounts on discounts. Models frequently add rates that must be multiplied.
- Numbers extracted from a document. Two failures compose here — extraction can pick the wrong figure, and arithmetic can mangle the right one — and the output gives no clue which happened.
The fix is a tool, not a prompt
Gao et al.’s PAL (2022) and Chen et al.’s Program of Thoughts (2022) both established the same thing: have the model emit code that computes the answer, execute the code, and use its output. The model does what it is good at — turning a word problem into a formal expression — and the arithmetic is done by something that does arithmetic. Both papers reported large gains over chain-of-thought prompting on numeric benchmarks, and it is now the default architecture for anything quantitative.
from decimal import Decimal, getcontext
getcontext().prec = 28
CALC_TOOL = {
"name": "calculate",
"description": ("Evaluate an arithmetic expression exactly. Use for ALL "
"arithmetic, including simple sums. Do not compute in prose."),
"parameters": {"type": "object", "required": ["expression"],
"properties": {"expression": {"type": "string",
"description": "Decimal arithmetic, e.g. (1250.40 * 0.21) + 99"}}},
}
SAFE = set("0123456789.+-*/() ")
def calculate(expression: str) -> str:
if not set(expression) <= SAFE: # never eval model output raw
raise ValueError("unsupported characters in expression")
return str(eval(expression, {"__builtins__": {}}, # noqa: S307
{"Decimal": Decimal}))
# The guardrail that matters as much as the tool: refuse to accept a numeric
# answer that did not come through it.
def extract_answer(response):
if response.tool_calls_used == 0 and contains_arithmetic(response.text):
raise ValueError("numeric claim produced without the calculator")Three practical notes. Use decimal arithmetic rather than binary floats for anything involving money, or you will trade a model rounding error for a float rounding error. Never eval model output without a character allowlist or a real expression parser — the model is an untrusted input source and a poisoned context can reach this code path. And instrument the guardrail: the count of responses containing arithmetic that bypassed the tool is the metric that tells you whether the fix is actually in force, and it is usually higher than anyone expects.
For the residual cases, verification is cheap: recompute the result a second way, or ask a second call to check the expression against the original problem statement rather than to redo the sum.