← Все заметки

Официальный пакет QuantLib печатает разные последние знаки на разных Linux

· English version

Мы поставили один и тот же официальный пакет QuantLib 1.43 с PyPI (wheel) на три Linux-системы и посчитали одни и те же двенадцать величин. На Ubuntu 24.04 (glibc 2.39) и Debian 13 (glibc 2.41) все результаты совпали до последнего бита. В образе manylinux_2_28 (AlmaLinux 8.10, glibc 2.28) пять из двенадцати не совпали, а цена с локальной волатильностью изменилась в седьмом значащем знаке. Код, входные данные и бинарник QuantLib были одни и те же. Различается системная математическая библиотека: пакет вызывает exp, log и pow из той glibc, на которой запущено, а glibc 2.28 округляет примерно один результат из тысячи иначе, чем glibc 2.39. Ниже — замеры, скрипт для проверки своих машин и что с этим делать.

Что запускали

ql_last_digits.py оценивает несколько опционов на фиксированных рыночных данных и печатает каждый результат с 17 значащими цифрами и в шестнадцатеричном виде, так что сравнение двух выводов — это сравнение битов. Входные данные строятся только через + и *, поэтому на всех машинах они одинаковы до бита. Ещё скрипт хеширует exp, log и pow системной математической библиотеки, по миллиону фиксированных входов на функцию.

Процессор Система glibc Python
A AMD Threadripper PRO 5975WX (Zen 3) AlmaLinux 8.10 (образ quay.io/pypa/manylinux_2_28_x86_64) 2.28 3.12.13
B та же машина Ubuntu 24.04 2.39 3.12.3
C AMD Ryzen 9 7950X3D (Zen 4) Debian 13 2.41 3.13.5

Везде стоял пакет с PyPI quantlib-1.43-cp39-abi3-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl (sha256 35cd2c17…); установленные файлы libQuantLib и _QuantLib на всех трёх системах совпадали побайтно. A и B работали на одном и том же процессоре, так что разницу между ними даёт не процессор.

Что изменилось

B и C напечатали одно и то же. A разошлась в пяти величинах из двенадцати:

Величина glibc 2.28 (A) glibc 2.39 и 2.41 (B, C) Относительная разница
Американский пут, Лонгстафф — Шварц 6.5526994108438767 6.5526994108438759 1,4e-16
Азиатский колл, Монте-Карло 6.5329120644316481 6.532912064431649 1,4e-16
Колл down-and-out, Монте-Карло 9.5748933379022443 9.5748933379022514 7,4e-16
Европейский пут, локальная волатильность, Монте-Карло 8.0916614100380553 8.0916636154283292 2,7e-7
Европейский пут, локальная волатильность, пути Соболя 8.2206887394419095 8.2206876199600138 1,4e-7

Остальные семь (дисконт-фактор, снос процесса, цена и дельта по формуле Блэка — Шоулза, европейская цена Монте-Карло, американская цена на биномиальном дереве и цена по формуле Хестона) совпали на всех трёх системах. Результат меняется, только если хотя бы один из вызовов exp, log или pow за ним попадает на вход, который две библиотеки округляют по-разному. Поэтому одни цены сдвигаются, а другие нет.

Сильнее всего сдвигается локальная волатильность: QuantLib выводит её из поверхности подразумеваемой волатильности конечными разностями, и изменение в один ulp, делённое на маленький шаг, перестаёт быть маленьким. Всё, что дифференцируется численно, например греки сдвигом или якобианы калибровки, может так же усилить разницу.

Наша большая тестовая программа на C++ печатает через ту же официальную библиотеку 867 строк, из них 840 чисел, и показывает ту же картину. Между glibc 2.28 и 2.39 различаются 205 чисел из 840 (24%) и 6 хешей сгенерированных путей, между glibc 2.39 и 2.41 не различается ничего. 201 из 205 чисел отличаются меньше чем на 1e-6 относительно. Самая большая разница, 6e-4, — снова цена с локальной волатильностью на путях Соболя; эта программа сама строит поверхность волатильности через log, так что у неё различаются уже входные данные.

Причина — системная математическая библиотека

Пакеты (wheel) для Linux на PyPI собираются по правилам manylinux: в них можно положить большинство библиотек, но никогда — glibc и её математическую библиотеку libm. Их пакет берёт из системы при запуске. pip install QuantLib==1.43 фиксирует бинарник QuantLib, но не код, который вычисляет exp(x).

Проверка математической библиотеки из того же скрипта, миллион входов на функцию:

Функция Различаются, glibc 2.28 против 2.39 На сколько glibc 2.39 против 2.41
exp 984 (0,098%) 1 ulp 0
log 1 276 (0,128%) 1 ulp 0
pow 761 (0,076%) 1 ulp 0

Ulp — единица последнего разряда 64-битного числа. Обе версии точны: каждый различающийся результат мы сравнили с точным значением, посчитанным до 50 знаков, и все они в пределах 0,53 ulp от него. «Правильной» версии здесь нет. Для log и pow почти во всех расхождениях ближе к точному значению старая glibc 2.28, для exp — в 712 случаях из 984.

Чтобы проверить, что разницу целиком объясняют эти три функции, мы подгрузили маленькую библиотеку shim.c, которая заменяет exp, log и pow одним и тем же кодом на всех системах. С ней все двенадцать величин совпали на A, B и C. Это диагностика, а не исправление: её результаты отличаются от любой штатной glibc.

Процессор тоже может влиять. glibc выбирает код этих функций по возможностям процессора. На машине B с выключенным в glibc кодом для FMA (GLIBC_TUNABLES=glibc.cpu.hwcaps=-FMA — так работал бы процессор без FMA) изменились три величины из двенадцати, а exp дала другой результат на 738 входах из миллиона. Версии glibc между 2.28 и 2.39 (например, 2.34 в RHEL 9 или 2.36 в Debian 12) мы не проверяли, поэтому не знаем, где именно произошло изменение.

Почему это важно

Для торгового решения ни одна из этих разниц не важна: статистическая ошибка цены Монте-Карло гораздо больше. Они важны везде, где результаты сравнивают точно:

Что с этим делать

  1. Зафиксировать математическую библиотеку. Запускать расчёт в контейнерном образе и сохранять дайджест образа вместе с результатами. Образ приносит свою glibc и libm, и версии на хосте перестают влиять. Контейнер фиксирует библиотеку, но не процессор, поэтому записывайте и модель процессора и проверяйте на том семействе процессоров, на которое полагаетесь.
  2. Сравнивать с допуском там, где биты не требуются. Подбирайте допуск под численную задачу, а не под машинный эпсилон: разница в один ulp в exp превратилась в 3e-7 относительно в цене с локальной волатильностью выше. Для цен Монте-Карло естественная граница — доля от оценки ошибки, которую даёт движок.
  3. Записывать окружение вместе с каждым результатом: версию glibc (ldd --version или os.confstr("CS_GNU_LIBC_VERSION") в Python), операционную систему или образ, модель процессора, версии Python и QuantLib. Первая строка вывода скрипта записывает версии glibc, Python и QuantLib.
  4. Если результаты должны совпадать бит в бит, решите, какая система эталонная, и сравнивайте на ней, а не на похожей.

Проверьте свои машины

python -m venv ql && ql/bin/pip install QuantLib==1.43
ql/bin/python ql_last_digits.py --dump this.bin > this.txt

Запустите те же две строки на другой машине или в контейнере с другой glibc, например:

docker run --rm -v "$PWD":/w -w /w quay.io/pypa/manylinux_2_28_x86_64 sh -c \
  '/opt/python/cp312-cp312/bin/python -m venv /tmp/ql && /tmp/ql/bin/pip install -q QuantLib==1.43 &&
   /tmp/ql/bin/python ql_last_digits.py --dump other.bin > other.txt'

Потом сравните:

python ql_last_digits.py --compare this.txt other.txt     # напечатанные величины
python ql_last_digits.py --compare this.bin other.bin     # exp, log и pow, вход за входом

Скрипт работает несколько секунд. Наши выводы со всех систем лежат рядом с ним, например glibc-2.28.txt и glibc-2.39.txt, а все сравнения — в comparisons.txt. На измерительной машине не было движка контейнеров, поэтому тот же образ мы запускали как chroot в непривилегированном user namespace; Docker или Podman делают то же самое.

Измерено 12 октября 2026 года, QuantLib 1.43 с PyPI.

Раскрытие: TAKT продаёт ускоренные сборки численных библиотек, вывод которых на той же системе совпадает с оригиналом бит в бит; см. наш пакет QuantLib и quantlib-takt на PyPI.

Заметка подготовлена с AI-ассистентом по нашим измерениям; скрипты запускались на Linux, как описано выше. Исправления присылайте на [email protected].

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

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

Исходный текст: quantlib-glibc-last-digits.ru.md