TrainRayActor subclass that owns the model on
the GPU. --train-backend decides which one miles/ray/train/actor_factory.py instantiates
on every trainer rank, and there are two choices.
Whichever you pick, the rest of the job talks to it through the same handful of methods, and
that short list is the whole contract between a backend and everything else in Miles:
That is also why switching backends does not touch the rest of your launch script. Rollout,
reward, eval, the RL algorithm and the SGLang engine all sit above this line, and so does
the GPU layout: disaggregated by default, where trainer and engines own separate GPUs,
or colocated with
--colocate, where they share GPUs and sleep / wake_up hand the
memory back and forth.
What does change is everything below the line, which is what the rest of this page is about.
Which one do you want?
Use Megatron-LM for large models and for anything that needs real parallelism. It is the recommended backend, the one every recipe in Models is tuned for, and the only one that can split a model inside itself. If the model is a 100 B+ MoE, if the job spans racks, or if fitting it at all depends on tensor / pipeline / expert parallelism, this is the answer. Use FSDP when you want the HuggingFace implementation trained verbatim. It loads a HF directory as-is, with no conversion step and no architecture flags to write, which makes it the fast path for bringing up a new architecture, for checking trainer numerics against the HF reference, and for models that fit under data parallelism alone. The rest follows from that split:Megatron-LM
Configuring this backend is a handful of decisions, in this order: what the architecture is, how to split it, where it sits relative to the rollout engines, how to fit it in memory, where the weights come from, and what you want to hook into.1. Describing the architecture
You do not re-declare Megatron’s flags to Miles. Miles imports Megatron’s whole argument surface at launch:--kv-channels, --rotary-base,
--moe-grouped-gemm, and the rest) already works. Miles then threads its own flags in
through an extra_args_provider (get_miles_extra_args_provider in
miles/utils/arguments.py), which is why Miles and Megatron flags share one CLI.
That import is also why you export the Megatron source before launching:
MODEL_ARGS, generated from
scripts/models/<family>.py. Most models need nothing beyond the stock
--num-layers / --hidden-size / .... For the ones that do, see
bringing in a new architecture below.
2. Choosing the parallelism
Megatron exposes five useful parallel dimensions, but you can’t combine them in arbitrary ways. Only a subset of TP × PP × CP × EP × ETP combinations is actually supported, and some legal combinations are slower than the recipe baseline. Start from the model recipe’s tested combination, then change one dimension at a time.
Do not assume TP, CP, EP and ETP can all be raised independently for a new model. The exact
set of supported combinations depends on the Megatron Core kernels and model spec in use.
Argument Groups lists the flags that belong in
PERF_ARGS.
3. Choosing the GPU layout
Parallelism says how the trainer splits the model. This says where the trainer sits relative to the SGLang engines, and there are two answers. Disaggregated is the default. The trainer takes--actor-num-nodes x
--actor-num-gpus-per-node GPUs, the engines take --rollout-num-gpus more, and the two
sets do not overlap. Nobody has to move: both halves stay resident on their own GPUs for the
whole run, so --offload-train / --offload-rollout default off and no phase pays an
offload cost. It is also the layout that lets the two halves actually run at the same time,
which is what Fully Async Rollout and train_async.py are for.
Under the synchronous loop in train.py the phases still alternate, so each set of GPUs is
idle while the other works.
--colocate puts the engines on the training GPUs and
the two take turns: generate, offload the engine, train, offload the trainer, repeat. It is
the right default when GPUs are the scarce resource, since the same 8 GPUs do both jobs
instead of standing idle during the other phase.
--colocate that are worth knowing before you use it:
--rollout-num-gpusis ignored and reconciled toactor_num_gpus_per_node x actor_num_nodes, since the engines are on the training GPUs by definition.--offload-trainand--offload-rolloutboth turn on, which is what makes the taking of turns possible. That is the memory story in the next section.- The trainer reserves HBM at init before SGLang starts, so
--sglang-mem-fraction-statichas to come down, typically to 0.8 or lower. Miles also defaults--sglang-cuda-graph-backend-prefill=disabledhere to avoid an NVLS OOM.
update_weights gets the weights across. Colocated, the engine
is on the same device, so the actor hands over CUDA IPC handles and nothing crosses the
network. Disaggregated, the weights have to travel, and --update-weight-transfer-mode
picks how: broadcast (the default) sends them over the training-to-engine process group,
p2p uses RDMA point-to-point, and disk-delta publishes
only the bytes that changed since the last sync for each engine to pull.
On a node with fewer than 8 usable GPUs, set --num-gpus-per-node too, otherwise the
rollout side still assumes 8. And --fully-async cannot be colocated: its whole point is
that rollout keeps generating while the trainer steps, which requires separate GPUs.
4. Fitting it in memory
Parallelism decides how the model is divided; this decides what is allowed to sit in HBM at all. Four things compete for it: parameters, gradients, optimizer state, and activations. On bf16 training the optimizer state is the heavy one, at 12 bytes per parameter for the fp32 master copy plus the two Adam moments, against 2 bytes for a bf16 parameter. Data parallelism divides that state, so a run with GPUs to spare may need none of what follows, and a run at DP=1 may need all of it. Two of the knobs apply to any layout, and the rest exist only because a colocated engine wants the GPU back.Either layout
Activations are the first thing to trade, because recompute is cheap and predictable. Every recipe passes some form of:--optimizer-cpu-offload keeps the master
weights and moments in host memory and runs Adam there, and
--overlap-cpu-optimizer-d2h-h2d hides the copies behind compute. Recipes that use it
usually add --use-precision-aware-optimizer, which lets Megatron hold narrower optimizer
state. Note the interaction with rematerialization below: precision-aware on the GPU stores
masters as int16 remainders inside TE FusedAdam, so there is nothing standalone left to
rebuild from.
Colocated only
Everything from here down hangs off--offload-train, which is on precisely because the
engine needs the same HBM during generation. It is what sleep / wake_up do, it is turned
on for you by --colocate, and in a disaggregated run there is nothing to make room for, so
none of it applies.
If the optimizer state does not fit while the step itself runs, offloading the actor
cannot help, because pause and resume happen at phase boundaries and everything is resident
again by the time Adam launches. That case is what streaming addresses:
fp32 moment dtype it is bit-identical to keeping the state on the GPU and costs
disk traffic every step; --stream-optimizer-state-moment-dtype bf16 cuts the volume by a
third. It requires the disk target and excludes --optimizer-cpu-offload.
Disk Offload has the full picture for both, including the
same-topology resume limit, what checkpointing costs, and measured sleep / wake numbers.
5. Getting weights in and out
Megatron trains from its owntorch_dist format: .distcp files that are
parallelism-agnostic, so you can change TP / PP / EP later without re-converting. Convert
once, up front:
torchrun --nnodes=<N> --nproc-per-node=8 .... Each recipe page lists the exact command.
What the run then writes looks like this:
--load, never a specific iter_*. The loader
reads latest_checkpointed_iteration.txt to pick the step.
Saving on demand. --save-trigger-sentinel <path> forces a save from outside the
process, independent of --save-interval:
force_sync=True (so async saves finalize first), and
only then is the sentinel deleted, which is why “file gone” means “checkpoint durable on
disk”. If the job crashes mid-save the sentinel survives, so the request stays pending for
the next run. Requires --save.
6. Hooking into the loop
Three extension points override Megatron behavior without forking it:
Typical uses: mixing in an auxiliary loss, instrumenting per-step metrics, clipping weights
surgically. See Customization.
Going deeper: bringing in a new architecture
Post-training runs on released checkpoints, so this is rarely your problem. When a model does need a custom module, Miles embeds the model’s official HuggingFace module inside Megatron’s scheduling rather than patching Megatron: a spec function undermiles_plugins/models/ is
selected with --spec <module> <function>, a bridge under miles_plugins/mbridge/
reconciles the parameter layouts, and parameters that must stay fp32 through Megatron’s bf16
cast are tagged with mark_param_dtype from
miles/backends/megatron_utils/fp32_param_utils.py. The model configs in scripts/models/
that pass --spec are the worked examples.
FSDP
The FSDP backend lives atmiles/backends/fsdp_utils/. One idea explains the whole thing:
nothing about the model is re-expressed for the trainer. Architecture comes from the
HuggingFace config.json, weights load through AutoModelForCausalLM.from_pretrained(),
and sharding, the distributed optimizer and mixed precision all come from PyTorch FSDP2
rather than from Miles.
So there is no conversion step, no MODEL_ARGS, and no spec to write for a model that
transformers already implements. The bill comes due on parallelism, which is why large
models and complex layouts belong on Megatron-LM.
1. Pointing it at a model
--hf-checkpoint is the whole model input: tokenizer, config and weights. Layer count is
read from the HF config, so Megatron’s architecture flags (--num-layers, --hidden-size,
--spec, MODEL_ARGS) simply do not apply here.
2. Sharding it
This backend is pure data parallel.miles/backends/fsdp_utils/parallel.py builds a single
device mesh with two dimensions:
The default,
dp_replicate=1, means one flat shard group over every training rank. Tensor,
pipeline, context, expert and expert-tensor parallelism are all fixed at size 1 in the FSDP
ParallelState, so the model has to fit within those two dimensions.
Context parallelism is not available here.
--context-parallel-size above 1 is rejected in
argument validation (miles/utils/arguments.py); the mesh has no CP dimension to build.world_size must divide by
--dp-replicate-size, otherwise the run fails in argument validation instead of deep inside
mesh construction.
Memory, once the layout is set:
Under
--colocate this backend also implements sleep / wake_up by moving the model and
the optimizer to host memory and back, gated on --offload-train. The deeper offload
targets are Megatron-only: --offload-train-target disk asserts the Megatron backend, and
--stream-optimizer-state-to-disk builds on it.
3. Precision
- bf16 by default;
--fp16switches the compute dtype. - An fp32 master copy of the weights is kept by default, which is what makes the
trainer to rollout weight sync bit-exact.
--no-keep-fp32-mastertrades it for memory when you do not need that guarantee. --attn-implementationpicks thetransformersattention backend:flash_attention_2by default, withflash_attention_3,sdpaandeagerpassed straight through.- An architecture with fussier numerics can register its own policy, see when an HF model needs help.
4. Checkpoints
--save writes PyTorch Distributed Checkpoint directories, one each for model, optimizer
and LR scheduler, plus a latest_checkpointed_iteration.txt tracker. So --load takes the
parent directory exactly like the Megatron backend does. These are FSDP-backend
checkpoints, not torch_dist ones, and the two formats are not interchangeable.
Limits
For large models, multi-rack jobs, or any recipe whose fit depends on tensor, pipeline or expert parallelism, use Megatron-LM.Going deeper: when an HF model needs help
Any HuggingFace causal LM loads. Some need small corrections around the edges: a weight layout SGLang does not expect, a stateful layer that must be reset per document, a class that needs patching before construction. Those live inmiles/backends/fsdp_utils/adaptations/specs/, one file per architecture, and an
architecture that needs none of them registers nothing.
Specs ship today for
qwen3, qwen3_moe, qwen3_5, glm4_moe_lite (GLM-4.7-Flash) and
nemotron_h; adaptations/specs/__init__.py is the source of truth for that list.
MoE is part of this backend rather than an exception to it: expert layers use the fused
Triton kernels in fsdp_utils/kernels/, the weight bridge unfuses batched experts at sync
time, and --use-rollout-routing-replay (R3) works through per-architecture routing
adapters.
Try it
scripts/run_qwen3_0_6b_fsdp.py,
scripts/run_qwen3_30b_a3b_fsdp.py, scripts/run_nemotron_3_nano_4b_fsdp.py. To compare
the two backends on one model, scripts/run_mcore_fsdp.py takes --train-backend as a flag.
For profiling: --use-pytorch-profiler with --profile-step-start / --profile-step-end,
--record-memory-history with --memory-snapshot-path, and --tensorboard-dir. See
Monitoring & Logging.
The other half: SGLang
SGLang is the inference engine no matter which training backend you picked. Three pieces of configuration matter. HuggingFace pointer. SGLang boots from--hf-checkpoint. Miles syncs the actor’s
weights from the trainer before the first training step, so the checkpoint at that path does
not need to be current. The tokenizer and the config.json-derived context length are
all SGLang reads at init.
Context length override. SGLang takes max context from config.json. To serve beyond it
during training, set --sglang-context-length.
Colocation memory. Under --colocate the trainer reserves VRAM during init before
handing off to SGLang, so drop --sglang-mem-fraction-static to 0.8 or lower to let both
fit.
Passthrough convention
Any flagpython -m sglang.launch_server accepts, Miles accepts with a --sglang- prefix:
--tp-sizefrom--rollout-num-gpus-per-engine--model-pathfrom--hf-checkpoint
miles/backends/sglang_utils/arguments.py.
Router
A router sits in front of the SGLang workers. Router-side flags take a--router- prefix:
--sglang-router-ip and --sglang-router-port and Miles treats the router as
external, skipping its own. Engines then register via /add_worker at startup.
Further reading
- Core concepts: the four objects that make up any Miles job.
- Launch script: the launch script, argument group by argument group.
- Fully Async RL: keep generation running continuously so rollout never waits on a training step.
- Configuration: the flag taxonomy and defaults.

