Weight synchronization¶
The trainer’s weights advance every optimizer step; the inference fleet only
sees new weights on a refit. The WeightSynchronizer abstraction
(weight_sync/) owns that transfer and hides the transport and topology from the
GRPO loop, which never branches on backend type or colocation.
The interface¶
WeightSynchronizer (weight_sync/interfaces.py) is a small ABC the loop drives
explicitly:
Method |
Role |
|---|---|
|
Called once at setup, after workers exist. Colocated transports prepare refit metadata; the collective transport also initializes its process group. |
|
Called after every training step. Flags that the inference weights are behind. |
|
Property — |
|
Transfers the latest policy weights into the generation backend. |
|
Releases communication resources. |
The loop’s usage is the staleness handshake: after an optimizer step it
calls mark_stale(); before generating, if is_stale, it calls
sync_weights() and then prepare_for_generation() on the engine.
The interface assumes global weight updates — all generation workers update
atomically and share one weight version. In async GRPO, heterogeneous weight
ages are tracked per-sample in the replay buffer (target_weight_versions), not
here.
Transports¶
create_weight_synchronizer() (weight_sync/factory.py) selects the transport
from the deployment topology and the generation backend:
Topology |
Backend |
Transport |
Class |
|---|---|---|---|
Non-colocated |
vLLM |
NCCL collective |
|
Colocated |
vLLM |
IPC / ZMQ |
|
Colocated |
SGLang |
HTTP |
|
Non-colocated SGLang is not supported (the factory raises NotImplementedError).
The non-colocated path additionally requires train_cluster and
inference_world_size.
Colocated vs. non-colocated lifecycle¶
This distinction shapes what sync_weights() does:
Colocated transports (IPC, HTTP) own GPU phase transitions internally — offload optimizer state before the refit,
prepare_for_generation, offload again after — because the policy and the engine share GPUs and must hand the device back and forth.The NCCL collective transport is a pure data mover. Policy and generation run on separate GPU clusters, so there are no phase transitions; it only forwards weights (and optional FP8 KV-cache scales) over the collective.
For colocated SGLang the engine’s GPU memory is released and re-acquired
around each generation phase when enable_memory_saver is set: finish_generation
posts release_memory_occupation so the server stops holding mem_fraction_static
of the device during training, and prepare_for_generation issues a two-phase
wake_up (weights, then KV cache) to resume it. With enable_memory_saver off the
server stays resident (the historical always-on behaviour). A colocated generation
worker also reserves only a capped GPU fraction
(min(0.2, 1/max_colocated_worker_groups)), while the dedicated, non-colocated
path still claims a full GPU.
sync_weights() accepts an optional kv_scales dict; only the collective
transport honors it, forwarding to policy.broadcast_weights_for_collective().
IPC and HTTP ignore it.
The refit seam¶
Whatever the transport, the trainer must expose its weights in the layout the inference engine expects — HF parameter names and shapes. The policy worker implements a refit seam that maps its internal representation to that layout:
The DTensor backend gathers sharded
DTensorparameters and emits the HF state dict.The JAX backend converts NNX arrays (via dlpack on GPU, numpy on CPU) and applies a model-specific inverse of that model’s HF loader. The dense path transposes linear kernels back to HF layout; Qwen3-MoE additionally re-expands fused expert tensors into the per-expert
experts.{i}.{gate,up,down}_projlayout the inference engine’s fused-MoE kernel consumes; Qwen3-Next adds its linear-attention parameters; and Gemma 4 maps its multi-parameter router and per-layer embedding table, keeping the experts fused because that is the layout its checkpoint and engine use. Each map is the exact inverse of the model’s loader, so a load→refit round-trip reproduces the HF state dict.
prepare_refit_info() reports the {hf_name: (shape, dtype)} map the collective
producer uses to pack the broadcast; the consumer side unpacks it into the
engine’s weights. The NCCL broadcast into a live engine is exercised on hardware
(see handoff/hardware-deferred-validation.md); the name-map inverse and the
JAX→torch value round-trip are covered by CPU tests.