andygrove commented on code in PR #4791:
URL: https://github.com/apache/datafusion-comet/pull/4791#discussion_r4104874244
##########
native/shuffle/src/schema_align.rs:
##########
@@ -119,10 +141,16 @@ impl SchemaAlignExec {
expected_field.data_type()
);
}
- true
+ match (actual_field.data_type(), expected_field.data_type()) {
+ (DataType::LargeUtf8, DataType::Utf8) =>
ColumnAction::CastLargeStringToString,
Review Comment:
With the flag on, reusing `SchemaAlignExec` above the aggregate hits the
warning just above and logs `ShuffleWriter input schema mismatch on col[0]
'col_0': child produced LargeUtf8, catalyst declared Utf8. Inserting a cast;
please file the upstream function bug at .../issues/4515` on each executor. I
saw it in my local runs. There is no shuffle writer or upstream bug involved
here, and the module doc says this operator is enclosed by shuffle on purpose.
Would a small operator dedicated to the cast-back be cleaner? It could record
its own metrics, which would also help with the metrics problem sunchao raised
in `planner.rs`.
##########
native/shuffle/src/schema_align.rs:
##########
@@ -72,9 +75,27 @@ fn warn_dedup() -> &'static Mutex<HashSet<String>> {
pub struct SchemaAlignExec {
child: Arc<dyn ExecutionPlan>,
target_schema: SchemaRef,
+ column_actions: Arc<Vec<ColumnAction>>,
cache: Arc<PlanProperties>,
}
+#[derive(Debug, Clone)]
+enum ColumnAction {
Review Comment:
It looks like the rebase brought back the `ColumnAction` enum that #5138
removed from this file. #5138 routed `SchemaAlignStream` through
`cast_and_stamp_schema`, so every batch is checked against the target schema
and a failed cast names the operator and the column path. Here the choice is
made once at plan time. `Passthrough` columns are no longer checked per batch,
and the `Cast` arm returns a bare arrow error. This operator sits in front of
every native shuffle writer, so that applies with the flag off too. Could the
Large-to-small columns be handled first, with the result then handed to
`cast_and_stamp_schema`?
##########
spark/src/main/scala/org/apache/comet/CometConf.scala:
##########
@@ -455,6 +455,26 @@ object CometConf extends ShimCometConf {
.booleanConf
.createWithDefault(false)
+ val COMET_AGG_USE_LARGE_DATATYPES: ConfigEntry[Boolean] =
+ conf(s"$COMET_EXEC_CONFIG_PREFIX.aggregation.useLargeDataTypes")
Review Comment:
The other aggregate keys use `spark.comet.exec.aggregate.*`, and
`config_conventions.md` asks for feature flags to end in `.enabled`. This key
will be covered by the versioning policy once it ships, so could we settle on
something like `spark.comet.exec.aggregate.largeGroupKeys.enabled` now?
##########
spark/src/main/scala/org/apache/comet/CometConf.scala:
##########
@@ -455,6 +455,26 @@ object CometConf extends ShimCometConf {
.booleanConf
.createWithDefault(false)
+ val COMET_AGG_USE_LARGE_DATATYPES: ConfigEntry[Boolean] =
+ conf(s"$COMET_EXEC_CONFIG_PREFIX.aggregation.useLargeDataTypes")
+ .category(CATEGORY_EXEC)
+ .doc(
+ "When true, Comet wraps Utf8/Binary group-by expressions inside a Cast
to " +
+ "LargeUtf8/LargeBinary immediately before each native HashAggregate,
and wraps the " +
+ "aggregate's output in a Projection that casts those columns back to
Utf8/Binary. " +
+ "This promotes DataFusion's per-task group-key byte buffer from
`i32` offsets " +
+ "(2 GiB hard cap) to `i64` offsets, removing the `offset overflow,
buffer size > " +
+ "2147483647` failure that can hit CUBE / GROUPING SETS /
COUNT(DISTINCT) workloads " +
+ "where a single partition accumulates more than 2 GiB of distinct
string keys. " +
+ "Scan, shuffle, and the JVM FFI boundary remain Utf8/Binary, so
neither downstream " +
+ "Spark nor Comet's shuffle (which does not support LargeUtf8) is
affected. The cast " +
+ "is an offset-width promotion and only rebuilds the offset buffer
(the value bytes " +
+ "are shared), so per-batch overhead is O(rows) not O(bytes).
Defaults to false " +
+ "because the cap is only reachable for very large per-partition
group cardinalities; " +
+ "enable it when you see the offset-overflow error.")
+ .booleanConf
+ .createWithDefault(true)
Review Comment:
I asked in July whether defaulting this to `true` was intentional, and the
doc string still says it defaults to false. I measured it on a release build.
`SELECT count(*), sum(s) FROM (SELECT k, sum(v) s FROM t GROUP BY k)` over 8M
rows with 4M distinct 21-byte keys took 494 ms at the median with the flag on
and 450 ms with it off, about 10% slower over five alternating runs.
Low-cardinality and 200-byte keys were within noise. My laptop was busy, so
treat the numbers as rough. The overflow needs more than 2 GiB of distinct key
bytes in one task, and apache/datafusion#24704 tracks the real fix. Could this
default to `false`, with the tuning guide pointing at it for the `offset
overflow` error? Either way the doc needs updating. It describes the cast-back
as a `Projection` and says the overhead is O(rows), but as sunchao pointed out,
the cast-back copies every key byte.
##########
native/spark-expr/src/conversion_funcs/string.rs:
##########
@@ -172,7 +172,20 @@ impl TimeStampInfo {
}
pub(crate) fn is_df_cast_from_string_spark_compatible(to_type: &DataType) ->
bool {
- matches!(to_type, DataType::Binary)
+ // Utf8 -> Binary is a zero-copy reinterpret; Utf8 -> LargeUtf8 is a pure
+ // offset-width widening handled by arrow's cast_byte_container. Both are
+ // Spark-equivalent (Spark's StringType and BinaryType are indifferent to
+ // Arrow's offset width).
+ matches!(to_type, DataType::Binary | DataType::LargeUtf8)
+}
+
+pub(crate) fn is_df_cast_from_large_string_spark_compatible(to_type:
&DataType) -> bool {
Review Comment:
I can't find a way to reach these new arms, or the matching ones in
`cast.rs`. `types.proto` has no Large type ids, so the serde never emits a
`Cast` to or from one. The Parquet schema adapter's casts already go through
the `is_adapting_schema` branch in `cast_array`. The comment also says
`SchemaAlignExec` pre-splits arrays before they get here, but `SchemaAlignExec`
builds its arrays directly and never calls this cast. Could these arms be
removed from this PR?
##########
spark/src/main/scala/org/apache/spark/sql/comet/util/Utils.scala:
##########
@@ -578,10 +578,15 @@ object Utils extends CometTypeShim with Logging {
}
def getFieldVector(valueVector: ValueVector, reason: String): FieldVector = {
- if (isSupportedFieldVector(valueVector)) {
- valueVector.asInstanceOf[FieldVector]
- } else {
- throw new SparkException(s"Unsupported Arrow Vector for $reason:
${valueVector.getClass}")
+ valueVector match {
+ case v if isSupportedFieldVector(v) =>
+ v.asInstanceOf[FieldVector]
+ // Accepted here but left out of isSupportedFieldVector, which
isArrowBacked uses to keep
Review Comment:
Which path needs `getFieldVector` to accept `LargeVarCharVector` now that
the aggregate casts back natively? As far as I can tell, the only Large vectors
on the JVM side come from PyArrow UDFs returning `large_string`. So this
changes native C2R export and broadcast serialization for that path without a
test. It also makes the `isSupportedFieldVector` doc, the `isArrowBacked`
comment and the `UtilsSuite` test comment wrong, since all three assume
`getFieldVector` rejects these vectors. Could this be dropped here, or moved to
its own PR with a PyArrow test? The same question applies to the `LargeUtf8`
passthrough at `columnar_to_row.rs:1041`.
##########
spark/src/test/scala/org/apache/comet/exec/CometAggregateSuite.scala:
##########
@@ -3344,4 +3350,111 @@ class CometAggregateSuite extends CometTestBase with
AdaptiveSparkPlanHelper {
}
}
+ // The test to reproduce `offset overflow` for aggregation queries, when
interim data
+ // get exploded 100x comparing to initial input size.
+ // It is not supposed to run on CI as the test requires significant RAM to
succeed
+ ignore("CUBE(9) + COUNT(DISTINCT) wide Utf8 keys: useLargeDataTypes
preserves correctness") {
Review Comment:
This test is `ignore`d, so CI never runs it. The `sparkConf` comment at line
60 still describes the off-heap and memory pool settings that 7252adb853
removed. This test now sets the pool through `withSQLConf`, which that comment
says won't take effect. You mentioned in July that CI passes with the flag on.
That covers the common path, but nothing exercises the split in
`compute_row_ranges` or checks the metrics. Could the byte cap be a parameter
so a Rust unit test can split a small batch? And could this test and the stale
comment be replaced with small tests that run? If the default moves to `false`,
those tests would also need to turn the flag on explicitly.
##########
native/core/src/execution/operators/shuffle_scan.rs:
##########
@@ -210,15 +210,20 @@ impl ShuffleScanExec {
let num_rows = batch.num_rows();
- // Extract column arrays, unpacking any dictionary-encoded columns.
- // Native shuffle may dictionary-encode string/binary columns for
efficiency,
- // but downstream DataFusion operators expect the value types
declared in the
- // schema (e.g. Utf8, not Dictionary<Int32, Utf8>).
+ // Coerce each decoded column to the catalyst-declared type:
Review Comment:
Since the aggregate now casts back before anything above it sees the batch,
I don't think this feature can put `LargeUtf8` into a shuffle block. If one
did, the remote read path would reject it, because `remote_schema.rs` treats
`LargeUtf8` against `Utf8` as an incompatible type. On the local path,
`ShuffleScanStream::poll_next` already reconciles every column through
`cast_and_stamp_schema`, so the extra cast here duplicates that. Could this go
back to unpacking dictionaries only? Returning an error instead of the old
`expect` is a good change and worth keeping.
##########
native/core/src/execution/planner.rs:
##########
@@ -1451,6 +1498,36 @@ impl PhysicalPlanner {
)?,
);
+ // Cast promoted group columns back to their original
Utf8/Binary type
+ // so LargeUtf8/LargeBinary never crosses the FFI boundary,
JVM columnar
+ // shuffle, or the Spark consumer path (all of which reject
Large*).
+ // Uses `SchemaAlignExec` (not a plain `ProjectionExec` +
`CastExpr`)
+ // because arrow's cast kernel rejects any single Large* array
whose
+ // value bytes exceed `i32::MAX` -- which is precisely the
regime this
+ // flag is used in. `SchemaAlignExec` splits each batch by row
ranges
+ // so every emitted small-offset chunk fits under 2 GiB.
+ let aggregate = if use_large &&
group_reverts.iter().any(Option::is_some) {
+ let agg_schema = aggregate.schema();
+ let target_fields: Vec<Field> = agg_schema
+ .fields()
+ .iter()
+ .enumerate()
+ .map(|(idx, f)| {
+ let target_dt = group_reverts
+ .get(idx)
+ .and_then(|r| r.clone())
+ .unwrap_or_else(|| f.data_type().clone());
+ Field::new(f.name(), target_dt, f.is_nullable())
+ .with_metadata(f.metadata().clone())
+ })
+ .collect();
+ let target_schema: SchemaRef =
Arc::new(Schema::new(target_fields));
+ SchemaAlignExec::try_new_or_passthrough(aggregate,
&target_schema)
Review Comment:
I can confirm this end to end with a full build. `SELECT k, sum(v) FROM t
GROUP BY k` on a string key shows the partial `CometHashAggregateExec` with
`output_rows=0` and `elapsed_compute=0` when the flag is on, against 20000 rows
and about 50 ms with it off. The final aggregate's `elapsed_compute` drops from
about 10 ms to 17 µs. When fixing it, note that `to_native_metric_node` skips
`output_rows` from `additional_native_plans`, so registering the
`AggregateExec` through `SparkPlan::new_with_additional` also needs the wrapper
to record its own `output_rows`. A test asserting these metrics on a
string-keyed aggregate would catch this. The one in `CometExecSuite` uses a
numeric key.
--
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]