ggjh-159 commented on issue #12805:
URL: https://github.com/apache/gluten/issues/12805#issuecomment-5326337239

   # Velox Stateful Path Key Hash Collision Fix
   
   ## 1. Problem
   
   The Velox `experimental/stateful/` path (streaming aggregation / join / 
rank) stores the hash value as the key identity inside state. Under hash 
collision, distinct keys are silently merged, corrupting data for stateful 
operators.
   
   **Concrete collision**: BIGINT keys `391` and `32728` both produce `fmix64 % 
INT_MAX = 250707955`. The two keys are recognized as the same group inside 
state, and accumulated values pollute each other.
   
   **Collision is inevitable**: callers use numPartitions=INT_MAX, and by the 
birthday paradox only ~2^16 distinct keys are needed for a 50% collision 
probability. In other words, collisions are not "an extremely rare edge case" — 
they are **guaranteed to fire in production**.
   
   ## 2. Root Cause
   
   ```
   KeySelector::partition(input)
     -> HashPartitionFunction::partition(...)
       -> VectorHasher::hash()        // compute 64-bit hash
       -> hash % numPartitions        // yields uint32_t
     <- std::map<int64_t, RowVectorPtr>   // <- this int64_t is the partition 
id (hash-derived)
   
   GroupWindowAggregator::advance():
     for (auto& [key, data] : keyToData) {
       windowState_->value(key, window);   // <- key is a hash-derived value, 
used as primary key for state lookup
       windowState_->update(key, window, acc);
     }
   ```
   
   Along this entire path **there is no original key field comparison 
whatsoever**. `KeySelector` treats hash as identity, diverging from the 
semantics of Velox's native `HashAggregation` — native `HashTable::groupProbe` 
still does precise key comparison via `RowContainer::equals` after a hash hit.
   
   ## 3. Fix Mechanism (Core)
   
   The core is to **replace V1 `KeySelector::partition` with Velox native 
`HashTable<false>::groupProbe`**: hash is used only for bucket positioning, and 
after a hash hit, `RowContainer::equals` does precise key comparison. This path 
is identical to the batch HashAggregation path, which is industry-proven.
   
   ### 3.1 KV separation
   
   The key side (used by HashTable probe) and the value side (each State's 
storage) are physically separated — this holds uniformly for all state types:
   
   - **key side**: key RowContainer, `[user_key columns...]`, pure user key, 
zero metadata fields (Velox native RowContainer, no modification)
   - **value side**: each State subclass decides its own data form — AccState's 
value is a RowContainer row (layout `[acc columns...]`); ValueState / ListState 
/ MapState hold V / list / map respectively; none contain ns
   
   **Why KV separation is mandatory**: in the window scenario, namespace 
(window ID) is dynamically generated — all ns cannot be known before access, so 
HashTable cannot do a joint (key, ns) probe. The namespace dimension must be 
stripped from the HashTable and maintained by the backend — the Heap backend 
carries it in StateTable's `(K, N) → State` mapping (StateTable is 
Heap-specific); the RocksDB backend has no StateTable and encodes ns directly 
into its composite key `[key-group][key][ns]`.
   
   ### 3.2 Three-layer architecture
   
   | Component | Responsibility | Implementation |
   |---|---|---|
   | **RowContainer** (key side only; the value side is not necessarily a 
RowContainer — its form is decided by each State type, e.g. AccState uses a 
RowContainer, separate from the key side) | memory management | Velox native, 
zero modification |
   | **HashTable** (inside KeySelector utility) | grouping: find key row by 
user key | Velox native, zero modification |
   | **KeyedStateBackendV2\<K\>** | persistence: (K, N) → State mapping, 
snapshot serialization | V2 new |
   
   ### 3.3 StateKey / Namespace abstraction
   
   Abstract base classes (virtual + template coexist; in template context the 
compiler devirtualizes to static calls):
   
   ```cpp
   class StateKey {
    public:
     virtual bool equals(const StateKey& other) const = 0;
     virtual uint64_t hash() const = 0;
     virtual uint32_t keyGroup() const = 0;  // derived: hash % maxParallelism
   };
   
   class Namespace {
    public:
     virtual bool equals(const Namespace& other) const = 0;
     virtual uint64_t hash() const = 0;   // participates in StateMap's 
composite (K, N) hash
   };
   ```
   
   Subclasses (instances are immutable; hash / keyGroup cached at construction):
   
   - `RowContainerStateKey : public StateKey` — holds `const char* row_` (view 
semantics, pointing into key RowContainer)
   - `VoidNamespace : public Namespace` — for non-window operators 
(GroupAggregation, etc.)
   - `RowContainerNamespace : public Namespace` — for window operators 
(GroupWindowAggregator)
   
   ### 3.4 State subclasses (templated on K/N)
   
   ```cpp
   class State { /* abstract */ };
   
   template <typename K, typename N> class AccState : public State {
    public:
     // batch: resolve N keys' value row pointers in one call; keys may contain
     // duplicates (per-row, feeds addRawInput directly) or be deduplicated 
(distinct iteration)
     void rows(folly::Range<const K*> keys, const N& ns, char** outRows);
     char* row(const K& key, const N& ns);
     RowContainer* valueRows();
     const std::vector<std::unique_ptr<Aggregate>>& aggregates() const;
   };
   
   template <typename K, typename N, typename V> class ValueState : public 
State;
   template <typename K, typename N, typename T> class ListState : public State;
   template <typename K, typename N, typename UK, typename UV> class MapState : 
public State;
   ```
   
   **Key design points**:
   
   - **API uses explicit `(K, N, x)` parameters**, not Flink's implicit 
`currentKey + currentNamespace`: Flink is single-row streaming where 
`setCurrentKey` is cheap; Velox is batch processing where one advance() 
processes N input rows with potentially different keys, so calling 
`setCurrentKey` per access is expensive and unnatural
   - **State handle is per-descriptor** (one instance per aggregate), not 
per-(K,N): one ValueState / AccState instance serves all keys, same granularity 
as Flink
   - **K/N are both compile-time fixed on the operator side**: operator classes 
hardcode K/N; on the hot path the compiler devirtualizes to static calls
   
   ### 3.5 StateDescriptor subclassing
   
   Mirrors the Flink StateDescriptor pattern: abstract base holds name, 
subclasses hold the value description fields:
   
   ```cpp
   class StateDescriptor { name_ };
   
   // velox-specific acc value description: row layout + accumulation semantics
   class AccStateDescriptor : public StateDescriptor {
     RowContainer* valueRows_;                              // describes how 
acc value is stored
     std::vector<std::unique_ptr<Aggregate>> aggregates_;   // describes how 
acc value is accumulated (native exec::Aggregate)
   };
   
   // Flink-style generic state descriptors
   template <typename V> class ValueStateDescriptor : public StateDescriptor;
   template <typename T> class ListStateDescriptor : public StateDescriptor;
   template <typename UK, typename UV> class MapStateDescriptor : public 
StateDescriptor;
   ```
   
   ### 3.6 KeyedStateBackendV2
   
   K is the key type and N is the namespace type. The backend class is 
templated on K only; N is a template parameter of the getOrCreate methods 
(mirroring Flink `AbstractKeyedStateBackend<K>`):
   
   ```cpp
   template <typename K>
   class KeyedStateBackendV2 {
    public:
     template <typename N>
     AccState<K, N>& getOrCreateAccState(const N& ns, const AccStateDescriptor& 
desc);
   
     template <typename N, typename V>
     ValueState<K, N, V>& getOrCreateValueState(const N& ns, const 
ValueStateDescriptor<V>& desc);
   
     // ... ListState / MapState likewise
   
     void snapshot(int64_t checkpointId);
     void restore(...);
   
    private:
     StateKeySerializer<K>* keySerializer_;
     std::unordered_map<std::string, std::unique_ptr<StateTableBase>> 
stateTables_;
   };
   ```
   
   ### 3.7 StateTable (bucketed by key-group)
   
   ```cpp
   template <typename K, typename N, typename S>
   class StateTable : public StateTableBase {
     // Layer 1: vector, bucketed by key-group (size = maxParallelism)
     // Layer 2: StateMap, a single-level map keyed by composite (K, N) — one 
hash lookup
     std::vector<StateMap<K, N, S>> buckets_;
   };
   ```
   
   Snapshot streams out bucket by bucket — no need to recompute key-group 
(bucket index IS the key-group). Aligns with Flink `StateTable<K,N,S>` 
structure (`StateMap[]` + StateMap internal `(K,N)→S` composite-key 
single-level map).
   
   ### 3.8 KeySelector utility
   
   ```cpp
   class KeySelector {
    public:
     void probe(const RowVectorPtr& input);                            // 
internally calls HashTable::groupProbe
   
     // Usage 1: per-row (main aggregation-accumulate path)
     folly::Range<const RowContainerStateKey*> keys() const;           // N 
entries, 1:1 with input rows
     // Usage 2: distinct (join build / rank / sorted agg, per-group processing)
     folly::Range<const RowContainerStateKey*> distinctKeys() const;   // D 
deduplicated
     const std::vector<SelectivityVector>& groupRows() const;          // input 
rows covered by key g
     folly::Range<const vector_size_t> newGroups() const;              // 
first-occurrence row numbers of new groups
     RowContainer* keyRowContainer() const;
   
    private:
     std::unique_ptr<HashTable<false>> hashTable_;
     exec::HashLookup lookup_;      // private: hit/hash details never exposed; 
StateKey construction internal to KeySelector
   };
   ```
   
   **Only StateKey is exposed to operators**: hit/hash details (HashLookup) 
stay fully private; `RowContainerStateKey(row, hash, maxParallelism)` 
construction happens only inside KeySelector — operators touch nothing but the 
StateKey type from probe to accumulation. Deduplication and the key→row mapping 
(distinct usage) are also done inside KeySelector, not by operators.
   
   ### 3.9 Removing StreamKeyedOperator
   
   The StreamKeyedOperator intermediate base class is deleted: its grouping is 
inefficient and cannot use Velox's batch processing capability. Operators 
directly extend StatefulOperator, compose a KeySelector + state handle, and 
implement their own advance().
   
   ### 3.10 KeyGroup algorithm
   
   ```
   hash       = VectorHasher::hash(user_key)    // 64-bit
   key_group  = hash % maxParallelism
   ```
   
   All four places (shuffle / probe / snapshot / restore) reuse the same 
algorithm naturally. Introducing `maxParallelism` decouples from parallelism 
(Flink core design): rescale does not change maxParallelism → key-group 
boundaries stay stable.
   
   ## 4. Operator integration
   
   ### 4.1 Class hierarchy
   
   ```
   StatefulOperator (only base class)
     └─ concrete stateful operators (direct inheritance, no intermediate keyed 
base)
          GroupAggregation / GroupWindowAggregator / WindowJoin /
          AppendOnlyTopNRanker / ...
            ├─ composes KeySelector (does groupProbe)
            └─ composes AccState<K, N> / ValueState<K, N, V> / ... handle
   
   StreamOperatorStateHandler
     └─ holds KeyedStateBackendV2<K>*
   
   StateBackend (Task-level)
     └─ createKeyedStateBackend<K>(...) → unique_ptr<KeyedStateBackendV2<K>>
   ```
   
   ### 4.2 Initialization flow example
   
   ```cpp
   void GroupAggregation::initializeStateBackend(StateBackend* stateBackend) {
     StatefulOperator::initializeStateBackend(stateBackend);
     keyedStateBackend_ = 
stateBackend->createKeyedStateBackend<RowContainerStateKey>(...);
   }
   
   void GroupAggregation::initializeState() {
     StatefulOperator::initializeState();
     valueRows_ = std::make_unique<RowContainer>(accTypes_, ..., pool_);
     auto desc = AccStateDescriptor("agg", valueRows_.get(), 
std::move(aggregates_), pool_);
     accState_ = 
&keyedStateBackend_->getOrCreateAccState(VoidNamespace::instance(), desc);
   }
   ```
   
   1. initializeStateBackend: the Task-level StateBackend is handed down to the 
operator, which creates the KeyedStateBackendV2 instance with K specified 
explicitly as the template argument (the method is currently non-virtual and 
must become virtual so operators can override it to specify K).
   2. Build the value RowContainer: owned by the operator; the acc row layout 
is defined by the operator.
   3. Build the AccStateDescriptor: holds the value description (acc row layout 
+ accumulation semantics).
   4. Get the state handle: the backend's getOrCreateAccState is called once 
(namespace and descriptor as parameters); the returned handle is stored as an 
operator member, and advance uses that member directly without re-fetching.
   
   <img width="846" height="348" alt="Image" 
src="https://github.com/user-attachments/assets/60f677c9-f4d3-4d11-8bed-b408d8cadfa8";
 />
   
   ### 4.3 advance flow example
   
   ```cpp
   void GroupAggregation::advance(RowVectorPtr input) {
     keySelector_.probe(input);
     outRows_.resize(input->size());
     accState_->rows(keySelector_.keys(), VoidNamespace::instance(), 
outRows_.data());
     for (auto& agg : accState_->aggregates())
       agg->addRawInput(outRows_.data(), rows, ...);
   }
   ```
   
   1. Grouping: KeySelector internally uses the HashTable for probe-based 
grouping, producing a hits sequence 1:1 with input rows for StateKey 
construction;
   2. State lookup: AccState queries per StateKey row by row; on miss it 
automatically creates the acc row, initializes it via the Aggregate's 
initializeNewGroups, and puts it into the state backend;
   3. Vectorized accumulation: each Aggregate calls addRawInput; outRows is 1:1 
with input rows and passed straight through as velox's groups parameter.
   
   <img width="1654" height="502" alt="Image" 
src="https://github.com/user-attachments/assets/b2097d08-59c4-429f-8f71-a42e1b42fd05";
 />


-- 
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]

Reply via email to