Prhub

#44226 [API] Add token offsets to render endpoints (/v1/.../render)

原始 PR 作者 hyeongyun0916 合并时间 2026-06-26 20:02 文件变更 13 提交数 28 评论 23 代码增减 +516 / -20

执行摘要

为 render 端点添加可选 token offsets

PR body 指出,部署在 vLLM 之上的外部服务编排层(如 llm-d)需要从 render 端点获取预处理结果(token IDs、chat template 应用后的 prompt 等),以进行 prefix-cache-aware 路由或负载均衡。但这些消费者通常需要在客户端侧重新分词来将文本 span 映射到 token 范围,不仅浪费计算,而且由于 chat template 和特殊 token 处理细节差异,难以精确对齐。vLLM 使用的 Hugging Face Fast tokenizer 在分词时已计算字符级偏移,此 PR 旨在将这一内部数据暴露给调用方。

值得精读。该 PR 设计严谨(显式能力模型、静态判断优先)、讨论深刻(测试策略、统一调用路径),并明确文档化已知局限。适合作为前端 API 演进的样板 PR。

讨论亮点
  • 设计决策:用基类方法替代 getattr:DarkLight1337 指出应避免使用 getattr 探测 is_fast,改为在 BaseRenderer 中默认返回 False 并由 HfRenderer 重写。作者随后实现 _can_produce_offsets(),消除了 MistralTokenizer 可能缺少 is_fast 属性的隐患。
  • 建议统一 tokenizer 调用方式:DarkLight1337 建议始终使用 __call__ 而非混合 encode(),作者执行后在确保输出 token ID 一致的前提下统一了路径。
  • 测试策略迭代:早期测试包含本地路径和 AI 生成的多余用例,经 reviewer 指出后,作者替换为基于真实小模型(hmellor/tiny-random-LlamaForCausalLM)的端到端测试,并精简协议层单元测试至关键断言。
  • 潜在安全性问题depthfirst-app 机器人发现当请求同时使用 truncation_sidetruncate_prompt_tokens 时,后分词阶段的截断不会同步更新 prompt_token_offsets,可能导致 len(offsets) != len(token_ids),影响下游依赖偏移量的系统。该观察标记为 LOW 严重性。

实现拆解

  1. 请求模型扩展:在 CompletionRequestChatCompletionRequest 中添加 return_token_offsets: bool | None 字段,默认 False,并通过 build_tok_params() 转发到 TokenizeParams.return_token_offsets
  2. 渲染器核心改造:在 BaseRenderer 中新增 _can_produce_offsets()(默认返回 False,由 HfRenderer 重写为检查 tokenizer.is_fast)和 _wants_offsets()(综合 flag、能力、多模态/预分词保护)。修改 _tokenize_prompt:当请求生效时,通过 tokenizer.__call__(..., return_offsets_mapping=True) 一次性获取 input_idsoffset_mapping,再经 _build_tokens_prompt() 构造包含 prompt_token_offsetsTokensPrompt。同步路径和异步路径(通过 make_async 包装)均统一采用 __call__ 方式。
  3. 内部数据传递:在 TokensPromptTokensInputengine_input 字典中增加 prompt_token_offsets 键(NotRequired),经 _process_tokens 向前传递,最终在 render_completion_request / render_chat_request 中提取并填充到 GenerateRequest.token_offsets 字段。
  4. 测试配套:新增单元测试覆盖渲染器层级(test_token_offsets.py)、协议模型层级(test_render_token_offsets.py)以及基于真实小模型的端到端集成测试(tests/entrypoints/serve/render/test_render.py 中新增 5 个用例),涵盖 fast/slow tokenizer、多模态保护、多 prompt 批处理等场景。
  5. 已知局限明确文档化:PR body 清晰列出了 GPT-OSS 聊天路径、非 render 端点的无操作行为、gRPC 协议扩展待后续等三项限制。
文件 模块 状态 重要度
vllm/renderers/base.py 渲染器 modified 8.28
tests/renderers/test_token_offsets.py 单元测试 added 7.69
tests/entrypoints/serve/render/test_render.py 集成测试 modified 6.94
vllm/entrypoints/openai/completion/protocol.py 协议层 modified 5.66
vllm/entrypoints/openai/chat_completion/protocol.py 协议层 modified 5.66
vllm/entrypoints/serve/disagg/protocol.py 协议层 modified 5.23

关键符号

_can_produce_offsets _wants_offsets _build_tokens_prompt _tokenize_prompt _tokenize_prompt_async

关键源码片段

vllm/renderers/base.py core-logic

核心变更文件:引入了渲染器级别的偏移能力模型(`_can_produce_offsets`、`_wants_offsets`),统一了 tokenization 路径(`_tokenize_prompt` 改用 `__call__`),并实现 `_build_tokens_prompt` 静默携带偏移数据。所有 renderer 子类的行为均由此控制。

# vllm/renderers/base.py ( 关键部分 )class BaseRenderer(ABC, Generic[_T]):
​
    def _can_produce_offsets(self) -> bool:
        """默认为 False,只有 HfRenderer 等子类会重写为检查 tokenizer.is_fast"""
        return False
​
    def _wants_offsets(self, prompt: TextPrompt, params: TokenizeParams) -> bool:
        """仅当标记开启 + 能力具备 + 非多模态 + 非预分词时才请求偏移"""
        return (
            params.return_token_offsets
            and self._can_produce_offsets()
            and not prompt.get("multi_modal_data")
            and not prompt.get("multi_modal_uuids")
        )
​
    @staticmethod
    def _build_tokens_prompt(
        token_ids: Sequence[int],
        prompt: TextPrompt,
        *,
        offset_mapping: Sequence[tuple[int, int]] | None = None,
    ) -> TokensPrompt:
        """构造 TokensPrompt,可选附加偏移数据"""
        if offset_mapping is not None:
            return TokensPrompt(
                prompt_token_ids=list(token_ids),
                prompt_token_offsets=[(int(s), int(e)) for s, e in offset_mapping],
                **prompt,
            )
        return TokensPrompt(prompt_token_ids=list(token_ids), **prompt)
​
    def _tokenize_prompt(self, prompt: TextPrompt, params: TokenizeParams) -> TokensPrompt:
        tokenizer = self.get_tokenizer()
        # 统一使用 __call__ 替代 encode,以便在需要时获取 offset_mapping
        encode_kwargs = params.get_encode_kwargs()
        if self._wants_offsets(prompt, params):
            encoding = tokenizer(prompt["prompt"], return_offsets_mapping=True, **encode_kwargs)
            token_ids = encoding["input_ids"]
            offset_mapping = encoding["offset_mapping"]
            return self._build_tokens_prompt(token_ids, prompt, offset_mapping=offset_mapping)
        # 未启用偏移时走原路径
        encoding = tokenizer(prompt["prompt"], **encode_kwargs)
        return self._build_tokens_prompt(encoding["input_ids"], prompt)

评论区精华

使用基类方法替代 getattr 检测 is_fast 设计

DarkLight1337 建议避免 `getattr(tokenizer, 'is_fast', False)`,改为在 `BaseRenderer` 中默认返回 False 并由子类重写。作者实现 `_can_produce_offsets()` 并消除潜在 `TypeError`。

结论:采纳,重构为 `_can_produce_offsets` 模板方法。 · 已解决

统一 tokenizer 调用方式为 __call__ 设计

DarkLight1337 建议始终使用 `__call__` 而非混合 `encode()` 以统一输出类型。作者执行并验证 token ID 一致。

结论:采纳,统一通过 `tokenizer(text, ...)` 获取 `BatchEncoding`。 · 已解决

将 offset_mapping 直接传递而非通过 with_offsets 标志 设计

作者最初使用 `with_offsets` 布尔标志,DarkLight1337 建议直接传递 `offset_mapping` 简化逻辑。

结论:采纳,`_build_tokens_prompt` 改为接收可选的 `offset_mapping` 参数。 · 已解决

测试策略:用真实模型 e2e 替代模拟和 AI 生成用例 测试

DarkLight1337 指出早期测试包含本地路径和 AI 生成的多余用例,要求重写为基于真实小模型的集成测试。作者替换为 `tiny-random-LlamaForCausalLM` 驱动的 e2e 测试并精简协议单元测试。

结论:采纳,测试套件改为主要依赖端到端真实模型测试。 · 已解决

truncation 场景下 offsets 不同步的安全问题 正确性

`depthfirst-app` 机器人发现当 `truncation_side` 和 `truncate_prompt_tokens` 同时使用时,后分词阶段的截断未更新 `prompt_token_offsets`,可能导致长度不匹配。

结论:标记为 LOW 严重性,未在本次 PR 中修复,需未来处理。 · acknowledged

风险与影响

  • 数据一致性风险:如 depthfirst-app 指出的,结合 truncation_side + truncate_prompt_tokens 时,offsets 未随 token ID 截断更新,可能导致长度不匹配,影响精度敏感的下游(如敏感词检测)。属于边界条件,当前未修复。
  • 性能影响:当 return_token_offsets=True 时,额外调用 tokenizer(text, return_offsets_mapping=True),但 PR body 声称 Fast tokenizer 的偏移计算是 BPE 匹配的副产品,开销为微秒级。非 render 端点虽误设 flag,但因其无实质处理,影响可忽略。
  • 兼容性:新字段默认为 False/None,现有客户端不受影响;响应中 token_offsets 默认为 None
  • 跨序列化边界GenerateRequest 中的 token_offsetslist[tuple[int,int]],经验证可正常通过 model_dumpmodel_validate 的 JSON 往返。
  • 用户(API 消费者):对使用 render 端点进行调度/路由/文案分析的用户,不再需要客户端重复分词,降低集成复杂度并提高对齐精度。需主动设置 return_token_offsets=True 才能获取。
  • 系统(vLLM 内部):对非 render 端点无影响;render 端点增加极微小的计算开销。代码改动仅涉及 render 层和协议模型,不影响核心推理路径。
  • 团队:文档清晰,代码结构(如 _can_produce_offsets 模板方法设计)为将来其他 tokenizer 支持偏移预留了扩展点。需跟进 .proto 文件更新和 GPT-OSS 路径。
truncation 时 offsets 不同步 仅限 Fast tokenizer 默认关闭兼容

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论