Skip to content

fix: report a canonical NaN for a float sum, not the platform's default - #10122

Merged
robert3005 merged 3 commits into
vortex-data:developfrom
gerchowl:upstream/472-canonical-nan-float-sum
Oct 9, 2026
Merged

robert3005 merged 3 commits into
vortex-data:developfrom
gerchowl:upstream/472-canonical-nan-float-sum

Conversation

@gerchowl

Copy link
Copy Markdown
Contributor

Fixes #10120.

Problem

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, whose result bit pattern IEEE leaves unspecified — and the targets disagree:

x86_64   inf + -inf  ->  0xfff8_0000_0000_0000   (sign bit set)
aarch64  inf + -inf  ->  0x7ff8_0000_0000_0000   (sign bit clear)

Stat::Sum is in PRUNING_STATS and StatsSet::write_flatbuffer serialises it, so that
platform-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 finalisation
sites — sum and sum_v2 each 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::NAN that sum_v2 already writes explicitly on its non-skip_nans poisoning path, so the two
paths 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 - inf differing per arch, as it should. Only
the persisted statistic is normalised.

Why canonicalise rather than record Sum as absent

Both work. Canonicalisation is the smaller change, and is_saturated already treats a NaN float sum as
terminal — 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 ±inf counts separately.

Test

sum::tests::test_sum_float_invalid_operation_is_canonical_nan asserts the exact bits for sum and
sum_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 (finite
sum untouched, overflow-to-infinity preserved).

Before the fix it fails on x86_64 (left: 0xfff8…, right: 0x7ff8…) and passes on aarch64 — asserting
the 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.

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 robert3005 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread vortex-array/src/aggregate_fn/fns/sum_v2/mod.rs Outdated
robert3005 and others added 2 commits October 9, 2026 14:50
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 robert3005 added the changelog/fix A bug fix label Oct 9, 2026

@robert3005 robert3005 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have adjusted the solution a little bit to localise it. Thank you for the contribution!

@codspeed

codspeed Bot commented Oct 9, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 58.57%

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 2176 untouched benchmarks
⏩ 409 skipped benchmarks1

Performance Changes

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)

Open in CodSpeed

Footnotes

  1. 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. ↩

@robert3005
robert3005 merged commit d451683 into vortex-data:develop Oct 9, 2026
115 of 116 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/fix A bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Persisted Stat::Sum for a float column with both +inf and -inf is architecture-dependent

2 participants