[Lilo][Miles runtime] Multi-node LoRA support and 256k Qwen3.8-27B definitions - #39
Conversation
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
80025cc to
eb2fe93
Compare
|
/devin review |
d95e8cc to
17b8b1a
Compare
b618ba6 to
5e34da8
Compare
33f5906 to
6f918b0
Compare
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
6f918b0 to
60a6b4d
Compare
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…g import resolves Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
60a6b4d to
cbed492
Compare
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
/devin review |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Ray actors inherit the raylet environment; on worker nodes that raylet starts before the backend process, so LILO_CHECKPOINT_VOLUME never reached them and every cross-node checkpoint commit silently no-opped. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Rank 0 cannot refresh its view of the checkpoint volume while the colocated engine holds a capture file open, so assemble the checkpoint with a server-side copy of what every node committed instead. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
| ) | ||
| if ray_address is None: | ||
| return | ||
| run_trainer(instance_id, ray_address=ray_address) |
There was a problem hiding this comment.
this works for miles backend but the megatron multi lora/FFT sides use nccl distributed raw
might be useful to provide examples of deployments where we use torchrun w rendezvous address over ray cluster for multinode support (maybe we can stack this into multiple PRs)
There was a problem hiding this comment.
Agreed on stacking a PR for this since this one is getting a bit large. Let me cut a Linear ticket
|
|
||
| selected = [ready[0]] | ||
| if ready[0].kind == OperationKind.FORWARD_BACKWARD: | ||
| limit = self.max_forward_backward_batch |
There was a problem hiding this comment.
this limit seems like it limits the original forward_backward coalescing requests? maybe fine if we set the limit large enough but having it set to 1 currently seems like it prevents any sort of coalescing
There was a problem hiding this comment.
This is a good callout. Let me try running a multi-node job without this cap
Actually going to merge this first since the config is validated; but tracking in this issue https://linear.app/modal-labs/issue/TRAIN-119/investigate-lilo-trainer-idlescheduling-time-150-sstep-at-256k.
I already have another 256k cl run going so once that finishes I'll follow up here
|
i think generally looks good it could also be worth seeing if we can get a wide-ep config working (ie. ep 16 across 2 nodes) as i think that would be another major use case of multi node (copying from slack) might be worth also seeing why train time is significantly worse than miles |
Resolved offline. There's 150s of idle time, but the actual forward / backwards time on is comparable with miles. Agreed we need to investigate this overhead time on Lilo with additional profiling, but the trainer is good here
Don't want to scope creep so will cut a ticket and put on the roadmap instead. We'll see where EP fits into our priorities |
Summary
Adds the Qwen3.8-27B 256k definition and the multi-node trainer support it needs in the Miles LoRA backend. 256k on a 27B model doesn't fit on one node.
qwen3_8_27b_miles_lora_256kis a 2-nodeTP2×CP8×DP1(16×H200) trainer that mirrors the raw-Miles baseline exactly, so the two compare on identical hardware; rollout is 2×H200 TP4.Validated e2e and benchmarked against another open source training framework (report)

Multi-node checkpoints are the substance of this PR
Definitions set
TRAINER_NODESand run under@modal.experimental.clustered(N, rdma=True). Rank 0 becomes the Ray head and the Lilo engine; the other nodesray start --address=headand idle until the head exits.A Modal Volume has one view per container, and every part of the checkpoint path got that wrong in a different way. The first 256k run (
lilo-27b-256k-pad-30-1789719362) trained 18 steps and then could never resume: checkpoint000018held only__0_0…__7_0, and every one of the nine chained resumes died withFileNotFoundError: /checkpoints/000018/.../miles/__16_0.distcp. Four distinct bugs were behind that, each fixed with a regression test:Representative election by hostname selects nothing.
_sync_checkpoint_volume()commits once per node, from one rank of each, elected bysocket.gethostname()— but every container in a Modal cluster reports"modal", sohosts.index(...) == rankonly ever picks global rank 0 and nodes 1..N-1 never commit._node_identity()now elects byMODAL_TASK_ID(distinct per container, inherited by the Ray workers it spawns), falling back to the hostname off Modal.Rank 0's rename publishes only rank 0's node. Miles has every rank write into
_tmp_<ckpt>/and rank 0 rename it into place; that rename runs against rank 0's local view, which contains 8 of 16 shards. Publication now goes through the volume's committed state:The local copy of rank 0's own files matters:
bulletin.publishreads the sampler adapter back locally right after writing it, and a server-side copy makes a file visible in committed state but not in the writing container.Committing must not reload. A post-commit
volume.reload()can never succeed on the node whose colocated engine holds a capture file open (there are open files preventing the operation: … __15_0.distcp is open from lilo.engine.backend_http), which killed a save mid-flight and surfaced client-side asbackend /persist_snapshot transport failed (RemoteProtocolError). Writers don't need their peers' shards, so commit no longer refreshes the committing node; readers still reload on the load path./persist_snapshotnever went through any of the above.MilesCommandBackend.persist_checkpoint()installs the capture with a localshutil.copytree, bypassing the Mileswrite_checkpoint_dirpatch entirely — so even with 1–3 fixed, every checkpoint the running job wrote was still node-0-only. It now installs from committed volume state, and refuses the save outright when the capture holds fewer shards than the world size:A checkpoint short of a node's shards loads on no rank but the ones that wrote it, and without this guard it is discovered only hours later, on the resume that needs it.
_publish_checkpoints_across_nodes()carries the same check for the shard-write path, and rank-0 publication failures are broadcast so peers fail instead of hanging until the distributed timeout.Trainer actors also had to be given
LILO_CHECKPOINT_VOLUME/LILO_BULLETIN_VOLUMEexplicitly (_WORKER_ENV_VARS) — Ray workers on the non-head nodes didn't inherit them, so they silently did no volume ops at all. Before placing actors,_require_cluster_nodespollsray.nodes()untilactor_num_nodesalive GPU nodes exposeworld_sizeGPUs (Ray registers worker resources asynchronously), so a missing node fails fast with a clear message instead of hanging in placement.256k-only trainer knobs (opt-in; smaller rungs are unchanged)
--distributed-timeout-minutes 120— one 250k-token forward/backward microbatch takes longer than NCCL's 10 min default, which is what killed the first attempt.Engine(max_forward_backward_batch=1)— caps how many compatibleforward_backwardrequests the scheduler coalesces into one backend call;Nonekeeps the old unbounded behaviour.align_sequences_to_parallel_layout— pads each sequence to a multiple of2*cp*tpso Megatron's zigzag CP chunks tile the TP ranks exactly, then trims the outputs back. Needed at TP8×CP3 (Miles only pads per-sequence to2*cp); TP2×CP8 divides evenly so no shipped definition sets it. Kept as the workaround until the padding fix lands upstream in Miles.Evidence
TP2×CP8×DP1fits 256k with headroom: per-rankmax_allocated82.4 GB /max_reserved118.3 GB of 141 GB. A third node was never a memory requirement.lilo-27b-256k-2n-20step-1790044500, 20/20 steps: meancmp/train_time_s2424.7 s (2403.7 s excluding step 0) vs raw Milesyz1wuem8~2125 s steady state — ~13% slower, not the ~77% reported when Lilo ran TP8×CP3 on 24 GPUs. End-to-end step time is faster (2448 s vs 2776 s) because Lilo's non-train time is ~44 s against Miles' ~599 s. Reward 0.485 mean, 814 tokens/GPU/s, prompt_len 250.4k, truncated_ratio 0. ~14 h wall, ~341 H200-hours.forward_backwardcalls: 128 × 17.4 s + ~180 s assemble/publish = 2404 s. Left as-is deliberately — coalescing would change what's being compared.load_slot ok=true) and subsequent saves write all 16.distcpshards, which is exactly the failure the original run died on. The 20-step matched runlilo-27b-256k-2n-20step-1790044500on this branch wrote complete 16-shard checkpoints at every one of its 20 saves (000000…000019, ~2.1 GiB each), step 18 included.The control-plane fix for reaping wedged engines is split out into #53 — it was an unrelated bugfix that this work happened to surface.
Link to Devin session: https://modal.devinenterprise.com/sessions/ca503f6afb54412498a66f69af517adf
Open in Devin Desktop: https://modal.devinenterprise.com/desktop/session/ca503f6afb54412498a66f69af517adf?variant=devin
Requested by: @micahtyong