Fix Parquet statistics pruning for predicates satisfied by NaN - #23735
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
The negation pushdown deliberately refuses to complement ordering
comparisons, because IEEE-754 makes every ordered comparison against a NaN
false, so NOT(a < b) is true exactly where a >= b is false. The stats
converter still complemented them one layer down: NOT(col < lit) became
col >= lit and then vmax >= lit.
For a row group holding {NaN, 1.0, 2.0} and lit = 50, the NaN row satisfies
NOT(col < 50), yet vmax >= 50 is false and the row group is pruned. cudf
files are immune because the writer drops min/max entirely when a NaN is
seen (PARQUET-1246), but Arrow writes min/max that merely exclude NaN -
pyarrow 23 yields min=1.0, max=2.0 for that chunk - so the hole is live for
Arrow-written files.
Give the converter the column data types and skip the rewrite for floating
point columns, relaxing instead. Equality is unaffected: NaN == x is false
and NaN != x is true, so those stay exact complements.
Costs pruning only for negated ordering comparisons on float columns, which
is what the existing ParquetPredicatePushdownTestAST expectation for
NOT(col0 < 100) OR IS_NULL(col0) now records.
Reported by @vuule.
The guard added for negated ordering comparisons was not sufficient. The
NOT_EQUAL leaf is unsound for the same reason and involves no negation at
all:
col != val --> vmin != vmax OR vmax != val
A chunk of {NaN, val} reports min == max == val, because Arrow and
parquet-mr both skip NaN when updating min/max, and is therefore
indistinguishable from a constant-val chunk. The transform prunes exactly
that shape - but NaN != val is true, so its NaN rows do satisfy the filter
and are dropped. This also reaches NOT(col == val), which the normalizer
complements into col != val.
Reproduced on a two row group pyarrow file [NaN, 5.0 | 7.0, 8.0] filtered
by x != 5.0: the first row group is pruned and only [7.0, 8.0] comes back.
Relax the leaf for floating point columns. The reader cannot be more
precise: the Parquet Statistics struct carries null_count, distinct_count
and the min/max exactness flags, but nothing about NaN, so a NaN-free chunk
is indistinguishable from one whose NaN was skipped. Costs pruning for
col != val on float columns only.
The other leaves stay sound because NaN never satisfies them: col < v,
col > v and col == v are all false for NaN, so excluding it from min/max
cannot make them prune a matching row. The unsound cases are exactly the
predicates NaN satisfies.
parquet-mr's DoubleStatistics.updateStats behaving like Arrow here was
confirmed by Paul Mattione, widening this from Arrow-written files to
Spark-written ones as well.
Inlining can_negate_ordering() hoisted the column lookup out of the short-circuit that protected it. extract_binary_operands() only reports a column reference for the `col op lit` and `lit op col` forms; for anything else it returns nullptr, so the unconditional _output_dtypes[binary_operands.col_ref->get_column_index()] dereferences null for any NOT wrapping a comparison neither of whose operands is a bare column, such as NOT((col + 1) > 5). The read is now nested inside the `col op lit` check rather than sitting in the condition alongside it, which is what the short-circuit was doing before. Behaviour is otherwise unchanged. Reproduced with the filter (col_a < 150) AND NOT((col_a + 10) > 50); the first conjunct is what makes a column stats-usable, so the converter is built at all. Covered by ParquetReaderTest.FilterNegationPushdown, which segfaults without this.
4f3162e to
8d83b2f
Compare
Two follow-ups from Lawrence's review of NVIDIA#23580. Call the rewrite by its name. "Negation normal form" is the standard term in mathematical logic for an expression whose negations appear only on atoms, reached by eliminating double negations and applying De Morgan's laws, which is exactly what the normalizer produces. Both class docs now say so. Make transform_operator's mode dispatch exhaustive, so that adding a slot to operator_transform and calling with it fails to compile rather than silently taking the NEGATE branch. The static_assert condition mentions `mode` deliberately: cudf builds as C++20, where a bare static_assert(false) in a discarded if-constexpr branch is ill-formed and fires unconditionally. Keeping the condition value-dependent defers it to instantiation, which is what makes the check fire only for an unhandled mode.
|
@pmattione-nvidia: #23709 also updated |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
Included review availability: Your plan provides up to 12 included reviews per hour; 2 remain after this review. 📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR passes output data types to Parquet statistics conversion, restricts unsafe floating-point predicate negation, and adds regression tests for NaN handling and negated expressions. ChangesParquet predicate filtering
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🟠 High · up to The PR changes Parquet predicate pruning, but the current implementation still contains undefined behavior in the statistics conversion path, which can make filtering unpredictable or cause runtime failures. This issue should be fixed before merging. Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
Thanks @igorpeshansky for such a detailed and thorough review. I have addressed your concerns in aa395d7 :) |
…/cudf into nan-ordering-negation
igorpeshansky
left a comment
There was a problem hiding this comment.
One test needs fixing.
| // Signed numeric types pass RGs 1,2,3, others pass RGs 2,3. Floats keep all 4: NaN makes | ||
| // every ordered comparison false, so `NOT(col0 < 100)` is not `col0 >= 100` and gets relaxed. | ||
| // Signed integral types pass RGs 1,2,3, others pass RGs 2,3. Floats keep all 4 as they may | ||
| // hold NaNs making every ordered comparison false and get relaxed instead. |
There was a problem hiding this comment.
Nit: I think this sentence needs at least a comma before the "and", and possibly another one before "making"…
| // hold NaNs making every ordered comparison false and get relaxed instead. | |
| // hold NaNs, making every ordered comparison false, and get relaxed instead. |
igorpeshansky
left a comment
There was a problem hiding this comment.
LGTM
modulo a couple of proposed readability and test tweaks.
|
/merge |
Description
This PR relaxes Parquet statistics based pruning for floating-point types to compensate for
NaNs. This is necessary because Arrow and parquet-mr writers omit NaNs from min/max statistics, which could cause stats transforms forNOT(col < lit)andcol != litto incorrectly prune row groups.Checklist