# Один и тот же бэктест на Python печатает другой коэффициент Шарпа на новом процессоре Мы запустили один и тот же код на Python с одной и той же версией numpy на двух машинах и получили разные числа: у коэффициента Шарпа меняются последние знаки. Причина не в коде и не в данных. На процессорах с AVX-512 numpy считает `np.log` и `np.exp` собственными векторными процедурами, и примерно каждый двадцатый результат округляется иначе. Оба результата точны, просто биты разные. Ниже — замеры, скрипт для проверки своих машин и способ получить одинаковые числа везде. Сразу предупреждение: самый распространённый рецепт в numpy 2.4 перестал работать, причём молча. ## Что мы увидели Проверяя [кейс оптимизатора vn.py](/showcase/vnpy-sweep/README.html), мы запустили встроенный оптимизатор vn.py на ряде из 100 000 баров на двух машинах. На обеих стоял numpy 2.5.3: - AMD Threadripper PRO 5975WX (Zen 3, AVX2); - AMD Ryzen 9 7950X3D (Zen 4, AVX-512). Параметры, их порядок, число сделок, доходности и просадки совпали, а два из десяти напечатанных коэффициентов Шарпа — нет: ```text AVX2: fast_window=31 slow_window=24 sharpe_ratio=0.61673083389321237 ... AVX-512: fast_window=31 slow_window=24 sharpe_ratio=0.61673083389321259 ... ``` ## Минимальное воспроизведение [numpy_log_cpu.py](numpy-avx512/numpy_log_cpu.py) строит 10 млн отношений, близких к 1, — так выглядит `balance[t] / balance[t-1]`. Строит только через `+` и `*`, поэтому вход на всех машинах один и тот же до бита. Затем применяет `np.log` и `np.exp` и считает годовой коэффициент Шарпа по десяти годам дневных логарифмических доходностей: ```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 ``` ## Насколько велика разница Каждое различающееся значение мы сверили с результатом, посчитанным с 50 значащими цифрами: | Функция | Различаются | Разница | Наибольшая ошибка AVX2 (glibc) | Наибольшая ошибка AVX-512 (numpy) | |---|---|---|---|---| | `np.log` | 460 676 из 10 млн (4,61%) | всегда 1 ulp | 0,502 ulp | 0,620 ulp | | `np.exp` | 449 797 из 10 млн (4,50%) | всегда 1 ulp | 0,506 ulp | 0,719 ulp | ulp — единица последнего разряда 64-битного числа. Там, где машины расходятся, результат AVX2 ближе к точному в 99% случаев, но оба отстоят от него меньше чем на один ulp. numpy сознательно допускает расхождение в несколько ulp между своими реализациями ради скорости ([numpy issue 23523](https://github.com/numpy/numpy/issues/23523)). Для торговых решений один ulp далеко ниже шума. Но коэффициент Шарпа складывает тысячи доходностей, и сдвиг на один ulp в части из них меняет его последние знаки. Это ломает всё, что сравнивает результаты точно: - регрессионные тесты и кэши результатов по хэшу; - проверки «новая версия совпадает со старой»; - аудит, который повторяет бэктест на другой машине; - и любую команду, у которой ноутбук и сервер расходятся на одном и том же прогоне. ## Как получить одинаковые числа на всех машинах numpy позволяет выключить AVX-512-процедуры переменной окружения. В numpy 2.4 названия возможностей сменились: AVX-512 теперь группа `X86_V4`, а старые названия игнорируются с предупреждением, которое Python по умолчанию не показывает. Самый частый рецепт из интернета, `NPY_DISABLE_CPU_FEATURES="AVX512F,AVX512CD,AVX512_SKX,..."`, на numpy 2.3 ещё работает, а на 2.4 и новее молча ничего не делает. Эта строка перечисляет оба написания. На машине с Zen 4 она дала биты AVX2 на numpy 2.2.6, 2.3.5, 2.4.4 и 2.5.3: ```sh NPY_DISABLE_CPU_FEATURES="X86_V4 AVX512_ICL AVX512_SPR AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL" \ python your_backtest.py ``` С ней машина на Zen 4 напечатала хэши Zen 3 и коэффициент Шарпа `0.92814377921504698`. - Проверить, не проигнорировал ли numpy какое-то название: `python -W always -c "import numpy"`. Проигнорированное название печатает `ImportWarning` с объяснением. - `numpy.show_runtime()` показывает, какие возможности используются. Цена — скорость: `np.log` по 10 млн значений занял на этой машине 30–33 мс вместо 11–12 мс. Для большинства бэктестов это незаметно на фоне остального прогона. ## Как с этим работает TAKT Наши сборки печатают один и тот же результат на всех процессорах, где мы их проверяли. «Совпадает с оригиналом» мы определяем для платформы из паспорта замера: если ваша эталонная машина с AVX-512, то эталоном служит её вывод. Если вам нужен бэктест, числа которого вообще не зависят от машины, такое мы тоже умеем. [Бесплатная проверка скорости](/ru/?src=note-avx512#start). Измерено 9 октября 2026 года на numpy 2.5.3 (Python 3.12 на Zen 3, Python 3.13 на Zen 4). **Исправление, 9 октября 2026.** В первой версии заметки говорилось, что AVX-512-версия `np.log` в numpy теряет до 29 бит возле `x = 1`. Это была наша ошибка измерения: вход строился через `np.exp`, который тоже различается между машинами, поэтому мы сравнивали логарифмы немного разных чисел. На одинаковом входе разница не больше одного ulp. Теперь скрипт строит вход без `np.exp`.