Repository navigation
Conversation
|
Documentation preview: https://vllm--59648.org.readthedocs.build/en/59648/ |
| header = f.read(header_len) | ||
| hasher.update(size_bytes) | ||
| hasher.update(header) | ||
| try: | ||
| entries = json.loads(header) | ||
| except json.JSONDecodeError: | ||
| return | ||
| data_start = 8 + header_len | ||
| for name in sorted(entries): | ||
| offsets = entries[name].get("data_offsets") | ||
| if not offsets: | ||
| continue | ||
| f.seek(data_start + offsets[0]) | ||
| hasher.update(f.read(min(_TENSOR_HEAD_BYTES, offsets[1] - offsets[0]))) |
There was a problem hiding this comment.
I'm wondering the cost of iterating and hashing each tensor entry for extremly large model like Kimi-K3. Is it possible to only hash fixed number of interleave chunks for each shard? 🤔
There was a problem hiding this comment.
Good question, I sized this before picking the scheme. A K3-scale checkpoint is around 70k tensors across roughly 50 shards, so this works out to about 1.3k sampled reads of 1 KiB each per shard, roughly 1.3 MB total. On NVMe that is tens of milliseconds; on a spinning disk at a pessimistic 10 ms seek per read it is ~13 s worst case, against the minutes that same disk spends streaming the shard's tens of GB. It runs once per daemon load, so it is noise next to the load itself. Iterating the entries costs nothing extra since the header is already parsed.
On fixed interleaved chunks: I went per-tensor deliberately. The gap in #59647 is value-only divergence, and the realistic cases (a merged LoRA, a norm-only touch, an expert-targeted fine-tune) rewrite only some matrices. Fixed byte ranges over a ~40 GB shard can easily skip every changed region, which reproduces the exact silent-stale-weights failure this PR closes. Sampling the head of every tensor keeps the property that each tensor contributes to the key, so a value change anywhere flips it.
Honest limit either way: this is a sampled fingerprint, not a digest of every byte, so a change confined to the middle of one tensor slips through; catching that means reading everything, which defeats the preload. If seek count on slow media ever becomes a real concern, the bounded variant that keeps the guarantee is coalescing adjacent tensors into span reads (the data region lays tensors out contiguously), not dropping tensors from the key. Happy to do that here or as a follow-up if you prefer.
There was a problem hiding this comment.
On NVMe that is tens of milliseconds; on a spinning disk at a pessimistic 10 ms seek per read it is ~13 s worst case, against the minutes that same disk spends streaming the shard's tens of GB.
Actually, besides local NVMe, models like K3 are more likely stored in NFS and shared across cluster nodes. So I think the bottleneck is the RPC in this scene instead. (For example, given 70K tensors and RTT=1ms, it will need 70s for sequential hashing with per-tensor seek+read)🤔
There was a problem hiding this comment.
NFS is exactly the case the worker pool is sized for. The fingerprint cost scales with tensor count, not model size: one 1 KiB pread per tensor, 32 in flight. A K3-scale checkpoint is about 70k tensors, so ~2.2k sequential rounds; at a 1 ms NFS RTT that is 2 to 3 seconds, and even at a congested 5 ms RTT it stays around 11 s, against the minutes NFS spends streaming the same checkpoint into memory. LAN NFS also batches better than this worst case, since 32 concurrent requests let the server coalesce.
The alternative of sampling only some tensors is where I would not go: the #59647 failure mode is a fine-tune that rewrites only part of the checkpoint, and any subsampling of tensors lets a value-only change slip through with probability proportional to the skip rate. Hashing every tensor head is what makes the key collision-free for that case. If the NFS cost still reads as too high, the knob I would rather turn is _FINGERPRINT_READ_WORKERS (64 workers halves the RTT-bound time) or a larger per-tensor read to catch mid-tensor edits, not dropping tensors from the hash.
|
@Isotr0py You're right, NFS RTT is the correct cost model for cluster checkpoints, and sequential per-tensor reads do land at ~70 s for K3-scale there. Addressed in e87d982: the sampled reads now go through a small thread pool (32 in flight), so the same ~70k reads finish in a few seconds. The header parse is unchanged, and pool.map preserves order, so the digest is byte-identical to sequential sampling and keys computed by the previous head stay valid. I kept per-tensor sampling rather than fixed interleave chunks for the reason from the earlier round: the divergences this key exists to catch (a merged LoRA, a norm-only touch, an expert-targeted fine-tune) rewrite only some matrices, and fixed byte ranges over a ~40 GB shard can skip every changed tensor. Concurrency removes the RPC wall time without giving up that property. Added a test with 64 tensors where only one changes value, which also pins down that the digest doesn't depend on read scheduling. I verified sequential and concurrent digests match on a 200-tensor safetensors layout here; this machine has no torch, so the suite itself still needs a run where torch lives. (AI assistance was used for the patch, disclosed per vLLM's AGENTS.md policy.) |
e87d982 to
af36772
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
af36772 to
63d2e71
Compare
|
Rebased onto current main (8733a21) to clear the conflict mergify flagged. It was test-file only: upstream added test_weight_cache_key_distinguishes_nvfp4_activation_override at the same spot where this PR appends the hash_checkpoint tests, so both are kept now. protocol.py had no upstream drift since the branch point. Reran the unit slice on the rebased head: 7 passed (the three hash_checkpoint tests, the dp_ranks key test, and the upstream nvfp4 override test). 63d2e71 is pushed. |
|
Adding measurements relevant to the network-filesystem latency discussion, and a question about an explicit opt-in to reuse an already selected preloaded instance. Version boundary: these results are from vLLM 0.31.0, not this PR's branch. I have not tested the concurrent fingerprint-reading implementation in this PR. Setup
Controlled A/BA process-local import hook instrumented both variants without modifying the installed vLLM files:
Runs were ordered A → B → B → A. Total startup is process launch to HTTP
Mean startup: 78.79 s → 46.85 s, saving 31.94 s / 40.5%. In A, the instrumented header reads accounted for 32.319 s and 31.791 s, reading only 203,664 bytes across 66 files. The remaining startup overhead was essentially unchanged. These are nested timings: the fingerprint time is included in model loading. All four engines logged successful zero-copy mapping of 980 tensors, and a short deterministic completion smoke test produced identical output. This is a small, environment-specific experiment, not a general performance or accuracy claim. One detail relevant to the earlier discussion: in 0.31.0, the engine calls Possible separate featureWould an explicit opt-in to trust a selected preloaded instance be a reasonable separate feature proposal? An operator could select an immutable cache identity / daemon instance and accept its resident weights as authoritative, avoiding checkpoint-file rescans while retaining architecture, dtype, quantization, parallel-layout, version, and GPU compatibility checks. Source-checkpoint changes would then require an explicit new preload rather than being rediscovered on every restart. The default validation behavior could remain unchanged. I am asking about a separate operator-controlled mode, not asking this correctness-fix PR to absorb that scope. |
|
Clean experiment, and the A → B → B → A ordering plus keeping the GPU UUID and other WeightCacheKey checks live in variant B makes the 31.9 s attribution convincing. 203 KB of header reads costing 32 s is pure latency-bound I/O, and your note that 0.31.0 pays it on every engine restart through On the opt-in question: yes, I think that's a reasonable separate feature, and it should stay separate. The scope split is clean. This PR is about correctness: when the fingerprint is consulted, it must reflect the actual checkpoint bytes. Your proposal is an operator-controlled mode that deliberately trades exactly that detection away for startup time, with explicit re-preload as the invalidation path. Those two pull in opposite directions, and merging them would make both worse. Design-wise, what you'd keep is the cheap half: architecture, dtype, quantization, parallel layout, version, and GPU compatibility are all config-derived comparisons, no file I/O, so an opt-in trust mode loses nothing by retaining them. What you'd skip is only the content hash. The risk boundary is that an in-place edit of the source checkpoint becomes invisible to the engine, so the mode needs to be loud: explicit flag, a startup log line stating which cache identity is being trusted and that source changes require a new preload. Operators on immutable model stores (which OSSFS deployments often are in practice) get your 40% back; everyone else keeps the default validation. One honest caveat on how the two interact: this PR's concurrent fingerprint reader should cut your 32 s substantially on a latency-bound FS, since parallelism is exactly what hides per-request latency, but your B variant shows the floor is ~0, so for operators willing to own explicit invalidation, opt-in trust wins outright. Worth opening as its own issue so the semantics debate (flag naming, what "immutable identity" means, how preload invalidation is signaled) happens there rather than on a correctness PR. Happy to chime in once it's up. Prepared with AI assistance (Kimi Code). |
63d2e71 to
bd0c1d5
Compare
|
Rebased onto current main; the conflicts were adjacent additions from the RoPE-parameters fingerprint that landed since (upstream's |
|
This pull request has merge conflicts that must be resolved before it can be |
…only Fixes vllm-project#59647. hash_checkpoint() hashed only each shard's safetensors header (names, dtypes, shapes, offsets), so two checkpoints with the same layout but different weight values, e.g. a base model and its fine-tune, produced the same WeightCacheKey. An engine started with --load-format ipc_cache against a daemon holding the other checkpoint then mapped the stale weights with no warning, while the CLI and API reported the new path. Mix the file size and the head (1 KiB) of every tensor's data region into the per-shard hash. The key stays stable for byte-identical copies in different directories (the vllm-project#54921 property), while any real fine-tune moves it. Reads stay at a few hundred KB per shard, negligible next to weight loading. Co-authored-by: Kimi Code <kimi-code@moonshot.cn> Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
On NFS each sampled pread costs one RPC RTT, so sequential per-tensor sampling of a K3-scale checkpoint (~70k tensors) takes over a minute at 1 ms RTT. Issue the sampled reads through a small thread pool (32 in flight) and mix them in sorted-name order as before. pool.map preserves order, so the digest is byte-identical to sequential sampling and keys already in the wild stay valid; wall time drops to a few seconds. Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Co-authored-by: Kimi Code <kimi-code@moonshot.cn>
bd0c1d5 to
80a015e
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
|
Merged current main ( Validation on the final head: 44 fingerprint, cache-key, daemon-reject, and RoPE tests pass. The complete |
Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
ec29900 to
c4bf23e
Compare
|
DCO follow-up: |
Overview
Fixes #59647.
hash_checkpoint()fingerprinted only safetensors headers (tensor names, dtypes, shapes, offsets), so checkpoints with identical layout but different weight values produced the sameWeightCacheKey, and anipc_cacheengine could silently map a daemon's stale weights. The fingerprint now also covers each shard's file size and a 1 KiB sample of every tensor's data region.Claims
Validation
Reproducer from the issue (two single-tensor checkpoints with identical headers, values 0.0 vs 1.0): keys collided before this change and differ after it. Added unit tests:
Independent end-to-end reproduction of the bug on the 0.30.0 release (fp16, WSL2) by @mitoyosh, with the control experiment that a layout-mismatched checkpoint still triggers the documented disk fallback: https://homelabpostmortem.com/2026/10/03/vllm-weight-cache-serves-another-checkpoints-weights/
test_hash_checkpoint_distinguishes_weight_values: same layout, different values must not share a key.test_hash_checkpoint_stable_across_directory_copies: byte-identical copies in different directories share a key (tensor sized past the 1 KiB sample so sampling is exercised).test_hash_checkpoint_without_safetensors_returns_none: unchanged fallback to the model path.Commands run (macOS, CPU-only env,
VLLM_TARGET_DEVICE=empty):End-to-end daemon + engine runs need a GPU, which I do not have locally; the GPU e2e coverage already lives in
test_ipc_cache_cold_start_and_warm_restartin the same file, and the key-comparison path is exercised bytest_weight_cache_key_distinguishes_dp_ranks.Details
Root cause:
_safetensors_header()hashed only the length-prefixed JSON header, which by construction contains no weight bytes. The header hash was introduced in #54921 to stop identifying checkpoints by path (so identical weights in different directories match), and CodeRabbit flagged the value-blind collision there at the time without a reply before merge.Design: per shard, hash file size, the raw header, and the first 1 KiB of each tensor's data region (
data_offsetsfrom the header). Per-tensor sampling was chosen over whole-file head/tail sampling because a small fine-tune can leave both file ends untouched while changing one interior tensor. Sorting entries by name keeps the mix order deterministic.Tradeoffs and limitations:
AI assistance (Kimi Code) was used to investigate the root cause, write the patch and tests, and run the validations above; the submitting human reviewed the final diff. Not duplicating existing work: the open PRs touching
weight_cache/protocol.py(#55490, #56000, #56047, #56999, #59435) do not changehash_checkpoint, and no open PR references #59647.Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.