Add elastic data sharding with node-local Ray execution - #1015
Merged
Conversation
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
fengrui-z
requested changes
Jul 24, 2026
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.
Motivation
Processing a large dataset on multiple independent nodes currently requires
users to manually split the input, assign files to nodes, track failures, and
merge the outputs.
This PR adds an elastic sharding example that:
documents the separate Launcher +
mpiruntopology required by MPIJob.This differs from a shared multi-node Ray cluster: the shared filesystem
coordinates work between nodes, while Ray is used independently inside each
node.
What does this PR change?
Elastic shard job
Add
demos/elastic_sharding/shard_job.pywith the following commands:preparemetadata in a manifest.
workerexecutor_type=ray;ray_address=localby default, so each node has an independent Rayruntime;
publishing success, so an expired attempt cannot overwrite or beat a
replacement attempt that already reclaimed the shard.
statusretrymergePAI-DLC entry point
Add a Worker-broadcast
dlcmode todemos/elastic_sharding/dlc_job.py. Keeptwo_node_test.pyas abackward-compatible strict two-node wrapper.
For DLC job types that execute the configured command on every Worker, the job
only needs one configured startup command:
All Worker instances in that topology run the same entry point. They use the shared job
directory to:
The Worker-broadcast entry point does not depend on platform-specific rank
environment variables. PAI-DLC MPIJob has a different topology: its configured
command runs only on the Launcher, so the Launcher must use DLC's generated
hostfile and
mpirun -np N -npernode 1to start one shard worker on each GPUWorker. The MPIJob Workers can still start independent node-local Ray runtimes.
By default, Workers have no per-node claim limit, so the live Workers can
finish all remaining shards even when fewer Workers start than expected.
An optional
--require-all-nodes --nodes Nstrict mode caps claims and verifiesthat all N Worker hostnames participated.
The DLC wrapper scopes
prepare-result.json,abort.json, phase locks, andfinalize-result.jsonto one submission generation. It resolves the generationfrom an explicit
--run-idor fromPAI_JOB_ID,DLC_JOB_ID, orJOB_ID,then stores coordination state below a hash of that identifier. Workers from
the same submission still share idempotent phase results, while a later
submission that reuses the same unchanged job directory no longer inherits a
transient prepare failure, stale abort, or failed finalize result.
Recipe and documentation
Filter operators.
runs through a CPU whitespace Mapper, GPU sentiment and topic classification
Mappers, a GPU CLIP similarity Filter, and a CPU length Filter in the
node-local Ray executor. Every GPU stage uses Ray task mode with
num_proc: 1andnum_gpus: 1, allowing one GPU per node to be reusedsequentially.
one-GPU tasks per GPU operator. This exercises all four cards inside one
claimed shard instead of processing four shards sequentially.
Compatibility
This is an additive demo and does not change existing Data-Juicer execution
paths or operator behavior.
The current implementation only accepts shard-independent Mapper and Filter
operators. Dataset-level operations such as deduplication, selection, grouping,
and aggregation are rejected during preparation.
Requirements and limitations
creation, atomic rename, and
fcntladvisory locks, such as NAS/NFS/CPFS.consistent paths.
ray_address=localstarts an independent Ray runtime for each shard attempt.participate; default elastic mode has no cap.
dlcinvocation needs one stable submission identity.PAI-DLC normally supplies it through a Job ID environment variable; other
launchers must pass the same explicit
--run-idto every Worker and use anew value for each new submission.
Worker and a CUDA-enabled PyTorch environment. Its three Hugging Face models
must be pre-downloaded or downloadable by every Worker.
When records omit optional nested keys, the merged multi-shard output can
represent those keys differently from a one-shard run (
nullversus absent).Record membership is preserved, but Ray output order and byte representation
are not guaranteed across runs. Callers that require byte-identical output
should normalize the input schema and sort by a stable record key after
processing.
Testing
Run:
Result:
The tests cover:
attempt has claimed the shard;
and transient finalize failures;
CPU -> GPU Mapper -> GPU Mapper -> GPU Filter -> CPU chain and explicitly
reserves one GPU for every CUDA operator;
four-way task concurrency, and one GPU per task;
The included Mapper/Filter recipe was also exercised end to end on a CPU-only
host with 64 CPU cores, no visible CUDA devices, Ray 2.55.1, and Data-Juicer
1.5.3:
Every completion record reported
executor_type=rayandray_address=local, and every shard succeeded on its first attempt. The 1-, 2-,and 4-shard runs preserved all six records in source order and produced
semantically equivalent non-null JSON data.
These timings are correctness smoke-test results, not an acceleration
benchmark. For this tiny input, repeated local Ray startup dominates runtime,
so production shards should be large enough to amortize per-attempt startup
cost.
The separate PAI-DLC PyTorch benchmark harness was also tested with:
The skipped test is an opt-in local Ray CPU integration test guarded by
RUN_TWO_NODE_CPU_SMOKE=1. The remaining tests cover deterministic benchmarkinput generation, PyTorchJob rank discovery, shared coordination paths,
failure propagation, GPU recipe validation, output validation, telemetry
summaries, report generation, and atomic test coordination.
PAI-DLC GPU scaling validation
The shard workflow was exercised on real PAI-DLC PyTorch jobs without
mpirun. DLC ran the same startup command on each node and suppliedRANK,WORLD_SIZE,MASTER_ADDR, andMASTER_PORT. Each node had 8 NVIDIA H20 GPUsand ran an independent node-local Ray runtime.
The benchmark recipe used:
num_proc: 8,num_gpus: 1,batch_size: 64, task execution mode, and 64Ray input blocks for every GPU stage.
The following table uses the interval from the earliest shard claim to the
latest shard completion for processing time. End-to-end time covers prepare
start through merge completion.
Both post-fix comparisons exceeded the benchmark's 1.6x initial scaling
threshold.
For the 400,000-row run, processing throughput increased from 363.412 rows/s
on 8 GPUs to 599.506 rows/s on 16 GPUs, and end-to-end latency fell by 38.16%.
Larger shards also amortized fixed model and Ray task overhead:
All 8 GPUs in the single-node runs and all 16 GPUs in the two-node runs
reported nonzero activity. Peak memory remained 1,295 MiB per H20, leaving
substantial room for larger batches. In exp04, increasing the input by 4x
increased processing time by only 2.512x on one node and 2.590x on two nodes;
processing throughput therefore improved by 59.26% and 54.42%, respectively.
Correctness checks passed for every run:
distinct Worker hostnames;
sample_idvalues;sample_id, the complete JSON records from one-node andtwo-node runs were identical;
output order.
These are initial scaling results rather than final statistical benchmarks.
Each configuration was observed once per run. The exp04 one-node and two-node
jobs overlapped in time and may have contended for shared storage or underlying
infrastructure. The exp03 and exp04 code commits also differed, and manifests
contained dirty or unavailable Git state. A final benchmark should use one
clean commit, run the one-node and two-node jobs sequentially, and repeat each
configuration at least three times.
The two-node traces also expose startup skew: exp03's second node claimed work
8.024 seconds after the first, while exp04's delay was 19.408 seconds. For
exp04, removing that skew would improve the estimated processing speedup from
1.650x to approximately 1.699x. A pre-claim all-ranks-ready barrier is a useful
follow-up optimization.
Manual validation status
The real DLC runs validate shared-storage visibility, distinct concurrent
Worker ownership, node-local 8-GPU Ray execution, first-attempt completion,
cross-node failure-aware coordination, final merge, and full semantic output
equivalence. Remaining work is performance tuning and a statistically
controlled repeated benchmark, not functional multi-node validation.