Skip to content

feat: multi lora async#1638

Open
mathewjhan wants to merge 33 commits into
radixark:mainfrom
mathewjhan:feat/multi-lora-async
Open

feat: multi lora async#1638
mathewjhan wants to merge 33 commits into
radixark:mainfrom
mathewjhan:feat/multi-lora-async

Conversation

@mathewjhan

@mathewjhan mathewjhan commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Allow disagg fully async training on multiple loras (all-linear, excluding expert) per step in Miles using megatron-bridge.

Two LoRAs trained together (DAPO + GSM8K)

image image

Feature

  • Train multiple LoRAs in a single training step
  • Use as a long running service, with in_place pause generation and LoRA upsert updates
  • Use as normal training job, stopping when no more LoRAs are left to train
  • FastAPI control plane to register and deregister adapter runs

Dependent PRs

SGLang

Unified above: sgl-project/sglang#31253 (for testing - do not need merge)

Megatron-Bridge (radixark)

Dev setup

Tested image: radixark/miles:dev-202607090055

You need to install the 3 forks:

SGLang

This fork has two small changes to SGLang (has both PRs):

  • Upsert semantics for loading LoRAs with the same lora name, to avoid having to unload; currently, unloading requires there to be zero requests for that LoRA to be loaded. When running pause generation before unload, we cannot complete more requests, when there are still running requests on unload, it is impossible to unload the adapter and miles will hang
  • Abort by rid prefix; this is useful to target specific requests under a "namespace", such as when a LoRA run has been deregistered from miles and we want to abort all of the requests for that LoRA adapter. The alternative is to track all requests for that LoRA adapter individually and send in individual abort requests, since SGLang currently doesn't support batch abort
git clone https://github.com/mathewjhan/sglang
cd sglang
git checkout sglang-miles-mathew-dev-2
uv pip install --system --break-system-packages --no-deps -e python

Megatron-Bridge

This fork adds a multi-lora support to Megatron-Bridge along with some helper methods/user api

git clone https://github.com/mathewjhan/Megatron-Bridge
cd Megatron-Bridge
git checkout radixark/bridge/multilora-2
uv pip install --system --break-system-packages --no-deps -e .

Miles

git clone https://github.com/mathewjhan/miles
cd miles
git checkout feat/multi-lora-async
uv pip install --system --break-system-packages --no-deps -e .

Running

see: examples/multi_lora

For normal training (not as a service):

  1. run examples/multi_lora/provision.sh
  2. configure W&B credentials and settings in examples/multi_lora/run_job.sh
  3. run examples/multi_lora/run_job.sh |& tee run.log

For multi-lora training as a long running service:

  1. run examples/multi_lora/provision.sh
  2. configure W&B credentials and settings in examples/multi_lora/run_service.sh
  3. run examples/multi_lora/run_service.sh |& tee run.log in one shell
  4. wait for the service to be ready ("No adapters; sleeping for 5.0s...")
  5. optionally run the smoke test in another shell
python3 examples/multi_lora/service_smoke.py \
      --api-url http://127.0.0.1:8068 \
      --data /root/gsm8k/train.parquet \
      --input-key messages \
      --label-key label \
      --rm-type math \
      --steps 5

Minor features added to existing code (backwards compatible)

  • Add AdapterRef and RewardSpec to Sample type so individual samples can access their own reward functions and adapter names during rollout

Missing features for future

  • Colocated + update_weight_from_tensor currently unsupported, only update_weight_from_distributed
  • Per adapter batch size currently unsupported, all adapters stepped at once (will be added later when decoupling the optimizer)
  • Currently doesn't checkpoint optimizer + scheduler state yet, but can be added later as future PR
  • Dataset checkpoint loading per adapter
  • MultiLoRA not applied to experts as of now due to more complex bookkeeping required (need to keep track of the [adapter index, routed experts] together for grouped gemm)
  • Doesn't support pure LoRA load from *.bin/*.safetensors yet, can be added later as a future PR, only resume from a megatron checkpoint or train from scratch
  • Support add_samples to recycle stale/aborted samples

@mathewjhan mathewjhan changed the title Feat/multi lora async feat: multi lora async Jul 12, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a fully-async multi-LoRA training framework, enabling concurrent training of multiple LoRA adapters against a shared base model. It adds a background rollout worker, an async round-robin data source, a Ray-based controller with an HTTP control-plane API, and integrates slot-keyed adapter management into the Megatron-LM and SGLang backends. The review feedback identifies a critical distributed deadlock risk in actor.py caused by non-deterministic set iteration during collective operations, which can be fixed by sorting. Additionally, the reviewer recommends replacing several assert statements with ValueError for input validation and closing the httpx.Client in the smoke test to prevent resource leaks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread miles/backends/megatron_utils/actor.py Outdated
Comment thread miles/utils/arguments.py
Comment thread examples/multi_lora/train_multi_lora_async.py Outdated
Comment thread miles/ray/multi_lora_controller.py
Comment thread examples/multi_lora/service_smoke.py
Comment thread examples/multi_lora/service_smoke.py
@yushengsu-thu yushengsu-thu self-assigned this Jul 12, 2026

@yushengsu-thu yushengsu-thu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ran a deep verification pass over this branch together with its two sglang dependency PRs (sgl-project#30912/#30913); every finding below was adversarially re-verified against the code. Two issues to flag on the miles side — one is a hard crash/correctness bug in the logprob-recompute path, the other is a lifecycle race that silently corrupts rollout data across adapter slot reuse. (Side note: the deeper sglang-side findings are filed on the two dependency PRs directly.)

Comment thread examples/multi_lora/multi_lora_async_rollout.py
Comment thread miles/utils/multi_lora.py
_build_prefill_scoring_payload keyed LoRA solely on is_lora_enabled(args)
(always true in multi-LoRA) and sent the single-adapter name miles_lora,
which is never registered on multi-LoRA engines -> every step crashed with
'adapters are not loaded' when --recompute-logprobs-via-prefill is on; the
batched path also applied payloads[0]'s lora_path to a mixed-adapter batch.

- resolve lora_path per sample: adapter samples score under their own
  __miles_slot_{N}, single-adapter LoRA keeps miles_lora, base keeps none
- batched scoring groups by (logprob_start_len, lora_path) and rejects
  mixed-adapter batch payloads
- centralize the engine-side slot adapter name as slot_lora_name(), shared
  by rollout, prefill scoring, and the weight-push mixin

Signed-off-by: Yusheng Su <yushengsu.thu@gmail.com>
The retire-time prefix abort fires exactly once (RETIRING->CLEANUP); requests
can survive it (a multi-turn group POSTs its next turn after the round, or a
request sits in the engine's tokenizer window) and, once the slot is reused
and the next adapter's weights are upserted into the same __miles_slot_{N},
keep decoding under the wrong weights with no error. Batch-time collection
filters already drop such groups from training data; these changes shrink the
window and stop wasting decode on them:

- MultiLoRABackend.free_slot: re-run the prefix abort right before the slot
  is released for reuse (second round, after a full step of settling)
- sglang_rollout.generate: refuse to POST for an adapter that is no longer
  sampleable (deregistered/cleaned up) and abort the sample locally instead
- the tokenizer-window escape itself is fixed engine-side (sgl-project#30912)

Signed-off-by: Yusheng Su <yushengsu.thu@gmail.com>
@yushengsu-thu

yushengsu-thu commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

@mathewjhan examples/multi_lora/tests/test_multi_lora_async_rollout.py::test_process_group_recycles_aborted fails on this branch (pre-existing at e371b2e, unrelated to the M1/M2 fixes): the test expects aborted groups to be re-queued, but the code path documents that re-queuing is not wired up. Which direction do you want — wire up re-queuing for aborted groups, or adjust the test to pin the current drop-and-count behavior? I'm happy to pick it up once the direction is set.

Re-queuing aborted groups (add_samples) is intended but deferred to a
future PR (confirmed by mathewjhan; left unimplemented for lack of
testing time). strict=True so wiring it up forces marker removal.

Signed-off-by: Yusheng Su <yushengsu.thu@gmail.com>
@yushengsu-thu

Copy link
Copy Markdown
Collaborator

Thanks for confirming — marked test_process_group_recycles_aborted as xfail(strict=True) in 50f6ff1 so the suite is green on this PR while still pinning the intended end-state behavior (wiring up re-queuing will XPASS and force the marker off). Two things worth bundling into that future PR: (1) the re-queuing itself (per-adapter source needs a write path), and (2) aborted/stale-dropped rows currently still count toward num_row, so adapters quietly train on fewer samples than configured — probably the more impactful half. Also worth skipping re-queue for RETIRING adapters (their slot is being freed; nothing can serve the regenerated group).

yushengsu-thu and others added 4 commits July 13, 2026 17:10
Each sglang tokenizer worker process holds its own LoRA registry with no
cross-worker sync (sgl-project/sglang#31084), so per-step adapter upserts
resolve on whichever worker the router picks and fail non-deterministically.
sglang rejects the upsert at runtime anyway; failing at launch avoids
burning GPU time until the first weight push.

Signed-off-by: Yusheng Su <yushengsu.thu@gmail.com>
@mathewjhan
mathewjhan force-pushed the feat/multi-lora-async branch from fdefa55 to f115610 Compare July 16, 2026 06:58
@yushengsu-thu

Copy link
Copy Markdown
Collaborator

Merged radixark/main into this branch to resolve the conflict (the branch was 36 commits behind). Single conflict in miles/rollout/sglang_rollout.py — an import-line collision between the multi-LoRA imports and main's new call_agent_abort_hook; resolved as the union. compileall clean; PR is now MERGEABLE.

Updated with Claude Code.

@mathewjhan
mathewjhan force-pushed the feat/multi-lora-async branch from 1fbe335 to 75d7538 Compare July 17, 2026 10:31
@mathewjhan
mathewjhan marked this pull request as ready for review July 17, 2026 20:29
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

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