← All notes

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:

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:

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.

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.

All notes · New notes: Telegram · RSS

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.

Source text: numpy-avx512-backtest.md