[Feature]: Lora support#336
Conversation
Signed-off-by: princepride <wangzhipeng628@gmail.com> Signed-off-by: Wang, Zhipeng | RASIA <zhipeng.wang@rakuten.com> Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: princepride <wangzhipeng628@gmail.com> Signed-off-by: Wang, Zhipeng | RASIA <zhipeng.wang@rakuten.com> Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: princepride <wangzhipeng628@gmail.com> Signed-off-by: Wang, Zhipeng | RASIA <zhipeng.wang@rakuten.com> Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: 汪志鹏 <wangzhipeng628@gmail.com> Signed-off-by: Wang, Zhipeng | RASIA <zhipeng.wang@rakuten.com> Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: princepride <wangzhipeng628@gmail.com> Signed-off-by: Wang, Zhipeng | RASIA <zhipeng.wang@rakuten.com> Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: princepride <wangzhipeng628@gmail.com> Signed-off-by: Wang, Zhipeng | RASIA <zhipeng.wang@rakuten.com> Signed-off-by: princepride <wangzhipeng628@gmail.com>
main vllm-project#264 added `from vllm.utils.system_utils import kill_process_tree` to vllm_engine.py but the no-vllm test stub registered vllm.utils as a plain module, so `pytest tests/utils` failed at collection with "'vllm.utils' is not a package". Make vllm.utils a package and stub system_utils.kill_process_tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: princepride <wangzhipeng628@gmail.com> Signed-off-by: Wang, Zhipeng | RASIA <zhipeng.wang@rakuten.com> Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: princepride <wangzhipeng628@gmail.com> Signed-off-by: Wang, Zhipeng | RASIA <zhipeng.wang@rakuten.com> Signed-off-by: princepride <wangzhipeng628@gmail.com>
- create_lora_instance: stop passing exclude_modules to Megatron-Bridge;
it asserts exclude_modules is empty when target_modules is set, so any
--exclude-modules run crashed at model build. Exclusion is already
applied to args.target_modules during arg validation.
- adapter_config.json: derive target_modules from the actually exported
adapter weight names (infer_hf_target_modules) instead of user args,
so fused-projection exports (e.g. q_proj -> linear_qkv -> q/k/v) can
no longer disagree with adapter_model.bin.
- Cache the AutoBridge per process (lru_cache) instead of rebuilding it
on every rank at every weight-update step.
- Delete stale step_{N} adapter dirs after saving a new one to bound
disk usage.
- Remove dead code: is_lora_weight_name, is_adapter_param_name, the
unread STABLE marker, parse_exclude_modules alias, lora_adapter_name
helper and LORA_ADAPTER_NAME constant, convert_target_modules_to_hf.
- Simplify VLLMEngine.load_lora_adapter: single /v1 endpoint (matches
pinned vLLM), unload failures now raise instead of being swallowed at
debug level (404 = not loaded yet), drop the tolerant _parse whose
return value nobody consumed.
- Drop log-and-reraise try/except and per-tensor cuda.synchronize in
the adapter export loop (.cpu() already synchronizes).
- Replace impossible getattr/hasattr guards with direct attribute
access (lora args are registered unconditionally), collapse the
duplicated lora_rank check in vime_validate_args, and document the
LoRA -> sleep level 1 coupling in release_memory_occupation.
Test fixtures gain lora_rank=0 to mirror the always-registered arg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Wang, Zhipeng | RASIA <zhipeng.wang@rakuten.com>
Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: Wang, Zhipeng | RASIA <zhipeng.wang@rakuten.com> Signed-off-by: princepride <wangzhipeng628@gmail.com>
Signed-off-by: princepride <wangzhipeng628@gmail.com>
Documentation build overview
42 files changed ·
|
There was a problem hiding this comment.
Code Review
This pull request introduces support for actor LoRA training and runtime vLLM adapter serving. It adds new command-line arguments, integrates PEFT LoRA adapter saving and loading into the checkpointing mechanism, and updates the weight update flow to support hot-reloading adapters on vLLM rollout engines. The review feedback identifies two key issues: a potential deadlock during weight updates if Ray calls on rank 0 fail, which can be resolved by broadcasting a status flag to other ranks, and a failure to respect the 'skip_load_to_model_and_opt' flag when loading LoRA adapters during checkpoint resume.
| @torch.no_grad() | ||
| def _update_lora_adapter(self) -> None: | ||
| """Export the current adapter and ask vLLM engines to load it by path.""" | ||
| rank = dist.get_rank() | ||
| # The barriers below must always be reached on rank 0, even when a Ray | ||
| # call raises, otherwise the other ranks would block on the barrier | ||
| # indefinitely instead of failing fast. | ||
| if rank == 0: | ||
| try: | ||
| ray.get([engine.pause_generation.remote() for engine in self.rollout_engines]) | ||
| ray.get([engine.flush_cache.remote() for engine in self.rollout_engines]) | ||
| finally: | ||
| dist.barrier(group=get_gloo_group()) | ||
| else: | ||
| dist.barrier(group=get_gloo_group()) | ||
|
|
||
| adapter_path = save_lora_adapter_for_vllm(self.model, self.args, self.weight_version) | ||
|
|
||
| if rank == 0: | ||
| try: | ||
| refs = [ | ||
| engine.load_lora_adapter.remote( | ||
| self.args.lora_adapter_name, | ||
| adapter_path, | ||
| weight_version=str(self.weight_version), | ||
| ) | ||
| for engine in self.rollout_engines | ||
| ] | ||
| ray.get(refs) | ||
| ray.get([engine.continue_generation.remote() for engine in self.rollout_engines]) | ||
| finally: | ||
| dist.barrier(group=get_gloo_group()) | ||
| else: | ||
| dist.barrier(group=get_gloo_group()) |
There was a problem hiding this comment.
If any of the Ray calls (like pause_generation or flush_cache) on rank 0 raise an exception, rank 0 will enter the finally block and call dist.barrier(). Ranks 1..N will also reach their dist.barrier() and unblock. However, rank 0 will then propagate the exception and crash/exit, while ranks 1..N will proceed to save_lora_adapter_for_vllm(). Inside save_lora_adapter_for_vllm(), ranks 1..N will call dist.barrier(). Since rank 0 has already crashed and will never reach this barrier, all other ranks will hang indefinitely.
To prevent this, we should broadcast a success/failure status flag from rank 0 to all other ranks using dist.broadcast on a CPU tensor, and raise an exception on other ranks if rank 0 failed.
@torch.no_grad()
def _update_lora_adapter(self) -> None:
"""Export the current adapter and ask vLLM engines to load it by path."""
rank = dist.get_rank()
status = torch.tensor([0], dtype=torch.int)
if rank == 0:
try:
ray.get([engine.pause_generation.remote() for engine in self.rollout_engines])
ray.get([engine.flush_cache.remote() for engine in self.rollout_engines])
status[0] = 1
except Exception as e:
logger.error("Failed to pause generation or flush cache on rollout engines: %s", e)
raise e
finally:
dist.broadcast(status, src=0, group=get_gloo_group())
else:
dist.broadcast(status, src=0, group=get_gloo_group())
if status[0] == 0:
raise RuntimeError("Rank 0 failed to pause generation or flush cache on rollout engines.")
adapter_path = save_lora_adapter_for_vllm(self.model, self.args, self.weight_version)
if rank == 0:
try:
refs = [
engine.load_lora_adapter.remote(
self.args.lora_adapter_name,
adapter_path,
weight_version=str(self.weight_version),
)
for engine in self.rollout_engines
]
ray.get(refs)
ray.get([engine.continue_generation.remote() for engine in self.rollout_engines])
status[0] = 1
except Exception as e:
status[0] = 0
logger.error("Failed to load LoRA adapter or continue generation on rollout engines: %s", e)
raise e
finally:
dist.broadcast(status, src=0, group=get_gloo_group())
else:
dist.broadcast(status, src=0, group=get_gloo_group())
if status[0] == 0:
raise RuntimeError("Rank 0 failed to load LoRA adapter or continue generation on rollout engines.")| if is_lora_enabled(args): | ||
| adapter_path = getattr(args, "lora_adapter_path", None) | ||
| if adapter_path is not None: | ||
| loaded, iteration = load_lora_adapter( | ||
| ddp_model, | ||
| adapter_path, | ||
| optimizer=optimizer, | ||
| opt_param_scheduler=opt_param_scheduler, | ||
| ) | ||
| if loaded: | ||
| logger.info("Successfully loaded LoRA adapter from %s", adapter_path) | ||
| if iteration is not None: | ||
| result = (iteration, result[1]) | ||
| else: | ||
| logger.warning( | ||
| "LoRA is enabled and --lora-adapter-path=%s was specified, but adapter weights " | ||
| "could not be loaded. Training will start with freshly initialized adapter weights.", | ||
| adapter_path, | ||
| ) |
There was a problem hiding this comment.
When skip_load_to_model_and_opt is True, we are supposed to skip loading weights to the model and optimizer. However, load_lora_adapter is currently called unconditionally, which will load the adapter weights into the model and optimizer state.
We should respect skip_load_to_model_and_opt by passing it to load_lora_adapter as skip_load_to_model, and only passing optimizer and opt_param_scheduler if skip_load_to_model_and_opt is False.
if is_lora_enabled(args):
adapter_path = getattr(args, | assert build_peft_lora_config(args, ["q_proj", "k_proj", "v_proj", "down_proj"]) == { | ||
| "peft_type": "LORA", | ||
| "task_type": "CAUSAL_LM", | ||
| "r": 8, |
There was a problem hiding this comment.
We don't add more unit test here.
Summary
#206
This PR adds end-to-end LoRA support for Megatron actor training with colocated vLLM rollout. When
--lora-rank > 0, the training actor applies LoRA adapters via Megatron-Bridge, exports PEFT-format adapters after each weight update, and hot-loads them into vLLM through the runtime LoRA API. Rollout requests then use the adapter name instead of the base model path.This enables parameter-efficient RL fine-tuning without full-weight sync between trainer and inference engines on every step.
Motivation
Full-weight synchronization between Megatron and colocated vLLM engines is expensive for large models. LoRA reduces the sync payload to small adapter weights and lets vLLM load adapters at runtime, which is a better fit for iterative RL training.
Key Changes
Training (Megatron)
--lora-rank,--lora-alpha,--lora-dropout,--lora-type(lora/canonical_lora),--target-modules,--exclude-modules,--lora-adapter-namelora_utils.py: target module parsing/conversion (HF ↔ Megatron), PEFT config generation, adapter export (adapter_model.bin+adapter_config.json)model_provider.py: apply LoRA directly after Bridge provider returns the actor model (instead of relying on pre-wrap hooks)actor.py: wake up offloaded model before LoRA export inoffload_train + colocatemodearguments.py: auto-enablevllm_enable_lora, validate LoRA requires--megatron-to-hf-mode bridgeand--colocateWeight Sync (Trainer → vLLM)
update_weight_from_tensor.py: when LoRA is enabled, skip full-weight IPC sync; export adapter and callload_lora_adapteron all rollout enginesvllm_engine.py:VLLM_ALLOW_RUNTIME_LORA_UPDATING=1when LoRA is enabledload_lora_adapter()with unload-then-load flowRollout
vllm_rollout.py/vllm_streaming_rollout.py: use adapter name (defaultvime_lora) as the"model"field in rollout requests when LoRA is enabledCleanup & Reliability
train.py: wrap main loop intry/finallyto dispose actor/rollout resourcesactor_group.py/actor.py: adddispose()for explicit wandb cleanup in Ray actorslogging_utils.py: callwandb.teardown()afterwandb.finish()to avoidBrokenPipeErrornoise on Ray actor exitTests
python3 train.py \ --hf-checkpoint /root/models/Qwen3-8B \ --ref-load /root/models/Qwen3-8B_torch_dist \ --load /root/models/Qwen3-8B \ --finetune \ --no-load-optim \ --no-load-rng \ --save /root/vime_lora_runs/qwen3-8b-dapo-lora-smoke \ --save-interval 1 \ --megatron-to-hf-mode bridge \ --actor-num-nodes 1 \ --actor-num-gpus-per-node 1 \ --colocate \ --calculate-per-token-loss \ --prompt-data /root/datasets/dapo-math-17k/dapo-math-17k.jsonl \ --input-key prompt \ --label-key label \ --apply-chat-template \ --rollout-shuffle \ --rm-type deepscaler \ --num-rollout 1 \ --rollout-batch-size 1 \ --n-samples-per-prompt 1 \ --rollout-max-response-len 1024 \ --rollout-temperature 0.8 \ --global-batch-size 1 \ --micro-batch-size 1 \ --advantage-estimator grpo \ --kl-loss-coef 0.00 \ --kl-loss-type k1 \ --kl-coef 0.00 \ --entropy-coef 0.00 \ --eps-clip 4e-4 \ --optimizer adam \ --lr 1e-6 \ --lr-decay-style constant \ --weight-decay 0.1 \ --adam-beta1 0.9 \ --adam-beta2 0.98 \ --tensor-model-parallel-size 1 \ --pipeline-model-parallel-size 1 \ --context-parallel-size 1 \ --expert-model-parallel-size 1 \ --expert-tensor-parallel-size 1 \ --use-dynamic-batch-size \ --max-tokens-per-gpu 4096 \ --rollout-num-gpus-per-engine 1 \ --rollout-num-gpus 1 \ --vllm-gpu-memory-utilization 0.45 \ --vllm-max-cudagraph-capture-size 32 \ --lora-rank 8 \ --lora-alpha 16 \ --lora-dropout 0.0 \ --lora-type lora \ --target-modules all-linear \ --only-train-params-name-list lora_A lora_B linear_in linear_out \ --lora-adapter-name vime_lora \ --attention-dropout 0.0 \ --hidden-dropout 0.0 \ --accumulate-allreduce-grads-in-fp32 \ --attention-softmax-in-fp32 \ --attention-backend flash \ --use-wandb \ --wandb-host https://api.wandb.ai \ --wandb-project vime-lora-smoke \ --wandb-group qwen3-8b-dapo-single-h100 \ --wandb-key "${WANDB_API_KEY}" \ --disable-wandb-random-suffixSupersedes #228 (re-opened from fork branch
princepride:lora-support).