comphead commented on code in PR #7173:
URL: https://github.com/apache/datafusion-comet/pull/7173#discussion_r4361443553


##########
native/spark-expr/tests/bloom_filter_spill.rs:
##########
@@ -0,0 +1,160 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::sync::Arc;
+
+use arrow::array::{Array, BinaryArray, BinaryBuilder, Int64Array, RecordBatch};
+use arrow::datatypes::{DataType, Field, Schema};
+use datafusion::common::{Result, ScalarValue};
+use datafusion::datasource::memory::MemorySourceConfig;
+use datafusion::datasource::source::DataSourceExec;
+use datafusion::execution::memory_pool::{GreedyMemoryPool, MemoryPool};
+use datafusion::execution::runtime_env::RuntimeEnvBuilder;
+use datafusion::execution::TaskContext;
+use datafusion::logical_expr::AggregateUDF;
+use datafusion::physical_expr::aggregate::{AggregateExprBuilder, 
AggregateFunctionExpr};
+use datafusion::physical_expr::expressions::{Column, Literal};
+use datafusion::physical_plan::aggregates::{AggregateExec, AggregateMode, 
PhysicalGroupBy};
+use datafusion::physical_plan::{common::collect, ExecutionPlan};
+use datafusion::prelude::SessionConfig;
+use datafusion_comet_spark_expr::{BloomFilterAgg, SparkBloomFilterVersion};
+
+fn bloom_state(aggregate: &AggregateFunctionExpr, values: &[i64]) -> 
Result<Vec<u8>> {
+    let mut accumulator = aggregate.create_accumulator()?;
+    accumulator.update_batch(&[Arc::new(Int64Array::from_iter_values(
+        values.iter().copied(),
+    ))])?;
+    let ScalarValue::Binary(Some(state)) = accumulator.state()?.remove(0) else 
{
+        panic!("a nonempty Bloom filter must have a binary state");
+    };
+    Ok(state)
+}
+
+#[tokio::test]
+async fn grouped_bloom_filter_merges_spilled_states() -> Result<()> {
+    const NUM_GROUPS: usize = 1024;
+    const BATCH_SIZE: usize = 8;
+    const NUM_BITS: i64 = 131_072;
+
+    let value_schema = Arc::new(Schema::new(vec![
+        Field::new("key", DataType::Int64, false),
+        Field::new("value", DataType::Int64, false),
+    ]));
+    let aggregate_udf = 
Arc::new(AggregateUDF::new_from_impl(BloomFilterAgg::new(
+        Arc::new(Literal::new(ScalarValue::Int64(Some(1024)))),
+        Arc::new(Literal::new(ScalarValue::Int64(Some(NUM_BITS)))),
+        DataType::Binary,
+        SparkBloomFilterVersion::V1,
+    )));
+    let aggregate_expr = Arc::new(
+        AggregateExprBuilder::new(aggregate_udf, 
vec![Arc::new(Column::new("value", 1))])
+            .schema(Arc::clone(&value_schema))
+            .alias("bloom")
+            .build()?,
+    );
+    let state_schema = Arc::new(Schema::new(vec![
+        Field::new("key", DataType::Int64, false),
+        Field::new("bits", DataType::Binary, false),
+    ]));
+
+    // Each pass has one valid, nonempty partial state per group. The complete 
set
+    // of 16 KiB filters exceeds the pool, so the two passes meet during sorted
+    // spill replay. Distinct values detect lost states as well as merge 
panics.
+    let mut batches = Vec::new();
+    for pass in 0..2 {
+        for start in (0..NUM_GROUPS).step_by(BATCH_SIZE) {
+            let mut states = BinaryBuilder::new();
+            for key in start..start + BATCH_SIZE {
+                states.append_value(bloom_state(
+                    &aggregate_expr,
+                    &[(pass * NUM_GROUPS + key) as i64],
+                )?);
+            }
+            batches.push(RecordBatch::try_new(
+                Arc::clone(&state_schema),
+                vec![
+                    Arc::new(Int64Array::from_iter_values(
+                        (start..start + BATCH_SIZE).map(|key| key as i64),
+                    )),
+                    Arc::new(states.finish()),
+                ],
+            )?);
+        }
+    }
+    let scan = 
Arc::new(DataSourceExec::new(Arc::new(MemorySourceConfig::try_new(
+        &[batches],
+        state_schema,
+        None,
+    )?)));
+    let aggregate = AggregateExec::try_new(
+        AggregateMode::Final,
+        PhysicalGroupBy::new_single(vec![(Arc::new(Column::new("key", 0)), 
"key".into())]),
+        vec![Arc::clone(&aggregate_expr)],
+        vec![None],
+        scan,
+        value_schema,
+    )?;
+    let pool = Arc::new(GreedyMemoryPool::new(8 * 1024 * 1024));
+    let runtime = RuntimeEnvBuilder::new()
+        .with_memory_pool(Arc::clone(&pool) as _)
+        .build_arc()?;
+    let context = Arc::new(
+        TaskContext::default()
+            .with_runtime(runtime)
+            .with_session_config(
+                // Exercise the sorted-spill replay path explicitly.
+                SessionConfig::new()
+                    .with_batch_size(BATCH_SIZE)
+                    
.set_bool("datafusion.execution.enable_migration_aggregate", false),

Review Comment:
   This test selects the legacy sorted-spill path with 
`enable_migration_aggregate=false`, but DataFusion 55.2 defaults it to `true` 
and I could not find that flag set anywhere in `native` or `spark/src/main`, so 
Comet runs the default path. As written, the only end-to-end regression 
exercises a path Comet does not use. Could the test run both settings, or could 
the description say why only the legacy path hands `merge_batch` multiple state 
rows? The test also depends on the pool size and the `spill_count() > 0` 
heuristic, so a DataFusion bump could break it without a Comet change. A Final 
aggregate over one batch holding two states, with no pool or spill, would cover 
the same contract through the real operator more cheaply. I did not run any of 
this.



##########
native/spark-expr/src/bloom_filter/bloom_filter_agg.rs:
##########
@@ -190,16 +190,83 @@ impl Accumulator for SparkBloomFilter {
             "Expect one element in 'states' but found {}",
             states.len()
         );
-        assert_eq!(states[0].len(), 1);
-        let state_sv = downcast_value!(states[0], BinaryArray);
-        self.merge_filter(state_sv.value_data())
+        let states = downcast_value!(states[0], BinaryArray);
+        // Spill replay can supply multiple states for a group. Use each row's
+        // slice so that sliced arrays also honor their offsets.

Review Comment:
   The comment says spill replay can supply multiple states, but `merge_batch` 
can receive any number of rows. As far as I can tell DataFusion's ungrouped 
path passes the whole input batch (`aggregate_stream.rs`) and the grouped path 
goes through `GroupsAccumulatorAdapter`, which slices a taken array per group, 
so neither needs a spill. If that is right, the failure is broader than spill 
replay and the description could say so. The comment could say why per-row 
slices are used instead: the array may be a slice, and `value_data()` would 
expose the whole backing buffer. I did not verify how many state rows a Comet 
native Final sees per batch.



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