[RL+VLM] Avoid retokenization drift for pre-tokenized (token-id) VLM 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>
This commit is contained in:
co-authored by
Byron Hsu
root
Cursor
Mick
parent
1988a2c9ea
commit
f6a5a1b59c
@@ -555,6 +555,9 @@ class Envs:
|
||||
SGLANG_MM_PRECOMPUTE_HASH = EnvBool(False)
|
||||
SGLANG_VIT_ENABLE_CUDA_GRAPH = EnvBool(False)
|
||||
SGLANG_MM_SKIP_COMPUTE_HASH = EnvBool(False)
|
||||
# For pre-tokenized (list[int]) multimodal prompts,
|
||||
# preserve the user's original tokens to avoid retokenization drift.
|
||||
SGLANG_MM_AVOID_RETOKENIZE = EnvBool(True)
|
||||
|
||||
|
||||
# VLM Item CUDA IPC Transport
|
||||
|
||||
@@ -1227,6 +1227,58 @@ class BaseMultimodalProcessor(ABC):
|
||||
return tensor
|
||||
return tensor.cpu()
|
||||
|
||||
def resolve_image_token_counts(self, images: List) -> List[int]:
|
||||
"""Per-image expanded token counts, computed without re-tokenizing.
|
||||
|
||||
Default implementation uses the transformers in-tree convention
|
||||
``_get_num_multimodal_tokens(image_sizes=...)`` (present on the in-tree
|
||||
VLM processors, e.g. Qwen-VL, Gemma3, GLM4V). Models whose processor
|
||||
does not implement it (e.g. Kimi) override this method.
|
||||
|
||||
"""
|
||||
assert images is not None
|
||||
image_sizes = [(image.height, image.width) for image in images]
|
||||
num_image_tokens = self._processor._get_num_multimodal_tokens(
|
||||
image_sizes=image_sizes
|
||||
).num_image_tokens
|
||||
return [int(count) for count in num_image_tokens]
|
||||
|
||||
@staticmethod
|
||||
def _expand_input_ids(
|
||||
original_ids: List[int],
|
||||
counts: List[int],
|
||||
placeholder_token_id: Optional[int],
|
||||
) -> List[int]:
|
||||
"""Rebuild final input_ids for a pre-tokenized (list[int]) prompt.
|
||||
|
||||
Keep the user's ORIGINAL tokens verbatim and expand the i-th image
|
||||
placeholder into ``counts[i]`` copies of ``placeholder_token_id``. The HF
|
||||
processor's re-tokenization is discarded, so non-media tokens cannot
|
||||
drift.
|
||||
|
||||
"""
|
||||
if placeholder_token_id is None:
|
||||
raise ValueError("placeholder_token_id is not set for this processor")
|
||||
|
||||
num_placeholders = sum(
|
||||
1 for token_id in original_ids if token_id == placeholder_token_id
|
||||
)
|
||||
if num_placeholders != len(counts):
|
||||
raise ValueError(
|
||||
f"prompt has {num_placeholders} image placeholder token(s) but "
|
||||
f"{len(counts)} image(s) were provided"
|
||||
)
|
||||
|
||||
rebuilt: List[int] = []
|
||||
next_image_idx = 0
|
||||
for token_id in original_ids:
|
||||
if token_id == placeholder_token_id:
|
||||
rebuilt.extend([placeholder_token_id] * counts[next_image_idx])
|
||||
next_image_idx += 1
|
||||
else:
|
||||
rebuilt.append(token_id)
|
||||
return rebuilt
|
||||
|
||||
def process_and_combine_mm_data(
|
||||
self,
|
||||
base_output: BaseMultiModalProcessorOutput,
|
||||
@@ -1276,6 +1328,48 @@ class BaseMultimodalProcessor(ABC):
|
||||
**kwargs,
|
||||
)
|
||||
all_collected_items = collected_items
|
||||
|
||||
# When SGLANG_MM_AVOID_RETOKENIZE is on, keep the user's exact tokens to avoid retokenize drift.
|
||||
# Drift happens when Retokenization is not identity: Decode(X) => String => Re-tokenize => Y, X != Y.
|
||||
if (
|
||||
envs.SGLANG_MM_AVOID_RETOKENIZE.get()
|
||||
and base_output.input_ids is not None
|
||||
and input_ids is not None
|
||||
and raw_images
|
||||
and not raw_audios
|
||||
and not raw_videos
|
||||
):
|
||||
assert isinstance(
|
||||
base_output.input_ids, list
|
||||
), f"expected list[int] input_ids, got {type(base_output.input_ids)}"
|
||||
try:
|
||||
counts = self.resolve_image_token_counts(raw_images)
|
||||
image_placeholder_token_id = mm_tokens.image_token_id
|
||||
if image_placeholder_token_id is None:
|
||||
raise ValueError(
|
||||
"image placeholder token id is not set for this processor"
|
||||
)
|
||||
processor_placeholder_count = int(
|
||||
(input_ids == image_placeholder_token_id).sum().item()
|
||||
)
|
||||
if processor_placeholder_count != sum(counts):
|
||||
raise ValueError(
|
||||
"processor image placeholder count mismatch: "
|
||||
f"processor={processor_placeholder_count}, "
|
||||
f"resolved={sum(counts)}"
|
||||
)
|
||||
input_ids = torch.tensor(
|
||||
self._expand_input_ids(
|
||||
base_output.input_ids,
|
||||
counts,
|
||||
image_placeholder_token_id,
|
||||
),
|
||||
dtype=input_ids.dtype,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Due to {e}, falling back to decode+retokenize, which may change prompt length (token drift)."
|
||||
)
|
||||
else:
|
||||
ret = None
|
||||
|
||||
|
||||
@@ -23,6 +23,21 @@ class KimiGridMMDataMixin:
|
||||
- self._tokenizer (with .encode())
|
||||
"""
|
||||
|
||||
def resolve_image_token_counts(self, images):
|
||||
"""Kimi's processor is remote-code and does not implement the
|
||||
transformers ``_get_num_multimodal_tokens`` convention; use its
|
||||
``media_tokens_calculator`` instead.
|
||||
|
||||
"""
|
||||
assert images is not None
|
||||
media_tokens_calculator = (
|
||||
self._processor.media_processor.media_tokens_calculator
|
||||
)
|
||||
return [
|
||||
int(media_tokens_calculator({"type": "image", "image": image}))
|
||||
for image in images
|
||||
]
|
||||
|
||||
def _num_image_tokens_from_grid(
|
||||
self, grid_thw: Union[torch.Tensor, np.ndarray, list, tuple]
|
||||
) -> int:
|
||||
|
||||
Reference in New Issue
Block a user