Rust std и musl неверно округляют fmaf на редких числах
Проверка: fmaf(0x97000800, 0x1cfff001, 0x00010002) должен дать 0x00010001; ошибочная реализация возвращает 0x00010002.

Разработчик под ником Shnatsel 2 сентября описал, как, реализуя fused multiply-add для библиотеки fearless_simd, нашёл ошибку округления на субнормальных числах, а затем ту же ошибку обнаружил в f32::mul_add стандартной библиотеки Rust, в std::simd и в функции fmaf() из libc musl. Результат отличается от правильного на один младший бит; в исходном контрпримере это около 15 частей на миллион.
Ошибка проявляется только там, где FMA эмулируется программно: на x86 это дешёвые или старые Intel без AVX2 и процессоры Hygon без аппаратного FMA, а также 32-битный ARM и старые встраиваемые тулчейны. 64-битный ARM, по словам автора, не затронут. На практике это касается детерминированных симуляций и численных библиотек, где одинаковый код обязан давать одинаковые биты на разных машинах: сетевые игры с lockstep, физические движки, воспроизводимые вычисления. Автор честно пишет, что не знает, насколько ошибка важна в реальных программах.
Ключевые выводы
- Ошибка: неправильное округление
fmafна субнормальных входах, отличие на один младший бит. - Затронуты
f32::mul_addиstd::simdв Rust,fmaf()в musl; проверка одна и та же:a = 0x97000800,b = 0x1cfff001,c = 0x00010002, верный ответ0x00010001, ошибочный0x00010002. - Исправлены
fearless_simdи ABIf128в компиляторе Rust на 32-битном ARM; патч в стандартную библиотеку Rust на момент публикации ждал ревью, исправления musl не влиты. - Аппаратный FMA (x86 с AVX2/FMA3, 64-битный ARM) не затронут: ошибка живёт в программной эмуляции.
- Контрпример к первой версии исправления в Rust нашла языковая модель, которую автор попросил искать ошибки.
Что такое FMA и откуда берётся лишний бит
Fused multiply-add вычисляет a*b+c с одним округлением в конце вместо двух: после умножения и после сложения. Так требует IEEE 754, и именно на это рассчитывают численные алгоритмы: одно округление даёт предсказуемую ошибку. Когда у процессора нет аппаратной инструкции, библиотека эмулирует FMA через операции с расширенной точностью и ручную коррекцию округления. Ошибка, которую нашёл автор, живёт в этой коррекции на субнормальных числах, то есть очень маленьких значениях, у которых часть мантиссы уже ушла в ноль. В таких случаях программная реализация округляла не в ту сторону.
Автор переносил формально верифицированный алгоритм и гонял его в CI на эмуляторах SIMD, добавив тесты на миллион субнормальных значений и ещё миллион случаев, требующих коррекции округления. Отдельно он попросил языковую модель искать контрпримеры, и она нашла случай, ломающий первую версию исправления для стандартной библиотеки Rust: там ошибка приводила ещё и к неправильной установке флагов исключений плавающей точки.
Как проверить свою платформу
Достаточно трёх чисел. Ниже проверка на C для fmaf из системной libc; в Rust то же самое делается через f32::from_bits и mul_add:
Если вывод 00010002, ваша libc или тулчейн эмулируют FMA с ошибкой. На машине с аппаратным FMA (любой современный x86 с AVX2, 64-битный ARM) результат будет верным независимо от библиотеки, поэтому проверять имеет смысл именно сборки под старое или встраиваемое железо и контейнеры на musl (Alpine) на таких хостах.
Состояние исправлений
fearless_simd: исправлено, можно использовать как реализацию FMA без этой ошибки.- Rust, тип
f128на 32-битном ARM: ошибка ABI исправлена в компиляторе (тип доступен только в nightly). - Стандартная библиотека Rust (
f32::mul_add,std::simd): патч на момент публикации ожидал ревью; номер версии с исправлением не назван. - musl: предложены минимальное исправление и полная переработка
fmaf(), ни то ни другое не влито. Автор отмечает, что код FreeBSD, откуда растёт реализация, мог копироваться и в другие тулчейны.
Кому это ничего не меняет: сервисам на современных x86 и ARM64, где FMA аппаратный, и коду, не зависящему от битовой точности. Кому стоит проверить: авторам физических движков и симуляций с детерминизмом между машинами, разработчикам под 32-битный ARM и тем, кто собирает статические бинарники под musl для старых серверов. Про то, как выбор libc влияет на скорость тех же бинарников, мы писали на примере Rust Coreutils; смежная тема из этой же недели: переписывание линкера mold на Rust. Что мы проверим дальше: когда патч попадёт в стабильный Rust и появится ли исправление в релизе musl.
Источники: Shnatsel: Implementing FMA and finding bugs in C and Rust standard libraries, Обсуждение на Lobsters












