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 three 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:
FSDP and torchtitan implement
train, update_weights and sleep / wake_up once, in the
shared TorchNativeTrainRayActor; each supplies only its model, optimizer and step runner.
Megatron implements the contract on its own, because its microbatch loop belongs to the
pipeline schedule and its checkpoints to torch_dist.
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. Use torchtitan when you want its model implementations and its parallelism. It brings full parallelism like Megatron-LM and loads an HF directory like FSDP, but the architecture has to be one torchtitan itself implements — that list, not a spec you write, is the limit. 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:
scripts/models/<family>.py,
selected by the megatron_model_type the script hands to execute_train. 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 each tensor over the training-to-engine process group,
broadcast_packed sends one packed byte buffer per bucket for non-colocated Megatron,
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 matches keeping the state on the GPU up to the grad norm’s
rounding 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 architecture flag block, 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, and the rest of scripts/models/) 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: both --offload-train-target disk and
--stream-optimizer-state-to-disk assert the Megatron backend.
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.
5. Compute kernels
By default (--kernel-backend native) every fused kernel comes from the wheels baked into the
image: flash_attn, causal_conv1d, flash-linear-attention and the rest, built out of band and
installed by docker/Dockerfile.
--kernel-backend hub additionally resolves a few of them from
Hugging Face Hub kernel repos instead. A kernel
repo ships one prebuilt variant per (torch, CUDA, C++ ABI, arch, OS), so the client picks a
variant at load time and imports it from the HF cache — no compiler on the training node, and no
image rebuild to change a kernel.
What miles ships, in
miles/backends/fsdp_utils/plugins/hf_kernels/presets.py (the whole
feature lives in that plugin package; the main FSDP path only calls HubKernels.prepare() once
and bind() per model):
Slots resolve independently. Before either model is bound, all ranks agree on each slot’s
availability, repository/ref, and kernel build identity. If any rank cannot load a slot or the
identities differ, every rank keeps its native implementation for that slot.
--kernel-strict
turns that collective fallback into an initialization error on every rank.
Custom mappings may omit entire slots, but each included slot must declare all the functions
listed by REQUIRED_SLOT_FUNCTIONS in presets.py; unknown slots and incomplete declarations
are rejected before anything downloads, regardless of --kernel-strict. Use repositories from
trusted Hub kernel publishers. Local kernel overrides are unsupported because their Hub
provenance cannot be verified across ranks.
These are module-level kernels: kernels.get_kernel() returns a module and miles rebinds the
free functions HF modeling code already looks up per forward. Nothing rebinds an nn.Module.forward,
so state_dict, _no_split_modules and the DTensor gather are untouched — which is why the binding
runs before apply_fsdp2 and the ref model takes the same call.
This is worth turning on for the packing path specifically. Every slot feeds a kernel that
sequence packing needs in order to reset state per
packed document, and each one fails differently when its wheel is missing:
- no
flash-linear-attention—transformersbindstorch_chunk_gated_delta_rule, whose signature ends in**kwargs, so the injectedcu_seqlensis accepted and ignored; - no
causal_conv1d— the handle isNoneand the forward drops toF.silu(self.conv1d(...)), which takes noseq_idx; - no
flash_attn— the NemotronH attention patch returns the unpatched dense forward.
--kernel-strict when those native wheels are absent: a non-strict fallback does not
make a native implementation that ignores boundaries safe for packed training.--true-on-policy-mode and --deterministic-mode: those
modes require the training kernel to match SGLang’s build exactly, and that equivalence has not been
established per kernel yet.
This loader requires access to Hub metadata even when the kernel binaries are already cached.
It does not consume kernels.lock, and kernels lock . && kernels download . does not make
--kernel-backend hub work under HF_HUB_OFFLINE=1. Offline provisioning remains a follow-up
in RFC #2207.
For repeatable online runs, provide a custom mapping with HubKernelSpec(revision="<commit SHA>", version=None, ...) for each selected repository, retaining the slot’s required functions. Major
versions such as version=1 follow moving v1 branches. The collective check ensures agreement
within a run; immutable revisions also pin the source across runs. A revision pin still requires
online metadata access with this loader.
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.
torchtitan
The torchtitan backend lives atmiles/backends/torchtitan_utils/. One idea explains the
whole thing: torchtitan’s Trainer is the black box. Miles does not assemble torchtitan
parts — it builds one Trainer.Config from your flags, hands it to torchtitan, and from then
on owns only three things: feeding it microbatches, taking the optimizer step, and streaming
the resulting weights to SGLang. Model construction, every parallelism, the pipeline
schedule, the HF checkpoint load, the optimizer and the LR schedule are all torchtitan’s.
That is why the flags below are torchtitan’s own field names: they are copied verbatim into
the config a torchtitan user would write by hand.
1. Picking a model
--titan-model-name names a model package and --titan-model-flavor one of the sizes it
registers. The name is looked up first under miles/backends/torchtitan_utils/models/, then
under torchtitan/models/, so a model torchtitan does not ship can be added on the miles side
without touching torchtitan (see below). --hf-checkpoint supplies the weights and tokenizer;
torchtitan’s own state-dict adapter converts them, so there is no offline conversion step.
--seq-length sizes the rotary tables and the buffers pipeline stages exchange, so it has
to be at least as long as the longest sequence you will train on — prompt plus response.
Miles rejects a value that leaves no room for a prompt rather than letting it fail inside a
kernel later.
2. Choosing the parallelism
--recompute-granularity full and --bf16 are accepted with Megatron’s meaning as
well (--gradient-checkpointing stays as the FSDP-side spelling).
Two notes on how they behave here:
- Context parallelism is internal. The trainer shards the sequence for attention and
gathers the logits back before the loss sees them, so the RL loss and its metrics behave
exactly as at
cp=1and cost the same memory. - Pipeline parallelism needs one shape for the whole run, so every microbatch is padded
to
--seq-length. Weight-tied flavors (qwen3 0.6B / 1.7B / 4B) cannot be pipelined — torchtitan refuses, by design.
3. Fitting it in memory
--gradient-checkpointing selects torchtitan’s full activation checkpointing. Per-rank
memory is set by the shard, pipeline and tensor degrees together; MoE dispatch buffers do
not shrink with sharding, so adding nodes only helps if it raises the shard degree —
replication alone does not.
4. Getting weights in and out
Nothing to configure: weights are streamed to SGLang under HF names through the model’s own state-dict adapter, over IPC when the engines are colocated and over NCCL broadcast when they are not.--fully-async works, and requires the disaggregated layout, since generation
continues while training runs.
Checkpoints are torchtitan’s, written under --save; a resumed run picks up the trainer’s
own step counter. Resuming needs --ci-disable-weight-update-checker if --ci-test is on,
because that check compares the engine against the original HF checkpoint.
Limits
MoE works, including expert parallelism and--use-rollout-routing-replay (R3). R3 matters
more than it looks on MoE: without it, training and rollout routing drift apart as the policy
moves, and the disagreement compounds over rollouts rather than staying flat.
Going deeper: a model torchtitan does not ship
torchtitan finds a model by importingtorchtitan.models.<name> and calling its
model_registry(flavor, attn_backend), which returns a ModelSpec: the model config, the
parallelize and pipelining functions, and the state-dict adapter that maps HF names to
torchtitan’s. Miles looks in miles/backends/torchtitan_utils/models/<name>/ first and expects
exactly the same function, so a miles-side package is a drop-in peer of a torchtitan one.
Most new architectures are a recombination of blocks torchtitan already has — MLA or GQA
attention, the sigmoid or softmax token-choice router, grouped experts, shared experts — and
then the package is a flavor table plus a model_registry that points at an existing
parallelize_fn and adapter. models/glm4_moe_lite/ is the worked example: GLM-4.7-Flash is
DeepSeek-V3’s layer with different dimensions and the same HF parameter names, so its package
builds the layer list with torchtitan’s DeepSeek-V3 helpers, sets the GLM sizes, and reuses
DeepSeekV3StateDictAdapter unchanged.
model.py with
the new block (subclassing torchtitan’s TransformerBlock / BaseAttention), then a
state_dict_adapter.py if the HF names differ, and only then a parallelize.py if the
existing sharding plans do not apply. Two checks belong with every package: a fast test that pins the flavor’s dimensions to the values recorded from the model’s config.json and checks the adapter’s map covers every key pattern recorded from model.safetensors.index.json, and an e2e case under tests/e2e/torchtitan/ — SGLang has to implement the architecture too, since it is what consumes the streamed weights.
Try it
scripts/run_qwen3_0_6b_fsdp.py flag for flag, so the two backends’
training curves can be read against each other on one model. The cases in
tests/e2e/torchtitan/ are the other reference: one per topology, each naming the mechanism
it exercises.
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:
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.

