Skip to content

[Example][Bugfix]: align custom rollout paths with vLLM router API - #75

Closed
CalvinXKY wants to merge 1 commit into
mainfrom
feature/examples-validation
Closed

[Example][Bugfix]: align custom rollout paths with vLLM router API#75
CalvinXKY wants to merge 1 commit into
mainfrom
feature/examples-validation

Conversation

@CalvinXKY

Copy link
Copy Markdown
Collaborator

Summary

Align remaining vime examples and related rollout hooks with the vLLM-first API surface after the SGLang removal on main. Custom generate paths now use args.router_ip / args.router_port instead of the deprecated vllm_router_* attributes, fixing smoke failures in examples such as multi_agent.

Changes

  • Examples: update multi_agent/agent_system.py and geo3k_vlm_multi_turn/rollout.py to call the router via router_ip / router_port
  • Docs: fix stale router field names in usage guides
  • Framework (minor): align --opd-type vllm with example scripts and teacher scoring via /v1/completions + prompt_logprobs

Validation

Smoke-tested on A100-server / vime_v22 using external scripts under run_script/validate_examples/ (not included in this PR):

Example Result
train_infer_mismatch_helper PASS
eval_multi_task PASS
fully_async PASS
multi_agent PASS (after router fix)
geo3k_vlm PASS
on_policy_distillation (megatron smoke) PASS
geo3k_vlm_multi_turn FAIL (example-level multimodal processor issue)

Removed on main and not covered here: retool, search-r1 (SGLang-only).

Test plan

  • multi_agent example completes at least one rollout
  • geo3k_vlm_multi_turn and geo3k_vlm smoke on available VLM checkpoints
  • No regressions in existing CI e2e tests (this PR does not modify CI test files)

@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 transitions the On-Policy Distillation (OPD) implementation from SGLang to vLLM, updating documentation, example scripts, command-line arguments, and reward processing logic to use the vLLM /v1/completions endpoint. Feedback on these changes highlights several robustness improvements in slime/rollout/on_policy_distillation.py: preventing potential alignment shifts when teacher logprobs are truncated by padding missing entries, adding defensive type-checking in _logprob_for_token to avoid AttributeErrors, and directly accessing sample.reward instead of calling sample.get_reward_value(args) to prevent KeyErrors when a global reward key is configured.

Comment thread slime/rollout/on_policy_distillation.py Outdated
Comment on lines +101 to +105
choice = reward["choices"][0]
plp = choice.get("prompt_logprobs") or []
per_pos = [
_logprob_for_token(plp[i], sample.tokens[i]) for i in range(1, min(len(plp), len(sample.tokens)))
]

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

If the teacher server truncates the input or returns fewer prompt logprobs than expected (len(plp) < len(sample.tokens)), the current list comprehension will truncate per_pos. When t_log_prob[-response_length:] is subsequently called, it will slice the last response_length elements of this truncated list, causing a silent alignment shift where logprobs are matched with the wrong tokens.

To prevent this, we should pad any missing logprobs with None (which _logprob_for_token safely handles as 0.0) so that per_pos always maintains a 1:1 index alignment with sample.tokens.

Additionally, we should defensively extract choice to avoid raising a KeyError or IndexError if the teacher response is empty or malformed.

Suggested change
choice = reward["choices"][0]
plp = choice.get("prompt_logprobs") or []
per_pos = [
_logprob_for_token(plp[i], sample.tokens[i]) for i in range(1, min(len(plp), len(sample.tokens)))
]
choice = reward.get("choices", [{}])[0] if isinstance(reward, dict) and reward.get("choices") else {}
plp = choice.get("prompt_logprobs") or []
per_pos = [
_logprob_for_token(plp[i] if i < len(plp) else None, sample.tokens[i])
for i in range(1, len(sample.tokens))
]

Comment thread slime/rollout/on_policy_distillation.py Outdated
Comment on lines +78 to +81
def _logprob_for_token(pos_entry: dict | None, token_id: int) -> float:
"""Pull the teacher's logprob for `token_id` out of one position's logprob dict."""
if pos_entry is None:
return 0.0

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

To prevent potential AttributeErrors if the teacher server returns an unexpected format where pos_entry is not a dictionary (e.g., a list or string), we should explicitly verify that pos_entry is an instance of dict before calling .get().

Suggested change
def _logprob_for_token(pos_entry: dict | None, token_id: int) -> float:
"""Pull the teacher's logprob for `token_id` out of one position's logprob dict."""
if pos_entry is None:
return 0.0
def _logprob_for_token(pos_entry: dict | None, token_id: int) -> float:
"""Pull the teacher's logprob for token_id out of one position's logprob dict."""
if not isinstance(pos_entry, dict):
return 0.0

The actual learning signal comes from the OPD KL penalty applied in compute_advantages_and_returns.
"""
"""Process rewards from teacher model and extract teacher log probabilities."""
raw_rewards = [sample.get_reward_value(args) for sample in samples]

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

Using sample.get_reward_value(args) here is risky. If args.reward_key is configured globally for other tasks, get_reward_value will attempt to look up that key in the teacher's JSON response dict. Since the teacher response only contains standard OpenAI-compatible fields (like choices), this will raise a KeyError.

Since we specifically need the raw teacher response dictionary here, we should access sample.reward directly.

Suggested change
raw_rewards = [sample.get_reward_value(args) for sample in samples]
raw_rewards = [sample.reward for sample in samples]

@CalvinXKY CalvinXKY changed the title fix(examples): align custom rollout paths with vLLM router API [Example][Bugfix]: align custom rollout paths with vLLM router API May 30, 2026
@CalvinXKY

Copy link
Copy Markdown
Collaborator Author

A100 smoke validation (vime_v22 @ A100-server)

Re-ran run_script/validate_examples/run_all.sh on 2026-05-30 against feature/examples-validation (PR branch files synced to NFS).

Example Result
train_infer_mismatch_helper PASS
eval_multi_task PASS
fully_async PASS
multi_agent PASS
on_policy_distillation_megatron PASS
geo3k_vlm PASS
geo3k_vlm_multi_turn PASS (after fix in latest commit)

Log: /data/nfs_87/xky/new_rl/logs/examples_validate/run_all_20260530_102300.log

geo3k_vlm_multi_turn fix

Previous failure was IndexError in Qwen3VL processor (image_grid_thw mismatch): the rollout re-applied apply_chat_template on a message that already contained separate image entries plus a chat-templated sample.prompt, duplicating vision slots.

Fixed by aligning with vllm_rollout:

  • processor path: processor(text=sample.prompt, **build_processor_kwargs(multimodal_inputs))
  • render message layout: text first, then image entries (same as standard MM render)

Retest log: smoke_geo3k_multi_turn_retest_20260530.logPASS

Out of scope (unchanged)

Example Status
search_r1 SKIP (missing Search-R1 data)
strands_sglang / tau_bench SKIP (deps not in vime_v22)
retool / search-r1 N/A (removed on main, SGLang-only)

aoshen02 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

I checked the corresponding geo3k_vlm_multi_turn code in upstream slime and miles, and I think this PR is fixing the right root cause for the initial prompt path: sample.prompt is already the dataset-rendered / chat-templated prompt, so the first turn should not wrap it back into a fresh chat message and run apply_chat_template again.

References:

So the change to build initial train-side features from sample.prompt + sample.multimodal_inputs is the right direction.

One thing I would still adjust before merging: please keep the multi-turn behavior closer to slime/miles instead of dropping it entirely.

Concrete suggestions:

  1. Keep separate helpers for the two cases:

    • initial dataset prompt: use processor(text=sample.prompt, **build_processor_kwargs(sample.multimodal_inputs))
    • later env observations: they are freshly-created messages from env.format_observation(...), so it is still reasonable to process those messages separately, as slime/miles do.
  2. Preserve observation multimodal features. This PR removes the old obs_train_feats collection. That avoids the initial double-template bug, but it also means later observation images/videos will no longer contribute to sample.multimodal_train_inputs. In slime/miles, the observation path still returns and merges per-turn multimodal_train_inputs: https://github.com/THUDM/slime/blob/bf14dc21/examples/geo3k_vlm_multi_turn/rollout.py#L212-L233

  3. Preserve the full token trajectory if possible. The slime/miles implementation appends prompt/observation tokens with loss_mask=0 and assistant tokens with loss_mask=1. In the current vLLM render path, sample.tokens only grows by generated assistant tokens, while the prompt and observation turns are only implicit in the render request. A closer mirror would be:

    • after /v1/chat/completions/render, keep the rendered prompt token_ids
    • append only the delta not already in sample.tokens with loss_mask=0 / logprob 0.0
    • append generated tokens with loss_mask=1
    • validate prefix alignment so silent template drift is caught early
  4. Add a regression test for the exact failure mode:

    • initial sample.prompt is already chat-templated and has multimodal inputs
    • the custom rollout must not call apply_chat_template([initial_message]) for processor features
    • later observation messages can still be processed as fresh messages
    • the number/order of image placeholders matches the processor output, especially image_grid_thw

In short: I would keep this PR's initial-prompt fix, but mirror slime/miles by preserving the observation multimodal path and full multi-turn token/loss-mask trajectory. That gives us the bug fix without losing the behavior the original slime/miles rollout was maintaining.

@CalvinXKY
CalvinXKY force-pushed the feature/examples-validation branch 2 times, most recently from 186a740 to 5cb8927 Compare June 1, 2026 07:45
…me/miles

Use processor(sample.prompt) for the initial turn, preserve observation multimodal encoding and full token/loss_mask trajectory on the vLLM render path.
@CalvinXKY
CalvinXKY force-pushed the feature/examples-validation branch from 5cb8927 to 5daa5c9 Compare June 1, 2026 07:51
@CalvinXKY

Copy link
Copy Markdown
Collaborator Author

Related to #120.

@CalvinXKY CalvinXKY closed this Jul 9, 2026
@CalvinXKY
CalvinXKY deleted the feature/examples-validation branch July 9, 2026 07:44
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