Prhub

#45460 [Bugfix] Return the tokenizer from maybe_make_thread_pool so it survives pickling

原始 PR 作者 waynehacking8 合并时间 2026-06-13 14:01 文件变更 2 提交数 2 评论 11 代码增减 +28 / -1

执行摘要

修复 tokenizer pickle 后变 None 的问题

修复 Issue #45433:线程池包装后的 tokenizer(TokenizerPoolCached*)经 pickle 反序列化后变成 None,破坏 Ray actor 或 multiprocessing 场景下通过 LLM.get_tokenizer() 获取 tokenizer 的功能。根源是 maybe_make_thread_pool 函数修改 tokenizer.__class__ 后没有返回值,而 TokenizerPool.__reduce__ 依赖该函数的返回值进行重建。

建议精读测试代码和 reviewer 讨论,以理解 tokenizer 包装设计中的权衡和未来可能的改进方向。

讨论亮点

Reviewer noooop 指出 maybe_make_thread_pool 存在过多包装和魔法,导致下游如 Ray 难以使用,提议将 API 内部化并重新设计。yzong-rh 认同问题,并解释了当前设计的历史原因——get_cached_tokenizer 在 tokenizer 注册时调用,而 maybe_make_thread_pool 需要在 setup 阶段之后以保留对 tokenizer 的修改。yzong-rh 建议未来采用更干净的 wrapper 方案。讨论中还涉及 get_tokenizer 应该返回原始 tokenizer 还是线程安全版本,waynehacking8 主张返回线程安全版本以保持并发安全。

实现拆解

  1. vllm/tokenizers/hf.pymaybe_make_thread_pool 函数末尾,在修改 tokenizer.__class__ 之后添加 return tokenizer,确保 __reduce__ 路径和外部调用者能获得包装后的 tokenizer。
  2. tests/tokenizers_/test_hf.py 新增 test_thread_pool_tokenizer_pickle 测试用例,验证:调用 maybe_make_thread_pool 后返回值不为 None、类型为 ThreadSafeHFTokenizerMixin;pickle 往返后仍不为 None、类型正确、encode 结果与原始 tokenizer 一致;对已包装的 tokenizer 再次调用 maybe_make_thread_pool 返回同一对象(幂等性)。
  3. 测试文件导入新增 ThreadSafeHFTokenizerMixinmaybe_make_thread_pool
文件 模块 状态 重要度
vllm/tokenizers/hf.py Tokenizer modified 5.47
tests/tokenizers_/test_hf.py 测试 modified 5.78

关键符号

maybe_make_thread_pool test_thread_pool_tokenizer_pickle

关键源码片段

vllm/tokenizers/hf.py core-logic

修复核心:在 `maybe_make_thread_pool` 末尾添加 `return tokenizer`,解决 pickle 反序列化为 None 的问题。

# vllm/tokenizers/hf.py 中 maybe_make_thread_pool 的关键部分
# 动态创建 TokenizerPool 子类,线程安全的代理
class TokenizerPool(...):
    # ... 线程安全方法 ...
    def __reduce__(self):
        # pickle 重建时调用 maybe_make_thread_pool(og_tokenizer, copies)
        return maybe_make_thread_pool, (og_tokenizer, copies)TokenizerPool.__name__ = f"TokenizerPool{og_tokenizer.__class__.__name__}"
tokenizer.__class__ = TokenizerPool
# 以下为修复:返回修改后的 tokenizer,确保 __reduce__ 重建时获得正确对象
# 之前缺少 return 语句,导致函数隐式返回 None,破坏 pickle 反序列化
return tokenizer
tests/tokenizers_/test_hf.py test-coverage

新增回归测试,覆盖 pickle 往返、类型断言和幂等性验证。

# tests/tokenizers_/test_hf.py 中新增的回归测试@pytest.mark.parametrize("model_id", ["gpt2"])
def test_thread_pool_tokenizer_pickle(model_id: str):
    # 回归测试 Issue #45433:线程池包装后的 tokenizer 经 pickle 反序列化应为非 None
    reference_tokenizer = AutoTokenizer.from_pretrained(model_id)
​
    pooled_tokenizer = maybe_make_thread_pool(deepcopy(reference_tokenizer))
    # 检查返回值不为 None 且是 ThreadSafe 类型
    assert pooled_tokenizer is not None
    assert isinstance(pooled_tokenizer, ThreadSafeHFTokenizerMixin)
​
    # pickle 往返:序列化再反序列化
    unpickled_tokenizer = pickle.loads(pickle.dumps(pooled_tokenizer))
    # 修复前 unpickled_tokenizer 为 None,这里就是验证点
    assert unpickled_tokenizer is not None
    assert isinstance(unpickled_tokenizer, ThreadSafeHFTokenizerMixin)
    # 功能等价性:编码结果应与原始 tokenizer 一致
    assert unpickled_tokenizer.encode("prompt") == reference_tokenizer.encode("prompt")
​
    # 幂等性:对已包装的 tokenizer 再次调用应返回同一对象
    assert maybe_make_thread_pool(pooled_tokenizer) is pooled_tokenizer

评论区精华

maybe_make_thread_pool 的封装是否合理 设计

noooop 质疑 maybe_make_thread_pool 存在过多包装和魔法,导致下游如 Ray 难以使用。yzong-rh 承认问题,解释了当前设计的历史原因,并建议未来采用更干净的 wrapper 方案。

结论:当前修复仅解决 pickle 问题;长期需重新设计 API 以明确内部 / 外部边界。 · 已解决

get_tokenizer 应返回原始 tokenizer 还是线程安全版本 设计

noooop 提出 get_tokenizer 返回类型不明确;waynehacking8 主张返回线程安全版本以保持并发安全;yzong-rh 认为返回原始 tokenizer 风险很小,但建议保留线程安全版本。

结论:当前保持返回线程安全版本(已打包的 tokenizer),未来重新设计 API 时再决定。 · 已解决

风险与影响

风险极低:只有两处改动,核心修复仅一行 return tokenizer。测试覆盖了 pickle 往返和幂等性。原有内部调用者(renderers/hf.py 中的一处调用)忽略返回值,不受影响。唯一潜在风险是如果外部代码依赖 maybe_make_thread_pool 返回 None 的行为(极不可能),则调用者需要适应。

影响范围限于 tokenizer 的 pickle 序列化场景。修复后,通过 LLM.get_tokenizer()AsyncLLM.get_tokenizer() 获取的 tokenizer 可以正确地在 Ray actor、multiprocessing 或 cloudpickle 路径中传输。对非 pickle 场景无影响。

关联 Issue

#45433 [Bug]: thread-pool tokenizer (maybe_make_thread_pool) unpickles to None — breaks get_tokenizer() across Ray / multiprocessing

完整报告

参与讨论