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


##########
native/spark-expr/src/comet_scalar_funcs.rs:
##########
@@ -131,7 +131,11 @@ pub fn create_comet_physical_fun_with_eval_mode(
             make_comet_scalar_udf!("ceil", spark_ceil, data_type)
         }
         "spark_sequence" => {
-            make_comet_scalar_udf!("spark_sequence", spark_sequence, data_type)
+            // Preserve the exact outer pool, including per-plan tracking and 
task-shared policy.
+            Ok(Arc::new(ScalarUDF::new_from_impl(SparkSequence::new(
+                data_type,
+                Arc::clone(&session_state.runtime_env().memory_pool),
+            ))))
         }

Review Comment:
   Since this branch was cut, #7171 added a separate 
`spark_sequence_recomputed` arm here for Spark 4.0.5, 4.1.4 and 4.2.1, built 
with `make_comet_scalar_udf!`. A rebase that keeps that arm as is leaves those 
versions outside the pool. Could the overflow mode become a field of 
`SparkSequence`, so both names go through the reservation, with a test of the 
recomputed name under a bounded pool?



##########
native/spark-expr/src/array_funcs/sequence.rs:
##########
@@ -184,39 +267,94 @@ where
             None => -1,
         }
     };
+    let scalar_length = if args
+        .iter()
+        .all(|arg| matches!(arg, ColumnarValue::Scalar(_)))
+    {
+        let s = start.value(0);
+        let e = stop.value(0);
+        Some(sequence_length(s, e, row_step(0, s, e))?)
+    } else {
+        None
+    };
+    let scalar_total = scalar_length
+        .map(|len| batch_length(len as u128 * num_rows as u128))
+        .transpose()?;
+    // Columns admit scratch first, then validate and cache each row length 
once before
+    // growing this same reservation for values. Scalars know their complete 
size already.
+    let initial = (|| {
+        let bitmap_count = if null_counts.iter().any(|count| *count != 0) {
+            2
+        } else {
+            0
+        };
+        let bytes = construction_scratch(num_rows, true, bitmap_count)?;
+        let bytes = bytes
+            .checked_add(value_bytes::<T>(scalar_total.unwrap_or(0))?)
+            .ok_or_else(size_error)?;
+        pool.map(|pool| reserve_construction(pool, bytes))
+            .transpose()
+    })();
+    // Keep error-only typed parsing in the cold helper so it need not borrow 
hot input locals.
+    let scratch_error = move |error| {
+        // Only resource exhaustion needs an allocation-free validation pass 
to keep Spark
+        // row errors and the batch element ceiling ahead of it. Propagate 
other pool errors,
+        // such as an interrupted Spark task, immediately without inspecting 
more input rows.
+        if scalar_length.is_none() && matches!(&error, 
DataFusionError::ResourcesExhausted(_)) {
+            validate_before_resource_error::<T>(args, num_rows, error)
+        } else {
+            error
+        }
+    };
+    let reservation = initial.map_err(scratch_error)?;
+    // Reuse validity once per batch instead of checking three inputs and 
rebuilding a
+    // bitmap row by row. Arrow unions sliced bitmaps with bulk word 
operations, allocating
+    // one accumulator and applying the remaining masks in place.
+    let input_nulls = [
+        start.nulls(num_rows),
+        stop.nulls(num_rows),
+        step.as_ref().and_then(|step| step.nulls(num_rows)),
+    ];
+    let nulls = NullBuffer::union_many(input_nulls.iter().map(Option::as_ref))
+        .filter(|nulls| nulls.null_count() != 0);
+    if nulls
+        .as_ref()
+        .is_some_and(|nulls| nulls.null_count() == num_rows)
+    {
+        // Reuse the accumulator so another all-null bitmap cannot exceed its 
capacity allowance.
+        return null_sequence::<T>(child_field, num_rows, nulls);
+    }
 
     // Validate every row before checking the batch total, preserving the 
first Spark error.
     // Retain lengths so short sequences do not repeat per-row arithmetic 
during generation.
-    let mut lengths = Vec::with_capacity(num_rows);
-    let mut total = 0usize;
-    for row in 0..num_rows {
-        if nulls.as_ref().is_some_and(|nulls| nulls.is_null(row)) {
-            lengths.push(0);
-            continue;
-        }
-        let s = start.value(row);
-        let e = stop.value(row);
-        let len = sequence_length(s, e, row_step(row, s, e))?;
-        total += len;
-        lengths.push(len);
-    }
-    // Arrow's List offsets must fit i32; Spark's per-row array limit is 
checked above.
-    if total > i32::MAX as usize {
-        return Err(DataFusionError::External(Box::new(
-            SparkError::SequenceBatchTooLarge {
-                total_elements: total.to_string(),
-            },
-        )));
+    let mut lengths = sequence_vec::<usize>(num_rows).map_err(scratch_error)?;
+    let total = if let Some(len) = scalar_length {
+        lengths.resize(num_rows, len);
+        scalar_total.unwrap()

Review Comment:
   For all-scalar inputs `scalar_length` is constant, but this still allocates 
and reserves a `num_rows` `usize` length cache (64 KiB at 8192 rows). Could the 
scalar path pass `cache_lengths = false`? The test that pins "four cached 
lengths" would change with it.



##########
native/spark-expr/src/array_funcs/sequence.rs:
##########
@@ -245,6 +383,133 @@ where
     )?))
 }
 
+// Registration and reservation have the same lexical lifetime, including 
failed try_grow.
+fn reserve_construction(pool: &Arc<dyn MemoryPool>, bytes: usize) -> 
Result<MemoryReservation> {
+    let reservation = MemoryConsumer::new("Sequence").register(pool);
+    reservation.try_grow(bytes)?;
+    Ok(reservation)
+}
+
+fn size_error() -> DataFusionError {
+    DataFusionError::ResourcesExhausted("Sequence construction size 
overflow".to_string())
+}
+
+fn bitmap_capacity(rows: usize) -> Result<usize> {
+    // Round bitmap bytes to Arrow's 64-byte allocation boundary.
+    Ok(rows.div_ceil(8).checked_add(63).ok_or_else(size_error)? & !63)
+}
+
+fn batch_length(total: u128) -> Result<usize> {
+    if total > i32::MAX as u128 {
+        return Err(DataFusionError::External(Box::new(
+            SparkError::SequenceBatchTooLarge {
+                total_elements: total.to_string(),
+            },
+        )));
+    }
+    Ok(total as usize)
+}
+
+fn value_bytes<T: Integral>(total: usize) -> Result<usize> {
+    total
+        .checked_mul(std::mem::size_of::<T::Native>())
+        .ok_or_else(size_error)
+}
+
+fn construction_scratch(rows: usize, cache_lengths: bool, bitmap_count: usize) 
-> Result<usize> {
+    let offsets = rows
+        .checked_add(1)
+        .and_then(|n| n.checked_mul(std::mem::size_of::<i32>()))
+        .ok_or_else(size_error)?;
+    let lengths = if cache_lengths {
+        rows.checked_mul(std::mem::size_of::<usize>())
+            .ok_or_else(size_error)?
+    } else {
+        0
+    };
+    // Bulk unions use one accumulator; two rounded bitmap sizes allow for 
capacity doubling.
+    // The full backing allocation of a borrowed, possibly sliced input bitmap 
is not charged.
+    let validity = bitmap_capacity(rows)?
+        .checked_mul(bitmap_count)
+        .ok_or_else(size_error)?;
+    offsets
+        .checked_add(lengths)
+        .and_then(|n| n.checked_add(validity))
+        .ok_or_else(size_error)
+}
+
+#[cold]
+fn validate_before_resource_error<T: Integral>(
+    args: &[ColumnarValue],
+    rows: usize,
+    resource_error: DataFusionError,
+) -> DataFusionError
+where
+    T::Native: Into<i64>,
+{
+    let validation = (|| -> Result<usize> {
+        let start = Input::<T>::new(&args[0])?;
+        let stop = Input::<T>::new(&args[1])?;
+        let step = args.get(2).map(Input::<T>::new).transpose()?;
+        let mut total = 0u128;
+        for row in 0..rows {
+            if start.is_null(row)
+                || stop.is_null(row)
+                || step.as_ref().is_some_and(|step| step.is_null(row))
+            {
+                continue;
+            }
+            let s = start.value(row);
+            let e = stop.value(row);
+            let step = step
+                .as_ref()
+                .map_or_else(|| if s <= e { 1 } else { -1 }, |step| 
step.value(row));
+            total += sequence_length(s, e, step)? as u128;
+        }
+        batch_length(total)
+    })();
+    validation.err().unwrap_or(resource_error)
+}
+
+fn sequence_vec<T>(capacity: usize) -> Result<Vec<T>> {
+    let mut values = Vec::new();
+    values.try_reserve_exact(capacity).map_err(|error| {
+        DataFusionError::ResourcesExhausted(format!("Sequence allocation 
failed: {error}"))
+    })?;
+    Ok(values)

Review Comment:
   On `main` a failed `try_reserve_exact` becomes `SequenceBatchTooLarge`, 
whose message tells the user to lower `spark.comet.batchSize`, and its doc in 
`native/common/src/error.rs` says it covers allocator refusal. Here allocator 
and pool refusals become a plain `ResourcesExhausted` with no hint. Could they 
keep the existing variant, or could its doc and message be updated?



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