Skip to content

fix: recover per-token ids and logprobs from content[] to stop RL KLD corruption#457

Closed
Dranoxgithub wants to merge 4 commits into
eval-protocol:mainfrom
Dranoxgithub:fix/fireworks-v1-return-token-ids
Closed

fix: recover per-token ids and logprobs from content[] to stop RL KLD corruption#457
Dranoxgithub wants to merge 4 commits into
eval-protocol:mainfrom
Dranoxgithub:fix/fireworks-v1-return-token-ids

Conversation

@Dranoxgithub

@Dranoxgithub Dranoxgithub commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

FireworksV1CompletionsClient.create_completion_from_prompt_ids is the sampling path for RL rollouts against Fireworks /v1/completions. Downstream, the trainer aligns each per-token inference logprob to its completion token id position-by-position, so completion_ids[i] and the per-token logprob at i must be the same length and the same tokens.

This client always requests boolean logprobs=True, so every response uses the new content[] logprobs shape (choices[0].logprobs.content[], where each entry carries token_id, token, bytes, logprob, and sampling_logprob). The legacy integer logprobs=1 shape (token_logprobs + logprobs.token_ids) is not requested and is intentionally not supported here. This PR makes the client read everything from that new-mode content[] array.

Bug 1 — alignment / KLD corruption (the actual bug)

Per-token ids and logprobs were read from two independent places:

  • ids: only from the top-level choices[].token_ids, which is populated only when return_token_ids=True — a flag this client never sent. When absent, ids were silently re-derived via tokenizer.encode(decode(text), add_special_tokens=False).
  • logprobs: from content[].logprob, always full length.

Retokenization does not reproduce the model's exact sequence — the trailing end-of-turn/EOS token decodes to empty text and is not re-added — so completion_ids = N while logprobs = N+1. The +1 shift misaligns every per-token logprob against the wrong token, and per-token KLD becomes garbage.

Observed on a GLM-family LoRA RFT deployment (MTP speculative decoding): completion_ids was one shorter than the logprobs on all 120 rows (diff=+1) and inference_kld climbed to ~60; aligning ids correctly made diff=0 and inference_kld flat ~0.028.

Fix: the content[] array already carries token_id per entry, so we read ids and logprobs from the same array, entry-by-entry. They are the same length and aligned by construction — no return_token_ids, no re-encode fallback.

Bug 2 — sampling precision (more of a feature)

In the content[] shape each entry exposes two numbers: sampling_logprob (the exact, full-precision value the sampler actually drew with) and logprob (a rounded/verification value). The client read the rounded logprob. We now prefer sampling_logprob (falling back to logprob only if absent), which is the correct inference-side value for KLD.

Changes

  • Read per-token completion_ids from content[].token_id and logprobs from content[].sampling_logprob in a single pass, so they are aligned by construction.
  • Drop the tokenizer.encode(...) re-encode fallback and the legacy id/logprob sources (token_logprobs, top-level token_ids, raw_output.completion_token_ids).
  • No longer send return_token_ids; rely on the content[] shape that boolean logprobs=True already returns.
  • Fail loud when content[] is absent (e.g. logprobs disabled or legacy shape returned) or when a content[] entry lacks token_id, instead of silently returning corrupted data.

Test plan

  • Unit: content[] with token_id + sampling_logprob -> completion_ids from token_id, logprobs from sampling_logprob (falls back to logprob), lengths equal, tokenizer never used to re-encode.
  • Unit: no content[] -> raises (no silent re-encode).
  • Unit: legacy token_logprobs shape (no content[]) -> raises (legacy not supported).
  • Unit: content[] entry missing token_id -> raises.
  • pytest tests/test_fireworks_v1_completions_client.py passes (12 tests).

Note

Medium Risk
Changes the RL sampling contract on a critical path (token/logprob alignment); correct for intended Fireworks responses but will error on legacy or incomplete logprob payloads that previously returned silently wrong data.

Overview
Fixes RL rollout corruption in FireworksV1CompletionsClient.create_completion_from_prompt_ids by building completion_ids and completion_logprobs in one pass from choices[0].logprobs.content[] (token_id + sampling_logprob), so lengths stay matched for per-token inference KLD.

Removes top-level token_ids, legacy token_logprobs / rounded logprob paths, and tokenizer re-encode when ids were missing (that path dropped EOS and shifted logprobs). Requires the boolean logprobs=True content[] shape and raises if content[], token_id, or sampling_logprob is missing instead of returning misaligned data.

Adds unit tests with a fake completions client covering the happy path, missing fields, absent content[], and legacy logprob shapes.

Reviewed by Cursor Bugbot for commit 03facac. Bugbot is set up for automated code reviews on this repo. Configure here.

…lient

The /v1/completions sampling path corrupted RL training via retokenization
drift. The request never set return_token_ids, so choices[].token_ids came
back empty and the client silently re-encoded decoded text. Retokenization
drops the trailing end-of-turn/EOS token, making completion_ids one shorter
than token_logprobs and misaligning every per-token logprob (inference KLD
spiked to ~60 vs ~0.028 with exact ids).

- Default return_token_ids=True in the request payload (overridable via
  request_params).
- Remove the tokenizer.encode() re-encode fallback; raise when exact ids are
  absent instead of silently returning corrupted data.
- Assert len(completion_token_ids) == len(completion_logprobs) at the boundary
  to catch any residual drift.

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

This comment was marked as outdated.

…shape

This client always requests boolean logprobs=True, so every response uses the
new content[] logprobs shape. Read the exact per-token ids and logprobs
together from content[] (token_id + sampling_logprob) so they are aligned by
construction.

Bug 1 (alignment / KLD corruption) — the real fix: previously ids were read
only from the top-level choices[].token_ids (populated by return_token_ids,
which this client never requested) and, when absent, silently re-derived via
tokenizer.encode(decode(text)). Retokenization drops the trailing
end-of-turn/EOS token, making completion_ids one shorter than the logprobs and
misaligning every per-token logprob (inference_kld ~60 vs ~0.028). Since
content[] already carries token_id per entry, reading ids there removes the
drift entirely — no return_token_ids, no re-encode fallback.

Bug 2 (precision) — more of a feature: prefer content[].sampling_logprob (the
exact value the sampler drew with) over content[].logprob (rounded).

- Drop legacy token_logprobs / top-level token_ids / raw_output id sources.
- Fail loud when content[] is absent or a content[] entry lacks token_id,
  instead of silently returning corrupted data.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Dranoxgithub Dranoxgithub changed the title fix: request exact token ids and fail loud in FireworksV1CompletionsClient fix: recover per-token ids and logprobs from content[] to stop RL KLD corruption Jul 6, 2026
Dranoxgithub and others added 2 commits July 6, 2026 10:15
Always read the per-token logprob from content[].sampling_logprob (the exact
value the sampler drew with). Fail loud if any content[] entry lacks
sampling_logprob instead of substituting the rounded content[].logprob or 0.0,
so silently degraded logprobs never reach RL training. Removes the now-unused
_extract_entry_logprob helper.

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

Copy link
Copy Markdown
Collaborator Author

Superseded by #458 (same commits, moved from a fork branch to an in-repo branch so CI runs with repo secrets).

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