Skip to content

[GLUTEN-13140][VL] Replace ArrowWritableColumnVector with Spark's ArrowColumnVector and ArrowWriter - #13280

Open
FelixYBW wants to merge 5 commits into
apache:mainfrom
FelixYBW:arrow-column-vector
Open

FelixYBW wants to merge 5 commits into
apache:mainfrom
FelixYBW:arrow-column-vector

Conversation

@FelixYBW

@FelixYBW FelixYBW commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What changes are proposed in this pull request?

ArrowWritableColumnVector was introduced by Gazelle back in 2019, and then Gluten reused it. We tried to upstream it in apache/spark#34396, while @cloud-fan @viirya @sunchao suggested to use Spark's ArrowColumnVector and ArrowWriter to implement the functionality of ArrowWritableColumnVector, which makes sense to us but the work is paused.

Recently, a couple of PRs in upstream Spark are going to enable more Arrow-based operators. Meanwhile the 3rd party libraries like lance-spark also introduce Arrow based operators. Our goal is to reuse those operators or have a thin wrapper around those operators. But one issue we meet is the compatibility of the ArrowColumnVector. The PR is going to use Spark's ArrowColumnVector and ArrowWriter to replace Gluten's old ArrowWritableColumnVector. So Gluten and Spark can transfer the ArrowColumnVector each other.

Lance. The Lance scan offload (#12958) currently needs its own LanceScanTransformer and RDD, because Gluten only accepted batches of ArrowWritableColumnVector. With this PR, ColumnarBatches.offload accepts batches of Spark's ArrowColumnVector. That's the Gluten side of the longer-term plan in #12958 (comment): if lance-spark's columnar reader gets an Arrow output mode that emits Spark ArrowColumnVectors, the vanilla BatchScanExec can be offloaded with only a batch-type rule. It would then keep Lance's metrics, runtime filtering and partitioning. That follow-up needs a change in lance-spark and isn't part of this PR.

It's part of #13140 (items G2 + G3). No Spark change is needed: ArrowColumnVector(ValueVector), getValueVector, ArrowWriter.create(VectorSchemaRoot) and MutableProjection have the same API in Spark 3.4 – 4.2.

  • RefCountedArrowColumnVector : a subclass of Spark's ArrowColumnVector that keeps Gluten's reference count. The batch lifecycle is unchanged: ColumnarBatches still retains and releases batches by reference count, and loads and offloads them in place.
  • Reading goes through Spark's accessors. They check for nulls on all supported Spark versions, so the ArrowColumnarBatch / ArrowColumnarRow workaround for Spark's ColumnarRow before 4.0 is removed.
  • Writing:
    • ColumnarPartialProjectExec: Spark's MutableProjection + ArrowWriter, replacing ArrowProjection / GenerateArrowProjection / InterpretedArrowProjection.
    • ColumnarPartialGenerateExec: ArrowWriter over the generated rows. The per-column size estimation is no longer needed.
    • ColumnarRangeExec, ExecUtil (range partition ids): set the Arrow vector directly.
  • ArrowColumnVectors (new): allocate a root from Gluten's allocator, wrap vectors, build a batch from rows with ArrowWriter, and adopt (move, or copy across allocator roots) a vector into Gluten's allocator. ColumnarBatches.offload adopts batches of plain Spark ArrowColumnVectors.
  • ColumnarArrowEvalPythonExec wraps the Python output vectors with RefCountedArrowColumnVector. Gluten's runner and its shims are unchanged.
  • LanceScanTransformer (-Plance): wraps the vectors it transfers out of the imported Arrow stream with ArrowColumnVectors.wrap, instead of ArrowWritableColumnVector.loadColumns. Its behavior is unchanged.
  • Docs: docs/developers/ArrowColumnarConversion.md is updated to describe RefCountedArrowColumnVector and ArrowColumnVectors.
  • VeloxColumnarWriteFilesExec reads the VARBINARY fragments column with getBinary. The old accessor allowed getUTF8String on binary columns; Spark's doesn't.
  • Removed: ArrowWritableColumnVector, WritableColumnVectorShim (spark34 – spark42), ArrowColumnarBatch, ArrowColumnarRow, ArrowColumnVectorUtils, and ArrowProjection with its code generators.

The new solution actually simplified the implementation with a net: +406 / −4,521 lines.

Not included: backends-bolt uses the same removed classes and is not migrated in this PR.

How was this patch tested?

  • ColumnarBatchTest: ported to batches built with ArrowWriter.
  • ArrowColumnVectorTest: new cases for the reference count, a decimal round trip, and offloading a Spark Arrow batch allocated from another allocator root.
  • ArrowEvalPythonExecSuite: a new case with two chained Arrow UDFs over many small batches with nulls, checking the results against vanilla Spark and the LoadArrowData / OffloadArrowData transitions around the UDFs.

The write paths now use ArrowWriter, which writes one row at a time like the code it replaces. I haven't benchmarked them yet.

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code (claude-opus-5-5)

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 14, 2026 07:18

This comment was marked as outdated.

@github-actions github-actions Bot added CORE works for Gluten Core VELOX labels Oct 14, 2026
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

Copilot AI balanced review requested due to automatic review settings October 20, 2026 19:49

This comment was marked as outdated.

FelixYBW and others added 3 commits October 20, 2026 13:35
…owColumnVector and ArrowWriter

Gluten's Java Arrow batches now consist of RefCountedArrowColumnVector, a
subclass of Spark's ArrowColumnVector that keeps Gluten's reference count.
The batch lifecycle is unchanged: ColumnarBatches still retains and releases
batches by reference count, and loads and offloads them in place.

- Reading goes through Spark's ArrowColumnVector accessors, which check for
  nulls on all supported Spark versions, so the ArrowColumnarBatch /
  ArrowColumnarRow workaround for Spark's ColumnarRow before 4.0 is no
  longer needed.
- Writing fills the Arrow vectors directly or with Spark's ArrowWriter:
  - ColumnarPartialProjectExec: Spark's MutableProjection + ArrowWriter
    (replaces ArrowProjection / GenerateArrowProjection /
    InterpretedArrowProjection).
  - ColumnarPartialGenerateExec: ArrowWriter over the generated rows.
  - ColumnarRangeExec, ExecUtil: set the Arrow vector directly.
- ArrowColumnVectors: allocate a root from Gluten's allocator, wrap
  vectors, build a batch from rows with ArrowWriter, and adopt (move or
  copy) a vector into Gluten's allocator. ColumnarBatches.offload adopts
  batches of plain Spark ArrowColumnVectors.
- ColumnarArrowEvalPythonExec wraps the Python output vectors with
  RefCountedArrowColumnVector.
- VeloxColumnarWriteFilesExec reads the VARBINARY fragments column with
  getBinary (the old accessor allowed getUTF8String on binary).
- Removed: ArrowWritableColumnVector, WritableColumnVectorShim (spark34 -
  spark42), ArrowColumnarBatch, ArrowColumnarRow, ArrowColumnVectorUtils,
  ArrowProjection and its code generators.

ArrowColumnVector, ArrowWriter.create(VectorSchemaRoot) and MutableProjection
are available with the same API in Spark 3.4 - 4.2, so no shim is needed.

Tests: ColumnarBatchTest ported to ArrowWriter-built batches; new
ArrowColumnVectorTest cases (ref count, decimal round trip, offloading a
Spark Arrow batch from another allocator root); new ArrowEvalPythonExecSuite
case with chained UDFs over many small batches with nulls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…wEvalPythonExecSuite

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 20, 2026 20:44
@FelixYBW
FelixYBW force-pushed the arrow-column-vector branch from c6a63d5 to 1b6ee72 Compare October 20, 2026 20:44

This comment was marked as outdated.

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

1 similar comment
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

…rrowColumnVector

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 20, 2026 23:40

This comment was marked as outdated.

@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@github-actions github-actions Bot added the DOCS label Oct 21, 2026
…ic ref count

- ColumnarBatches.adopt copies instead of moving a vector when moving would
  empty it for someone else: a Gluten RefCountedArrowColumnVector passed
  through in a mixed batch, or a vector used by more than one column.
- ArrowColumnVectors.fromRows closes the root if writing fails.
- RefCountedArrowColumnVector updates its count with compare-and-set, never
  goes below 0, and rejects retain() after release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 21, 2026 06:19
@FelixYBW

FelixYBW commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

I went through all 18 Copilot comments. They come down to 7 distinct points. e10fa72 fixes 3 of them; the rest are false positives or optional.

Fixed in e10fa72

  1. ColumnarBatches.adopt could move buffers out of vectors that are still in use (new in this PR). When a foreign batch's vectors share Gluten's allocator root, adopt transfers their buffers instead of copying them. That empties a vector for anyone else holding it, in two cases:

    • A mixed batch: a Spark Arrow operator's output that passes Gluten RefCountedArrowColumnVector columns through next to its own columns. The pass-through columns may still be retained elsewhere.
    • The same vector used in more than one column, e.g. SELECT a, a. The second column reads the vector the first one just emptied.

    Main doesn't have this problem only because its offload rejects foreign and mixed batches outright. The fix copies such vectors and keeps the zero-copy move for plain Spark vectors used once.

  2. ArrowColumnVectors.fromRows leaked the root when writing failed. It now closes the root before rethrowing.

  3. RefCountedArrowColumnVector's reference count wasn't race-safe. It now uses compare-and-set, never goes below 0, and retain() after release throws IllegalStateException.

2 and 3 are also bugs in the current code on main, not regressions from this PR:

  • ArrowWritableColumnVector.retain() / close() do the same non-atomic check-then-decrement, with a non-volatile closed flag.
  • ColumnarPartialProjectExec and ColumnarPartialGenerateExec allocate with ArrowWritableColumnVector.allocateColumns(...) and don't release the vectors if a projection throws. Only the task allocator's cleanup at task end gets the memory back.

They're fixed here because the new code puts both in one place.

New tests in ArrowColumnVectorTest:

  • testOffloadSameRootVectorUsedTwice
  • testOffloadMixedBatchKeepsGlutenColumn
  • testFromRowsReleasesRootOnFailure
  • testRefCount now also checks that retain-after-release throws.

The two adopt tests fail on the previous commit with IndexOutOfBoundsException (emptied vectors) and pass with the fix. ArrowColumnVectorTest and ColumnarBatchTest pass locally (12/12, Spark 4.0 / Scala 2.13 / JDK 17).

False positives

  1. "setValueCount is missing" in ColumnarRangeExec and ExecUtil, and "toBatch should set value counts" (5 comments). VectorSchemaRoot.setRowCount already calls setValueCount(rowCount) on every vector.
  2. "ArrowWriter is never closed". Spark's ArrowWriter has no close(); its only methods are write, finish and reset.

Optional, not changed

  1. adopt's copy path uses default sizing. VectorAppender already sets the value count. Pre-sizing the target would only reduce reallocations when copying across allocator roots.
  2. base("s") in ArrowEvalPythonExecSuite is a readability nit; the test is correct as written.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

22 open issues

And 2 more that still need to be addressed.

Review effort: Lite

Comment on lines +103 to +113
val root = ArrowColumnVectors.allocateRoot(schema)
val vector = root.getVector(0).asInstanceOf[BigIntVector]
vector.allocateNew(numRows)
for (i <- 0 until numRows) {
val value = current + i * step
vectors(0).putLong(i, getSafeMargin(value))
vector.set(i, getSafeMargin(value))
}
vectors.foreach(_.setValueCount(numRows))
root.setRowCount(numRows)
current += numRows * step

val batch = new ColumnarBatch(vectors.asInstanceOf[Array[ColumnVector]], numRows)
batch
ArrowColumnVectors.toBatch(root)
Comment on lines +147 to +160
val pidRoot =
ArrowColumnVectors.allocateRoot(new StructType().add("pid", IntegerType))
val pidVec = pidRoot.getVector(0).asInstanceOf[IntVector]
pidVec.allocateNew(cb.numRows)
convertColumnarToRow(cb).zipWithIndex.foreach {
case (row, i) =>
val pid = rangePartitioner.get.getPartition(partitionKeyExtractor(row))
pidVec.putInt(i, pid)
pidVec.set(i, pid)
}
pidRoot.setRowCount(cb.numRows)
val pidBatch = VeloxColumnarBatches.toVeloxBatch(
ColumnarBatches.offload(
ArrowBufferAllocators.contextInstance(),
new ColumnarBatch(Array[ColumnVector](pidVec), cb.numRows)))
ArrowColumnVectors.toBatch(pidRoot)))
Comment on lines +50 to +54
def toBatch(root: VectorSchemaRoot): ColumnarBatch = {
new ColumnarBatch(
wrap(root.getFieldVectors).map(_.asInstanceOf[ColumnVector]),
root.getRowCount)
}
Comment on lines +269 to +274
if (isForeignArrowBatch(input)) {
// A batch of Spark ArrowColumnVectors (e.g. the output of a Spark Arrow operator) is owned
// by its producer and only valid until the next one is requested: take ownership of its
// vectors in a Gluten batch and offload that one, leaving the input to its producer.
return offload(allocator, adopt(allocator, input));
}
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

* <p>Reading goes through Spark's accessors. To write, fill the Arrow vectors (e.g. with Spark's
* {@code ArrowWriter}) and wrap them, see {@link ArrowColumnVectors}.
*/
public final class RefCountedArrowColumnVector extends ArrowColumnVector {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have dropped Spark 3.2, now reference-counting can be done in columnar batch wise by inheriting Spark API ColumnarBatch.

We'd plan it as another future refactor to simplify code further. Many batch utilities can be removed then.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CORE works for Gluten Core DOCS VELOX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants