Skip to content

[tinker] 9/n towards Kimi K2.6: colocated engine wake/offload for cold sample paths - #2031

Open
casper-hansen wants to merge 5 commits into
NovaSky-AI:mainfrom
casper-hansen:casper/kimi-9-tinker-sampling
Open

[tinker] 9/n towards Kimi K2.6: colocated engine wake/offload for cold sample paths#2031
casper-hansen wants to merge 5 commits into
NovaSky-AI:mainfrom
casper-hansen:casper/kimi-9-tinker-sampling

Conversation

@casper-hansen

@casper-hansen casper-hansen commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What

Two fixes that make sampling work reliably on the SkyRL-Train backend outside the train -> save_weights -> sample happy path:

  • Colocated engines are slept right after init and around every training op, and only save_weights_for_sampler woke them -- so a cold sample (a request against an already-synced adapter with no weight sync of its own in between, e.g. after a training op slept the engines) queued against sleeping engines and hung forever. Track engine sleep state on the backend, wake (weights + KV cache) on the sample path after offloading any GPU-resident trainer via the new WorkerDispatch.offload_for_sampling, and normalize to the asleep state before save_weights_for_sampler's wake -> broadcast -> wake dance.
  • Lazy engine bring-up runs on the first sampling-related call, which in a multi-tenant service can land right after another tenant's forward/forward_backward left the trainer GPU-resident; under colocate_all the engines' startup allocation then fails ("Engine core initialization failed"). Offload the trainer first, matching the build path's build -> offload -> engines order.

Scope change

An earlier revision of this PR also made create_sampling_client(base_model=...) (API-side model_id "") work on this backend. That was dropped after discussion with the maintainers (commit "Drop base-model sampling from the cold-sample fixes"): sample() keeps rejecting an empty model_id as unknown, and inference-engine model names resolve through resolve_policy_model_name only, exactly as on main. The remaining changes are model-agnostic engine sleep/wake/offload bookkeeping.

Part of the Kimi K2.x series (follow-up to #1862). Independent of the other PRs in the series.

Made with Cursor

…ine wake/offload

Four fixes that make sampling work reliably on the SkyRL-Train backend
outside the train->save_weights->sample happy path:

- create_sampling_client(base_model=...) maps to model_id "" on the API
  side, but sample() validated every model_id against the registered
  adapters and rejected "" as unknown. Treat falsy model_ids as
  base-model requests.
- Under LoRA weight sync (megatron + merge_lora=false),
  resolve_policy_model_name() returns the skyrl-lora adapter alias, so
  base-model sampling 404'd on vLLM: the alias only exists after the
  first sampler-weight save, and applying adapter deltas to a base-model
  request would be wrong anyway. Resolve falsy model_ids to
  generator.inference_engine.served_model_name / the policy model path.
- Colocated engines are slept right after init and around every training
  op, and only save_weights_for_sampler woke them -- so a cold sample
  (base model, or an already-synced adapter) queued against sleeping
  engines and hung forever. Track engine sleep state on the backend,
  wake (weights + KV cache) on the sample path after offloading any
  GPU-resident trainer via the new WorkerDispatch.offload_for_sampling,
  and normalize to the asleep state before save_weights_for_sampler's
  wake->broadcast->wake dance.
- Lazy engine bring-up runs on the first sampling-related call, which in
  a multi-tenant service can land right after another tenant's
  forward/forward_backward left the trainer GPU-resident; under
  colocate_all the engines' startup allocation then fails ("Engine core
  initialization failed"). Offload the trainer first, matching the build
  path's build -> offload -> engines order.

Co-authored-by: Cursor <cursoragent@cursor.com>

@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 mechanisms to manage the state of colocated inference engines (sleeping/waking) and offload the trainer to CPU during sampling to prevent out-of-memory (OOM) errors. It also handles base-model sampling requests where the model ID is empty. The review feedback highlights a potential AttributeError in _wake_inference_engines_for_sampling due to a missing None check on self._dispatch, which should be guarded similarly to other methods.

Comment thread skyrl/backends/skyrl_train_backend.py Outdated
return
if not self._engines_asleep:
return
self._dispatch.offload_for_sampling("policy")

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.

high

In _wake_inference_engines_for_sampling, self._dispatch is accessed directly without a None check. However, in _ensure_inference_engines (line 427), self._dispatch is explicitly guarded with if self._dispatch is not None:. To prevent a potential AttributeError: 'NoneType' object has no attribute 'offload_for_sampling' when the dispatch layer is not yet initialized, we should add a similar guard here.

Suggested change
self._dispatch.offload_for_sampling("policy")
if self._dispatch is not None:
self._dispatch.offload_for_sampling("policy")

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.

Declining this one — the None case is unreachable here by construction: _wake_inference_engines_for_sampling early-returns unless self._inference_engines_initialized, and that flag is only set True at the end of _ensure_inference_engines, two lines after it dereferences self._dispatch (set_inference_engine_client). So whenever the wake path runs, _dispatch is provably non-None. The guard you're pointing at in _ensure_inference_engines exists precisely because that code runs before this wiring is guaranteed.

Comment thread skyrl/backends/skyrl_train/workers/worker_dispatch.py Outdated
…engines

offload_for_sampling only offloaded the named role (callers passed
"policy"), so a preceding critic forward/forward_backward left the
critic GPU-resident on the cold-sample and lazy engine bring-up paths
and could OOM the engines' startup allocation. Offload every tracked
GPU-resident model instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
@avigyabb
avigyabb self-requested a review August 17, 2026 23:26
@avigyabb

Copy link
Copy Markdown
Collaborator

Hey Casper, thanks so much for putting this up! If my understanding is correct:

sample() now calls _wake_inference_engines_for_sampling() which will allow us to send sampling requests to vLLM. However, in the FFT path _create_new_inference_client() will put the weights in level 2 sleep when calling client.sleep(). As a result, we might sample from vLLM using garbage weights in the FFT path. Before this PR, when we would sample without syncing the weights to vLLM, the request would hang since vLLM had not allocated resources on the GPU for the weights and the KV cache. I think the silent sampling of garbage weights might be confusing for users that happen to run into this. What do you think?

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 8b0a909. Configure here.

Comment thread skyrl/backends/skyrl_train_backend.py
avigyabb and others added 2 commits September 4, 2026 21:46
Resolve conflict in save_sampler_checkpoint: keep both the PR's
_engines_asleep reset and main's _inference_adapter_ids tracking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Avi Basnet <avigyabb@stanford.edu>
Per maintainer review: keep the SkyRL-Train backend's contract that every
sampled model_id is a registered policy. sample() rejects an empty model_id
as unknown again and _sample_with_remote_client resolves names through
resolve_policy_model_name only, as on main. The engine sleep-state
tracking, the wake on the sample path and the trainer offload before engine
bring-up are unchanged; their docstrings no longer cite base-model
sampling as a motivating case.

Made with Cursor
casper-hansen pushed a commit to casper-hansen/SkyRL that referenced this pull request Sep 5, 2026
…kpoint-sleep

Picks up the base-model sampling removal from NovaSky-AI#2031: sample() rejects an
empty model_id as unknown again and the per-request model name resolves
through resolve_policy_model_name only. Kept this branch's
_engines_sleep_level tracking (level-2 wake refusal, try/finally around the
sampler sync) through the conflicts.
@casper-hansen casper-hansen changed the title [tinker] 9/n towards Kimi K2.6: base-model sampling and colocated engine wake/offload [tinker] 9/n towards Kimi K2.6: colocated engine wake/offload for cold sample paths Sep 5, 2026
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.

3 participants