# Официальный пакет QuantLib печатает разные последние знаки на разных Linux Мы поставили один и тот же официальный пакет 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](quantlib-glibc/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](quantlib-glibc/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) мы не проверяли, поэтому не знаем, где именно произошло изменение. ## Почему это важно Для торгового решения ни одна из этих разниц не важна: статистическая ошибка цены Монте-Карло гораздо больше. Они важны везде, где результаты сравнивают точно: - **UAT против продакшена.** Оценка на тестовом сервере с RHEL 8 и на боевом хосте с Ubuntu 24.04 может разойтись в последних знаках, и сверка с нулевым допуском покажет расхождение, которого не вызывало ни одно изменение кода. - **Регрессионные тесты с эталонными файлами.** Тест, который хранит цены с полной точностью, проходит на машине разработчика и падает в CI или начинает падать после обновления операционной системы. - **Аудит и валидация моделей.** Валидатор перезапускает модель на другой системе, чтобы воспроизвести опубликованную цифру, получает другой седьмой знак, и это приходится объяснять. - **Кэши и хеши результатов.** Кэш с ключом по хешу результатов промахивается без видимой причины. ## Что с этим делать 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. **Если результаты должны совпадать бит в бит,** решите, какая система эталонная, и сравнивайте на ней, а не на похожей. ## Проверьте свои машины ```sh python -m venv ql && ql/bin/pip install QuantLib==1.43 ql/bin/python ql_last_digits.py --dump this.bin > this.txt ``` Запустите те же две строки на другой машине или в контейнере с другой glibc, например: ```sh 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' ``` Потом сравните: ```sh 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](quantlib-glibc/glibc-2.28.txt) и [glibc-2.39.txt](quantlib-glibc/glibc-2.39.txt), а все сравнения — в [comparisons.txt](quantlib-glibc/comparisons.txt). На измерительной машине не было движка контейнеров, поэтому тот же образ мы запускали как chroot в непривилегированном user namespace; Docker или Podman делают то же самое. Измерено 12 октября 2026 года, QuantLib 1.43 с PyPI. Раскрытие: TAKT продаёт ускоренные сборки численных библиотек, вывод которых на той же системе совпадает с оригиналом бит в бит; см. наш [пакет QuantLib](/showcase/quantlib-mc/README.html) и [quantlib-takt на PyPI](https://pypi.org/project/quantlib-takt/). Заметка подготовлена с AI-ассистентом по нашим измерениям; скрипты запускались на Linux, как описано выше. Исправления присылайте на hello@taktcycles.com.