Summary
This RFC proposes delta-based weight sync for disaggregated RL with SkyRL. The trainer computes byte-exact XOR deltas against the previously published BF16 checkpoint-format weights, compresses the deltas, publishes a manifest, and asks inference workers to fetch/process the next version before pausing generation. During the weight update, inference workers load the prepared weights from disk.
Motivation
RL training on disaggregated clusters allows users to better utilize available resources in disparate regions , different cloud providers or different hardware. A key bottleneck for disaggregated RL is weight transfer between inference and trainer nodes. During RL training, most of the weights are typically not updated in a given training step. A recent study found that only about 1-3% of the weights change after an optimization step, observed across model families in both sync and async RL setups (link). Disaggregated RL is thus enabled with delta weight updates: transmit only the compressed deltas every weight sync.
Goals
- Support RL training in fully disaggregated clusters: Support RL with training and inference running on separate clusters. 1
- Support weight sync with delta weights: Support weight syncing in SkyRL with delta weights for checkpoint-to-storage: Weight transfer via checkpoint-to-disk or cloud storage with training and inference workers in separate clusters.
- Support weight transfer for different quantization schemes: Support weight transfer between training and inference where inference engine can use different quantization schemes or kernels (specifically, support weight transfer in checkpoint format in vLLM)
- One interesting use-case: Train on NVIDIA in BF16 but do inference on AMD in FP8.
Non-Goals
- Support sparse weight transfer via NCCL: Sparse weight transfer can also be used for RL training in the same ray cluster to optimize payload during weight sync. However, this is not a focus of this doc. There is additional latency in delta computation that leads to worse performance. Further, there are additional complications around vLLM weight loading here (to support goal 3.) that would complicate the weight transfer design.
Functional Requirements
- Compute exact deltas of weights on the trainer without any precision related rounding errors.
- Transfer deltas from the trainer in BF16 checkpoint-format2 weights with some form of lossless compression to save on network bandwidth
- Compute the updated weights on the inference engine in an exact way from the synced deltas and from the previous BF16 weights. Verify with reference post-delta checksum on the trainer. A downstream effect we would like to avoid is any potential drift from repeated weight sync of deltas.
- Scale delta-based weight sync to large model sizes (350B+).
Design
Before going into the design, it is helpful to categorize the steps on the inference side as two operations:
- Fetch + Process: Fetch the delta weights from storage and process them to compute the new updated weights
- Load: Load the new weights into the inference engines. This can involve additional postprocessing ex: quantization
Overall trainer flow:
- Store a CPU based snapshot of the previous version of the weights in uint8 on rank 0
- During a weight sync, iterate over the parameters chunk by chunk and all gather the weights for a chunk
- Copy a chunk from GPU to CPU
- Convert to bytes and compute XOR deltas for all the weights
- Compress the deltas using lossless compression and store the deltas on CPU
- Update the CPU based snapshot to use the new uint8 bytes for the current weight chunk
- After computing deltas for all the weights, publish to cloud storage
Overall inference flow:
- Split up Fetch + Process and Load into two separate stages
- Run Fetch + Process before pausing generation (with a
/fetch_weights endpoint).
- Pause generation and run Load (with
/update_weights)
The sequence of operations in a weight update are: /fetch_weights -> /pause -> /start_weight_update -> /update_weights -> /finish_weight_update -> /resume
Other Design Options Considered
Option 1: CPU Snapshot on Trainer, CPU Snapshot on Inference, Deltas applied to the live parameters
Note that we need a CPU snapshot of BF16 dtype weights on the inference side due to requirement 3. Applying the diff to the live parameters could be done in the following way:
- Store a snapshot of the BF16 weights on CPU for each worker. Each worker only needs to think about its shard, so the snapshot contains sharded weights per parameter and it is in worker format (see : Delta weight updates in SkyRL)
- On receiving weights from the trainer, stage the sharded snapshot on CPU into the layerwise reload buffers on GPU
- Process the received deltas via
model.load_weights as a dummy full parameter tensor (NaNs in elements that are not present)
- In the final copy operation, we copy the transformed weights into the buffers. However, instead of a direct copy, we do a modified version where we apply the non-NaN parameters to the layerwise buffer as a delta, and then copy into parameter memory
- Update the sharded snapshot on CPU
This approach can also support in-cluster, NCCL based delta weight updates in a similar way - deltas can be transmitted via NCCL and the same approach for applying deltas holds.
This approach has the following cons:
- Layerwise reload complexity : Need to deal with internals of layerwise reloading and delta some internal vllm loading functions
- Fully synchronous fetch and load
- Large CPU memory requirements on Inference:
(number of parameters x num_engines_per_node x 2) bytes
- Large CPU memory requirements on the Trainer:
(number of parameters x 2)
Option 2: CPU Snapshot on Trainer, Disk based Snapshot on Inference, Deltas applied on the fly
At a high level, the flow on the inference side is:
- Inference ranks will store the current version of the weights in BF16 model loading format (“checkpoint format”) on local disk.
- During a weight update, one of the inference ranks will first download the delta to the local disk if not already done.
- Each rank will iterate over the saved weights on disk parameter by parameter, load the corresponding delta and compute the updated weights.
- One of the inference ranks in each node will trigger a background update and save the updated weights in a new location on disk
- The updated weights are fed to
model.load_weights as an iterator.
- The updated weights on disk will be used as the reference weights for the next update.
This approach has the following cons:
- Fully synchronous fetch + load
- Large CPU memory requirements on the Trainer:
(number of parameters x 2)
- Slower disk based process on the inference side: Even with some pipelining, the bottleneck is synchronous writes into disk during each weight update
The chosen design has the same CPU memory requirements on the Trainer as the above options, but it enables much smaller pause times with decoupled fetch and load.
Detailed Design
For the detailed design, please refer to the design doc
Detailed Flow
Trainer Flow
For each sync:
- All-gather weights on GPU and convert to Huggingface format on rank 0
- Compare each tensor against the trainer's last published snapshot on CPU
- Build XOR delta bytes for changed tensors.
- Compress each delta with zstd.
- Upload compressed deltas as safetensor files with deltas keyed by parameter name, along with
manifest.json
- Advance the trainer snapshot after the version has been written successfully.
- Ask inference workers to fetch and apply that exact version.
Note: the current implementation only performs the delta computation and upload on rank 0, but the design allows for multiple senders.
Receiver Flow
/fetch_weights
- Inference ranks receive a weight update request from the trainer/ controller with the delta manifest.
- Inference ranks will inspect the current local checkpoint directory and try to grab the writer lock. The rank that grabs the lock first will download the new delta into the local directory and write the new version of the weights. Other ranks wait until the lock is released.
- The writer rank will check the
target_version by the controller as well as the version currently available in local_checkpoint_dir by reading state.json.
- The writer rank marks
write_in_progress as true in state.json
- Until the current
version becomes the target_version, perform the following:
- Download the delta file for
version +1
- For each delta tensor present in the delta file, load the corresponding tensor from
weights/ on disk using the tensor metadata
- Decompress the delta and apply the delta to the tensor to get the new weights
- Save the new tensors to disk
- Writer rank releases the writer lock and marks
write_in_progress as false.
/update_weights3
- Each rank inspects the delta manifest received from the trainer
- Ranks inspect the
local_checkpoint_dir to verify that the target_version of the weights are present on disk. If not present, ranks error out.
- If present, the ranks will proceed to iteratively load the weights from disk to GPU memory
Implementation
| Component |
Category |
Responsibility |
| DeltaWeightSyncConfig |
Config |
User-facing configuration for checkpoint-delta weight sync |
| DeltaTransferStrategy |
Weight Sync |
Creates the sender, receiver init payload and vLLM transfer engine |
| DeltaWeightTransferSender |
Trainer |
Owns the trainer-side weight send logic |
| DeltaPublisher |
Trainer |
Maintains the CPU byte snapshot, computes XOR deltas, compresses payloads and publishes manifests |
| DeltaManifest / DeltaTensorRecord |
Data Model |
Version and per-tensor metadata for a published delta |
| RemoteInferenceClient.fetch_weights |
Control Plane |
Runs the pre-pause receiver-side fetch+process phase on every inference worker |
| DeltaWeightTransferEngine |
Inference |
vLLM transfer engine for delta weight sync |
| LocalCheckpointStore |
Inference |
Maintains the local checkpoint and applies deltas into it |
For more details on the implementation, please see the design doc
Prior Work
The current design is inspired by the decoupled fetch and load design implemented in Slime, and is inspired by blogs from Fireworks and Cognition on delta weight updates which detail the same ideas. The SkyRL design differs in the trainer integration, the manifest schema, the local checkpoint store behaviour and in integrating with vLLM.
Further reading
For more details on limitations and performance with the above approach, refer to the doc.
Delta weight sync roadmap
We will put up a roadmap of features for delta weight sync in the RFC after initial support is merged.
Summary
This RFC proposes delta-based weight sync for disaggregated RL with SkyRL. The trainer computes byte-exact XOR deltas against the previously published BF16 checkpoint-format weights, compresses the deltas, publishes a manifest, and asks inference workers to fetch/process the next version before pausing generation. During the weight update, inference workers load the prepared weights from disk.
Motivation
RL training on disaggregated clusters allows users to better utilize available resources in disparate regions , different cloud providers or different hardware. A key bottleneck for disaggregated RL is weight transfer between inference and trainer nodes. During RL training, most of the weights are typically not updated in a given training step. A recent study found that only about 1-3% of the weights change after an optimization step, observed across model families in both sync and async RL setups (link). Disaggregated RL is thus enabled with delta weight updates: transmit only the compressed deltas every weight sync.
Goals
Non-Goals
Functional Requirements
Design
Before going into the design, it is helpful to categorize the steps on the inference side as two operations:
Overall trainer flow:
Overall inference flow:
/fetch_weightsendpoint)./update_weights)The sequence of operations in a weight update are:
/fetch_weights->/pause->/start_weight_update->/update_weights->/finish_weight_update->/resumeOther Design Options Considered
Option 1: CPU Snapshot on Trainer, CPU Snapshot on Inference, Deltas applied to the live parameters
Note that we need a CPU snapshot of BF16 dtype weights on the inference side due to requirement 3. Applying the diff to the live parameters could be done in the following way:
model.load_weightsas a dummy full parameter tensor (NaNs in elements that are not present)This approach can also support in-cluster, NCCL based delta weight updates in a similar way - deltas can be transmitted via NCCL and the same approach for applying deltas holds.
This approach has the following cons:
(number of parameters x num_engines_per_node x 2)bytes(number of parameters x 2)Option 2: CPU Snapshot on Trainer, Disk based Snapshot on Inference, Deltas applied on the fly
At a high level, the flow on the inference side is:
model.load_weightsas an iterator.This approach has the following cons:
(number of parameters x 2)The chosen design has the same CPU memory requirements on the Trainer as the above options, but it enables much smaller pause times with decoupled fetch and load.
Detailed Design
For the detailed design, please refer to the design doc
Detailed Flow
Trainer Flow
For each sync:
manifest.jsonNote: the current implementation only performs the delta computation and upload on rank 0, but the design allows for multiple senders.
Receiver Flow
/fetch_weights
target_versionby the controller as well as the version currently available inlocal_checkpoint_dirby readingstate.json.write_in_progressastrueinstate.jsonversionbecomes thetarget_version, perform the following:version+1weights/on disk using the tensor metadatawrite_in_progressasfalse./update_weights3
local_checkpoint_dirto verify that thetarget_versionof the weights are present on disk. If not present, ranks error out.Implementation
For more details on the implementation, please see the design doc
Prior Work
The current design is inspired by the decoupled fetch and load design implemented in Slime, and is inspired by blogs from Fireworks and Cognition on delta weight updates which detail the same ideas. The SkyRL design differs in the trainer integration, the manifest schema, the local checkpoint store behaviour and in integrating with vLLM.
Further reading
For more details on limitations and performance with the above approach, refer to the doc.
Delta weight sync roadmap
We will put up a roadmap of features for delta weight sync in the RFC after initial support is merged.
Footnotes
We previously had this with the GLOO-based weight sync (over TCP/IP) in SkyRL (back in the old days), but it is no longer supported ↩
See Appendix: Delta weight updates in SkyRL for terminology on weight loading ↩
Weight updates have three API calls:
/start_weight_update,/update_weightsand/finish_weight_update. We only mention the second here since it contains the core weight update operations ↩