Repository navigation
Flush denormal floats to zero for ONNX inference and compute threads - #1619
Merged
Merged
Conversation
The Chinese and Japanese OCR tests ran 15-25x slower on the Mac Intel CI runner than anywhere else: up to 185 s per test, against about 8 s on the AMD Windows runners. English and Korean OCR was just as fast there as elsewhere. The large PaddleOCR server model that Chinese and Japanese share produces denormal (subnormal) floats, and Intel CPUs (the runner is an i7-8700B) handle many operations on them in microcode. AMD and Apple CPUs pay much less, which is why only Mac Intel was slow. ONNX Runtime leaves denormals on unless asked. Turn on flush-to-zero, so the CPU treats denormal inputs and results as zero: - Common/Cpp/Hardware/FloatingPointMode.h: enable_flush_denormals_to_zero() and FlushDenormalsToZeroScope, for x86-64 (MXCSR FTZ and DAZ) and ARM64 (FPCR.FZ). - Every ONNX Runtime Run() call runs inside FlushDenormalsToZeroScope, because ONNX runs part of each inference on the calling thread. The scope restores the thread's previous mode afterwards. - ONNX Runtime's global thread pool uses SetGlobalDenormalAsZero(). - Every GlobalThreadPools thread turns flush-to-zero on when it starts. This also sped up image matching, e.g. PokemonLA::MMOSpriteMatcher. On the Mac Intel runner, the unit test step went from 463 s to 117 s. All unit tests pass on all six CI platforms. Production builds run OCR on the CPU by default (ONNX USE_GPU is off), so Intel users reading Chinese or Japanese text should get the same speedup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Leave this PR open to check tests. I will still take a look at Claude's vibe code to double check its implementation |
Collaborator
|
The "blast radius" for this is huge. Is it possible you restrict it to only the ONNX threads? |
Mysticial
reviewed
Oct 19, 2026
Review feedback: turning flush-to-zero on for every GlobalThreadPools thread changes floating-point behavior for far more code than needs it. Drop that, so it's only on: - on ONNX Runtime's own thread pool (SetGlobalDenormalAsZero()), and - on the thread calling Run(), only for the length of the call (FlushDenormalsToZeroScope restores the previous mode afterwards). The calling-thread scope is still needed: ONNX Runtime runs part of each inference on the calling thread, which its thread pool option doesn't cover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The Chinese and Japanese OCR tests were 15–25× slower on the Mac Intel CI runner than on any other runner. That made its unit test step take 463 s, against about 100 s elsewhere. English and Korean OCR tests were just as fast there as on other runners.
20260102_02_chiSim_5.jpg20260102_01_chiSim_7.jpgjojoaman_01_chiTra.jpgdeoxys_1_jpn.pngThe cause is denormal (subnormal) floats, values below about 1.2e-38:
Changes
Turn on flush-to-zero, so the CPU treats denormal inputs and results as zero, but only while ONNX Runtime runs a model:
Common/Cpp/Hardware/FloatingPointMode.h(new): the RAII classFlushDenormalsToZeroScope, plus the functions it uses. On x86-64 it sets MXCSR's FTZ and DAZ bits; on ARM64 (GCC and Clang), FPCR's FZ bit. Elsewhere it does nothing.Run()call runs insideFlushDenormalsToZeroScope: PaddleOCR recognition and detection, YOLOv5, and Segment Anything. The scope restores the calling thread's previous mode when the call returns.SetGlobalDenormalAsZero(). ONNX Runtime only supports this on x86.All other code keeps the default floating-point behavior. An earlier version of this PR also turned flush-to-zero on for every
GlobalThreadPoolsthread. Review asked to limit it, and the measurements below show that part gave no measurable gain.Is
SetGlobalDenormalAsZero()alone enough?No. ONNX Runtime runs part of every inference on the thread that calls
Run(), which its thread-pool option doesn't cover. On a 4-core runner the intra-op pool is that caller plus one worker.To check, three variants ran on Mac Intel at the same time, all passing 24,349 tests:
SetGlobalDenormalAsZero())Run()scope)*Sum of
FlavorPowerScreenDetector,DonutBerriesReaderandStatsReadertest times. "Before" is from PR #1555's run. The "before" column comes from an earlier run; the other three ran at the same time.Mac Intel runner speed varies a lot between runs: the every-thread version took 117 s, 141 s and 227 s in three runs. So compare variants from the same time.
Effect on users
Production builds run OCR on the CPU by default:
ONNX_OPTIONS.USE_GPUdefaults to off and is hidden. So Windows and Mac users with Intel CPUs who read Chinese or Japanese text should get a similar speedup. AMD and Apple Silicon users should see little change.The only behavior change: inside an ONNX inference, a float below about 1.2e-38 becomes exactly 0. All unit tests give the same results.
Results
This PR's own CI on the current commit:
*The first attempt segfaulted inside the
Kernels::AudioChannelMixerx64_AVX512 tests. Those only run on runners with AVX-512, and that code isn't touched here. It looks like a separate bug in that kernel and needs its own fix.Per-test times include waiting for a language's PaddleOCR model, which loads on first use, so single test classes can move a lot between runs.
Testing
FloatingPointMode.hwith a small program on arm64 and x86-64 (under Rosetta): a denormal product is 0 inside the scope, and the previous mode is restored afterwards.🤖 Generated with Claude Code