Explainer for NVIDIA-NeMo/RL #4465
(faster Mooncake CPU-RDMA checkpoints). All permalinks pin the PR head 7847eeb.
Background. The data plane moves rollout rows between processes. With the
mooncake_cpu backend, every process is a Mooncake client (it puts and gets rows over RDMA), and
most are also an owner: a block of their RAM, the
segment,
holds rows. A checkpoint save writes each owner's rows to disk, so the save must call every owner and waits for the slowest.
Dotted words have a short explanation: hover or tap them.
A process takes part in a save only if its segment is bigger than 0
(tq_mooncake_checkpoint.py:1390).
So "who is on the save path" is just "who has a segment". The PR changes that in two steps. vLLM workers now attach with
segment_size=0
(vllm_worker_async.py:559).
And when mooncake_cpu.storage_unit_segment_size > 0, every client gets 0
(transfer_queue.py:859)
and new MooncakeStorageUnit actors own all memory
(mooncake_storage_unit.py:42).
That is the same shape the simple backend already had with TransferQueue's SimpleStorageUnit.
Where it lands: RL only. No TransferQueue or Mooncake change. Three lines decide who owns a segment;
the rest is wiring: start_storage_units
in SingleController setup, and the units joining the save list in
single_controller.py:414.
@@ vllm_worker_async.py L550 โ L559 ยท setup_token_capture - dp_client = build_data_plane_client(dp_cfg, bootstrap=False) + # Own no segment: a save would otherwise wait on this actor, whose event + # loop is busy serving rollouts. Capture writes land in other segments. + dp_client = build_data_plane_client(dp_cfg, bootstrap=False, segment_size=0) @@ transfer_queue.py L812 โ L859-L861 ยท _init_tq (the config every client attaches with) - "global_segment_size": int(mooncake_cfg.global_segment_size), + "global_segment_size": 0 + if mooncake_cfg.storage_unit_segment_size + else int(mooncake_cfg.global_segment_size), @@ tq_mooncake_checkpoint.py L1423-L1425 ยท configure_checkpoint_workers manager._checkpoint_workers = list(workers) + _, owners = _live_participants(manager) + manager._checkpoint_workers = list(owners.values())
The save code itself did not change. Both layouts run the same save: call every process with a segment
(:1388-1391).
The only difference is which processes have one. The simple backend gets the same node picker through
simple.storage_unit_placement, which pins TransferQueue's own storage units to chosen nodes
(transfer_queue.py:740).
TypeError: only vLLM accepts the new storage_segments argument. review comment โ setup.py:1971run_grpo.py, SFT v2) leave no process owning memory, so every put fails with a Mooncake error that never mentions storage units. review comment โ transfer_queue.py:860max(4 ร staging_buffer_size, local_buffer_size). review comment โ transfer_queue.py:222placement: inference puts all of them on the vLLM nodes. review comment โ grpo_math_1B.yaml:622