The same Python backtest prints a different Sharpe ratio on a newer CPU
We ran the same Python code with the same numpy version on two machines and got different numbers:
the Sharpe ratio changes in its last digits. The cause is not the code or the data. On CPUs with
AVX-512, numpy computes np.log and np.exp with its own vectorised routines, and about one result
in twenty is rounded differently. Both results are accurate; they are just not the same bits. Below
are the measurements, a script to check your own machines, and a way to get the same numbers
everywhere. One warning first: the widely shared workaround stopped working, silently, in numpy 2.4.
What we saw
While checking the vn.py optimizer case, we ran vn.py's own brute-force optimizer on a 100,000-bar series on two machines. Both had numpy 2.5.3:
- an AMD Threadripper PRO 5975WX (Zen 3, AVX2);
- an AMD Ryzen 9 7950X3D (Zen 4, AVX-512).
The parameters, their order, trade counts, returns and drawdowns were identical, but two of the ten printed Sharpe ratios were not:
AVX2: fast_window=31 slow_window=24 sharpe_ratio=0.61673083389321237 ...
AVX-512: fast_window=31 slow_window=24 sharpe_ratio=0.61673083389321259 ...
A minimal reproduction
numpy_log_cpu.py builds 10 million ratios close to 1, which is what
balance[t] / balance[t-1] looks like, using only + and *, so the input is the same bits on
every machine. It applies np.log and np.exp and computes an annualised Sharpe ratio from ten
years of daily log returns:
Zen 3 (AVX2): numpy 2.5.3 | CPU features used: X86_V3
input hash: cdbe5b3e6769854d
np.log hash: 758f5856e4acbbb5
np.exp hash: 1e0b3fc690120efb
Sharpe ratio: 0.92814377921504698
Zen 4 (AVX-512): numpy 2.5.3 | CPU features used: X86_V3, X86_V4, AVX512_ICL
input hash: cdbe5b3e6769854d
np.log hash: da531faf4c71aae5
np.exp hash: 82f7841235e8b1b8
Sharpe ratio: 0.9281437792150472
How big the difference is
We compared every differing value with the result computed to 50 significant digits:
| Function | Values that differ | Difference | Largest error, AVX2 (glibc) | Largest error, AVX-512 (numpy) |
|---|---|---|---|---|
np.log |
460,676 of 10 million (4.61%) | always 1 ulp | 0.502 ulp | 0.620 ulp |
np.exp |
449,797 of 10 million (4.50%) | always 1 ulp | 0.506 ulp | 0.719 ulp |
An ulp is one unit in the last place of a 64-bit float. Where the two machines disagree, the AVX2 result is the closer one in 99% of cases, but both are within one ulp of the exact value. numpy accepts differences of a few ulp between its implementations by design, in exchange for speed (numpy issue 23523).
For trading decisions one ulp is far below the noise. A Sharpe ratio sums thousands of log returns, and a one-ulp change in some of them moves its last digits. That breaks anything that compares results exactly:
- regression tests and result caches keyed by a hash;
- "is the new version identical to the old one" checks;
- audits that rerun a backtest on another machine;
- and any team whose laptop and server disagree about the same run.
Getting the same numbers on every machine
numpy lets you switch off its AVX-512 routines with an environment variable. The feature names
changed in numpy 2.4: AVX-512 is now the group X86_V4, and the old names are ignored with a warning
Python hides by default. The recipe most often quoted online,
NPY_DISABLE_CPU_FEATURES="AVX512F,AVX512CD,AVX512_SKX,...", still works on numpy 2.3 and silently
does nothing on 2.4 and newer.
This line lists both spellings and gave the AVX2 bits on numpy 2.2.6, 2.3.5, 2.4.4 and 2.5.3 on the Zen 4 machine:
NPY_DISABLE_CPU_FEATURES="X86_V4 AVX512_ICL AVX512_SPR AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL" \
python your_backtest.py
With it, the Zen 4 machine printed the Zen 3 hashes and the Sharpe ratio 0.92814377921504698.
- To see whether numpy ignored a name, run
python -W always -c "import numpy". An ignored name prints anImportWarningthat says so. numpy.show_runtime()shows the features in use.
The price is speed: np.log over 10 million values took 30–33 ms instead of 11–12 ms on that machine.
For most backtests that is negligible next to the rest of the run.
How TAKT handles it
Our builds print the same result on every CPU we have tested. We define "identical to the original" for the platform recorded in the measurement passport: if your reference machine has AVX-512, the reference output is the AVX-512 one. If you need a backtest whose numbers do not depend on the machine at all, that is something we can build too. Ask for a free speed check.
Measured on 9 October 2026 with numpy 2.5.3 (Python 3.12 on Zen 3, Python 3.13 on Zen 4).
Correction, 9 October 2026. The first version of this note said numpy's AVX-512 np.log loses
up to 29 bits near x = 1. That was our measurement error: the inputs were made with np.exp, which
also differs between the machines, so we compared logarithms of slightly different numbers. On
identical inputs the difference is at most one ulp. The script now builds its inputs without np.exp.
Optional. By enabling comments, you consent to using the comment service operated by TAKT (ABX DEVELOPMENT LLP) at comments.taktcycles.com, delivered through Cloudflare, and its connections to GitHub. The receiving services obtain connection data, including your IP address. Our service receives this note’s public address and description. The feature accesses any saved comment sign-in and, if you sign in, remembers it in this website’s local storage between visits, with no fixed automatic deletion period. You can read comments without signing in. Posts and reactions are public on GitHub; choosing Preview also sends your unpublished draft to GitHub. You can withdraw permission using “Disable comments and forget sign-in”. See our Privacy Notice and Recipients Schedule for details, including international transfers. You can read this note without enabling comments.
Discuss this note on GitHub. Comments live in a public GitHub Discussion. The link opens GitHub, where GitHub’s terms and privacy statement apply; reading needs no account, posting needs a GitHub account.