Repository navigation
fix: ship the functions of a native plan's ScalaUDFs once, in one serialization stream - #23
Merged
Merged
Conversation
…ialization stream The codegen dispatcher closure-serialized every ScalaUDF expression on its own, so a function captured by several expressions, and an object several functions capture, was written once per expression: ltv_flag's project of ten UDFs over one ~150 MB map built ~1.35 GB of bytes on the driver per plan conversion, again on every AQE re-plan, and failed with Java heap space. Each expression now carries its ScalaUDF functions as references. The native plan serialization appends one pool holding all functions its expressions reference, written in one Java serialization stream so shared captured objects are written once, and caches it per function set for AQE re-conversions. Each task registers the pools of its plans and deserializes a pool once, on its first use. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…which the shaded jar cannot load Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
experiment-platform.ltv_flagon Comet 1.1-11 died withJava heap spacein the 3G driver (vanilla passes). The codegen dispatcher (CometScalaUDF.emitJvmCodegenDispatch) closure-serialized every ScalaUDF expression in its own stream. The job's project has 10 UDFs, 9 of them capture one ~150 MBexpResultsmap, so each plan conversion built ~1.35 GB of byte arrays (plus copies), and AQE re-converts the plan after every stage. Vanilla serializes the plan in one stream and pays 155 MiB once.Fix
emitJvmCodegenDispatchserializes the bound expression with eachScalaUDF.functionreplaced by a small arity-matching reference (CometSharedFunctionRef(id)); functions are checked for serializability once per function identity (null stream) so non-serializable UDFs still fall back to Spark at plan time.CometExec.serializeNativePlan(every native plan serialization) appends one function pool for the functions the plan's expressions reference (unknownOperatorfield 99, ignored by the native side): all functions in one Java-serialization stream, so objects they share are written once. The pool is cached per function set (soft values), so AQE re-conversions reuse the bytes instead of serializing again.CometExecIteratorregisters the pools of its plan for the task (root and nested children, e.g. the shuffle writer's inlined child); a pool is deserialized once per task on first use and dropped at task completion.CometScalaUDFCodegenresolves the references when it deserializes an expression.Tests
CometCodegenSuitetest "UDFs that capture one large object ship it once per serialized plan": 8 UDF expressions over 3 functions sharing a 4 MiB array. Serialized native plan < 2 x 4 MiB (the same test without the references measured 37.9 MB, i.e. one copy per expression); one pool build, pool deserialized on executors, a second run of the query reuses the cached pool (no rebuild); results match Spark.🤖 Generated with Claude Code