Skip to content

14.4 Distributed Sync, Async, and MoE Training

Appendix B.1 RL Training Systems already covered the fundamentals — sampling, asynchrony, distributed parallelism. This chapter raises the vantage point to framework-level architecture and frontier industrial practice: how veRL uses HybridFlow to orchestrate multiple models under one roof, how AReaL/LlamaRL use full asynchrony to break down the wall between generation and training, how DeepSeek V3's DualPipe does pipeline parallelism on MoE, and how a 10,000-GPU cluster gets profiled and tuned.

36.1 A Deep Dive into the veRL Architecture

veRL (Volcano Engine Reinforcement Learning) is the RL training framework ByteDance open-sourced in 2024, described in HybridFlow, arXiv:2409.19256. It has become the de facto mainstream framework for LLM RL training, adopted by the training scripts of teams like Qwen, DeepSeek, Llama, and Mistral.

The Core Design of HybridFlow

HybridFlow abstracts RLHF/GRPO/PPO training as single-controller multi-model orchestration:

┌─────────────────────────────────────────────────────────┐
│              Single Controller (Driver)                  │
│  - Algorithm logic (the PPO/GRPO main loop)              │
│  - Resource scheduling (which GPUs run which model)      │
└──────────┬──────────────────────────────────────────────┘

   ┌───────┼───────┬─────────────┬─────────────┐
   │       │       │             │             │
   ▼       ▼       ▼             ▼             ▼
┌──────┐ ┌──────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│Actor │ │Critic│ │Reference │ │Reward    │ │Rollout   │
│(FSDP)│ │(FSDP)│ │(Frozen)  │ │Model     │ │Engine    │
│      │ │      │ │          │ │          │ │(vLLM)    │
└──────┘ └──────┘ └──────────┘ └──────────┘ └──────────┘
   ▲       ▲       ▲             ▲             ▲
   │       │       │             │             │
   └───────┴───────┴─────────────┴─────────────┘
              ResourcePool (a GPU set)

Three Core Abstractions

1. ResourcePool

GPUs are grouped, and each group can host one or more models:

python
# veRL config example (simplified)
resource_pools = {
    "actor_pool": num_gpus=8,    # Actor gets 8 GPUs
    "critic_pool": num_gpus=4,   # Critic gets 4 GPUs
    "rollout_pool": num_gpus=8,  # Rollout gets 8 GPUs
    "ref_pool": num_gpus=2,      # Reference model gets 2 GPUs
}

Different models can either share GPUs (colocate) or have dedicated GPUs (disaggregated):

python
# Colocate: Actor and Rollout share the same GPU group
mapping = {
    "actor": "actor_rollout_pool",
    "rollout": "actor_rollout_pool",  # shared!
    "critic": "critic_pool",
    "ref": "ref_pool",
}

2. Worker

Each Worker is an independent model instance that encapsulates the concrete training/inference logic:

python
class ActorWorker:
    def __init__(self, model_config):
        self.model = FSDPActor(model_config)

    def update(self, batch):
        # PPO/GRPO loss computation + backward pass
        loss = compute_ppo_loss(batch, self.model)
        loss.backward()
        self.optimizer.step()

    def get_weights(self):
        # Sync weights out to the Rollout Engine
        return self.model.state_dict()

class RolloutWorker:
    def __init__(self, model_config):
        self.engine = vLLMEngine(model_config)

    def generate(self, prompts):
        return self.engine.generate(prompts)

    def sync_weights(self, new_weights):
        self.engine.load_weights(new_weights)

3. Driver (Single Controller)

The Driver is the main loop of the RL algorithm — it orchestrates all the Workers:

python
class PPODriver:
    def train(self, num_epochs):
        for epoch in range(num_epochs):
            # 1. Have the Actor expose its current weights to Rollout
            weights = self.actor_worker.get_weights()
            self.rollout_worker.sync_weights(weights)

            # 2. Sample with the current policy
            prompts = sample_prompts(self.dataset)
            responses = self.rollout_worker.generate(prompts)

            # 3. Score rewards with the Reward Model
            rewards = self.reward_worker.score(prompts, responses)

            # 4. Compute values with the Critic
            values = self.critic_worker.value(prompts, responses)

            # 5. Compute advantages + PPO loss, update the Actor
            advantages = compute_gae(rewards, values)
            self.actor_worker.update(prompts, responses, advantages)

            # 6. Update the Critic
            self.critic_worker.update(prompts, responses, rewards)

What "Hybrid" Means in HybridFlow

Hybrid refers to a unified hybrid-parallel strategy — within a single framework you can combine:

  • 3D Parallelism: TP (tensor parallel) × PP (pipeline parallel) × DP (data parallel)
  • Colocate vs. Disaggregated: models can share GPUs or hold them exclusively
  • Multiple training backends: FSDP, Megatron, DeepSpeed ZeRO
  • Multiple inference backends: vLLM, SGLang, HuggingFace generate

veRL is the first framework to make all of these dimensions configurable. DeepSpeed-Chat and OpenRLHF are more constrained along some of these axes.

Core Differences from Other Frameworks

DimensionveRL (HybridFlow)OpenRLHFNeMo-AlignerTRL
OrchestrationSingle-controllerSingle-controllerMulti-controllerSingle-process
Resource allocationArbitrary combinationStrict separationNVIDIA stackSingle GPU
Training backendFSDP + MegatronFSDP/DeepSpeedMegatronAccelerate
Inference backendvLLM/SGLangvLLMTRT-LLMHF generate
Typical scale8-1024 GPUs8-256 GPUs8-512 GPUs1-8 GPUs

Chapter 7's GRPO practice is built on veRL.

36.2 OpenRLHF / NeMo-Aligner / TRL Compared

OpenRLHF

OpenRLHF, arXiv:2405.11143 is maintained by the OpenLLMAI team and is one of the earliest open-source RLHF frameworks.

Core design:

  • Distributed scheduling built on Ray
  • Strict Actor/Critic/Ref/RM separation — each model runs in its own Ray Actor process
  • Emphasis on simplicity and ease of use
python
# OpenRLHF PPO training (pseudocode)
from openrlhf import PPOTrainer, ModelGroup

actor = ModelGroup(num_gpus=8, backend="deepspeed")
critic = ModelGroup(num_gpus=8, backend="deepspeed")
ref = ModelGroup(num_gpus=4)
reward = ModelGroup(num_gpus=4)
vllm = VLLMRollout(num_gpus=8)

trainer = PPOTrainer(actor, critic, ref, reward, vllm)
trainer.train(dataset, num_epochs=100)

Where it fits: research settings and medium-scale training (8-256 GPUs). SimpleRL and Llama-3.1 post-training have both used OpenRLHF.

NeMo-Aligner

NeMo-Aligner is NVIDIA's official stack, deeply integrated with Megatron-LM and TRT-LLM.

Core design:

  • Megatron training backend (the strongest large-model parallelism story available)
  • TRT-LLM inference backend (NVIDIA's own inference optimization)
  • Favors full-stack NVIDIA optimization

Where it fits: NVIDIA clusters, very large models (70B+), workloads chasing peak performance. The Nemotron series and Llama-3 training on NVIDIA clusters both use NeMo.

TRL (Transformer Reinforcement Learning)

TRL is HuggingFace's lightweight framework.

Core design:

  • Built on Accelerate (HuggingFace's distributed abstraction)
  • Single-process model, sharded automatically by Accelerate
  • Ease of use above all — 10 lines of code to run PPO
python
from trl import PPOTrainer, PPOConfig, AutoModelForCausalLMWithValueHead

model = AutoModelForCausalLMWithValueHead.from_pretrained("gpt2")
config = PPOConfig(batch_size=8)
trainer = PPOTrainer(config, model)
trainer.train(dataset)

Where it fits: learning, prototyping, small-scale experiments (1-8 GPUs). Not suited for production-grade training.

The Four Frameworks Compared

FrameworkEase of usePerformanceScale ceilingIndustrial adoption
veRLMediumHigh1024+ GPUsQwen, DeepSeek, internal at ByteDance
OpenRLHFHighMedium256 GPUsSimpleRL, various open-source projects
NeMo-AlignerLowVery high512+ GPUsNVIDIA customers, Nemotron
TRLVery highLow8 GPUsResearch, teaching

Recommendation:

  • Learning, prototyping: TRL
  • Research, medium scale: OpenRLHF or veRL
  • Large-scale production: veRL or NeMo-Aligner (depending on the hardware stack)

36.3 The Rollout Engine and vLLM Integration

99% of RL training time is spent on rollout (Appendix B.1). The rollout engine is the core of the performance bottleneck, and vLLM is the de facto standard.

The Core Optimizations in vLLM

1. PagedAttention

A conventional KV cache is allocated contiguously, which leads to severe memory fragmentation. vLLM borrows the paging mechanism from operating systems and splits the KV cache into fixed-size blocks:

python
# Conventional: KV cache allocated contiguously
seq_len = 2048
kv_cache = torch.empty(batch_size, seq_len, num_heads, head_dim)
# Memory utilization: 50-70%

# vLLM PagedAttention: block-based
block_size = 16
blocks = allocate_blocks(num_blocks)
# Memory utilization: 95%+

Memory utilization goes from 50-70% to 95%+, and batch size grows 2-4×.

2. Continuous Batching

Conventional batching waits until an entire batch has finished generating before swapping it out. vLLM does dynamic batching instead — the moment a sequence finishes, a new one takes its place:

Time:  ──────────────────────────────────────►
Seq A: [tok][tok][tok][tok][EOS]
Seq B: [tok][tok][tok][tok][tok][tok][EOS]
Seq C:           [tok][tok][tok][tok][EOS]  ← joins right after A finishes
Seq D:                    [tok][tok][tok][EOS]  ← joins right after C finishes

Throughput improves 5-10× over static batching.

3. Speculative Decoding

A small model drafts a handful of tokens first, and the large model verifies them in parallel:

python
def speculative_decode(prompt, draft_model, target_model, num_draft=4):
    while not done:
        # 1. The small model generates num_draft tokens
        draft_tokens = draft_model.generate(prompt, max_tokens=num_draft)

        # 2. The large model verifies them in parallel
        target_logits = target_model.forward(prompt + draft_tokens)

        # 3. Accept tokens that match, reject and regenerate otherwise
        for i, token in enumerate(draft_tokens):
            if target_logits[i].argmax() == token:
                prompt.append(token)
            else:
                prompt.append(target_logits[i].argmax())
                break

Throughput improves 2-3× (for typical LLM inference).

vLLM's Role in RL Training

Inside veRL, vLLM serves as the RolloutWorker:

python
class VLLMRolloutWorker:
    def __init__(self, model_path, tensor_parallel_size=8):
        from vllm import LLM
        self.engine = LLM(
            model=model_path,
            tensor_parallel_size=tensor_parallel_size,
            enable_prefix_caching=True,  # key: reuse KV across multiple samples of the same prompt in GRPO
            gpu_memory_utilization=0.9,
        )

    def generate(self, prompts, sampling_params):
        # Batch generation
        return self.engine.generate(prompts, sampling_params)

    def sync_weights(self, new_weights):
        # vLLM 0.5+ supports online weight updates
        self.engine.load_weights(new_weights)

Prefix Caching matters especially for GRPO — when the same prompt produces responses, the KV cache for the shared prefix (the prompt portion) can be reused, saving 70-80% of the memory and time.

SGLang: vLLM's Challenger

SGLang, developed by the LMSYS team, outperforms vLLM in agentic settings:

  • RadixAttention: manages the KV cache with a radix tree, reusing it across requests
  • Programmatic Frontend: supports complex control flow (multi-turn calls, branches, loops)
  • Constrained Decoding: built-in JSON and regex-constrained generation

In industrial practice:

  • vLLM: general-purpose rollout, single-turn generation
  • SGLang: agentic rollout, multi-turn, structured output
  • TRT-LLM: extreme optimization on NVIDIA hardware

36.4 GPU Memory Optimization: ZeRO, FSDP, Gradient Checkpointing

Memory is the core bottleneck in LLM training. A 70B model trained in bf16 with full parameters needs ~1.5 TB of memory — an 80GB H100 can't come close to holding that on its own.

Breaking Down Training Memory

Training memory has four components:

For a 70B model:

  • Weights: 140 GB
  • Gradients: 140 GB
  • Adam state (m, v, master weights): 560 GB
  • Activations: ~100 GB (depends on batch size and sequence length)
  • Total: ~940 GB

A single 80GB H100 falls far short.

ZeRO (Zero Redundancy Optimizer)

DeepSpeed ZeRO, arXiv:1910.02054 shards the training state across multiple GPUs:

StageWhat's shardedSavingsCommunication cost
ZeRO-1Optimizer stateLow
ZeRO-2Optimizer + GradientMedium
ZeRO-3Optimizer + Gradient + Weight× (N = number of GPUs)High

ZeRO-3 shards the weights too, so each GPU only stores of them — but the forward and backward passes need an all-gather to reconstruct them.

python
# DeepSpeed ZeRO-3 config
config = {
    "zero_optimization": {
        "stage": 3,
        "overlap_comm": True,
        "contiguous_gradients": True,
        "sub_group_size": 1e9,
        "reduce_bucket_size": 5e8,
    },
    "bf16": {"enabled": True}
}

FSDP (Fully Sharded Data Parallel)

PyTorch's native equivalent of ZeRO-3, and easier to use than DeepSpeed:

python
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP

model = LlamaForCausalLM(config)
model = FSDP(
    model,
    sharding_strategy=ShardingStrategy.FULL_SHARD,  # equivalent to ZeRO-3
    mixed_precision=MixedPrecision(param_dtype=torch.bfloat16),
    cpu_offload=CPUOffload(offload_params=False),  # optional CPU offload
)

veRL defaults to FSDP — it's more stable than DeepSpeed and fits more naturally into the PyTorch ecosystem.

Gradient Checkpointing

Instead of sharding the model, this trades compute for memory — intermediate activations aren't saved during the forward pass, and are recomputed during the backward pass:

python
from torch.utils.checkpoint import checkpoint

class CheckpointedBlock(nn.Module):
    def forward(self, x):
        # Wrap the transformer block with checkpoint
        return checkpoint(self._forward, x, use_reentrant=False)

    def _forward(self, x):
        return self.transformer_block(x)

Activation memory drops from to ( is the number of layers), at the cost of running the forward pass twice — training slows down 20-30%.

The Memory Budget When Combining All Three

For a 70B model (8 H100 80GB GPUs):

ConfigurationPer-GPU memoryTraining speed
Full parameters + Adam (baseline)940 GB (OOM)-
ZeRO-3118 GB (OOM)-
ZeRO-3 + Gradient Checkpointing30 GB
ZeRO-3 + Gradient Checkpointing + LoRA8 GB1.2×

LoRA (Chapter 6) trains only a small set of parameters, cutting memory requirements sharply. Industrial-grade 70B RL training typically uses LoRA + FSDP.

36.5 Asynchronous RL Training

The bottleneck of synchronous training is covered in detail in Appendix B.1 — the GPU sits 99% idle waiting for rollout. Asynchronous training decouples generation from training so the two run at the same time. Below are three flagship 2025 frameworks that put this into practice.

LlamaRL

LlamaRL, Meta arXiv:2505.24034 is the distributed RL framework Meta released in May 2025.

Core innovation: fully decentralized — there is no master node; every worker is autonomous.

python
# LlamaRL architecture (simplified)
class LlamaRLWorker:
    def run(self):
        while True:
            # Each worker decides its own job
            if self.role == "rollout":
                prompts = self.fetch_from_queue()
                responses = self.generate(prompts)
                self.push_to_train_queue(responses)

            elif self.role == "train":
                batch = self.fetch_from_rollout_queue()
                self.update(batch)
                self.broadcast_weights()  # asynchronous broadcast

Advantages:

  • No single point of failure
  • Easy to scale horizontally (just add workers)
  • Suited to extreme scale (10k+ GPUs)

Measured result: running Llama-3-70B GRPO on 4096 GPUs, it's 10.4× faster than synchronous training.

AReaL (Asynchronous RL)

AReaL: A Large-Scale Asynchronous Reinforcement Learning System for Language Reasoning, arXiv:2505.24298 is a large-scale asynchronous LLM RL system open-sourced by Ant Group and Tsinghua in 2025.

Core innovation: fully asynchronous rollout + staleness-aware PPO. Rollout workers continuously generate samples, and training workers consume a batch the moment it's available. The system controls sample staleness and adds a correction term in the PPO update for samples generated by a stale policy, mitigating the drift that comes from "the generating policy is already K steps behind the current training policy."

python
# AReaL's key algorithm (simplified)
def staleness_aware_update(batch, current_weights):
    # batch records the policy version and log-prob at rollout time
    gen_log_probs = batch["gen_log_probs"]
    current_log_probs = compute_log_probs(batch, current_weights)
    importance_weights = torch.exp(current_log_probs - gen_log_probs)

    # Clip importance weights so stale samples don't cause outsized gradients
    clipped_weights = torch.clamp(importance_weights, 0.8, 1.2)
    loss = -(clipped_weights * advantages).mean()

    return loss

Advantages:

  • Allows training on stale data — strict on-policy sampling isn't required
  • The buffer can accumulate a large volume of data
  • Training and generation are fully decoupled

Measured result: running 671B MoE GRPO on 1024 GPUs, it's 2.77× faster than synchronous training.

AgentRL

AgentRL: Scaling Agentic Reinforcement Learning with a Multi-Turn, Multi-Task Framework, arXiv:2510.04206 is a multi-turn, multi-task agentic RL framework released in October 2025, with code at THUDM/AgentRL.

Core innovation: a fully-asynchronous generation-training pipeline + a unified environment interface. On the training side, three pools of workers — rollout, actor, and reference — sample and update asynchronously. On the environment side, a function-call API, containerized environments, a centralized controller, and task workers manage heterogeneous tasks. On the algorithm side, cross-policy sampling and task advantage normalization strengthen multi-turn exploration and stabilize multi-task training, respectively.

python
# AgentRL's asynchronous training structure (simplified)
rollout_workers.stream_trajectories(task_manager)
actor_workers.update_policy(buffer.sample())
reference_workers.compute_kl(buffer.sample())
controller.route_function_calls(task_workers)

Advantages:

  • Supports multi-turn, multi-task agentic RL
  • Decouples trajectory collection from policy updates asynchronously
  • Manages environment deployment through a controller / task worker / transport layer
  • Used to build AutoGLM

Where it fits: training SWE-Agent, Computer Use, and Deep Research Agent workloads.

The Three Async Frameworks Compared

FrameworkMain contributorCore mechanismSpeedupWhere it fits
LlamaRLMetaFully decentralized10.4×Extreme-scale dense models
AReaLAnt Group and TsinghuaFully asynchronous rollout + staleness-aware PPO2.77×Large-scale LLM RL
AgentRLTHUDM / ZhipuMulti-turn multi-task + unified environment interfacenot reported in the paperAgent training

36.6 MoE + RL Training

DeepSeek V3, Qwen3, and GLM-4.5 are all MoE architectures. MoE brings new challenges to RL training.

What Makes MoE Different

An MoE model's parameters are distributed unevenly — most parameters live in the experts, and each sample only activates a small subset of them:

MoE model structure (DeepSeek V3):
┌──────────────────────────────────────────┐
│ Dense part (attention, etc.): 20B params │
├──────────────────────────────────────────┤
│ MoE part:                                │
│  - 256 experts × 5B params = 1.28T       │
│  - each sample activates 8 experts       │
│  - actual active params: 40B             │
└──────────────────────────────────────────┘
Total params: 1.3T, active params: 60B

Three Challenges of MoE RL Training

1. Uneven Expert Load

Some experts get activated frequently while others sit idle. This causes:

  • Uneven compute load (some GPUs overloaded)
  • Skewed training-data distribution (some experts undertrained)

Fix: an Expert Balancing Loss:

python
def expert_balancing_loss(router_logits, num_experts):
    # Compute the activation frequency of each expert
    router_probs = torch.softmax(router_logits, dim=-1)
    expert_freq = router_probs.mean(dim=0)  # [num_experts]

    # Encourage a uniform distribution
    target_freq = 1.0 / num_experts
    balance_loss = ((expert_freq - target_freq) ** 2).mean()

    return balance_loss

2. Communication Overhead

MoE experts are spread across multiple GPUs (Expert Parallelism), so every sample requires an all-to-all communication step:

GPU 0: expert 0,1,2     ──┐
GPU 1: expert 3,4,5     ──┼── all-to-all ── all-to-all back after processing
GPU 2: expert 6,7,8     ──┤
GPU 3: expert 9,10,11   ──┘

Fix: DeepEP (DeepSeek Expert Parallelism), which optimizes the all-to-all communication pattern.

3. High Token-Level IS Variance

The GRPO family already noted this — under MoE, different tokens route to different experts, so token-level importance-sampling ratios swing wildly and gradient variance balloons.

Fix: GSPO (Group Sequence Policy Optimization) — move the IS ratio from the token level to the sequence level:

python
# PPO/GRPO: token-level IS
token_ratio = exp(log_prob_new - log_prob_old)  # each token independent

# GSPO: sequence-level IS
sequence_log_prob_new = sum(log_prob_new_per_token)
sequence_log_prob_old = sum(log_prob_old_per_token)
sequence_ratio = exp(sequence_log_prob_new - sequence_log_prob_old)
# the whole sequence shares one ratio

The entire Qwen3 lineup (including 235B-A22B) is trained on GSPO.

DeepSeek V3's MoE RL

The RL training practice behind DeepSeek V3 (671B MoE, 37B active):

  • DualPipe: pipeline-parallel optimization (details in 36.7)
  • FP8 training: uses FP8 to cut memory and compute (arXiv:2412.19437)
  • MTP (Multi-Token Prediction): predicts multiple tokens at once, raising the density of the training signal

Step Flash (StepFun)

Step Flash is a MoE RL optimization StepFun released in 2025:

  • Dynamic Expert Allocation: adjusts the number of experts dynamically based on the token distribution within a batch
  • Sparse Gradient Sync: only syncs gradients for experts that were actually activated
  • Cache-aware Routing: takes KV-cache locality into account when routing

GLM-4.5 (Zhipu)

GLM-4.5 is trained with the slime framework (THUDM/slime):

  • Megatron training backend
  • SGLang inference backend
  • Native MoE optimization (DeepEP communication, fp8 rollout)

36.7 DualPipe and Best-Fit Packing

DualPipe

The DeepSeek V3 paper, arXiv:2412.19437 introduces DualPipe — bidirectional pipeline parallelism.

Conventional pipeline parallelism (PP) suffers from a bubble problem:

GPU 0: [F0][F1][F2][F3]              [B3][B2][B1][B0]
GPU 1:       [F0][F1][F2][F3]   [B3][B2][B1][B0]
GPU 2:             [F0][F1][F2][F3][B3][B2][B1][B0]
                   ↑                ↑
                 forward           backward
                    the bubble is large

DualPipe runs forward and backward at the same time — the forward pass of stage N and the backward pass of stage N-1 overlap on the same GPU:

GPU 0: [F0|B0][F1|B1][F2|B2][F3|B3]  ← forward and backward overlap
GPU 1:       [F0|B0][F1|B1][F2|B2][F3|B3]
GPU 2:             [F0|B0][F1|B1][F2|B2][F3|B3]
                                    almost no bubble

The bubble ratio drops from the conventional ( is the number of PP stages, is the number of micro-batches) to .

python
# DualPipe pseudocode
class DualPipeScheduler:
    def schedule(self, num_stages, num_micro_batches):
        schedule = []
        for step in range(num_micro_batches + num_stages - 1):
            for stage in range(num_stages):
                # The same stage does both forward and backward in the same step
                fwd_mb = step - stage
                bwd_mb = step - (num_stages - 1 - stage)
                if fwd_mb >= 0 and fwd_mb < num_micro_batches:
                    schedule.append(("forward", stage, fwd_mb))
                if bwd_mb >= 0 and bwd_mb < num_micro_batches:
                    schedule.append(("backward", stage, bwd_mb))
        return schedule

Best-Fit Packing

Conventional micro-batch assignment is uniform — every GPU gets the same number of micro-batches. But under MoE, different experts carry different loads, so uniform assignment leaves the cluster unbalanced.

Best-Fit Packing: use a bin-packing algorithm to assign differently sized micro-batches to GPUs:

python
def best_fit_pack(items, bin_capacity):
    """items are micro-batches of varying size; bin_capacity is the capacity of one GPU"""
    bins = [[]]
    for item in sorted(items, reverse=True):  # largest first
        # Find the fullest bin that still has room
        best_bin = None
        best_remaining = float('inf')
        for bin in bins:
            remaining = bin_capacity - sum(bin)
            if item <= remaining < best_remaining:
                best_bin = bin
                best_remaining = remaining
        if best_bin is None:
            bins.append([item])
        else:
            best_bin.append(item)
    return bins

Best-Fit Packing raises DeepSeek V3's GPU utilization from 70% to 95%.

36.8 Performance Profiling and Bottleneck Analysis

Performance tuning for RL training has to be grounded in profiling — not guesswork.

Profiling Tools

1. PyTorch Profiler

python
from torch.profiler import profile, ProfilerActivity

with profile(
    activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA],
    record_shapes=True,
    profile_memory=True,
) as prof:
    trainer.train_step()

# Print the top 10 most time-consuming operations
print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10))

2. NVIDIA Nsight Systems

bash
# Run training under nsys
nsys profile -o rl_train_profile python train.py

# View the timeline in the Nsight Systems GUI
nsys-ui rl_train_profile.qdrep

This visualizes the execution time of every CUDA kernel, CPU-GPU synchronization points, and communication overhead.

3. veRL's Built-In Profiler

veRL ships RL-specific profiling:

python
from verl.utils.profiler import RLProfiler

with RLProfiler() as p:
    trainer.train()
    p.print_summary()
# Output:
#   rollout time: 3500s (85%)
#   actor update time: 120s (3%)
#   critic update time: 80s (2%)
#   weight sync time: 30s (0.7%)
#   communication: 400s (10%)

Typical Bottlenecks and Fixes

BottleneckSymptomFix
Slow rolloutRollout eats 80%+ of the timeAdd rollout GPUs, use vLLM prefix caching
Slow weight syncSync eats 5%+ of the timeUse LoRA, pack transfers over NCCL
Communication overheadAll-reduce eats 10%+ of the timeIncrease batch size, use gradient accumulation
Activation memory blowupOOMGradient checkpointing
Uneven expert loadSome GPUs at 90%+, others at 30%Expert balancing loss, dynamic routing
Straggler problemThe longest sequence in the batch dictates the wall-clock timeLength bucketing, Seer divided rollout

MFU (Model FLOPs Utilization)

The gold-standard metric for training efficiency:

The H100's bf16 peak is ~1000 TFLOPS. Typical MFU for LLM RL training:

ConfigurationMFU
Dense + FSDP + checkpointing35-45%
MoE + EP + DualPipe50-60%
Asynchronous RL (generation/training separated)70-80% (the rollout portion is accelerated by vLLM)

An MFU below 30% signals significant room for optimization — usually a communication or rollout bottleneck.

36.9 Practice at 10,000-GPU Scale

Put everything above together and you get RL training practice on a 2025-era 10,000-GPU cluster.

A Typical Configuration

Take GRPO training of Qwen3-235B-A22B (235B total parameters, 22B active MoE) as an example:

yaml
# Cluster configuration
total_gpus: 12288 # 12k H100s
intra_node_bandwidth: 900 GB/s # NVLink
inter_node_bandwidth: 50 GB/s # InfiniBand

# Model parallelism
tensor_parallel: 8 # TP=8 (within a node)
pipeline_parallel: 4 # PP=4 (across nodes)
expert_parallel: 16 # EP=16
data_parallel: 24 # DP=24

# Training configuration
algorithm: GSPO # a GRPO variant optimized for MoE
batch_size_per_gpu: 1
gradient_accumulation: 32
seq_len: 32768
group_size: 8 # GRPO generates 8 samples per prompt

# Async configuration
async_mode: disaggregated
rollout_buffer_size: 100000
weight_sync: lora # sync only the LoRA adapter
weight_sync_method: nccl_packed

Measured Performance

text
Training 1 epoch (10B tokens):
  Total time: 24 hours
  GPU hours: 294912

Time breakdown:
  Rollout: 18 hours (75%)
  Actor update: 3 hours (12.5%)
  Critic update: 2 hours (8%)
  Weight sync: 0.5 hours (2%)
  Other: 0.5 hours (2.5%)

MFU: 52% (MoE + DualPipe + FP8)

Key Lessons from 10,000-GPU Training

1. Failure Is the Norm

Across 12,288 GPUs, 5-10 fail on an average day. This forces certain practices:

  • Checkpoint frequency: save every 30 minutes so you can roll back on failure
  • Redundant design: keep 8 spare GPUs for every 1024 in use
  • Automatic restart: detect failure and resume automatically from the most recent checkpoint

2. Communication Is the Performance Killer

Cross-node communication is slow, which shapes how a 10,000-GPU cluster's network gets designed:

  • Topology-aware placement: prefer neighboring GPUs when forming a tensor-parallel group
  • Overlap communication with compute: kick off the gradient all-reduce while the backward pass is still running
  • Gradient bucketing: merge small gradients to cut the number of communication rounds

3. MoE Routing Stability

During MoE training the expert routing can suddenly collapse — every token starts routing to a handful of experts. Monitor for it:

python
# Monitor expert load in real time
def monitor_expert_balance(model):
    while training:
        for layer in model.moe_layers:
            router_probs = layer.router.get_recent_probs()
            entropy = -torch.sum(router_probs * torch.log(router_probs + 1e-10))
            if entropy < threshold:  # routing entropy too low
                alert(f"Layer {layer.id}: expert routing collapse!")
        time.sleep(60)

4. The Data Pipeline Is a Hidden Bottleneck

A 10,000-GPU cluster consumes millions of tokens per second, and data loading itself can become the bottleneck:

  • Prefetching: prepare the next 10 batches of data ahead of time
  • Data compression: store data in a more compact format
  • Distributed storage: spread data across multiple SSDs to avoid a single-point I/O bottleneck

Chapter Summary

Distributed RL training systems are the core engineering discipline of the LLM era:

  1. veRL (HybridFlow) is the mainstream framework — single-controller multi-model orchestration with flexible resource allocation
  2. OpenRLHF/NeMo-Aligner/TRL each occupy their own niche — research, the NVIDIA stack, lightweight teaching
  3. vLLM/SGLang are the core of the rollout engine — PagedAttention, Continuous Batching, Prefix Caching
  4. ZeRO/FSDP/Checkpointing solve the memory problem — LoRA + FSDP + Checkpointing is the standard recipe for 70B training
  5. Asynchronous training (LlamaRL/AReaL/AgentRL) is the direction for 2025 — 10× speedups, tolerance for off-policy data
  6. MoE + RL needs GSPO, Expert Balancing, DualPipe, and Best-Fit Packing working together
  7. 10,000-GPU clusters push engineering to its limits — routine failures, communication bottlenecks, monitoring and alerting, data pipeline pressure

Chapter 15, Industrial LLM RL Practice revisits how these techniques land in practice from a product perspective — this chapter has been the engineering perspective.

Further Reading

现代强化学习实战课程