← Все заметки

Один и тот же бэктест на Python печатает другой коэффициент Шарпа на новом процессоре

· English version

Мы запустили один и тот же код на Python с одной и той же версией numpy на двух машинах и получили разные числа: у коэффициента Шарпа меняются последние знаки. Причина не в коде и не в данных. На процессорах с AVX-512 numpy считает np.log и np.exp собственными векторными процедурами, и примерно каждый двадцатый результат округляется иначе. Оба результата точны, просто биты разные. Ниже — замеры, скрипт для проверки своих машин и способ получить одинаковые числа везде. Сразу предупреждение: самый распространённый рецепт в numpy 2.4 перестал работать, причём молча.

Что мы увидели

Проверяя кейс оптимизатора vn.py, мы запустили встроенный оптимизатор vn.py на ряде из 100 000 баров на двух машинах. На обеих стоял numpy 2.5.3:

Параметры, их порядок, число сделок, доходности и просадки совпали, а два из десяти напечатанных коэффициентов Шарпа — нет:

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 строит 10 млн отношений, близких к 1, — так выглядит balance[t] / balance[t-1]. Строит только через + и *, поэтому вход на всех машинах один и тот же до бита. Затем применяет np.log и np.exp и считает годовой коэффициент Шарпа по десяти годам дневных логарифмических доходностей:

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).

Для торговых решений один 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:

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.

Цена — скорость: np.log по 10 млн значений занял на этой машине 30–33 мс вместо 11–12 мс. Для большинства бэктестов это незаметно на фоне остального прогона.

Как с этим работает TAKT

Наши сборки печатают один и тот же результат на всех процессорах, где мы их проверяли. «Совпадает с оригиналом» мы определяем для платформы из паспорта замера: если ваша эталонная машина с AVX-512, то эталоном служит её вывод. Если вам нужен бэктест, числа которого вообще не зависят от машины, такое мы тоже умеем. Бесплатная проверка скорости.

Измерено 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.

Все заметки · Новые заметки: Telegram · RSS (на английском)

Обсудить заметку на GitHub. Комментарии — в открытом обсуждении GitHub Discussions. Ссылка ведёт на GitHub, где действуют его условия и политика конфиденциальности; читать можно без аккаунта, писать — с аккаунтом GitHub.

Исходный текст: numpy-avx512-backtest.ru.md