Skip to content

feat: add experimental native support for in-memory cache, disabled by default - #5051

Open
andygrove wants to merge 8 commits into
apache:mainfrom
andygrove:feat/native-in-memory-cache
Open

feat: add experimental native support for in-memory cache, disabled by default#5051
andygrove wants to merge 8 commits into
apache:mainfrom
andygrove:feat/native-in-memory-cache

Conversation

@andygrove

Copy link
Copy Markdown
Member

Which issue does this PR close?

Closes #2391.

Rationale for this change

This PR continues the work started by @pchintar in #4591, who is no longer able to work on it. All credit for the original implementation goes to them. This branch is based on their branch with latest apache/main merged in, and picks up the remaining review feedback and CI failures.

Comet currently has limited support for Spark's in-memory cache.

When a table is cached and later read, the cached data cannot be consumed directly by Comet operators. Instead, the execution plan falls back to Spark's cache scan path and introduces an additional CometSparkColumnarToColumnar conversion before execution can continue in Comet.

This extra conversion adds overhead to cached table scans and prevents cached data from remaining on a native Comet execution path.

What changes are included in this PR?

A native cache path for in-memory cached tables behind a new configuration:

spark.comet.exec.inMemoryCache.enabled

When enabled:

  • Cached data is stored using a Comet-specific cache serializer (compressed Arrow IPC).
  • Cached data is represented as CometCachedBatch.
  • Cached tables are scanned using CometInMemoryTableScanExec.
  • Cached data can be consumed directly by Comet operators without introducing a CometSparkColumnarToColumnar conversion.
  • Per-batch column statistics (lower bound, upper bound, null count, row count) are written in the format expected by SimpleMetricsCachedBatchSerializer, so Spark's buildFilter can prune cached batches before they are decoded.

When disabled:

  • Spark's existing cache serializer continues to be used.
  • Existing cache scan behavior is preserved.

How are these changes tested?

CometInMemoryCacheSuite covers:

  • Comet-native cache scan over CometCachedBatch
  • Fallback behavior when native cache support is disabled
  • Multi-partition cached tables
  • Empty cached tables
  • Projection-only cache reads
  • Shuffle execution after cached table scans
  • Stats-based batch pruning
  • Empty projection scans (SELECT count(*))
  • Floating-point pruning with NaN values
  • Fallback read path for Spark DefaultCachedBatch

CometInMemoryCacheBenchmark compares the native cache path against the existing fallback path for repeated cached table scans and selective-filter scans exercising cache pruning.

Benchmark results (release build, Apple M3 Ultra, JDK 17, Spark 3.5 profile, 5M-row cached table):

Repeated full scan (SELECT sum(id), sum(k), sum(v))

Case Best (ms) Avg (ms) Relative
Comet cache disabled 180 201 1.0x
Comet cache enabled 121 128 1.5x

Selective filter (WHERE id >= 4500000 AND id < 4750000)

Case Best (ms) Avg (ms) Relative
Comet cache disabled 46 53 1.0x
Comet cache enabled 42 48 1.1x

Performance follow-ups are tracked in #4781.

InMemoryRelation resolves spark.sql.cache.serializer once per JVM and
memoizes the instance in a static field. Test suites share a forked JVM,
so whichever suite caches a table first pins the serializer for every
suite that follows. CometInMemoryCacheSuite runs after CometExecSuite in
the exec group, so its configured serializer was ignored, every cached
batch was a DefaultCachedBatch, and 9 of its 11 tests failed on all Spark
profiles.

Reset the memoized serializer around the suite, via a test-only shim for
the private[columnar] clearSerializer.

Also address review feedback:

- Fall through to the SparkToColumnar path when the native cache is
  enabled but the relation was cached by a foreign serializer, so
  enabling the feature is never worse for a scan than leaving it off.
  Covered by a new CometExecSuite test.
- Explain on CometInMemoryTableScanExec.output why the declared output
  can be narrower than the emitted batch width for an empty projection.
- Drop the import made redundant by the org.apache.spark.sql.comet._
  wildcard.
@andygrove
andygrove marked this pull request as ready for review July 27, 2026 15:03
@andygrove andygrove changed the title feat: add native support for in-memory cache feat: add experimental native support for in-memory cache, disabled by default Jul 27, 2026
@andygrove

Copy link
Copy Markdown
Member Author

I found some issues with data type coverage and will push some fixes soon.

…serializer

Three defects found while reviewing type coverage, each with a regression
test that fails without the corresponding fix.

Pruning dropped every batch for columns without bounds. `tracksBounds`
only computes bounds for the types it lists, so a collated `StringType`
on Spark 4.x left them null. Spark still builds a partition filter for
such a column because a collated string literal is an `AtomicType`, and
comparing against null bounds yields null, which the generated predicate
treats as false. A filtered query over a collated string column returned
zero rows instead of the matching ones, with no error. `buildFilter` now
drops predicates over columns that have no bounds, keeping `IsNull` and
`IsNotNull` which only read null and row counts.

Interval columns broke `cache()`. `Utils.getFieldVector` has no case for
`DurationVector` or `IntervalYearVector`, so materializing a cached
relation containing a `DayTimeIntervalType` or `YearMonthIntervalType`
column threw `Unsupported Arrow Vector for serialize`. The serializer
now reports the schemas its Arrow writer supports and delegates the rest
to Spark's default cache serializer.

Reading a `DefaultCachedBatch` failed for any non-primitive column.
`supportsColumnarOutput` returned true unconditionally while
`decodeDefaultCachedBatch` decoded through `ColumnAccessor.decompress`,
whose `PassThrough` decoder only handles the seven primitive physical
types. Spark's own serializer gates `supportsColumnarOutput` on exactly
those types to avoid this. Reading a cached string column threw
`scala.MatchError: PhysicalStringType`. The cached format is now decided
by schema alone rather than by a runtime config, so a relation is either
entirely Comet format or entirely delegated to Spark, and the hand-rolled
`DefaultCachedBatch` decoder is gone. `spark.sql.cache.serializer` is a
static conf, so a format that could flip mid-session was never readable
back reliably. `CometExecRule` checks the schema before choosing the
native scan.

Also add coverage for all supported types, the row read path over
`CometCachedBatch`, and a reordered full-width projection.
@andygrove

Copy link
Copy Markdown
Member Author

@0lai0 @manuzhang @sandugood @peterxcli could you help with reviews on this PR?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Explore options for accelerating InMemoryTableScanExec

3 participants