Parallel World Unshimming Algorithm
August 4, 2026 ยท View on GitHub
This document describes the dependency analysis used to reduce the amount of RAPIDS Accelerator bytecode that must live in the parallel-world layout of the dist jar.
The goal is to move classes from spark-shared into the conventional root jar
layout when they are safe to load before a Spark shim has been selected. A class
is root-safe only when it has no static dependency path to Spark-version-specific
bytecode. The analysis is bytecode-based and uses strongly connected components
to find migration batches that can be moved together.
See also:
- Shim Development, for shim design patterns and validation commands.
- Distribution Packaging, for the files that control explicit packaging exceptions.
analyze-parallel-world-deps.py, for the implementation.
Class Layout Model
The dist jar contains three relevant class locations:
root: the conventional jar layout. These classes are visible before any parallel-world Spark version is selected.spark-shared: one copy of bytecode that was bitwise-identical across all selected Spark shims.sparkXYZ: version-specific parallel-world directories such asspark330,spark350, orspark411.
The analyzer models a class as a pair:
(class name, location)
The location is part of the node identity because the same class name can occur in multiple places in the assembled dist jar.
flowchart TB Root["root layout"] Shared["spark-shared"] S330["spark330"] S350["spark350"] S411["spark411"] Root --> Shared Shared --> S330 Shared --> S350 Shared --> S411
An edge means "this class has a static bytecode dependency on that class." Dependencies are extracted from class-file constant pools, including class references and type descriptors.
When a dependency name exists in more than one location, the analyzer resolves it using the same layout precedence that matters for this packaging problem:
root- the source class location
spark-shared- remaining Spark-version-specific locations
This keeps the graph close to the classes that would actually be loaded.
Version-Specific Nodes
A node is treated as Spark-version-specific when either of these is true:
- its location is a parallel-world directory matching
spark[0-9][0-9a-z]*; - its class name contains a classifier package segment such as
com.nvidia.spark.rapids.spark330.
Classifier package classes are usually bootstrap or facade classes. Direct dependencies on them should stay narrow and intentional.
Reachability
The first graph question is:
Which root or
spark-sharedclasses can eventually reach Spark-version-specific bytecode?
The analyzer answers this by reversing the graph and starting a breadth-first
search from every version-specific node. Every upstream root or spark-shared
class reached by that reverse search is contaminated and cannot be considered
root-safe without further refactoring.
flowchart LR A["A: spark-shared"] B["B: spark-shared"] C["C: spark-shared"] V["V: spark350"] D["D: spark-shared"] E["E: spark-shared"] A --> B B --> C C --> V D --> E
In this example, A, B, and C are blocked because they have a path to
V. D and E are root-safe if no other edge reaches a version-specific node.
Shortest blocked paths are useful for refactoring because they explain the first version-sensitive boundary that needs to be cut. They are not the migration order.
Why SCCs
Classes often form dependency cycles. If one class in a cycle moves to the root layout, the rest of that cycle usually has to move or be refactored with it. Strongly connected components (SCCs) make that explicit.
flowchart LR A["GpuFoo"] B["GpuFooMeta"] C["GpuFooUtils"] D["Stable API"] A --> B B --> A B --> C C --> D
GpuFoo and GpuFooMeta are one SCC because each can reach the other. They are
a single migration unit. GpuFooUtils and Stable API are separate components.
The analyzer uses Tarjan's algorithm to compute SCCs. It then builds a component DAG. Since an edge means "source depends on target", component edges are reversed before Kahn's topological sort so the final order is dependency-first: dependencies appear before the classes that use them.
flowchart LR Stable["component 1: stable helpers"] Utils["component 2: utilities"] Rules["component 3: rules"] Rules --> Utils Utils --> Stable
The analyzer prints this as:
component 1: stable helpers
component 2: utilities
component 3: rules
That is the preferred order for human-sized unshimming batches.
Candidate Selection
For each run, the analyzer computes:
- direct classifier-package dependencies;
- root or
spark-sharedclasses with a path to version-specific code; - root-safe
spark-sharedSCCs in dependency-first order.
A good migration candidate is a root-safe spark-shared SCC. Small SCCs can be
reviewed directly. Large SCCs are usually a sign that a shared utility layer
should be moved first or that a Spark-specific edge needs to be pushed behind a
stable API.
Do not make a class root-safe by replacing typed dependencies with reflection.
Prefer a stable interface in sql-plugin-api, a one-way shim descriptor, or a
small Java-only helper module when the code is runtime plumbing that does not
need Scala or shim-specific APIs.
Packaging Policy
The packaging flow still requires bitwise identity for classes promoted
from spark-shared to the root layout. Automatic promotion of all eligible
common classes is opt-in via UNSHIM_PROMOTE_DEFAULT_SPARK_SHARED_CLASSES=1;
the SCC analysis answers a different question: whether the promoted class is
safe to load from the root layout.
The relevant packaging files are:
dist/keep-in-spark-shared.txt: class files that must remain inspark-sharedeven if they are bitwise-identical.dist/unshimmed-common-from-single-shim.txt: explicit root-layout files taken from one representative shim, mainly resources and special root artifacts.dist/unshimmed-from-each-spark3xx.txt: per-shim root artifacts such as classifier-package service providers.
The intended steady state, to be enabled by follow-up source/unshim PRs, is:
- new common classes are unshimmed by default when they are bitwise-identical and root-safe;
- exceptions are explicit and small;
- reviewers can use the analyzer output to understand why a class stays in
spark-shared.
Workflow
Build representative two-shim layouts for each Scala line:
./build/buildall --profile=330,358 --module=dist
python3 dist/scripts/analyze-parallel-world-deps.py \
dist/target/parallel-world \
--show-topo \
--show-reachability
./build/buildall --profile=350,411 --scala213 --module=dist
python3 dist/scripts/analyze-parallel-world-deps.py \
scala2.13/dist/target/parallel-world \
--show-topo \
--show-reachability
For repeated local iteration after the per-shim jars have already been built, use the direct parallel-world assembler:
python3 dist/scripts/build-unshim-parallel-world.py \
--mvn-base-dir . \
--source-dir . \
--project-version 26.08.0-SNAPSHOT \
--scala-binary-version 2.12 \
--buildvers 330,358
Then rerun the analyzer on dist/target/parallel-world.
Use JSON output for exact counts or scripts:
python3 dist/scripts/analyze-parallel-world-deps.py \
dist/target/parallel-world \
--format=json \
--limit=50
Reading The Output
Direct classifier-package dependencies should stay small. Any unexpected edge
usually means common code is reaching around the shim boundary.
Nearest version targets from shortest blocked paths ranks the version-specific
classes that block the most root or spark-shared classes. These are often the
best places to add a bridge or descriptor.
Nearest paths from root/spark-shared code to version-specific code shows the
concrete dependency chain to inspect.
Root-safe spark-shared SCCs in dependency-first order is the review queue. Move
or validate earlier components before later components that depend on them.
Review Checklist
Before promoting or keeping a batch:
- Confirm the classes are bitwise-identical across the selected shims.
- Confirm the SCC has no path to version-specific nodes in both Scala lines.
- Check for Databricks or vendor-shim siblings separately; they are common outliers and should stay in their own review unit when possible.
- Keep explicit exception lists small and explain each new entry.
- Validate representative Scala 2.12 and Scala 2.13 builds.