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

Signed-off-by: Russell Bryant <rbryant@redhat.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Russell Bryant committed Aug 18, 2026 at 19:14 UTC ef47a897e2ad9a404cce9c9e7df15934deb8ffbe
10 files changed +204 -77
docs/features/kv_offloading_usage.md
+6 -4
@@ -152,10 +152,12 @@ Inside that subdirectory, blocks are sharded across hash-prefix subdirectories t
152
153 #### Cross-Process Sharing
154
155 -To enable KV cache sharing between multiple vLLM instances using the same `root_dir` (e.g., via a shared PVC), the `PYTHONHASHSEED` environment variable must be set to the same fixed value (e.g., `"0"`) on every instance. Without this, each process initializes `NONE_HASH` (the chain-hash seed for block content hashes) with random bytes, producing different block filenames for identical token content.
155 +KV cache sharing between multiple vLLM instances using the same `root_dir` (e.g., via a shared PVC) works by default: `NONE_HASH` (the chain-hash seed for block content hashes) is derived from a fixed default seed, so identical token content produces identical block filenames across instances. To use a custom shared seed instead, set the `PYTHONHASHSEED` environment variable to the same value on every instance.
156 +
157 +The exception is the non-cryptographic `xxhash` and `xxhash_cbor` values of `--prefix-caching-hash-algo`, which seed `NONE_HASH` randomly per process so the seed stays unpredictable. Sharing a cache across instances with those algorithms requires setting the same `PYTHONHASHSEED` on every instance.
158
159 ```bash
158 -PYTHONHASHSEED=0 vllm serve ...
160 +PYTHONHASHSEED=<shared-value> vllm serve ...
161 ```
162
163 ### Object Store (OBJ)
@@ -182,13 +184,13 @@ The object-store tier (`type: "obj"`) offloads blocks to an S3-compatible object
184 | `region` | no | `""` | Bucket region, if the endpoint requires one. |
185 | `ca_bundle` | no | `""` | CA bundle path for TLS verification. |
186
185 -Object keys follow the same run-configuration digest scheme as the filesystem tier (see [On-Disk Layout](#on-disk-layout)) and are stored under the optional `prefix`. The [Cross-Process Sharing](#cross-process-sharing) requirement (`PYTHONHASHSEED`) applies to shared buckets as well, so instances sharing a bucket produce identical keys for identical content. At startup the tier probes object store connectivity and fails fast with a configuration error if the bucket is unreachable.
187 +Object keys follow the same run-configuration digest scheme as the filesystem tier (see [On-Disk Layout](#on-disk-layout)) and are stored under the optional `prefix`. The [Cross-Process Sharing](#cross-process-sharing) behavior applies to shared buckets as well, so instances sharing a bucket produce identical keys for identical content; set a shared `PYTHONHASHSEED` if you want a custom seed. At startup the tier probes object store connectivity and fails fast with a configuration error if the bucket is unreachable.
188
189 ### P2P (Including P/D)
190
191 The P2P tier (`type: "p2p"`) shares completed KV blocks between vLLM instances over RDMA via NIXL. Each instance binds a control socket on `host:port` and exchanges blocks directly with peers — no shared filesystem required.
192
191 -The `PYTHONHASHSEED` environment variable must be set to the same fixed value (e.g. `"0"`) on all nodes so that block content hashes match across instances (see [Cross-Process Sharing](#cross-process-sharing)). This is enforced: a P2P instance started without `PYTHONHASHSEED` set fails at startup, and each peer's value is verified during the connect handshake — a peer advertising a different `PYTHONHASHSEED` is rejected.
193 +Block content hashes must match across instances for peers to exchange blocks (see [Cross-Process Sharing](#cross-process-sharing)). This works by default via the deterministic `NONE_HASH` seed, so setting `PYTHONHASHSEED` is optional. If you do set it, it must be the same value on all nodes. Each peer's effective seed is verified during the connect handshake — a peer advertising a different seed is rejected. With the `xxhash`/`xxhash_cbor` algorithms the seed is random per process, so `PYTHONHASHSEED` must be set on every peer or the handshake rejects them.
194
195 | Key | Required | Default | Notes |
196 | --- | --- | --- | --- |
docs/features/mooncake_store_connector_usage.md
+5 -3
@@ -235,10 +235,12 @@ Strict isolation requires a Mooncake master started with `--enable_multi_tenants
235
236 ### Reproducible Block Hashes Across Processes
237
238 -The `MooncakeStoreConnector` relies on consistent block hashes across all vLLM processes sharing the distributed store. Because Python randomizes its hash seed per process by default, identical prompts can produce different block hashes on different processes — preventing cross-process prefix cache hits.
238 +The `MooncakeStoreConnector` relies on consistent block hashes across all vLLM processes sharing the distributed store. Block hashes chain from `NONE_HASH`, which is derived from a fixed default seed, so identical prompts produce identical block hashes across processes by default — enabling cross-process prefix cache hits without extra configuration.
239
240 -Set a fixed `PYTHONHASHSEED` on every instance that shares the store (DP ranks, separate prefiller/decoder nodes, and any other vLLM process pointed at the same Mooncake store):
240 +The exception is the non-cryptographic `xxhash`/`xxhash_cbor` values of `--prefix-caching-hash-algo`, which seed `NONE_HASH` randomly per process; sharing a store with those requires `PYTHONHASHSEED`.
241 +
242 +To use a custom shared seed, set the same `PYTHONHASHSEED` on every instance that shares the store (DP ranks, separate prefiller/decoder nodes, and any other vLLM process pointed at the same Mooncake store):
243
244 ```bash
243 -PYTHONHASHSEED=0 vllm serve ...
245 +PYTHONHASHSEED=<shared-value> vllm serve ...
246 ```
tests/v1/core/test_kv_cache_utils.py
+63 -5
@@ -20,7 +20,7 @@ from vllm.multimodal.inputs import (
20 PlaceholderRange,
21 )
22 from vllm.sampling_params import SamplingParams
23 -from vllm.utils.hashing import sha256, sha256_cbor
23 +from vllm.utils.hashing import sha256, sha256_cbor, xxhash, xxhash_cbor
24 from vllm.utils.mem_constants import GiB_bytes
25 from vllm.v1.core.kv_cache_manager import KVCacheManager
26 from vllm.v1.core.kv_cache_utils import (
@@ -199,14 +199,20 @@ def new_mamba_spec(
199 def test_none_hash(monkeypatch, hash_fn):
200 import vllm.v1.core.kv_cache_utils
201
202 - # case 1: PYTHONHASHSEED is not set, use random
202 + # case 1: PYTHONHASHSEED is not set -> deterministic default seed so that
203 + # independent processes compute identical block hashes for identical
204 + # content (e.g. for KV cache reuse across nodes).
205 with monkeypatch.context() as m:
206 m.delenv("PYTHONHASHSEED", raising=False)
207 reloaded_kv_cache_utils = importlib.reload(vllm.v1.core.kv_cache_utils)
208 reloaded_kv_cache_utils.init_none_hash(hash_fn)
207 - assert reloaded_kv_cache_utils.NONE_HASH is not None
208 - assert isinstance(reloaded_kv_cache_utils.NONE_HASH, bytes)
209 - assert reloaded_kv_cache_utils.NONE_HASH != b""
209 + none_hash = reloaded_kv_cache_utils.NONE_HASH
210 + assert isinstance(none_hash, bytes)
211 + assert none_hash != b""
212 + assert none_hash == hash_fn(reloaded_kv_cache_utils.DEFAULT_NONE_HASH_SEED)
213 + # deterministic across re-initialization within the same environment
214 + reloaded_kv_cache_utils.init_none_hash(hash_fn)
215 + assert none_hash == reloaded_kv_cache_utils.NONE_HASH
216
217 # case 2: PYTHONHASHSEED is set, use the seed and hash_fn
218 with monkeypatch.context() as m:
@@ -218,6 +224,58 @@ def test_none_hash(monkeypatch, hash_fn):
224 assert hash_fn("python hash seed") == reloaded_kv_cache_utils.NONE_HASH
225
226
227 +@pytest.mark.parametrize("non_crypto_fn", [xxhash, xxhash_cbor])
228 +def test_none_hash_seed_random_for_non_crypto(monkeypatch, non_crypto_fn):
229 + """Non-cryptographic algorithms keep the per-process random seed.
230 +
231 + A deterministic seed is safe for SHA-256, whose collision resistance does
232 + not depend on a secret, but xxHash is not collision resistant: a known seed
233 + would let an attacker precompute colliding blocks offline. Keep the
234 + unpredictable seed there unless the operator opts into a shared one.
235 + """
236 + # PYTHONHASHSEED unset -> unpredictable, differs per resolution.
237 + with monkeypatch.context() as m:
238 + m.delenv("PYTHONHASHSEED", raising=False)
239 + seeds = {kv_cache_utils.resolve_none_hash_seed(non_crypto_fn) for _ in range(5)}
240 + assert len(seeds) == 5
241 + assert kv_cache_utils.DEFAULT_NONE_HASH_SEED not in seeds
242 +
243 + # PYTHONHASHSEED set -> operator opt-in wins, so peers can share a cache.
244 + with monkeypatch.context() as m:
245 + m.setenv("PYTHONHASHSEED", "12345")
246 + assert kv_cache_utils.resolve_none_hash_seed(non_crypto_fn) == "12345"
247 +
248 +
249 +@pytest.mark.parametrize("hash_fn", [sha256, sha256_cbor])
250 +def test_none_hash_seed_deterministic_for_crypto(monkeypatch, hash_fn):
251 + with monkeypatch.context() as m:
252 + m.delenv("PYTHONHASHSEED", raising=False)
253 + seed = kv_cache_utils.resolve_none_hash_seed(hash_fn)
254 + assert seed == kv_cache_utils.DEFAULT_NONE_HASH_SEED
255 + assert seed == kv_cache_utils.resolve_none_hash_seed(hash_fn)
256 +
257 +
258 +def test_get_none_hash_seed_reports_effective_seed(monkeypatch):
259 + """P2P advertises the seed NONE_HASH was actually derived from.
260 +
261 + The P2P tier is constructed before init_none_hash runs, so it must read the
262 + resolved seed lazily rather than re-deriving it.
263 + """
264 + import vllm.v1.core.kv_cache_utils
265 +
266 + with monkeypatch.context() as m:
267 + m.delenv("PYTHONHASHSEED", raising=False)
268 + reloaded = importlib.reload(vllm.v1.core.kv_cache_utils)
269 + reloaded.init_none_hash(sha256)
270 + assert reloaded.get_none_hash_seed() == reloaded.DEFAULT_NONE_HASH_SEED
271 +
272 + with monkeypatch.context() as m:
273 + m.setenv("PYTHONHASHSEED", "12345")
274 + reloaded = importlib.reload(vllm.v1.core.kv_cache_utils)
275 + reloaded.init_none_hash(sha256)
276 + assert reloaded.get_none_hash_seed() == "12345"
277 +
278 +
279 def test_kv_cache_block():
280 # Test KVCacheBlock initialization
281 block = KVCacheBlock(block_id=0)
tests/v1/kv_offload/tiering/p2p/test_manager.py
+35 -16
@@ -15,6 +15,8 @@ from types import SimpleNamespace
15 import numpy as np
16 import pytest
17
18 +from vllm.utils.hashing import sha256
19 +from vllm.v1.core.kv_cache_utils import DEFAULT_NONE_HASH_SEED, init_none_hash
20 from vllm.v1.kv_offload.base import LookupResult, ReqContext, ScheduleEndContext
21 from vllm.v1.kv_offload.tiering.base import JobResult, TransferJob
22 from vllm.v1.kv_offload.tiering.p2p import manager as manager_module
@@ -123,23 +125,12 @@ def _init_offloading_spec() -> SimpleNamespace:
125
126
127 # ---------------------------------------------------------------------------
126 -# Tests for __init__ PYTHONHASHSEED assertion
128 +# Tests for __init__ hash seed resolution
129 # ---------------------------------------------------------------------------
130
131
130 -class TestInitHashSeedAssertion:
131 - def test_missing_pythonhashseed_raises(self, monkeypatch):
132 - """P2P instance refuses to start when PYTHONHASHSEED is unset."""
133 - monkeypatch.delenv("PYTHONHASHSEED", raising=False)
134 - with pytest.raises(ValueError, match="PYTHONHASHSEED"):
135 - P2PSecondaryTierManager(
136 - offloading_spec=_init_offloading_spec(),
137 - primary_kv_view=memoryview(bytearray(16)),
138 - )
139 -
140 - def test_pythonhashseed_set_succeeds(self, monkeypatch):
141 - """With PYTHONHASHSEED set, __init__ records it for the handshake."""
142 - monkeypatch.setenv("PYTHONHASHSEED", "12345")
132 +class TestInitHashSeed:
133 + def _build(self, monkeypatch) -> P2PSecondaryTierManager:
134 monkeypatch.setattr(manager_module, "NixlTransport", lambda *a, **k: object())
135 monkeypatch.setattr(manager_module, "ZmqTransport", lambda *a, **k: object())
136 monkeypatch.setattr(
@@ -147,11 +138,39 @@ class TestInitHashSeedAssertion:
138 "from_offloading_spec",
139 lambda **k: SimpleNamespace(get_run_config=lambda: {}),
140 )
150 - mgr = P2PSecondaryTierManager(
141 + return P2PSecondaryTierManager(
142 offloading_spec=_init_offloading_spec(),
143 primary_kv_view=memoryview(bytearray(16)),
144 )
154 - assert mgr._hash_seed == "12345"
145 +
146 + def test_missing_pythonhashseed_uses_default(self, monkeypatch):
147 + """P2P falls back to the deterministic default seed when unset."""
148 + monkeypatch.delenv("PYTHONHASHSEED", raising=False)
149 + mgr = self._build(monkeypatch)
150 + init_none_hash(sha256)
151 + assert mgr._get_hash_seed() == DEFAULT_NONE_HASH_SEED
152 +
153 + def test_pythonhashseed_set_succeeds(self, monkeypatch):
154 + """With PYTHONHASHSEED set, the handshake advertises it."""
155 + monkeypatch.setenv("PYTHONHASHSEED", "12345")
156 + mgr = self._build(monkeypatch)
157 + init_none_hash(sha256)
158 + assert mgr._get_hash_seed() == "12345"
159 +
160 + def test_seed_resolved_after_init_none_hash(self, monkeypatch):
161 + """The seed is read lazily, not at construction time.
162 +
163 + This tier is built before init_none_hash runs, and a non-cryptographic
164 + hash algorithm seeds NONE_HASH randomly, so resolving in __init__ would
165 + advertise a value that does not match the NONE_HASH actually in use.
166 + """
167 + monkeypatch.delenv("PYTHONHASHSEED", raising=False)
168 + mgr = self._build(monkeypatch)
169 + assert mgr._hash_seed is None
170 + monkeypatch.setattr(
171 + manager_module, "get_none_hash_seed", lambda: "random-seed-abc"
172 + )
173 + assert mgr._get_hash_seed() == "random-seed-abc"
174
175
176 # ---------------------------------------------------------------------------
vllm/v1/core/kv_cache_utils.py
+59 -18
@@ -15,7 +15,7 @@ from typing import Any, NamedTuple, NewType, TypeAlias, cast, overload
15 from vllm import envs
16 from vllm.config import VllmConfig
17 from vllm.logger import init_logger
18 -from vllm.utils.hashing import sha256_cbor, xxhash_cbor
18 +from vllm.utils.hashing import xxhash, xxhash_cbor
19 from vllm.utils.math_utils import cdiv, round_up
20 from vllm.utils.mem_utils import format_gib
21 from vllm.utils.torch_utils import get_dtype_size
@@ -87,32 +87,73 @@ logger = init_logger(__name__)
87
88 # The hash seed for the first block of any prefix block sequence.
89 #
90 -# We use a random value to avoid hash collisions or PYTHONHASHSEED environment
91 -# variable if set such that processes can share the seed if needed. This aligns
92 -# with the behavior of Python's hash() function, which also uses a random seed
93 -# if PYTHONHASHSEED is not set.
90 +# For cryptographic hash algorithms it is derived deterministically from a fixed
91 +# default seed, so independent vLLM processes compute identical block hashes for
92 +# identical content and can share a prefix cache (e.g. KV cache reuse across
93 +# nodes) without extra configuration. This does not weaken collision resistance,
94 +# which for SHA-256 does not depend on keeping the seed secret; ``cache_salt``
95 +# remains the mechanism for intentional cache isolation.
96 +#
97 +# Non-cryptographic algorithms keep a per-process random seed, because a
98 +# predictable seed would let an attacker precompute colliding blocks offline
99 +# (see #12621). Setting PYTHONHASHSEED overrides the seed in both cases.
100 #
101 # The function `init_none_hash` initializes this variable globally.
102 NONE_HASH: BlockHash
97 -_CBOR_HASH_FUNCTIONS = frozenset({sha256_cbor, xxhash_cbor})
103
104 +# Fixed seed used when the PYTHONHASHSEED environment variable is not set and
105 +# the hash algorithm is cryptographic.
106 +DEFAULT_NONE_HASH_SEED = "vllm-none-hash"
107 +
108 +# Algorithms that are not collision resistant, so the seed must stay secret.
109 +_NON_CRYPTO_HASH_FUNCTIONS = frozenset({xxhash, xxhash_cbor})
110 +
111 +# The seed NONE_HASH was derived from, set by init_none_hash.
112 +_NONE_HASH_SEED: str | None = None
113
100 -def init_none_hash(hash_fn: Callable[[Any], bytes]):
101 - global NONE_HASH
114
115 +def resolve_none_hash_seed(hash_fn: Callable[[Any], bytes]) -> str:
116 + """Resolve the seed to derive NONE_HASH from.
117 +
118 + PYTHONHASHSEED wins if set. Otherwise cryptographic algorithms get the
119 + fixed default (shareable across processes) and non-cryptographic ones get
120 + fresh random bytes, keeping the seed unpredictable where collision
121 + resistance depends on it.
122 + """
123 hash_seed = os.getenv("PYTHONHASHSEED")
104 - if hash_seed is None and hash_fn in _CBOR_HASH_FUNCTIONS:
124 + if hash_seed is not None:
125 + return hash_seed
126 + if hash_fn in _NON_CRYPTO_HASH_FUNCTIONS:
127 + return os.urandom(32).hex()
128 + return DEFAULT_NONE_HASH_SEED
129 +
130 +
131 +def get_none_hash_seed() -> str:
132 + """Return the seed NONE_HASH was derived from.
133 +
134 + Components that must agree on NONE_HASH across processes (the P2P tier
135 + advertises this during its connect handshake) read the resolved seed here
136 + instead of re-deriving it, so they observe the random seed too. Falls back
137 + to the deterministic seed before ``init_none_hash`` has run.
138 + """
139 + if _NONE_HASH_SEED is None:
140 + return DEFAULT_NONE_HASH_SEED
141 + return _NONE_HASH_SEED
142 +
143 +
144 +def init_none_hash(hash_fn: Callable[[Any], bytes]):
145 + global NONE_HASH, _NONE_HASH_SEED
146 +
147 + _NONE_HASH_SEED = resolve_none_hash_seed(hash_fn)
148 + if hash_fn in _NON_CRYPTO_HASH_FUNCTIONS and os.getenv("PYTHONHASHSEED") is None:
149 logger.warning(
106 - "PYTHONHASHSEED is not set. This will lead to non-reproducible "
107 - "block-hashes when using CBOR-based hash functions such as "
108 - "sha256_cbor or xxhash_cbor. Consider setting PYTHONHASHSEED to a "
109 - "fixed value for reproducibility."
150 + "Using a random per-process NONE_HASH seed because %s is not "
151 + "collision resistant. Block hashes are therefore not reproducible "
152 + "across processes; set PYTHONHASHSEED to a shared value to reuse "
153 + "the prefix cache across instances, or use sha256.",
154 + hash_fn.__name__,
155 )
111 -
112 - if hash_seed is None:
113 - NONE_HASH = BlockHash(os.urandom(32))
114 - else:
115 - NONE_HASH = BlockHash(hash_fn(hash_seed))
156 + NONE_HASH = BlockHash(hash_fn(_NONE_HASH_SEED))
157
158
159 @dataclass(slots=True)
vllm/v1/kv_offload/tiering/fs/manager.py
+8 -7
@@ -95,13 +95,14 @@ class FileSystemTierManager(SecondaryTierManager):
95 get_finished_jobs() polls job completion and returns completed JobResults.
96
97 Cross-process sharing:
98 - In order to enable KV cache sharing between multiple vLLM instances
99 - using the same ``root_dir`` (e.g., via a shared PVC) the environment
100 - variable ``PYTHONHASHSEED`` must be set to the same fixed value
101 - (e.g., "0") on all instances. Without this, each process initializes
102 - ``NONE_HASH`` (the chain-hash seed for block content hashes) with
103 - random bytes, producing different block filenames for identical token
104 - content.
98 + KV cache sharing between multiple vLLM instances using the same
99 + ``root_dir`` (e.g., via a shared PVC) works by default: ``NONE_HASH``
100 + (the chain-hash seed for block content hashes) is derived from a fixed
101 + default seed, so identical token content produces identical block
102 + filenames across instances. Setting the ``PYTHONHASHSEED`` environment
103 + variable to the same value on all instances overrides the default seed,
104 + and is required to share a cache when using a non-cryptographic
105 + prefix-caching hash algorithm, which seeds ``NONE_HASH`` randomly.
106 """
107
108 medium: ClassVar[Medium] = Medium.STORAGE
vllm/v1/kv_offload/tiering/p2p/manager.py
+18 -17
@@ -8,7 +8,6 @@ Owns transports and a single bidirectional P2PSession per remote peer.
8
9 from __future__ import annotations
10
11 -import os
11 import time
12 import uuid
13 from collections.abc import Iterable, Sequence
@@ -19,6 +18,7 @@ from typing_extensions import override
18
19 import vllm.envs as envs
20 from vllm.logger import init_logger
21 +from vllm.v1.core.kv_cache_utils import get_none_hash_seed
22 from vllm.v1.kv_offload.base import (
23 LookupResult,
24 OffloadKey,
@@ -247,20 +247,15 @@ class P2PSecondaryTierManager(SecondaryTierManager):
247 **kwargs: Reserved for future tier-specific options.
248 """
249 super().__init__(offloading_spec, primary_kv_view, tier_type)
250 - # Block hashes chain from NONE_HASH, seeded from PYTHONHASHSEED
251 - # (see init_none_hash in v1/core/kv_cache_utils.py). Peers with
252 - # different seeds compute different hashes for identical content, so
253 - # lookups silently miss and no KV crosses the wire. Require it here so
254 - # a misconfigured P2P instance fails at startup rather than degrading
255 - # silently; the value is also verified against each peer on handshake.
256 - hash_seed = os.getenv("PYTHONHASHSEED")
257 - if hash_seed is None:
258 - raise ValueError(
259 - "PYTHONHASHSEED must be set for P2P KV offload so that block "
260 - "hashes match across instances. Set it to a fixed value (e.g. "
261 - "PYTHONHASHSEED=0) on every P2P peer."
262 - )
263 - self._hash_seed = hash_seed
250 + # Block hashes chain from NONE_HASH (see v1/core/kv_cache_utils.py).
251 + # Peers whose seeds differ compute different hashes for identical
252 + # content, so lookups silently miss and no KV crosses the wire. The
253 + # seed is advertised and verified against each peer during the
254 + # handshake, so a mismatch is rejected loudly instead of degrading
255 + # silently. Resolved lazily in _get_hash_seed: this tier is built
256 + # before init_none_hash runs, and a non-cryptographic hash algorithm
257 + # seeds NONE_HASH randomly, so the value is only known afterwards.
258 + self._hash_seed: str | None = None
259 if host is None:
260 host = envs.VLLM_P2P_SIDE_CHANNEL_HOST
261 if port is None:
@@ -602,6 +597,12 @@ class P2PSecondaryTierManager(SecondaryTierManager):
597 # Internal
598 # ------------------------------------------------------------------
599
600 + def _get_hash_seed(self) -> str:
601 + """The seed NONE_HASH was derived from, resolved on first session."""
602 + if self._hash_seed is None:
603 + self._hash_seed = get_none_hash_seed()
604 + return self._hash_seed
605 +
606 def _get_or_create_session(self, peer_id: str) -> P2PSession:
607 """Return the existing session for peer_id, or open one outbound.
608
@@ -621,7 +622,7 @@ class P2PSecondaryTierManager(SecondaryTierManager):
622 local_id=self._local_id,
623 transport=self._data,
624 local_block_len=self._data.block_len,
624 - local_hash_seed=self._hash_seed,
625 + local_hash_seed=self._get_hash_seed(),
626 conn=conn,
627 )
628 self._sessions[peer_id] = session
@@ -643,7 +644,7 @@ class P2PSecondaryTierManager(SecondaryTierManager):
644 local_id=self._local_id,
645 transport=self._data,
646 local_block_len=self._data.block_len,
646 - local_hash_seed=self._hash_seed,
647 + local_hash_seed=self._get_hash_seed(),
648 conn=conn,
649 )
650 logger.info(
vllm/v1/kv_offload/tiering/p2p/session/client.py
+3 -2
@@ -312,8 +312,9 @@ class ClientRole:
312 if st is not None and load is not None:
313 logger.warning(
314 "P2PSession %s: load request %s (job_id=%d) timed out; "
315 - "load job completed with failure. If this recurs, ensure "
316 - "PYTHONHASHSEED is set to the same value on all nodes.",
315 + "load job completed with failure. If this recurs, ensure all "
316 + "nodes use the same prefix-cache hash seed (matching "
317 + "PYTHONHASHSEED, if set) and hash algorithm.",
318 self._peer_id,
319 kv_request_id,
320 load.job_id,
vllm/v1/kv_offload/tiering/p2p/session/protocol.py
+4 -3
@@ -125,9 +125,10 @@ class ConnectMsg:
125 BLOCK_LEN: Size in bytes of each block (must match between peers).
126 CONFIG_FINGERPRINT: SHA-256 prefix of the model configuration.
127 Peers with different fingerprints are incompatible.
128 - HASH_SEED: The peer's PYTHONHASHSEED. Block hashes chain from a seed
129 - derived from it, so peers with different values compute different
130 - hashes for identical content and must not exchange blocks.
128 + HASH_SEED: The peer's effective prefix-cache hash seed (PYTHONHASHSEED
129 + if set, otherwise the built-in default). Block hashes chain from a
130 + seed derived from it, so peers with different values compute
131 + different hashes for identical content and must not exchange blocks.
132 """
133
134 TYPE = "connect"
vllm/v1/kv_offload/tiering/p2p/session/session.py
+3 -2
@@ -458,9 +458,10 @@ class P2PSession:
458 )
459 if msg[ConnectMsg.HASH_SEED] != self._local_hash_seed:
460 raise ValueError(
461 - f"PYTHONHASHSEED mismatch from {self.peer_id}: "
461 + f"hash seed mismatch from {self.peer_id}: "
462 f"remote={msg[ConnectMsg.HASH_SEED]!r}, "
463 - f"local={self._local_hash_seed!r}"
463 + f"local={self._local_hash_seed!r}. Ensure PYTHONHASHSEED "
464 + "(if set) matches on all P2P peers."
465 )
466 self._transport.add_remote_peer(
467 self.peer_id,