Skip to content

[Lilo][Miles runtime] Multi-node LoRA support and 256k Qwen3.8-27B definitions - #39

Merged
micahtyong merged 23 commits into
mainfrom
devin/1789702581-lilo-27b-256k-multinode
Sep 22, 2026
Merged

micahtyong merged 23 commits into
mainfrom
devin/1789702581-lilo-27b-256k-multinode

Conversation

@micahtyong

@micahtyong micahtyong commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

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_256k is a 2-node TP2×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)
Screenshot 2026-09-22 at 4 48 08 PM

Multi-node checkpoints are the substance of this PR

Definitions set TRAINER_NODES and run under @modal.experimental.clustered(N, rdma=True). Rank 0 becomes the Ray head and the Lilo engine; the other nodes ray start --address=head and 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: checkpoint 000018 held only __0_0…__7_0, and every one of the nine chained resumes died with FileNotFoundError: /checkpoints/000018/.../miles/__16_0.distcp. Four distinct bugs were behind that, each fixed with a regression test:

  1. Representative election by hostname selects nothing. _sync_checkpoint_volume() commits once per node, from one rank of each, elected by socket.gethostname() — but every container in a Modal cluster reports "modal", so hosts.index(...) == rank only ever picks global rank 0 and nodes 1..N-1 never commit. _node_identity() now elects by MODAL_TASK_ID (distinct per container, inherited by the Ray workers it spawns), falling back to the hostname off Modal.

  2. 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:

    write_shards(tmp); _sync_checkpoint_volume("commit")      # every node commits its shards
    on rank 0:  volume.copy_files(peer_shards, final, recursive=True)   # server-side, no local view needed
                shutil.copy2(own_shards, final)                         # keeps rank 0's own view complete

    The local copy of rank 0's own files matters: bulletin.publish reads 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.

  3. 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 as backend /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.

  4. /persist_snapshot never went through any of the above. MilesCommandBackend.persist_checkpoint() installs the capture with a local shutil.copytree, bypassing the Miles write_checkpoint_dir patch 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:

    if shards and len(shards) < world_size:
        raise RuntimeError(f"capture {src} holds {len(shards)} of {world_size} shards; ...")

    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_VOLUME explicitly (_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_nodes polls ray.nodes() until actor_num_nodes alive GPU nodes expose world_size GPUs (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 compatible forward_backward requests the scheduler coalesces into one backend call; None keeps the old unbounded behaviour.
  • align_sequences_to_parallel_layout — pads each sequence to a multiple of 2*cp*tp so Megatron's zigzag CP chunks tile the TP ranks exactly, then trims the outputs back. Needed at TP8×CP3 (Miles only pads per-sequence to 2*cp); TP2×CP8 divides evenly so no shipped definition sets it. Kept as the workaround until the padding fix lands upstream in Miles.

Evidence

  • 2-node TP2×CP8×DP1 fits 256k with headroom: per-rank max_allocated 82.4 GB / max_reserved 118.3 GB of 141 GB. A third node was never a memory requirement.
  • Matched 20-step run lilo-27b-256k-2n-20step-1790044500, 20/20 steps: mean cmp/train_time_s 2424.7 s (2403.7 s excluding step 0) vs raw Miles yz1wuem8 ~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.
  • The residual gap is Lilo's 128 separate forward_backward calls: 128 × 17.4 s + ~180 s assemble/publish = 2404 s. Left as-is deliberately — coalescing would change what's being compared.
  • Post-fix, a resume loaded a 16-shard multi-node checkpoint (load_slot ok=true) and subsequent saves write all 16 .distcp shards, which is exactly the failure the original run died on. The 20-step matched run lilo-27b-256k-2n-20step-1790044500 on 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


Devin Review

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@micahtyong

Copy link
Copy Markdown
Contributor Author

/devin review

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Starting Devin Review.

Devin Review

devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1789604737-lilo-27b-longcontext branch from d95e8cc to 17b8b1a Compare September 18, 2026 16:55
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1789702581-lilo-27b-256k-multinode branch from b618ba6 to 5e34da8 Compare September 18, 2026 16:57
@devin-ai-integration
devin-ai-integration Bot changed the base branch from devin/1789604737-lilo-27b-longcontext to main September 18, 2026 17:04
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1789702581-lilo-27b-256k-multinode branch 2 times, most recently from 33f5906 to 6f918b0 Compare September 18, 2026 17:18
micahtyong and others added 4 commits September 18, 2026 23:45
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>
@devin-ai-integration
devin-ai-integration Bot force-pushed the devin/1789702581-lilo-27b-256k-multinode branch from 6f918b0 to 60a6b4d Compare September 18, 2026 23:46
@micahtyong micahtyong changed the title Miles LoRA backend: multi-node trainers + Qwen3.8-27B 256k definition [Lilo][Miles runtime] Add support for multi-node multi-node trainers (cp4, seq length < 256k) Sep 18, 2026
micahtyong and others added 4 commits September 18, 2026 23:54
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>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@micahtyong

Copy link
Copy Markdown
Contributor Author

/devin review

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Starting Devin Review.

Devin Review

@micahtyong
micahtyong marked this pull request as ready for review September 21, 2026 23:03
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

Devin Review

Comment thread src/lilo/backends/miles_runtime/actor.py
Comment thread tests/backends/test_miles_actor.py Outdated
micahtyong and others added 8 commits September 21, 2026 23:14
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>
@devin-ai-integration devin-ai-integration Bot changed the title [Lilo][Miles runtime] Add support for multi-node multi-node trainers (cp4, seq length < 256k) [Lilo][Miles runtime] Multi-node Miles trainers and 256k Qwen3.8-27B definitions Sep 22, 2026
Comment thread src/lilo/backends/miles_runtime/actor.py
Comment thread src/lilo/backends/miles_runtime/actor.py Outdated
Comment thread src/lilo/backends/miles_runtime/actor.py Outdated
Comment thread src/lilo/backends/miles_runtime/actor.py Outdated
Comment thread src/lilo/backends/miles_runtime/actor.py Outdated
Comment thread src/lilo/backends/miles_lora.py
Comment thread src/lilo/providers/modal/definitions/qwen3_8_27b_miles_lora_256k.py
Comment thread src/lilo/providers/modal/ray_cluster.py Outdated
Comment thread src/lilo/providers/modal/ray_cluster.py Outdated
Comment thread tests/providers/test_miles_256k_definition.py Outdated
micahtyong and others added 4 commits September 22, 2026 19:31
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>
@micahtyong micahtyong changed the title [Lilo][Miles runtime] Multi-node Miles trainers and 256k Qwen3.8-27B definitions [Lilo][Miles runtime] Multi-node LoRA support and 256k Qwen3.8-27B definitions Sep 22, 2026
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on stacking a PR for this since this one is getting a bit large. Let me cut a Linear ticket

Comment thread src/lilo/engine/server.py

selected = [ready[0]]
if ready[0].kind == OperationKind.FORWARD_BACKWARD:
limit = self.max_forward_backward_batch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@micahtyong micahtyong Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@kailash109

kailash109 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

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

@micahtyong

Copy link
Copy Markdown
Contributor Author

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

it could also be worth seeing if we can get a wide-ep config working

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

@micahtyong

micahtyong commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Tracking some follow-ups (see below this message) based on Kailash's comments

#59 — trainer idle/scheduling time (~150 s/step at 256k)
#60 — Qwen 256k config on Megatron multi-LoRA + FFT
#61 — expert parallelism + validated wide-EP confi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants