Repository navigation
fix: report a canonical NaN for a float sum, not the platform's default - #10122
robert3005 merged 3 commits into
Conversation
2aa35b5 to
66f8d28
Compare
A float sum with `skip_nans` steps over NaN inputs, so a column holding both
`+inf` and `-inf` lets the two infinities meet in the accumulator and evaluates
`inf + -inf`. That is an IEEE 754 *invalid* operation, and IEEE does not specify
the resulting bit pattern — targets disagree:
x86_64 inf + -inf -> 0xfff8_0000_0000_0000 (sign bit set)
aarch64 inf + -inf -> 0x7ff8_0000_0000_0000 (sign bit clear)
Sums are persisted as `Stat::Sum` (`PRUNING_STATS`, written via
`StatsSet::write_flatbuffer`), so the platform's choice reaches the file: the
same logical table written on x86_64 and on aarch64 produced files differing in
exactly that one bit. For a content-addressed format, where the hash of the bytes
is the artifact's identity, that makes identity host-dependent.
Report any NaN sum as the canonical quiet NaN, in both `sum` and `sum_v2` (each
finalises floats through its own path). A NaN sum carries no payload information
— the value is "not a number" — so nothing is lost, and the statistic becomes a
function of the data alone. This also makes the *computed* NaN agree with the
`f64::NAN` that `sum_v2` already writes explicitly on its non-`skip_nans`
poisoning path.
Only NaN is affected. A finite sum is untouched, and a sum that legitimately
overflows to an infinity keeps that infinity, whose bits IEEE does specify.
Signed-off-by: Lars Gerchow <lars.gerchow@gmail.com>
robert3005
left a comment
There was a problem hiding this comment.
I think we can tone down the level of comments here. I think it's enough to mention these are different across architectures and we normalise to one value
Move the NaN canonicalisation out of the sum kernels and into StatsSet::write_flatbuffer. Every persisted Stat::Sum goes through this path, so merged stats are covered too. Stats that are already written are read back unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Robert Kruszewski <github@robertk.io>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Robert Kruszewski <github@robertk.io>
robert3005
left a comment
There was a problem hiding this comment.
I have adjusted the solution a little bit to localise it. Thank you for the contribution!
Merging this PR will improve performance by 58.57%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ⚡ | Simulation | decode_primitives[i64, (1000, 32)] |
38.5 µs | 24.3 µs | +58.57% |
Tip
Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.
Comparing gerchowl:upstream/472-canonical-nan-float-sum (6cc69a0) with develop (2b4c682)
Footnotes
-
409 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
Fixes #10120.
Problem
A float sum with
skip_nanssteps over NaN inputs, so a column holding both+infand-inflets thetwo infinities meet in the accumulator and evaluates
inf + -inf. That is an IEEE 754 invalidoperation, whose result bit pattern IEEE leaves unspecified — and the targets disagree:
Stat::Sumis inPRUNING_STATSandStatsSet::write_flatbufferserialises it, so thatplatform-chosen value reaches the file: the same logical table written on x86_64 and on aarch64
produced files differing in exactly one bit. Bisection and the dual-arch measurements are in
#10120.
Change
Reports any NaN sum as the canonical quiet NaN (
0x7ff8_0000_0000_0000), at both float finalisationsites —
sumandsum_v2each finalise floats through their own path.A NaN sum carries no payload information (the value is "not a number"), so nothing is lost and the
statistic becomes a function of the data alone. It also makes the computed NaN agree with the
f64::NANthatsum_v2already writes explicitly on its non-skip_nanspoisoning path, so the twopaths stop disagreeing about which NaN they mean.
Deliberately narrow: only NaN is touched. A finite sum is unchanged, and a sum that legitimately
overflows to an infinity keeps that infinity — those bits are specified. The fix also does not touch
hardware NaN semantics; a direct probe still shows
inf - infdiffering per arch, as it should. Onlythe persisted statistic is normalised.
Why canonicalise rather than record
Sumas absentBoth work. Canonicalisation is the smaller change, and
is_saturatedalready treats a NaN float sum asterminal — so the codebase currently treats it as a real value, and making it absent would be the
larger semantic change. Happy to switch if you would rather the stat not exist in that case, or to
track
±infcounts separately.Test
sum::tests::test_sum_float_invalid_operation_is_canonical_nanasserts the exact bits forsumandsum_v2, on both[+inf, -inf]and the 7-value pathological column that surfaced this in the wild(NaN payload, ±0.0, ±inf,
MIN_POSITIVE, largest subnormal), plus the two negative controls (finitesum untouched, overflow-to-infinity preserved).
Before the fix it fails on x86_64 (
left: 0xfff8…,right: 0x7ff8…) and passes on aarch64 — assertingthe bits is the point.
cargo test -p vortex-array --lib: 3877 passed, 0 failed.Relationship to #10121
Same defect class — a write-time statistic leaking into the serialised bytes — in a different place.
That one is build-configuration dependent, this one is architecture dependent. They are independent
changes and touch disjoint files.
Developed with an agentic AI coding tool (Claude Code) under human direction, as described in
CONTRIBUTING's AI Assistance section. Lars Gerchow is the responsible party and has reviewed and
validated the change, including the cross-architecture measurements above and the A/B test with
canonicalisation disabled. Please classify per your Human vs. Agent distinction as you see fit.