Skip to content

fix(opd): score teacher over /inference/v1/generate (text + multimodal) - #141

Merged
CalvinXKY merged 3 commits into
mainfrom
fix/opd-vllm-completions-12
Jun 3, 2026
Merged

fix(opd): score teacher over /inference/v1/generate (text + multimodal)#141
CalvinXKY merged 3 commits into
mainfrom
fix/opd-vllm-completions-12

Conversation

@aoshen02

@aoshen02 aoshen02 commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Problem (test_qwen2.5_0.5B_opd_vllm, CI #12)

The OPD test launches a stock vllm.entrypoints.openai.api_server teacher and points --rm-url at /v1/completions. But the built-in reward function (vime/rollout/on_policy_distillation.py) — rewritten in #18 when SGLang was removed — POSTs vime's disaggregated /inference/v1/generate contract instead:

  • request: top-level token_ids + nested sampling_params
  • response: top-level prompt_logprobs

/inference/v1/generate is mounted only on the disagg serve app (vllm/entrypoints/serve/disagg/api_router.py), not on the stock OpenAI server. So the teacher returns HTTP 400.

Worse, reward_func used resp.raise_for_status(): on non-200 aiohttp raises ClientResponseError, whose response headers are a CIMultiDictProxy that can't be pickled. When Ray ships the exception to the driver the real 400 is replaced by an opaque can't pickle CIMultiDictProxy.

The intended contract is already documented by merged PR #62 ("OPD teacher uses /v1/completions with echo=True + prompt_logprobs=1", consuming choices[0].prompt_logprobs) and matched by the test's --rm-url. This PR realigns the code to it.

Note vs #99: #99 flips the test's --rm-url to /inference/v1/generate, but the teacher there is still the stock OpenAI server, which doesn't serve that route → it would 404. Fixing the code to speak /v1/completions (this PR) keeps the standard, portable contract and needs no teacher/endpoint change in the test.

Fix (vime/rollout/on_policy_distillation.py, single file)

  • reward_func → OpenAI /v1/completions body: prompt=sample.tokens, max_tokens=1, temperature=0, echo=True, prompt_logprobs=1, skip_special_tokens=False. model only sent when --opd-teacher-model is set (vLLM accepts a missing model → default served model; _is_model_supported(None)→True).
  • replace raise_for_status() with an explicit if resp.status != 200: raise RuntimeError(status, body[:1000])picklable across Ray, and surfaces the real teacher error instead of the CIMultiDictProxy mask.
  • post_process_rewards → read choices[0].prompt_logprobs (was top-level). Per-position shape is identical (dict[token_id→Logprob] | None, pos 0 = None), so _logprob_for_token / length-assert / trim logic is unchanged.

Matches the verl gold standard (extract_prompt_logprobs reads output.prompt_logprobs[1:] from vLLM completions) and slime's parent HTTP-teacher design.

Validation

gb200 single node, 2 student + 1 teacher GPU (shrink of the 4+1 layout), image vime-vllm:sync1916-arm-cpuvalidate, EXIT_RC=0:

  • Teacher vLLM server ready (HTTP 200) — no 400, no CIMultiDictProxy.
  • teacher_log_probs flow into the OPD KL penalty:
    • rollout 0: rollout_log_probs=-0.1547, teacher_log_probs=-0.1589, opd_reverse_kl=0.0044
    • rollout 1: rollout_log_probs=-0.1546, teacher_log_probs=-0.1596, opd_reverse_kl=0.0041
  • Full job (2 rollouts + train + eval) exits cleanly.

Fixes #12.

🤖 Generated with Claude Code

@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 migrates the On-Policy Distillation (OPD) teacher logprobs endpoint from the custom /inference/v1/generate to the standard, OpenAI-compatible /v1/completions endpoint. This involves updating the request payload format (e.g., using prompt instead of token_ids and enabling echo) and adjusting the response parsing to extract logprobs from choices[0].prompt_logprobs. Additionally, it replaces resp.raise_for_status() with custom error handling to prevent serialization issues with Ray. The review feedback suggests reusing a persistent aiohttp.ClientSession to avoid performance bottlenecks and using errors="replace" when decoding the error response text to prevent potential decoding exceptions.

Comment thread vime/rollout/on_policy_distillation.py Outdated
if teacher_model:
payload["model"] = teacher_model

async with aiohttp.ClientSession() as session:

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

Instantiating a new aiohttp.ClientSession on every call to reward_func is a performance bottleneck. It prevents connection pooling and keep-alive reuse, resulting in a new TCP connection and SSL handshake for every request. Under high concurrency, this can also lead to socket exhaustion.

Consider reusing a persistent session. For example, you can lazily initialize a module-level session:

_session: aiohttp.ClientSession | None = None

async def get_session() -> aiohttp.ClientSession:
    global _session
    if _session is None or _session.closed:
        _session = aiohttp.ClientSession()
    return _session

And then use it in reward_func without the async with block for the session itself:

    session = await get_session()
    async with session.post(args.rm_url, json=payload) as resp:
        ...

Comment thread vime/rollout/on_policy_distillation.py Outdated
# "can't pickle CIMultiDictProxy" error when Ray serializes the
# exception back to the driver. Raise a plain, picklable error that
# surfaces the real status + body instead.
body = await resp.text()

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 await resp.text() without specifying an error handling policy can raise a UnicodeDecodeError if the teacher server returns non-UTF-8 binary data or corrupted text on error (e.g., due to a proxy issue or misconfigured gateway). Specifying errors="replace" ensures that the response body is always decoded successfully into a string, preventing the original HTTP error from being masked by a decoding exception.

Suggested change
body = await resp.text()
body = await resp.text(errors="replace")

@aoshen02
aoshen02 force-pushed the fix/opd-vllm-completions-12 branch 2 times, most recently from 4b20bc1 to 68b5824 Compare June 3, 2026 09:27
@aoshen02 aoshen02 changed the title fix(opd): score teacher over /v1/completions, not /inference/v1/generate fix(opd): score teacher over /inference/v1/generate (text + multimodal) Jun 3, 2026
@aoshen02
aoshen02 force-pushed the fix/opd-vllm-completions-12 branch from 68b5824 to 2e313b3 Compare June 3, 2026 09:41
The built-in OPD reward_func (vime/rollout/on_policy_distillation.py) sent vime's
disaggregated /inference/v1/generate request body (token_ids + nested
sampling_params, top-level prompt_logprobs response) but the CI test pointed
--rm-url at /v1/completions, so the teacher rejected the unknown schema with HTTP
400 — which resp.raise_for_status() turned into an aiohttp ClientResponseError whose
CIMultiDictProxy headers fail to pickle across Ray, masking the real error as
"can't pickle CIMultiDictProxy".

Fix: align the test URL to /inference/v1/generate (the endpoint the body targets and
the one vime's own rollout uses), and harden + extend reward_func:

- text: POST {token_ids, sampling_params{max_tokens:1, temperature:0,
  prompt_logprobs:1, skip_special_tokens:False}}; model only when --opd-teacher-model
  is set (vLLM accepts a missing model -> default served model).
- multimodal: render the (text + image_url) messages via the teacher's
  /v1/chat/completions/render to get token_ids + features, attach the student's
  canonical full prompt+response token_ids (re-aligning the feature placeholders),
  and score with prompt_logprobs — reusing vime.rollout.vllm_rollout's proven
  render->features helpers. This is why /inference/v1/generate is used over the
  OpenAI /v1/completions: it is the only vLLM endpoint that carries multimodal
  features, so one code path scores both text and image teachers.
- replace resp.raise_for_status() with an explicit non-200 -> RuntimeError carrying
  the status + body (picklable across Ray; surfaces the real teacher error).
- post_process reads top-level prompt_logprobs (GenerateResponse shape).

Validated on gb200, EXIT_RC=0 both:
- text (Qwen2.5-0.5B, gsm8k): teacher_log_probs flow into opd_reverse_kl.
- multimodal (Qwen3-VL-8B self-distill, geo3k): render->features->generate scores
  image samples, teacher_log_probs flow into opd_reverse_kl. (MM weight sync needs
  PR #111's qwen3-vl non-colocate update_weights fix.)

Fixes #12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>
@aoshen02
aoshen02 force-pushed the fix/opd-vllm-completions-12 branch from 2e313b3 to 2f3e50f Compare June 3, 2026 13:40
Signed-off-by: aoshen02 <aoshen@inferact.ai>
@CalvinXKY

Copy link
Copy Markdown
Collaborator

The PR description says fixing the code to speak /v1/completions avoids changing the teacher, but the actual code uses /inference/v1/generate and the test also flips --rm-url to it — while the teacher is still the stock OpenAI server which doesn't serve that route. The test will 404? Need to either update the teacher launch to use the disagg serve app, or update the PR description to match the actual approach.

Signed-off-by: aoshen02 <aoshen@inferact.ai>
@CalvinXKY
CalvinXKY merged commit 0079e52 into main Jun 3, 2026
11 of 13 checks passed
@aoshen02
aoshen02 deleted the fix/opd-vllm-completions-12 branch June 8, 2026 14:17
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