Skip to content

[bug] Fix LoRA disk reloading and sleep issues - #1606

Merged
SumanthRH merged 3 commits into
NovaSky-AI:mainfrom
hao-aaron:lora-disk-fix
May 2, 2026
Merged

SumanthRH merged 3 commits into
NovaSky-AI:mainfrom
hao-aaron:lora-disk-fix

Conversation

@hao-aaron

@hao-aaron hao-aaron commented May 1, 2026 •

Copy link
Copy Markdown
Collaborator

Two bugs fixed here:

  1. When lora is enabled with merge_lora=False, a lora reload request with load_inplace=True causes each step to trigger load from disk causing massive slowdowns. This requires an upstream vllm fix, but I provide a temporary workaround by unloading the lora first then loading with load_inplace=False. This could be the issue behind _SKYRL_USE_NEW_INFERENCE=1 (default) causes ~15x performance regression vs legacy path #1582
  2. New inference code path does not override sleep level 2 to level 1 when using lora, since when we are using unmerged LoRA weights we should not discard the base weights. Discarding base weights with level 2 sleep led to garbage output

Validated by running previously slow lora run and ensuring nonzero reward: https://wandb.ai/sky-posttraining-uc-berkeley/gsm8k_0.5b_lora/runs/f44fhwwl


Open in Devin Review

x
Signed-off-by: hao-aaron <ahao@anyscale.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: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

@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 uses_lora_weight_sync flag to the RemoteInferenceClient to manage VRAM usage during sleep cycles, specifically forcing a lower sleep level to preserve the base model when only LoRA adapters are synced. It also refactors update_lora_from_disk to perform an unload-then-load sequence, bypassing a vLLM performance issue related to load_inplace. The review feedback suggests hardening error handling when parsing JSON responses from the inference server to avoid masking HTTP errors with content type exceptions.

Comment on lines +1134 to +1136
if resp.status >= 400:
body = await resp.json()
raise_for_status(resp, body)

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.

medium

The call to resp.json() is risky here. If the server returns a non-JSON error response (e.g., a 502 Bad Gateway from a proxy or a 500 Internal Server Error with a plain text stack trace), await resp.json() will raise an aiohttp.ContentTypeError. This masks the actual HTTP status code and error message, making debugging harder. It is safer to wrap the JSON parsing in a try-except block or check the content type before parsing, especially since raise_for_status is already designed to handle a None body by falling back to the standard resp.raise_for_status().

Suggested change
if resp.status >= 400:
body = await resp.json()
raise_for_status(resp, body)
if resp.status >= 400:
try:
body = await resp.json()
except Exception:
body = None
raise_for_status(resp, body)

Comment on lines 1142 to 1144
if resp.status >= 400:
body = await resp.json()
raise_for_status(resp, body)

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.

medium

Similar to the _unload_on_server implementation, calling resp.json() directly on a non-2xx response can lead to a ContentTypeError if the error body is not valid JSON. Wrapping this in a try-except block ensures that the actual HTTP error is surfaced via raise_for_status even if the error response body is malformed or plain text.

Suggested change
if resp.status >= 400:
body = await resp.json()
raise_for_status(resp, body)
if resp.status >= 400:
try:
body = await resp.json()
except Exception:
body = None
raise_for_status(resp, body)

active_lora_name: Optional[str] = None
"""Name of the active LoRA adapter. If set, generation requests use this adapter instead of the base model."""

uses_lora_weight_sync: bool = False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need a separate flag here? Can't we use just active_lora_name is not None as the condition?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines +1105 to +1112
Implementation: unload then load (with load_inplace=False), rather than a single
load with load_inplace=True. Reason: vLLM's /v1/load_lora_adapter caches the
LoRARequest object server-side keyed by lora_name (see OpenAIServingModels.lora_requests).
If load_inplace=True is set on that cached request, every subsequent generate request
causes LRUCacheWorkerLoRAManager.add_adapter to re-read the adapter from disk on every
model step (line 299-307 of vllm/lora/worker_manager.py checks `load_inplace` per-step).
The cleanest fix is upstream (clear load_inplace before caching); until that lands,
unloading first lets us reuse the same lora_name without ever needing load_inplace=True.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: Docstring stylke is not consistent with the rest of the codebase. Can you clean this up and also convert this into a TODO:

The cleanest fix is upstream (clear load_inplace before caching); until that lands,
unloading first lets us reuse the same lora_name without ever needing load_inplace=True.

hao-aaron added 2 commits May 1, 2026 19:39
x
Signed-off-by: hao-aaron <ahao@anyscale.com>
x
Signed-off-by: hao-aaron <ahao@anyscale.com>
@SumanthRH
SumanthRH merged commit 2d06942 into NovaSky-AI:main May 2, 2026
5 of 6 checks passed
SumanthRH pushed a commit that referenced this pull request May 6, 2026
Bug fixes found in #1616

While testing out the tinker CI, found a port collision bug.

async def _run_server(self) -> None:
    """Internal method to run the HTTP server."""
    # Release the port reservation right before vLLM rebinds.
    if self._port_reservation is not None:
self._port_reservation.close() # ← step A
        self._port_reservation = None
    ...
    sock_addr = (self._cli_args.host, self._cli_args.port)
    sock = create_server_socket(sock_addr)
There exists a window of time where we free port so vllm can take it,
but other actors can also. Fixed this by allocating a specific,
non-overlapping port range for each actor.

Additionally, changed the disk reloading workaround
#1606 created here to more
closely mirror the upstream change
vllm-project/vllm#41482, adding a new custom
endpoint with the load_inplace override.

---------

Signed-off-by: hao-aaron <ahao@anyscale.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
SumanthRH pushed a commit that referenced this pull request May 13, 2026
This PR introduces a tinker ci, closing
#1553. It uses
tinker_cookbook.recipes.math_rl.train. Currently the `REWARD_MIN_VALUE`
is set to 0, but will be tightened once we have a couple of runs. Added
both colocated and fully async tests.


https://wandb.ai/sky-posttraining-uc-berkeley/gsm8k_tinker_ci?nw=nwuserahao

https://wandb.ai/sky-posttraining-uc-berkeley/gsm8k_tinker_fully_async_ci?nw=nwuserahao


While testing out the tinker CI, found a port collision bug.
```
async def _run_server(self) -> None:
    """Internal method to run the HTTP server."""
    # Release the port reservation right before vLLM rebinds.
    if self._port_reservation is not None:
        self._port_reservation.close()                          # ← step A
        self._port_reservation = None
    ...
    sock_addr = (self._cli_args.host, self._cli_args.port)
    sock = create_server_socket(sock_addr)
```
There exists a window of time where we free port so vllm can take it,
but other actors can also. Fixed this by allocating a specific,
non-overlapping port range for each actor.

Additionally, changed the disk reloading workaround
#1606 created here to more
closely mirror the upstream change
vllm-project/vllm#41482, adding a new custom
endpoint with the load_inplace override.

---------

Signed-off-by: hao-aaron <ahao@anyscale.com>
Signed-off-by: ahao-anyscale <ahao@anyscale.com>
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.

_SKYRL_USE_NEW_INFERENCE=1 (default) causes ~15x performance regression vs legacy path

2 participants