Prhub

#51875 [Core] Make prefix-cache NONE_HASH deterministic by default

原始 PR 作者 russellb 合并时间 2026-08-19 07:14 文件变更 10 提交数 4 评论 10 代码增减 +204 / -77

执行摘要

默认固定 NONE_HASH 种子,跨节点前缀缓存开箱即用

用户反馈所有 vLLM 实例必须设置相同的 PYTHONHASHSEED 才能跨节点复用 KV cache;PR body 明确写道:"Reported by a user who found that all vLLM instances need the same PYTHONHASHSEED to reuse KV cache across nodes"。该随机种子可追溯到 #12621——当时 prefix caching 使用 Python 内置 hash(),可预测的 hash(None) 使故意碰撞可行;而当前默认哈希算法已是 SHA-256,碰撞抵抗力不依赖秘密种子,且 cache_salt 是官方支持的缓存隔离机制,因此随机种子不再提供有意义的安全防护,只增加分布式部署的操作负担,同时使基于 CBOR 的哈希默认不可复现。

值得精读。核心设计决策有三个:一是将种子解析收敛为单一入口 resolve_none_hash_seed,让两个正确性耦合的调用点共用同一规则;二是区分加密与非加密哈希的安全策略——SHA-256 的碰撞抵抗力不依赖种子保密,xxHash 则需要保持随机;三是 P2P 阶段采用惰性解析解决「tier 构建早于 init_none_hash」的初始化顺序问题。建议观察 4 个 commit 的演进(初始实现 → 提取 helper → 区分算法 → merge main),体会 review 驱动设计收敛的过程。

讨论亮点

三条核心交锋:

  1. claude[bot](设计):指出种子解析表达式在 init_none_hash 与 P2PSecondaryTierManager.init 重复,而两个调用点正确性耦合(握手以种子作为 NONE_HASH 一致性的代理),规则一旦漂移会静默校验错误值,建议提取共享 helper。russellb 在 commit 1243fbccc2 中实现 resolve_none_hash_seed() 解决。
  2. sfeng33(安全):初版对 xxhash/xxhash_cbor 也无条件应用固定种子,移除了 #12621 添加的多租户加固——xxHash 不具碰撞抵抗力,公开种子可被离线预计算碰撞块。russellb 在 commit a83334e1ac 中让这两类算法保留 per-process 随机种子,并补充 warning 说明取舍。
  3. sfeng33(正确性):移除启动 ValueError 后握手是唯一守卫,且只比较种子字符串而不比较哈希算法,算法不匹配的 peers 会连接成功却静默不传 KV。russellb 承认该缺口先于本 PR 存在(旧启动校验也只检查 PYTHONHASHSEED 是否设置,CONFIG_FINGERPRINT 不覆盖 prefix_caching_hash_algo),并借该评论暴露并修复了惰性解析时序问题——P2P tier 构建早于 init_none_hash,提前解析会通告过期默认种子。

实现拆解

  1. 重构种子解析(vllm/v1/core/kv_cache_utils.py):新增模块级常量 DEFAULT_NONE_HASH_SEED = "vllm-none-hash"、非加密算法集合 _NON_CRYPTO_HASH_FUNCTIONS = frozenset({xxhash, xxhash_cbor}) 与状态变量 _NONE_HASH_SEED;抽出单一入口 resolve_none_hash_seed() 统一「PYTHONHASHSEED 优先 → 非加密算法随机 → 加密算法固定默认」的规则;新增 get_none_hash_seed() 供 P2P 握手读取实际生效种子;init_none_hash() 改为调用上述函数,并仅在非加密算法且未设置 PYTHONHASHSEED 时打出可复现性警告。NONE_HASH = hash_fn(seed) 的派生链路不变,变化的只是种子来源。
  2. P2P tier 解除硬依赖(vllm/v1/kv_offload/tiering/p2p/manager.py):删除 __init__ 中缺失 PYTHONHASHSEED 即抛 ValueError 的逻辑,self._hash_seed 初始化为 None;新增 _get_hash_seed(),在 _get_or_create_session_accept_new_peers 首次建会话时调用 get_none_hash_seed() 惰性解析并缓存。时序背景是 P2P tier 在 OffloadingConnectorScheduler.__init__ 中构建,早于 core.py 中 init_none_hash 的执行,提前解析会在非加密算法场景通告过期默认值。
  3. 握手协议与错误路径文案(p2p/session/protocol.py、session.py、client.py)ConnectMsg.HASH_SEED 字段语义更新为「有效前缀缓存哈希种子(设置时取 PYTHONHASHSEED,否则取内置默认)」;握手不匹配的 ValueError 与 load 超时日志从「必须设置 PYTHONHASHSEED」改为「确认各节点哈希种子与哈希算法一致」,避免误导运维只去设置环境变量。
  4. 文档与注释同步(fs/manager.py 类 docstring、kv_offloading_usage.md、mooncake_store_connector_usage.md):把「必须设置 PYTHONHASHSEED」改为「默认即可共享;xxhash/xxhash_cbor 例外仍需共享种子」,示例命令从 PYTHONHASHSEED=0 改为 PYTHONHASHSEED=<shared-value> 以表达「可选的自定义共享种子」语义。
  5. 测试配套(tests/v1/core/test_kv_cache_utils.py、tests/v1/kv_offload/tiering/p2p/test_manager.py):新增加密/非加密算法分区的种子测试、确定性断言与 get_none_hash_seed() 生效种子测试;P2P 侧将 TestInitHashSeedAssertion 反转为 TestInitHashSeed,覆盖默认种子回退、显式种子透传与「种子在 init_none_hash 之后惰性解析」三条路径。运行结果为 test_kv_cache_utils.py 与 P2P 两个测试文件合计 241 项、test_prefix_caching.py 89 项全部通过,pre-commit 全绿。
文件 模块 状态 重要度
vllm/v1/core/kv_cache_utils.py 前缀缓存 modified 8.42
tests/v1/core/test_kv_cache_utils.py 前缀缓存 modified 7.06
vllm/v1/kv_offload/tiering/p2p/manager.py P2P 层 modified 7.3
tests/v1/kv_offload/tiering/p2p/test_manager.py P2P 层 modified 6.69
vllm/v1/kv_offload/tiering/p2p/session/protocol.py 握手协议 modified 5.4
vllm/v1/kv_offload/tiering/fs/manager.py 文件存储 modified 4.91
vllm/v1/kv_offload/tiering/p2p/session/client.py P2P 会话 modified 4.33
vllm/v1/kv_offload/tiering/p2p/session/session.py P2P 会话 modified 4.33
docs/features/kv_offloading_usage.md 文档 modified 2.68
docs/features/mooncake_store_connector_usage.md 文档 modified 2.44

关键符号

resolve_none_hash_seed get_none_hash_seed init_none_hash _get_hash_seed

关键源码片段

vllm/v1/core/kv_cache_utils.py core-logic

核心变更所在:NONE_HASH 的种子来源、解析规则与初始化逻辑全部在此重构,是本 PR 的行为契约变化源头。

# 固定默认种子:未设置 PYTHONHASHSEED 且使用加密哈希算法时使用,
# 让独立进程对相同内容算出相同的块哈希,实现开箱即用的跨节点缓存共享
DEFAULT_NONE_HASH_SEED = "vllm-none-hash"# 非加密哈希算法清单:碰撞抵抗力弱,种子必须保持不可预测
# (这是 #12621 引入的多租户加固,防止攻击者用公开种子离线预计算碰撞块)
_NON_CRYPTO_HASH_FUNCTIONS = frozenset({xxhash, xxhash_cbor})# NONE_HASH 实际派生自的种子,由 init_none_hash 写入;
# P2P 握手需要读取该值,向对端通告本端的种子
_NONE_HASH_SEED: str | None = None
​
​
def resolve_none_hash_seed(hash_fn: Callable[[Any], bytes]) -> str:
    """解析用于派生 NONE_HASH 的种子,规则只此一处。    优先级:PYTHONHASHSEED 环境变量 >(非加密算法时)新随机字节 >
    加密算法时的固定默认值。SHA-256 的碰撞抵抗力不依赖种子保密,
    因此可以使用公开的固定种子;xxHash 不具备碰撞抵抗力,
    必须保持随机以免攻击者离线预计算碰撞块。
    """
    hash_seed = os.getenv("PYTHONHASHSEED")
    if hash_seed is not None:
        return hash_seed
    if hash_fn in _NON_CRYPTO_HASH_FUNCTIONS:
        return os.urandom(32).hex()
    return DEFAULT_NONE_HASH_SEED
​
​
def get_none_hash_seed() -> str:
    """返回 NONE_HASH 实际派生自的种子。    P2P tier 在连接握手中通告该值,作为双方 NONE_HASH 一致性的代理;
    它在 init_none_hash 运行前可能被调用,此时回退到默认种子。
    """
    if _NONE_HASH_SEED is None:
        return DEFAULT_NONE_HASH_SEED
    return _NONE_HASH_SEED
​
​
def init_none_hash(hash_fn: Callable[[Any], bytes]):
    global NONE_HASH, _NONE_HASH_SEED
​
    _NONE_HASH_SEED = resolve_none_hash_seed(hash_fn)
    # 非加密算法在未显式设置 PYTHONHASHSEED 时会得到随机种子,
    # 跨进程复用前缀缓存需要显式共享种子,这里用警告说明取舍
    if hash_fn in _NON_CRYPTO_HASH_FUNCTIONS and os.getenv("PYTHONHASHSEED") is None:
        logger.warning(
            "Using a random per-process NONE_HASH seed because %s is not "
            "collision resistant. Block hashes are therefore not reproducible "
            "across processes; set PYTHONHASHSEED to a shared value to reuse "
            "the prefix cache across instances, or use sha256.",
            hash_fn.__name__,
        )
    NONE_HASH = BlockHash(hash_fn(_NONE_HASH_SEED))
vllm/v1/kv_offload/tiering/p2p/manager.py dependency-wiring

P2P tier 的行为反转点:从「强制 PYTHONHASHSEED 否则启动失败」改为「惰性解析有效种子并在握手期校验」,同时解决 tier 构建早于 init_none_hash 的时序问题。

# hash 种子延迟到首次建会话时解析:P2P tier 在 OffloadingConnectorScheduler
# 的 __init__ 期间构建,早于 core.py 中 init_none_hash 的执行时机;
# 若采用非加密哈希算法,NONE_HASH 的种子是随机的,提前解析只会通告
# 一个过期的默认值,让握手通过对端间却实际无法传输 KV
self._hash_seed: str | None = None
​
​
def _get_hash_seed(self) -> str:
    """NONE_HASH 派生的种子,首次建会话时惰性解析并缓存结果。"""
    if self._hash_seed is None:
        self._hash_seed = get_none_hash_seed()
    return self._hash_seed

评论区精华

提取 resolve_none_hash_seed 消除种子解析重复 设计

claude[bot] 指出 os.getenv("PYTHONHASHSEED", DEFAULT_NONE_HASH_SEED) 在 init_none_hash 与 P2PSecondaryTierManager.__init__ 中重复出现,而两个调用点正确性耦合——P2P 握手以种子作为 NONE_HASH 一致性的代理,规则一旦漂移会静默校验错误值。

结论:russellb 在 commit 1243fbccc2 中提取 resolve_none_hash_seed(),两个调用点改为调用同一函数。 · 已解决

非加密哈希算法必须保留随机种子 安全

sfeng33 指出初版 init_none_hash 对 xxhash/xxhash_cbor 也无条件应用固定种子,移除了 #12621 针对多租户场景的加固——xxHash 不具碰撞抵抗力,公开种子可被离线预计算碰撞块。

结论:russellb 在 commit a83334e1ac 中让 xxhash/xxhash_cbor 保留 per-process 随机种子,只有 sha256/sha256_cbor 使用固定默认值,并增加 warning 说明取舍。 · 已解决

移除启动校验后握手只比种子、不比算法 正确性

sfeng33 指出移除 P2P 启动 ValueError 后连接握手是唯一守卫,且握手只比较 HASH_SEED 字符串、不比较 prefix-caching 哈希算法,算法不匹配的 peers 会连接成功但静默不传 KV。russellb 承认该缺口先于本 PR 存在,并借该评论暴露了惰性解析的必要性——P2P tier 构建早于 init_none_hash,xxhash 场景下提前解析会通告过期默认种子。

结论:种子改为 _get_hash_seed() 惰性解析并缓存;算法比较缺口被记录为已知问题,留待后续通过将算法纳入 OffloadingCacheConfig 解决。 · partially-resolved

风险与影响

(1)安全:非加密算法在未设置 PYTHONHASHSEED 时保留随机种子,安全策略依赖 _NON_CRYPTO_HASH_FUNCTIONS 集合与 vllm/utils/hashing.py 的算法注册保持同步,未来新增非加密哈希算法时若忘记登记会静默退化为固定默认种子。(2)分布式一致性:P2P 握手只比较 HASH_SEED 字符串、不比较哈希算法,russellb 在讨论中确认这是先于本 PR 存在的缺口,sha256 与 xxhash 的 peers 仍可能握手成功但静默不传输 KV。(3)时序回归:P2P tier 在 OffloadingConnectorScheduler.__init__ 中构建、早于 core.py 的 init_none_hash,_get_hash_seed() 必须惰性解析,否则会通告过期默认值;已有测试 test_seed_resolved_after_init_none_hash 覆盖。(4)行为变化:未设置 PYTHONHASHSEED 的部署从随机种子切换为固定种子,对多租户隔离的影响取决于哈希算法,SHA-256 下安全;显式设置者的行为不变。

影响范围:10 个文件、+204/-77,横跨 vllm/v1 核心缓存逻辑、P2P/FS 两个 offload tier、握手协议与三份文档。对用户:共享 FS/对象存储/Mooncake/P2P 的分布式部署不再需要手工同步 PYTHONHASHSEED,默认即可跨进程命中前缀缓存,P2P 部署的启动校验从硬失败改为握手期校验。对系统:提升跨实例缓存复用率、降低重复 prefill 计算。对团队:核心缓存契约发生默认行为变更,后续新增哈希算法须登记到 _NON_CRYPTO_HASH_FUNCTIONS;这是一个向后兼容的操作负担消除型改进,影响程度中高。

核心路径变更 分布式一致性 安全权衡 默认行为变更

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论