Prhub

#47772 [Bugfix][Pooling] Align CrossEncoder token type ids after truncation

原始 PR 作者 Sunt-ing 合并时间 2026-07-08 11:59 文件变更 2 提交数 2 评论 0 代码增减 +85 / -3

执行摘要

修复 CrossEncoder 左截断后 token type ids 错位导致分数异常

PR 描述明确指出:当使用 truncate_prompt_tokenstruncation_side="left" 时,left-truncated prompt 可能全部来自 document 段,但 compress_token_type_ids 基于原始未截断的 token_type_ids 计算的分割点仍然将截断后的 token 标记为 query 类型,导致 CrossEncoder 模型(如 cross-encoder/ms-marco-MiniLM-L6-v2)返回错误分数。PR 背景提到最近合并的 #47082 保留了 extra_kwargs 但未更新截断后的分割边界。

值得精读,特别是需要处理 CrossEncoder token type ids 对齐的开发者。_apply_post_tokenization_to_token_type_ids 函数的实现可以作为参考模式,用于其他需要与 prompt 同步后分词处理的场景。

讨论亮点

该 PR 没有 review 评论或讨论。仅有的 review 是 claude[bot] 的自动评论(指出 fork 的 PR 自动审查被禁用)和 noooop 的批准。

实现拆解

  1. 新增独立函数 _apply_post_tokenization_to_token_type_idsvllm/entrypoints/pooling/scoring/io_processor.py):对 token_type_ids 列表应用与 engine prompt 相同的后分词处理逻辑,包括填充(pad)和截断(truncate),处理负数 pad_length/max_length 表示使用 max_input_tokens、默认截断方向(优先 tok_params 后 tokenizer),并区分 leftright 截断方向。
  2. 调整 ScoringIOProcessor._pre_process 方法:将 token_type_ids 的弹出和 apply_post_tokenization 的调用顺序重构——先弹出 token type ids,再对 engine prompt 应用后分词处理,最后将 token type ids 通过新函数处理后,传递给 compress_token_type_ids。这样保证 token_type_ids 与截断后的 prompt 长度和内容对齐。
  3. 保留现有 extra_kwargscache_salt 等已有额外参数在重构后的代码中仍然通过 params.extra_kwargs 传递。
  4. 新增单元测试 test_token_type_ids_follow_post_tokenizationtests/entrypoints/pooling/scoring/test_cross_encoder_offline.py):使用模拟的 CrossEncoderIOProcessor 实例测试两种场景:左截断 16 tokens 后 compressed_token_type_ids 应为 0(全部来自 document);填充到 40 tokens 后 compressed_token_type_ids 仍为 16(原有分割点)。
文件 模块 状态 重要度
vllm/entrypoints/pooling/scoring/io_processor.py 入口处理 modified 7.38
tests/entrypoints/pooling/scoring/test_cross_encoder_offline.py 测试 modified 5.55

关键符号

_apply_post_tokenization_to_token_type_ids ScoringIOProcessor._pre_process

关键源码片段

vllm/entrypoints/pooling/scoring/io_processor.py core-logic

核心逻辑所在文件。新增 `_apply_post_tokenization_to_token_type_ids` 函数,并重构 `_pre_process` 中 token_type_ids 的处理顺序。

# vllm/entrypoints/pooling/scoring/io_processor.pydef _apply_post_tokenization_to_token_type_ids(
    tokenizer: Any,
    tok_params: TokenizeParams,
    token_type_ids: list[int],
) -> list[int]:
    # 处理填充逻辑
    pad_length = tok_params.pad_prompt_tokens
    if pad_length is not None and pad_length < 0:
        pad_length = tok_params.max_input_tokens
    if pad_length is not None and pad_length > len(token_type_ids):
        pad_token_type_id = token_type_ids[-1] if token_type_ids else 0
        token_type_ids = token_type_ids + [pad_token_type_id] * (
            pad_length - len(token_type_ids)
        )
​
    # 处理截断逻辑
    max_length = tok_params.truncate_prompt_tokens
    if max_length is not None and max_length < 0:
        max_length = tok_params.max_input_tokens
​
    if max_length is None or max_length >= len(token_type_ids):
        return token_type_ids
    if max_length == 0:
        return token_type_ids[:0]
​
    # 确定截断方向:优先使用 tok_params,其次 tokenizer 默认值
    side = tok_params.truncation_side or (
        tokenizer.truncation_side if tokenizer is not None else None
    )
    if side == 'left':
        return token_type_ids[-max_length:]
    return token_type_ids[:max_length]# ... 在 _pre_process 方法中的使用(核心变更部分):
# 先保留 token_type_ids 再 apply_post_tokenization,确保二者同步
token_type_ids = engine_prompt.pop('token_type_ids', None)
tok_params.apply_post_tokenization(self.tokenizer, engine_prompt)if token_type_ids is not None:
    params = pooling_params.clone()
    # 传入截断后的 token_type_ids 进行压缩
    compressed = compress_token_type_ids(
        _apply_post_tokenization_to_token_type_ids(
            self.tokenizer, tok_params, token_type_ids
        )
    )
    params.extra_kwargs = {
        **(params.extra_kwargs or {}),
        'compressed_token_type_ids': compressed,
    }
    pooling_params_list.append(params)
else:
    pooling_params_list.append(pooling_params)
tests/entrypoints/pooling/scoring/test_cross_encoder_offline.py test-coverage

新增 `test_token_type_ids_follow_post_tokenization` 单元测试,通过模拟 CrossEncoderIOProcessor 验证左截断和填充场景下 compressed_token_type_ids 的正确性。

# tests/entrypoints/pooling/scoring/test_cross_encoder_offline.pydef test_token_type_ids_follow_post_tokenization():
    # 使用 object.__new__ 绕过 __init__ 来构造模拟处理器
    processor = object.__new__(CrossEncoderIOProcessor)
    processor.tokenizer = SimpleNamespace(truncation_side='right', pad_token_id=-1)
    processor.renderer = SimpleNamespace(process_for_engine=lambda prompt, _: prompt)
    processor.model_config = None
    # mock get_score_prompt: 返回 32 个 token,前 16 个类型 0(query),后 16 个类型 1(document)
    processor.get_score_prompt = lambda **_: (
        '',
        {
            'prompt_token_ids': list(range(32)),
            'token_type_ids': [0] * 16 + [1] * 16,
        },
    )
​
    # 场景 1: left-truncation 到 16 tokens -> 只保留 document 段
    engine_inputs, pooling_params = processor._pre_process(
        ScoringData(data_1=['query'], data_2=['document']),
        TokenizeParams(
            max_total_tokens=None,
            truncate_prompt_tokens=16,
            truncation_side='left',
        ),
        PoolingParams(task='classify', extra_kwargs={'cache_salt': 'salt'}),
    )
​
    assert engine_inputs[0]['prompt_token_ids'] == list(range(16, 32))
    # compressed 值应为 0 ( 全部为 document 类型 )
    assert pooling_params[0].extra_kwargs == {
        'cache_salt': 'salt',
        'compressed_token_type_ids': 0,
    }
​
    # 场景 2: padding 到 40 tokens ( 原有 32 tokens)
    engine_inputs, pooling_params = processor._pre_process(
        ScoringData(data_1=['query'], data_2=['document']),
        TokenizeParams(max_total_tokens=None, pad_prompt_tokens=40),
        PoolingParams(task='classify'),
    )
​
    assert engine_inputs[0]['prompt_token_ids'] == list(range(32)) + [-1] * 8
    # compressed 值应保持原有分割点 16
    assert pooling_params[0].extra_kwargs == {'compressed_token_type_ids': 16}

评论区精华

没有提炼出高价值讨论线程

当前评论区没有形成足够清晰的争议点或结论,后续有更多讨论时会体现在这里。

风险与影响

低风险。变更集中在 CrossEncoder 评分路径的 token_type_ids 处理逻辑,不影响其他模型或非 CrossEncoder 的 pooling 请求。测试覆盖了左截断和填充两种场景,并与 HF 参考分数进行了 E2E 对比。_apply_post_tokenization_to_token_type_ids 是新增辅助函数,不修改原有函数签名或行为;_pre_process 中仅调整了 token_type_ids 的处理时机(先 pop 后 apply_post_tokenization),对于没有 token_type_ids 的请求走原路径。

  • 用户影响:使用 CrossEncoder 模型且设置了 truncation_side="left" 的用户将获得正确的评分结果;无截断或默认 right 截断的用户不受影响。
  • 系统影响:无性能影响;额外函数调用很小。
  • 团队影响:为后续类似的 post-tokenization 对齐需求提供了可复用的模式(_apply_post_tokenization_to_token_type_ids)。
核心路径变更(CrossEncoder token type ids 处理)

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论