Prhub

#26864 Fix multimodal synthetic benchmark prompt generation to exclude special tokens

原始 PR 作者 bowenwan6 合并时间 2026-06-05 06:27 文件变更 2 提交数 3 评论 10 代码增减 +47 / -4

执行摘要

修复多模态基准测试提示生成中特殊标记污染问题

在运行多模态合成基准测试(--dataset-name image)时,gen_mm_prompt生成的提示文本可能包含如<|video_pad|>等特殊标记,而这些标记在没有对应payload的情况下会导致服务器返回No data iterator found for token错误,使得基准测试结果不可靠。该问题在Qwen3-VL等模型中复现,且通过payload审计确认8/12900条提示包含被禁止的特殊标记。此PR旨在从生成侧剔除所有特殊标记,确保基准测试数据的合法性。

建议合并。此PR修复了真实用户发现的多模态基准测试数据生成正确性问题,代码变更简洁,有单元测试覆盖,且通过了review的讨论和验证。

讨论亮点

Review过程中有两个核心讨论:

  • 性能优化:Gemini Code Assist建议给新增函数添加@lru_cache(maxsize=1),避免每次gen_mm_prompt都重复扫描词汇表。作者采纳并实现,与现有get_available_tokens模式一致。
  • 设计简化:JustinTong指出新函数可以直接复用已有的get_available_tokens(已缓存)而无需重新调用tokenizer.get_vocab(),避免冗余扫描和isinstance守卫检查。最终实现改为基于get_available_tokens构建过滤,代码更简洁。

实现拆解

  1. 提取辅助函数并添加缓存:在python/sglang/benchmark/datasets/common.py中新增get_available_multimodal_text_tokens(tokenizer, image_pad_id),使用@lru_cache(maxsize=1)装饰以避免重复词汇表扫描。该函数从tokenizer.all_special_ids构建排除集,并额外将image_pad_id加入排除集(若不为None)。最终返回get_available_tokens(tokenizer)中不在排除集中的token id列表。

  2. 更新gen_mm_prompt:将原本直接调用tokenizer.get_vocab().values()并逐一移除image_pad_id的逻辑,改为调用新的get_available_multimodal_text_tokens函数,简化实现并统一过滤逻辑。同时修复了image_pad_id检查条件,从if image_pad_id:改为if image_pad_id is not None:以正确处理token id为0的情况。

  3. 添加单元测试:在test/registered/bench_fn/test_benchmark_datasets_api.py中新增test_gen_mm_prompt_excludes_special_tokens方法。使用create_lightweight_tokenizer构造包含多模态特殊标记的轻量tokenizer,通过mockrandom.choices捕获候选池,断言候选池中不包含任何特殊标记的token id,并确保候选池非空。

文件 模块 状态 重要度
python/sglang/benchmark/datasets/common.py 基准生成 modified 6.71
test/registered/bench_fn/test_benchmark_datasets_api.py 基准测试 modified 6.45

关键符号

get_available_multimodal_text_tokens gen_mm_prompt

关键源码片段

python/sglang/benchmark/datasets/common.py core-logic

核心变更,新增过滤辅助函数并修改 gen_mm_prompt 以正确排除所有特殊标记。

import random
from functools import lru_cache# 复用已缓存的通用有效 Token 获取函数
from sglang.benchmark.datasets.common import get_available_tokens# 缓存装饰器确保多模态文本 Token 池只计算一次
@lru_cache(maxsize=1)
def get_available_multimodal_text_tokens(tokenizer, image_pad_id):
    """Get valid token ids for synthetic multimodal text prompts."""
    # 收集所有特殊 Token 的 ID,防御性地使用 or [] 避免 None
    excluded_token_ids = set(getattr(tokenizer, "all_special_ids", []) or [])
    if image_pad_id is not None:
        excluded_token_ids.add(image_pad_id)
    # 仅从已缓存的通用池中过滤,避免重复扫描完整词表
    return [
        token_id
        for token_id in get_available_tokens(tokenizer)
        if token_id not in excluded_token_ids
    ]
​
​
def gen_mm_prompt(tokenizer, image_pad_id, token_num):
    """Generate a random prompt of specified token length using tokenizer vocabulary."""
    # 直接使用过滤后的多模态安全池
    all_available_tokens = get_available_multimodal_text_tokens(tokenizer, image_pad_id)
    selected_tokens = random.choices(all_available_tokens, k=token_num)
    return tokenizer.decode(selected_tokens)
test/registered/bench_fn/test_benchmark_datasets_api.py test-coverage

新增确定性单元测试,验证特殊标记排除逻辑。

def test_gen_mm_prompt_excludes_special_tokens(self):
    # 创建轻量 tokenizer,包含标准词汇
    tokenizer = create_lightweight_tokenizer()
    # 注入一组多模态特殊标记(模拟 Qwen3-VL 等模型)
    multimodal_special_tokens = [
        "<|image_pad|>",
        "<|video_pad|>",
        "<|vision_start|>",
        "<|vision_end|>",
        "<|vision_pad|>",
    ]
    tokenizer.add_special_tokens(
        {"additional_special_tokens": multimodal_special_tokens}
    )
    special_token_ids = set(
        tokenizer.convert_tokens_to_ids(multimodal_special_tokens)
    )
    image_pad_id = tokenizer.convert_tokens_to_ids("<|image_pad|>")
​
    # 使用字典捕获 random.choices 被调用时的候选池
    captured_population = {}
​
    def fake_choices(population, k):
        captured_population["tokens"] = population
        return population[:k]
​
    # 替换 random.choices 以拦截候选池,避免真正随机采样
    with patch(
        "sglang.benchmark.datasets.common.random.choices",
        side_effect=fake_choices,
    ):
        gen_mm_prompt(tokenizer, image_pad_id, token_num=8)
​
    sampled_pool = set(captured_population["tokens"])
    # 断言候选池中没有任何特殊标记
    self.assertFalse(special_token_ids & sampled_pool)
    # 断言候选池非空
    self.assertTrue(sampled_pool)

评论区精华

添加 lru_cache 避免重复词汇表扫描 性能

Gemini Code Assist 在第一次 review 中指出 `get_available_multimodal_text_tokens` 在每次调用 `gen_mm_prompt` 时都会重复扫描完整的 tokenizer 词汇表,对于大型 tokenizer 会带来性能瓶颈,建议添加 `@lru_cache(maxsize=1)`。

结论:作者采纳建议,在函数定义前添加了 `@lru_cache(maxsize=1)` 装饰器,与同文件中 `get_available_tokens` 的模式一致。 · 已解决

复用 get_available_tokens 避免重复扫描 设计

在后续评论中,JustinTong 指出由于辅助函数已被缓存,重复扫描的代价已不大,但可以进一步简化:当前实现直接调用 `tokenizer.get_vocab()`,但同文件已有 `get_available_tokens` 函数负责从词汇表获取整数 token id 并包含防御性 `isinstance` 检查。建议复用该函数以消除冗余扫描和守卫检查。

结论:最终实现改为遍历 `get_available_tokens(tokenizer)` 的结果,将 `isinstance` 检查委托给通用函数,代码更简洁且避免重复。 · 已解决

风险与影响

此变更仅影响基准测试数据生成路径,不涉及模型前向、解码或服务性能。主要风险在于tokenizer.all_special_ids可能在不兼容的tokenizer中返回非预期值(如空列表),但代码中已有防御性取值getattr(tokenizer, "all_special_ids", []) or []。此外,缓存机制假设tokenizerimage_pad_id在单次基准测试运行中不会变化,这符合现有使用场景。单元测试覆盖了典型的Qwen3-VL特殊标记集合,但仍需注意其他tokenizer引入的新特殊标记类型。

直接用户:执行python -m sglang.bench_serving --dataset-name image的基准测试用户将不再因特殊标记导致HTTP 400错误,基准测试结果更加稳定。间接用户:依赖SGLang基准测试结果进行模型评估的开发者和CI系统将获得更可靠的数据。影响范围限定在基准测试模块,不影响生产推理路径。团队维护:新增的辅助函数与现有get_available_tokens风格一致,易于理解和扩展。

数据生成路径变更

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论