# 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](/showcase/vnpy-sweep/README.html), 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: ```text 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](numpy-avx512/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: ```text 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](https://github.com/numpy/numpy/issues/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: ```sh 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 an `ImportWarning` that 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](/?src=note-avx512#start). 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`.