ggjh-159 opened a new issue, #12805:
URL: https://github.com/apache/gluten/issues/12805
### Backend
VL (Velox)
### Bug description
## Bug description
The Velox stateful streaming path uses `HashPartitionFunction` to compute a
hash of the key columns, then uses this **hash value as the key identity** for
state storage — discarding the original key values entirely. When two different
keys produce the same hash (hash collision), their data is merged into the same
state, causing silent data corruption in aggregation, join, and other keyed
operations.
Additionally, the current pre-grouping approach (`KeySelector::partition` +
per-key state get/put) prevents the use of Velox's native batch vectorized
processing (`Aggregate::addRawInput`), negating the performance advantage that
Velox is supposed to provide.
## Root Cause
### How keys are currently processed
1. `KeySelector::partition()` calls `HashPartitionFunction::partition()`
which computes a 64-bit hash of the key columns via `VectorHasher`, then takes
`hash % numPartitions` (where `numPartitions = INT_MAX`) to produce a
`uint32_t` partition number.
2. This partition number is used as the **map key** in `std::map<int64_t,
RowVectorPtr>` to group rows — different keys with the same hash are merged
into the same group.
3. The partition number is passed to `setCurrentKey(uint32_t)` as the state
key identity. All subsequent state operations (`state_->value(key, window)`,
`state_->update(key, window, acc)`) use this hash value, not the original key.
**Source code references:**
```
KeySelector.cpp:44-52 — partition value used as map key
KeySelector.cpp:85-91 — map<int64_t, RowVectorPtr> grouped by partition
number
KeyedStateBackend.h:60 — setCurrentKey(uint32_t key)
GroupWindowAggregator.cpp:87-90 — partition result used as key for
setCurrentKey
GroupWindowAggregator.cpp:99 — windowState_->value(key, window) using
hash as key
HashPartitionFunction.cpp:115-117 — partitions[i] = hashes_[i] %
numPartitions_
```
### Why this causes data corruption
`HashPartitionFunction` is designed for **data partitioning** (deciding
which subtask receives data), not for **key identity**. In Velox's native batch
`HashAggregation`, the hash is used only for bucket lookup, and
`HashTable::compareKeys` performs a precise comparison of the original key
values to distinguish keys that hash to the same bucket. The stateful path
**bypasses this safeguard** by using the hash value directly as the key — there
is no original-key comparison anywhere in the state access path.
### Contrast with Flink
Flink never uses hash values as key identity:
- **Heap StateBackend**: keys are compared using `equals()` on the original
Java objects (`CopyOnWriteStateMap.java:278`)
- **RocksDB StateBackend**: keys are serialized to byte arrays and compared
using lexicographic byte comparison (`BytewiseComparator`)
- Hash functions (`murmurHash(key.hashCode())`) are used **only** for key
group assignment (deciding which parallel subtask owns a key), never for key
equality
## Collision Can Definitely Occur
The hash value is `uint64_t % INT_MAX`, stored in `uint32_t`. Since the
range is finite (~2.1 billion) while the number of possible distinct keys is
unbounded, collisions are mathematically guaranteed to occur given enough keys.
The concrete example above (key=391 and key=32728) demonstrates that collisions
exist even among small integer keys — no adversarial input is needed.
## Reproduction Scenario
### Minimal example
```sql
-- A simple GROUP BY aggregation with SUM
CREATE TABLE source (
key BIGINT,
value BIGINT
) WITH ('connector' = 'from-elements');
INSERT INTO sink
SELECT key, SUM(value) AS total
FROM source
GROUP BY key;
```
### Concrete collision example
Velox's `VectorHasher` uses `folly::hasher<int64_t>` for BIGINT keys, which
applies `fmix64` (MurmurHash3 finalizer). Two different BIGINT keys `391` and
`32728` produce different 64-bit hashes but collide after `% INT_MAX`:
```
key=391: fmix64(391) = 8035620079152987179, % INT_MAX = 250707955
key=32728: fmix64(32728) = 11952965464888349291, % INT_MAX = 250707955
```
Both keys hash to partition number `250707955`. Since this partition number
is used as the key identity for state operations, the state for `key=391` and
`key=32728` is shared — their data is silently merged.
### Reproduction SQL
```sql
-- Source with two keys that collide
CREATE TABLE source (
key BIGINT,
value BIGINT
) WITH ('connector' = 'from-elements', 'data' = '[
{"key": 391, "value": 100},
{"key": 32728, "value": 200}
]');
-- GROUP BY SUM should produce separate results per key
SELECT key, SUM(value) AS total
FROM source
GROUP BY key;
```
### Expected vs actual behavior
**Expected output**:
```
key=391, total=100
key=32728, total=200
```
**Actual output** (one of the following, depending on which key is processed
first):
```
key=391, total=300 ← includes value from key=32728
key=32728, total=300 ← or both show the merged sum
```
Or one key's result is missing entirely. **No error is raised** — the
corruption is silent.
In practice, with 65,000+ distinct keys in a streaming aggregation,
collisions are statistically likely to occur naturally without any crafted
input.
## Impact
### Affected operators
All operators that use `KeySelector` and `setCurrentKey(uint32_t)`:
| Operator | File | Impact |
|---|---|---|
| GroupAggregate | `StreamKeyedOperator.cpp:53` | Aggregation results
include wrong keys' data |
| GroupWindowAggregate | `GroupWindowAggregator.cpp:87` | Window aggregation
merges different keys' windows |
| WindowJoin | `WindowJoin.cpp:94` | Join matches incorrect keys |
| LocalWindowAggregate | `LocalWindowAggregator.cpp:52` | Same as
GroupWindowAggregate |
| WindowAggregate | `WindowAggregator.cpp:76` | Same as GroupWindowAggregate
|
| StreamRank (TopN) | `AppendOnlyTopNRanker.cpp:83` | Rank includes wrong
keys |
| StreamJoin | `StreamJoin.cpp:69` | Join on wrong keys |
### Checkpoint corruption
State is checkpointed with the hash value as the key. After recovery, the
corrupted state persists — there is no way to distinguish which original keys
were merged.
### Cannot leverage Velox batch processing
The current design pre-groups data by partition number via
`KeySelector::partition()`, then processes each group sequentially with per-key
`state.get() → accumulate → state.put()`. This two-pass approach (group, then
process) prevents the use of Velox's native `Aggregate::addRawInput`, which can
process an entire `RowVector` in a single vectorized batch via
`HashTable::groupProbe` + `compareKeys`.
Velox's batch `HashAggregation` achieves high throughput by:
1. Single-pass `groupProbe`: hash lookup + `compareKeys` (original key
comparison) in one traversal
2. `Aggregate::addRawInput`: vectorized SIMD accumulation across all rows
simultaneously
3. No per-key state get/put in the hot path
The current stateful path gains none of these benefits because:
- The pre-grouping step (`KeySelector::partition`) is an extra O(n) traversal
- Per-key `accumulate()` (currently an empty implementation in
`GroupWindowAggsHandler.cpp`) does not use vectorized accumulation
- Per-key `state.get()/state.put()` adds O(distinct_keys) state backend
operations per batch
## Key Source Files(bigo-sg/velox)
| File | Path (relative to velox root) | Role |
|---|---|---|
| `KeySelector.h` | `velox/experimental/stateful/KeySelector.h` | Key
grouping (problem origin) |
| `KeySelector.cpp` | `velox/experimental/stateful/KeySelector.cpp:29-92` |
`partition()` uses hash as map key |
| `HashPartitionFunction.cpp` |
`velox/exec/HashPartitionFunction.cpp:76-122` | Hash computation + modulo |
| `KeyedStateBackend.h` |
`velox/experimental/stateful/state/KeyedStateBackend.h:60-69` |
`setCurrentKey(uint32_t)` + `currentKey_` type |
| `GroupWindowAggregator.cpp` |
`velox/experimental/stateful/GroupWindowAggregator.cpp:87-90` | Partition →
setCurrentKey chain |
| `StateMap.h` | `velox/experimental/stateful/state/StateMap.h:132-152` |
`e->key_ == key` compares hash values, not original keys |
| `StateTable.h` | `velox/experimental/stateful/state/StateTable.h:37-50` |
Key group assignment by hash |
| `GroupWindowAggsHandler.cpp` |
`velox/experimental/stateful/window/GroupWindowAggsHandler.cpp` | All methods
empty (no vectorized accumulation) |
### Gluten version
main branch
### Spark version
None
### Spark configurations
_No response_
### System information
_No response_
### Relevant logs
```bash
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]