Skip to main content
In Miles a training backend is one class: a 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:
so every Megatron flag your checkpoint needs (--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:
In a launch script the architecture flags live in 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.
Colocated shares one set of GPUs. --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.
Three things follow from --colocate that are worth knowing before you use it:
  • --rollout-num-gpus is ignored and reconciled to actor_num_gpus_per_node x actor_num_nodes, since the engines are on the training GPUs by definition.
  • --offload-train and --offload-rollout both 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-static has to come down, typically to 0.8 or lower. Miles also defaults --sglang-cuda-graph-backend-prefill=disabled here to avoid an NVLS OOM.
The layout also decides how 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:
The optimizer step can run on the CPU. --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:
The fp32 masters and Adam moments live in per-bucket files on NVMe, and the step brings in one bucket at a time, so peak residency is one bucket instead of the whole state. At the default 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 own torch_dist format: .distcp files that are parallelism-agnostic, so you can change TP / PP / EP later without re-converting. Convert once, up front:
For models larger than a single node, drive the converter with torchrun --nnodes=<N> --nproc-per-node=8 .... Each recipe page lists the exact command. What the run then writes looks like this:
Always pass the parent directory to --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:
A request fired at any moment during an iteration is consumed at that iteration’s save point. The checkpoint is written with 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 under miles_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 at miles/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.
The mesh is checked before anything is built: 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; --fp16 switches 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-master trades it for memory when you do not need that guarantee.
  • --attn-implementation picks the transformers attention backend: flash_attention_2 by default, with flash_attention_3, sdpa and eager passed 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

No TP / PP / CP / EP. The model must fit under dp_replicate × dp_shard.No LoRA. LoRA is Megatron-only.
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 in miles/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

Launchers with the same recipe shape: 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 flag python -m sglang.launch_server accepts, Miles accepts with a --sglang- prefix:
Two flags are set by Miles rather than by you:
  • --tp-size from --rollout-num-gpus-per-engine
  • --model-path from --hf-checkpoint
The integration lives at miles/backends/sglang_utils/arguments.py.

Router

A router sits in front of the SGLang workers. Router-side flags take a --router- prefix:
Set --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.