Skip to content

[RL+VLM] Avoid retokenization drift for pre-tokenized (token-id) VLM requests - #26555

Merged
ByronHsu merged 10 commits into
sgl-project:mainfrom
ByronHsu:codex/kimi-vlm-token-id-expansion
Jun 1, 2026
Merged

ByronHsu merged 10 commits into
sgl-project:mainfrom
ByronHsu:codex/kimi-vlm-token-id-expansion

Conversation

@ByronHsu

@ByronHsu ByronHsu commented May 28, 2026

Copy link
Copy Markdown
Collaborator

TL;DR

For RL, we want token-in, token-out on both the inference and training sides so the two see the exact same token ids. Today the VLM path breaks this: when a request comes in as input_ids, the server decodes them back to a string and then re-tokenizes that string. Decode → re-tokenize is not guaranteed to round-trip, so the prompt the model actually runs on can differ from what the client sent ("retokenization drift").

This is harmless for plain text, but in RL it can cause a catastrophic trainer–inference mismatch — especially with router replay, where the recorded expert-routing decisions are indexed per token and must line up exactly.

Example:

trainer    prompt = [1, 2]
inference  prompt = [1, 2]  ->  decoded to a string  ->  re-tokenized to [3]   (drift!)

# routing is recorded per token:
inference produced routing for 1 token,
but the trainer replays 2 tokens  ->  length mismatch, replay is corrupted

Modification

  • BaseMultimodalProcessor.process_and_combine_mm_data: for a pre-tokenized (list[int]) prompt, rebuild the final input_ids from the user's original tokens and expand only the image placeholders, instead of adopting the HF re-tokenized prompt. Non-media tokens can no longer drift. The whole path is guarded by SGLANG_MM_AVOID_RETOKENIZE (default on).
  • resolve_image_token_counts: per-image expanded token counts computed without re-tokenizing. The base implementation uses the transformers in-tree convention _get_num_multimodal_tokens(image_sizes=...) (Qwen-VL, Gemma3, GLM4V, ...); Kimi overrides it with media_tokens_calculator. On genuine failure it raises; the caller logs the cause and falls back to decode+retokenize.
  • _expand_input_ids: copies the original tokens verbatim and expands the i-th image placeholder into counts[i] copies of the image token id. Raises ValueError when the number of placeholders does not match the number of images.

Test

test/registered/vlm/test_token_id_retokenize_e2e.py launches a real Qwen2.5-VL server twice with SGLANG_MM_AVOID_RETOKENIZE off then on, sending the same predefined non-canonical prompt (the word "Describe" split into "D"+"escribe", which decodes to the same text but re-encodes to the single merged token) plus one image:

  • flag OFF → the prompt re-tokenizes (drift): prompt_tokens shrinks by the drift delta.
  • flag ON → no drift: prompt_tokens keeps the original length (image placeholder expanded).

Asserts prompt_tokens[on] - prompt_tokens[off] == drift_delta.


CI States

Latest PR Test (Base): ⏳ Run #26699777289
Latest PR Test (Extra): ❌ Run #26699777237

@github-actions github-actions Bot added the Multi-modal multi-modal language model label May 28, 2026
@ByronHsu
ByronHsu marked this pull request as draft May 28, 2026 07:38

@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 direct token-id space expansion for Kimi VLM prompts to resolve off-by-one length drift issues caused by decoding and re-tokenizing. It adds a dedicated processor module, integrates it into the Kimi K2.5 and VL processors, and includes both a reproduction script and unit tests. The reviewer suggested an optimization in kimi_token_ids.py to calculate token counts directly from existing offsets instead of redundantly calling the media token calculator.

Comment on lines +68 to +73
for image in images:
num_tokens = processor._processor.media_processor.media_tokens_calculator(
{"type": "image", "image": image}
)
media_text_parts.append(processor.mm_tokens.image_token * num_tokens)
mediums.append({"type": "image", "image": image})

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 media_tokens_calculator is called again for each image here, which is redundant because the token counts have already been calculated and are encoded in image_offsets as (start, end) ranges. We can optimize this by calculating num_tokens directly from image_offsets using end - start + 1, avoiding redundant CPU overhead.

Suggested change
for image in images:
num_tokens = processor._processor.media_processor.media_tokens_calculator(
{"type": "image", "image": image}
)
media_text_parts.append(processor.mm_tokens.image_token * num_tokens)
mediums.append({"type": "image", "image": image})
for image, (start, end) in zip(images, image_offsets):
num_tokens = end - start + 1
media_text_parts.append(processor.mm_tokens.image_token * num_tokens)
mediums.append({"type": "image", "image": image})

Move the Kimi-specific token-id expansion into the shared
process_and_combine_mm_data path, so any VLM that receives a pre-tokenized
(list[int]) prompt rebuilds the final input_ids from the user's original
tokens instead of the HF re-tokenized prompt. This avoids decode/re-tokenize
length drift (decode + re-tokenize is not identity) for all VLMs, not just Kimi.

- resolve_image_token_counts: base method using the HF
  _get_num_multimodal_tokens convention; Kimi overrides it with
  media_tokens_calculator (its processor lacks that convention).
- _expand_input_ids: splice per-image counts into the original ids; O(1)
  length validation against the HF output; on mismatch, warn and fall back
  to the HF re-tokenized ids (no worse than before).
- Guard the whole path with SGLANG_MM_AVOID_RETOKENIZE (default on).
- Remove the bespoke kimi_token_ids.py path and repro script; rewrite the
  unit test around the generic helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ByronHsu ByronHsu changed the title Fix Kimi VLM token-id prompt expansion Avoid prompt retokenization for token-id VLM requests May 29, 2026
root and others added 5 commits May 29, 2026 07:52
Simplify the avoid-retokenize branch in BaseMultimodalProcessor (assert the
pre-tokenized input_ids is a list and drop the redundant tensor round-trip and
length-only safety check), and add coverage:

- Unit test (Qwen2.5-VL, Kimi-K2.5): a predefined non-canonical prompt is
  preserved verbatim with the flag ON and drifts with it OFF.
- E2E tests launching real servers with SGLANG_MM_AVOID_RETOKENIZE off/on and
  comparing prompt_tokens (Qwen2.5-VL 1-GPU; Kimi-K2.5 8-GPU nightly).

Co-authored-by: Cursor <cursoragent@cursor.com>
resolve_image_token_counts and _expand_input_ids now raise on genuine
failures (missing count API, placeholder/image count mismatch) rather than
returning None. The avoid-retokenize path catches the exception, logs the
real cause, and falls back to decode+retokenize. Guard the path on raw_images
and update the helper unit tests to the raising contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop the unit and Kimi e2e test files; the Qwen e2e
(test_token_id_retokenize_e2e.py) is the single retained test for the
avoid-retokenize behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ByronHsu ByronHsu changed the title Avoid prompt retokenization for token-id VLM requests [VLM] Opt-in to avoid prompt retokenization for token-id VLM requests May 29, 2026
@ByronHsu ByronHsu changed the title [VLM] Opt-in to avoid prompt retokenization for token-id VLM requests Avoid retokenization drift for pre-tokenized (token-id) VLM requests May 29, 2026
@ByronHsu
ByronHsu marked this pull request as ready for review May 29, 2026 08:42
Avoid the abstract base TestCase being collected and run with no model set.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ByronHsu ByronHsu changed the title Avoid retokenization drift for pre-tokenized (token-id) VLM requests [RL+VLM] Avoid retokenization drift for pre-tokenized (token-id) VLM requests May 29, 2026
@ByronHsu

Copy link
Copy Markdown
Collaborator Author

/tag-and-rerun-ci

Co-authored-by: Cursor <cursoragent@cursor.com>
and input_ids is not None
and raw_images
):
assert isinstance(

@mickqian mickqian May 31, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

do we need to take care of video/audio placeholder in this PR?

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.

we can do it later. We don't have use cases for video/audio RL for now.

@yhyang201

Copy link
Copy Markdown
Collaborator

/tag-and-rerun-ci

@ByronHsu
ByronHsu merged commit f6a5a1b into sgl-project:main Jun 1, 2026
238 of 272 checks passed
mqhc2020 pushed a commit to mqhc2020/sglang that referenced this pull request Jun 2, 2026
…requests (sgl-project#26555)

Co-authored-by: Byron Hsu <byron+per@periodiclabs.ai>
Co-authored-by: root <root@slurm-h200-209-231.slurm-compute.tenant-slurm.svc.cluster.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mick <mickjagger19@icloud.com>
hanming-lu pushed a commit that referenced this pull request Jun 3, 2026
…requests (#26555)

Co-authored-by: Byron Hsu <byron+per@periodiclabs.ai>
Co-authored-by: root <root@slurm-h200-209-231.slurm-compute.tenant-slurm.svc.cluster.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mick <mickjagger19@icloud.com>
ByronHsu added a commit to ByronHsu/sglang that referenced this pull request Jun 8, 2026
…requests (sgl-project#26555)

Co-authored-by: Byron Hsu <byron+per@periodiclabs.ai>
Co-authored-by: root <root@slurm-h200-209-231.slurm-compute.tenant-slurm.svc.cluster.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mick <mickjagger19@icloud.com>
(cherry picked from commit f6a5a1b)
Chronostasys pushed a commit to MindLab-Research/sglang that referenced this pull request Aug 24, 2026
…requests (sgl-project#26555)

Co-authored-by: Byron Hsu <byron+per@periodiclabs.ai>
Co-authored-by: root <root@slurm-h200-209-231.slurm-compute.tenant-slurm.svc.cluster.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mick <mickjagger19@icloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Multi-modal multi-modal language model run-ci

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants