prime-rl from a single GPU to a 1000-GPU cluster: single-node and multi-node deployments, FSDP / expert parallelism / context parallelism, and throughput benchmarking. See Training for detailed documentation of the trainer configuration and Inference for the inference configuration.
Table of Contents
Single-Node vs. Multi-Node Deployment
Therl, sft, and inference entrypoints all accept a [deployment] block (type = "single_node" or "multi_node") that picks how the trainer / orchestrator / inference processes are placed across hardware. Single-node runs locally; multi-node currently goes through SLURM — the launcher writes an sbatch script that places inference replicas, the orchestrator, and the trainer with the right rendezvous endpoints, IPs, ports, and shared-filesystem paths wired in.
Single-Node
RL Placement
rl defaults to 1 trainer GPU and 1 inference GPU. To give inference 6 GPUs with data parallelism and the trainer the remaining 2 on an 8-GPU node:
CUDA_VISIBLE_DEVICES (or all visible GPUs): inference first, trainer next. To target a specific physical subset, pin CUDA_VISIBLE_DEVICES before launching.
For quick A/B ablations on the same node, run two RL instances side-by-side in separate shells, each pinned to half the GPUs and a separate inference port, and track both in one dashboard:
SFT and Torchrun
uv run sft handles distributed launch internally. To scale from 1 to N GPUs, set the deployment GPU count (or just let it pick up WORLD_SIZE). For non-default layouts, the manual equivalent is:
--local-ranks-filter 0 keeps console output to rank 0 only; per-rank stdout/stderr is still captured in <run_dir>/logs/latest/trainer/torchrun/.
Multi-Node
Multi-node deployments (RL or SFT) are launched via SLURM — set[deployment] type = "multi_node" plus the matching [slurm] block, and the launcher writes the sbatch script that places inference, orchestrator, and trainer across the requested nodes with the inter-process wiring set up correctly. See SLURM § Examples for full configs.
Parallelism Knobs
FSDP
FSDP2 is the default model sharding strategy. By default the trainer fully shards parameters, gradients, and optimizer state across the data-parallel mesh. Tweakable knobs:Expert Parallelism
EP shards MoE expert weights across the EP mesh, dramatically reducing the FSDP communication volume per layer and improving the training throughput.ep defaults to "auto". At startup it loads the model config to check whether the model is MoE. For MoE models it resolves to min(world_size // dp_replicate, 8): the FSDP island size, capped at 8. For non-MoE models it resolves to 1 (no-op). "auto" does not look at num_experts or cp; the resolved degree must still be a multiple of cp and divide num_experts, so set it explicitly when it does not. Set ep to an explicit integer to override:
type = "deepep" and tune num_sms plus optional token_chunk_size in the same dispatch table. Routed-expert precision is selected separately with [trainer.model.moe.compute] (bf16, deepgemm_fp8, or mxfp8).
Context Parallelism
CP shards a single sequence across multiple GPUs along the token dimension — for long-context sequences. Preferulysses: it gets the most throughput and is the only style that works for hybrid linear-attention/Mamba models (Qwen3.5, NemotronH) and for VLMs. Each model class declares its supported styles in cp_support; an unsupported cp_style, or a model with no CP support at all, is rejected at setup.
ulysses head-shards Q/K/V, so the CP degree must divide num_attention_heads. GQA models with fewer KV heads than the CP degree (e.g. NemotronH: 32 query heads, 2 KV heads) are supported via KV-head replication; the CP degree must then be a multiple of num_key_value_heads. Nemotron-H redistributes its Mamba activations between sequence- and head-parallel layouts in the model-owned Mamba path; the CP degree must divide mamba_num_heads and n_groups.
Activation Checkpointing and Offloading
AC and AC offloading are enabled by default (full mode). Both AC modes retain stateful MoE routing updates and DeepEP communication that cannot safely replay. Selective AC uses the same transformer-block boundaries and additionally retains distributed communication, expensive matrix multiplications, grouped GEMMs, and supported attention kernels:
targets to operator names (for example, "aten::mm") or namespaces (for example, "prime_rl_collectives") to replace the default selective targets. Correctness-required operations remain retained.
Activation offloading still applies to tensors saved by autograd, but tensors retained by the checkpoint policy remain on the accelerator.
ac_offloading is also on by default with max_inflight_activations = 5. We’ve observed this feature to be very effective, lowering the peak memory usage by 30-40% in some cases, while only losing ~3-5% of throughput. To disable either, set model.ac = "None" or model.ac_offloading = "None".
Optimizer Offloading
State-only optimizer offload is enabled by default (model.offload = "optimizer"). For full offload, set model.offload = "full"; this keeps BF16 compute weights on GPU and runs CPU optimizer chunks as gradients become ready during backward. Full offload only supports AdamW and SignSGD (optim.type = "sign_sgd") and disables gradient clipping. SignSGD is stateless, so it halves the host RAM footprint versus AdamW (8 instead of 16 bytes per parameter: FP32 master + FP32 accumulated gradient, no moments).
LM Head Chunking
The vanilla LM head materializes a[batch * seq, vocab] logits tensor on every step — a major memory tax when the vocabulary is large (often >100K). fused_lm_head_token_chunk_size swaps in a custom fused linear + logprob/entropy kernel that streams through chunk_size tokens at a time, avoiding the materialization. It defaults to 8192 for RL training:
Memory-Tight Recipe
The kitchen-sink config for fitting large MoE on limited GPUs at acceptable throughput. AC, AC offloading, compile, fused LM head chunking, optimizer offload, and EP auto-resolution are on by default — only CP needs to be set explicitly (and EP overridden if auto-resolution is not desired):8192), torch.compile (fullgraph=False), AC (full mode), AC offloading (max_inflight_activations=5), and optimizer CPU offload. Walks through every memory lever in order: FSDP+EP shard the weights, CP shards the activations along the token dim, AC + AC offloading shrink the activation footprint, fused LM head chunks the loss, torch.compile reduces fragmentation, optim offload moves Adam state off GPU. Apply selectively — each knob has a throughput cost.
SLURM
Therl, sft, and inference entrypoints all submit to SLURM when a [slurm] table is present — there’s no separate entrypoint.
The prime-rl checkout and itsCompile and JIT caches are node-local by default: the launcher setsuvvenv must live on a shared filesystem visible to every node. The generated sbatch script runs a singleuv sync --all-extras --all-packageson the batch node (not once per node), so all ranks share that one environment — a node-local venv would leave the other nodes stale.
TRITON_CACHE_DIR=/tmp/triton-<uid> for every component, and VLLM_CACHE_ROOT=/tmp/.vllm-cache-<uid> and FLASHINFER_WORKSPACE_BASE=/tmp/.flashinfer-cache-<uid> for inference. Ranks writing one cache on a shared filesystem can crash or hang. Override them in [env_vars] or a component’s env_vars.
Activation
A SLURM config is usually a thin overlay that adds[slurm] (and [deployment] for multi-node) on top of a base config. Configs are composed left-to-right via the @ CLI syntax — see Configuration § TOML Composition:
<run_dir>/launcher/. SLURM stdout and stderr go to launcher/logs/. Local launches do not create launcher/.
[deployment] Block
[deployment] is a discriminated union picked by type — single_node or multi_node for RL/SFT, with an extra disaggregated variant for inference. RL multi-node:
Examples
Full multi-node configs ship underexamples/advanced/:
nemotron-3-super/swe.toml— 4 trainer + 1 inference node RL on a 120B hybrid-Mamba MoE with NCCL weight broadcast.glm-5.3/— large-scale and P/D-disaggregated inference across the GLM-5 family.glm-4.5-air/swe-2-node.toml— train a 100B+ MoE on only two nodes: a single-node sign-SGD trainer (full optimizer offload) paired with one inference replica at batch size 64. An overlay on the sharedswe-budget.tomlbase — launch withuv run rl @ swe-budget.toml @ swe-2-node.toml.
[deployment] type = "multi_node" on an inference TOML — each node runs an independent vLLM replica (TP and DP must fit within one node), fronted by a single global router on node 0. Point clients at the router URL the launcher prints.
NIXL weight broadcast
Set[weight_broadcast] type = "nixl" to use receiver-driven NIXL weight transfer. Before the first SLURM run, install the NIXL/UCX build and the ModelExpress service binaries on the shared filesystem:
slurm.launch_modelexpress = false and configure weight_broadcast.host and weight_broadcast.port.
The launcher requires the CUDA and InfiniBand transports from third_party/ucx. Each NIXL process selects the active InfiniBand port nearest its GPU; an explicitly configured UCX_NET_DEVICES takes precedence. Inference ranks start their pulls at different trainer ranks so concurrent workers distribute traffic across all available source rails.
ModelExpress exchanges peer metadata during startup. Weight updates reuse prepared NIXL requests, post every trainer-rank read in a transfer group concurrently, and use versioned NIXL notifications for source readiness and buffer credits.
By default, the trainer and inference worker each allocate one transfer arena. Set weight_broadcast.overlap_transfer_and_replay = true to allocate two arenas on both sides and replay one weight group while receiving the next. The additional arena is the size of the largest transfer group per GPU; allocation errors are reported instead of silently disabling overlap.
Custom Templates
For unusual partitions, module loads, or environment setup, supply your own Jinja2 template:src/prime_rl/templates/ — copy one as a starting point.
Benchmarking
To benchmark a parallelism config before committing a multi-day run, run a short training with fake data and a step cap, and read throughput / MFU / step time / peak memory from the logs:Throughput, MFU, and Peak Mem. to the console. For machine-readable numbers, the file monitor writes monitors/file/metrics.jsonl under the run’s output directory by default (monitors.file); aggregate perf/throughput, perf/mfu, time/step, and perf/peak_memory from the run’s metrics.jsonl — skip the first step, it is warmup. benchmarks/scripts/run_single_benchmark.py does exactly this and is what the CI benchmark matrix runs.