Skip to main content
Every recipe ships as a Python launch script under scripts/, and starting a run is one command:
This page explains what that command does and how to change what it runs. For the meaning of individual flags, see the CLI Reference.

How a launch script starts a training job

A launch script is a recipe, not the training process. It assembles the full train_diffusion.py command line, starts a local Ray cluster, and submits the command as a Ray job — submitted rather than run directly, so the driver lives inside the cluster and sees every node’s GPUs.

The structure of a launch script

Every launcher follows the same layout:

ScriptArgs — script options as flags and MILES_SCRIPT_* env vars

@U.dataclass_cli exposes each ScriptArgs field twice: as a --kebab-case CLI option (--num-rollout) and as an environment variable with the MILES_SCRIPT_ prefix (MILES_SCRIPT_NUM_ROLLOUT). A command-line value beats the env var, which beats the field default. The env form is how wrappers and cluster tooling inject machine-specific values without editing the script. Options shared by every launcher, from the ExecuteTrainConfig base class and repo convention: wandb flags are emitted only when WANDB_API_KEY is set — logging turns on by exporting the key, with no script change.

execute() — the flag groups

The command line is assembled as one string block per concern, then concatenated — the parser sees a flat list, and the groups follow concern, not flag prefix (a --diffusion-* flag can live in rollout_args, a --micro-batch-size-* flag in perf_args).

Ways to override a recipe

From lightest to heaviest:
  1. Append flags with --extra-args. The value lands at the end of the command line, and for an argparse flag given twice the later occurrence wins — so it overrides anything the recipe sets:
  2. Set a script option, as a flag or a MILES_SCRIPT_* env var.
  3. Edit the script. The launcher is the canonical home of a recipe’s hyperparameters; change the flag blocks directly for anything you want to keep.
The Typer flags are also the CI interface: an e2e test names the recipe and passes its knobs (script="scripts/run_diffusion_grpo_sd3_ocr_sglang.py", args=["--num-rollout", "2", ...]), so launch scripts must stay runnable with no arguments and configurable through ScriptArgs — CI drives them the same way you do.

What execute_train runs on your machine

  1. Kills stale sglang / ray / miles processes.
  2. Starts a fresh cluster with export CUDA_VISIBLE_DEVICES=... && ray start --head — the device list must be in the raylet’s own environment; set per job or per actor it never reaches the scheduler, which then places work on excluded GPUs.
  3. Builds the Ray runtime env: NCCL socket vars, MASTER_ADDR, PYTHONPATH, plus anything from --extra-env-vars.
  4. Submits the job: ray job submit -- python3 train_diffusion.py <flags>.

The batch-size arithmetic

Diffusion adds a dimension LLM RL does not have: one sample expands into several train pairs, one per trained denoising step.
The trajectory-level knobs are locked by the batch-knob invariant; contradictory values abort at parse time. global_batch_size counts samples and must divide by dp_size (= train world size ÷ --sequence-parallel-size). Most recipes use the 2D tile — it states “S samples × T timesteps per forward” directly, which the flat knob cannot express. Wan2.2 is the exception: --micro-batch-size 2 is its way of keeping each forward on one side of the high/low-noise expert boundary. Worked example (the Qwen-Image recipe):
Two failure modes to recognize:

Adapting a recipe

Start from the closest existing script and change one group at a time:

Multi-node training

The worked example is scripts/run_diffusion_grpo_wan22_pickscore_17gpu_multinode.py: wan2.2-A14B full finetune on 2 nodes × 8 GPUs (train + rollout colocated) plus 1 reward GPU on a separate node. Multi-node runs submit into a cluster you build yourself; the launcher only submits (MILES_SCRIPT_EXTERNAL_RAY=1).

Bring up the cluster

Each node runs ONE of the blocks below, chosen by its role. The two export lines must be in the environment of the ray start daemon itself, which is why every block repeats them. On the head node:
On every other training node:
On the reward node:
  • ulimit -n: a non-interactive ssh shell defaults to 1024 open files. The raylet inherits it and, once the FSDP actors and engines connect, dies with epoll: Too many open files — but the driver log shows a misleading ActorUnavailableError: ... RpcError: Socket closed. Verify with grep "Max open files" /proc/$(pgrep -f raylet | head -1)/limits after ray start.
  • CUDA_VISIBLE_DEVICES follows the same rule the launcher automates single-node: it must be in the raylet’s environment, and with an external cluster the launcher refuses a --cuda-visible-devices of its own.
ray status on the head should show every node and the full GPU count.

Submit

Reward workers (--pickscore-num-workers 4 --pickscore-num-gpus-per-worker 0.25, no --colocate-reward) are default-scheduled and land on the only free GPU: the reward node.

Verify the run is healthy

  • With --rollout-patch-group wan and --sglang-attention-backend torch_sdpa, train/model_output_mean_abs_diff is expected to be exactly 0.0 from the first optimizer step. The 4-GPU proxy E2E standard records 0.0, and the documented 17-GPU runs sustained it over 200 rollouts. Any nonzero value indicates a train/rollout parity regression; check the patch group, backend, dtype, and versions on every rank.
  • Both training nodes near 100% GPU util during rollout; a 100%-vs-idle split between nodes means the reward workers were packed onto one node.

Multi-node pitfalls

Next