Skip to content

Add file-level model weight cache control plane - #2829

Open
xianzhiT wants to merge 2 commits into
kvcache-ai:mainfrom
xianzhiT:feature/file-level-weight-cache-v2
Open

Add file-level model weight cache control plane#2829
xianzhiT wants to merge 2 commits into
kvcache-ai:mainfrom
xianzhiT:feature/file-level-weight-cache-v2

Conversation

@xianzhiT

@xianzhiT xianzhiT commented Jul 10, 2026

Copy link
Copy Markdown

Description

Add a file-level weight caching control plane on top of MooncakeStore, so model checkpoints can be imported into the store and served to inference engines (e.g. SGLang) as plain files — without a tensor-level, GPU-coupled save path.

The idea is to treat MooncakeStore as a generic model cache that is decoupled from the inference engine: weights are stored file-level (HuggingFace safetensors chunked with a per-model manifest), and engines load files exactly as they would from disk, only the source changes (disk → MooncakeStore). This control plane provides the management side — import / list / inspect / verify / delete — independently of any serving process.

The matching SGLang connector (data plane) is in sgl-project/sglang#30728. Design discussion / RFC: #2282 (Unified KVCache and Model Weight Management); this PR implements Phase 1 (store-backed weight loading) plus the import/manage control plane.

Module

  • Python Wheel (mooncake-wheel)
  • Docs

Type of Change

  • New feature

How Has This Been Tested?

Test commands:

cd mooncake-wheel
python -m pytest tests/test_weight_model_cache.py tests/test_weight_model_cli.py -v

End-to-end (cross-machine, one storage node + one 8×H20 node):

# import a checkpoint into MooncakeStore
python -m mooncake.weight_store.cli \
    --master-server-addr <master_ip:50051> \
    --metadata-server <metadata_ip:8080> \
    --protocol rdma --rdma-devices <mlx5_x> \
    --local-hostname <host_ip:port> \
    model import \
    --checkpoint-id Qwen3-235B-A22B \
    --model-id Qwen/Qwen3-235B-A22B \
    --revision main \
    --source /path/to/local/weights

# confirm it landed
python -m mooncake.weight_store.cli ... model list
python -m mooncake.weight_store.cli ... model inspect Qwen3-235B-A22B
python -m mooncake.weight_store.cli ... model verify  Qwen3-235B-A22B

Then loaded Qwen3-235B-A22B from the store via the SGLang connector in ~30s
with correct inference output.

Test results:

  • Unit tests pass
  • Integration tests pass (if applicable)
  • Manual testing done (cross-machine import + SGLang load + inference, above)

Checklist

  • I have performed a self-review of my own code
  • I have formatted my code using ./scripts/code_format.sh
  • I have run pre-commit run --all-files and all hooks pass
  • I have updated the documentation (if applicable)
  • I have added tests to prove my changes are effective
  • For changes >500 LOC: I have filed an RFC issue ([RFC] Unified KVCache and Model Weight Management in Mooncake Store #2282)

AI Assistance Disclosure

  • AI tools were used (specify below)

AI assistance (Claude Code) was used for parts of the implementation, tests, and documentation. All changes have been reviewed and are understood by the human submitter.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces an experimental file-level model weight cache control plane for Mooncake Store, adding a command-line interface, a model cache client, keyspace helpers, and comprehensive unit tests. The implementation is solid, but several improvements should be made to enhance robustness, correctness, and performance. Specifically, the store setup result check should safely handle None returns, and the size parser should support TB and PB units. Additionally, file materialization should write to a temporary file and atomically rename it to prevent corruption, index updates should be made atomic to avoid race conditions during concurrent imports, redundant store existence checks should be removed to optimize round-trips, and chunk cleanup during import failures should be wrapped in try-except blocks to ensure all orphaned chunks are removed.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

except TypeError:
result = store.setup(config)

if result != 0:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The setup method of MooncakeDistributedStore might return None on success in some environments or wrappers. Checking result != 0 could lead to a false-positive RuntimeError if None is returned. It is safer and more consistent with the rest of the codebase to check result not in (0, None).

Suggested change
if result != 0:
if result not in (0, None):

Comment on lines +205 to +209
units = {
"KB": 1024,
"MB": 1024**2,
"GB": 1024**3,
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adding support for Terabytes (TB) and Petabytes (PB) to the size parser makes it more robust and future-proof, especially when dealing with extremely large models or storage segments.

    units = {
        "KB": 1024,
        "MB": 1024**2,
        "GB": 1024**3,
        "TB": 1024**4,
        "PB": 1024**5,
    }

Comment on lines +25 to +31
WEIGHT_SUFFIXES = {
".bin",
".gguf",
".pt",
".pth",
".safetensors",
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider adding .ckpt to the list of recognized weight suffixes. It is a very common format used by many models (e.g., Stable Diffusion checkpoints).

Suggested change
WEIGHT_SUFFIXES = {
".bin",
".gguf",
".pt",
".pth",
".safetensors",
}
WEIGHT_SUFFIXES = {
".bin",
".ckpt",
".gguf",
".pt",
".pth",
".safetensors",
}

Comment on lines +264 to +276
with output.open("wb") as output_file:
for chunk_key in self._record_chunks(record):
chunk = self._get(chunk_key)
output_file.write(chunk)
digest.update(chunk)
size += len(chunk)
if size != record.size:
raise ValueError(
f"file size mismatch for {record.path}: "
f"expected {record.size}, got {size}"
)
if digest.hexdigest() != record.sha256:
raise ValueError(f"sha256 mismatch for {record.path}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Writing directly to the target output_path can leave a partially written or corrupted file if the operation is interrupted or fails halfway through. A more robust pattern is to write to a temporary file in the same directory and atomically rename it to the target path once the size and checksum verification succeeds.

Suggested change
with output.open("wb") as output_file:
for chunk_key in self._record_chunks(record):
chunk = self._get(chunk_key)
output_file.write(chunk)
digest.update(chunk)
size += len(chunk)
if size != record.size:
raise ValueError(
f"file size mismatch for {record.path}: "
f"expected {record.size}, got {size}"
)
if digest.hexdigest() != record.sha256:
raise ValueError(f"sha256 mismatch for {record.path}")
temp_output = output.with_suffix(f".{time.time_ns()}.tmp")
try:
with temp_output.open("wb") as output_file:
for chunk_key in self._record_chunks(record):
chunk = self._get(chunk_key)
output_file.write(chunk)
digest.update(chunk)
size += len(chunk)
if size != record.size:
raise ValueError(
f"file size mismatch for {record.path}: "
f"expected {record.size}, got {size}"
)
if digest.hexdigest() != record.sha256:
raise ValueError(f"sha256 mismatch for {record.path}")
temp_output.rename(output)
except BaseException:
if temp_output.exists():
temp_output.unlink()
raise

Comment on lines +299 to +306
def _add_to_indexes(self, manifest: ModelFileManifest) -> None:
all_models = self._read_index(model_index_key())
all_models.add(manifest.checkpoint_id)
self._write_index(model_index_key(), all_models)

per_model = self._read_index(model_id_index_key(manifest.model_id))
per_model.add(manifest.checkpoint_id)
self._write_index(model_id_index_key(manifest.model_id), per_model)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The read-modify-write cycle for updating index files (weight/index/models and weight/index/model/{sha256}) is not atomic. If multiple clients import checkpoints concurrently, they can overwrite each other's updates, leading to lost index entries. Consider using a proper metadata database/catalog backend or prefix-based key listing if supported by the store to avoid a single global index object.

Comment on lines +317 to +329
def _read_index(self, key: str) -> set[str]:
if not self._exists(key):
return set()
value = self.store.get(key)
if value is None:
return set()
if isinstance(value, str):
payload = value.encode("utf-8")
else:
payload = bytes(value)
if not payload:
return set()
return set(json.loads(payload.decode("utf-8")))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling _exists before get causes an unnecessary extra round-trip to the store. Since self.store.get(key) already returns None if the key does not exist, we can directly call get and handle the None case, which is much more efficient.

    def _read_index(self, key: str) -> set[str]:
        value = self.store.get(key)
        if value is None:
            return set()
        if isinstance(value, str):
            payload = value.encode("utf-8")
        else:
            payload = bytes(value)
        if not payload:
            return set()
        return set(json.loads(payload.decode("utf-8")))

Comment on lines +391 to +394
except BaseException:
for chunk_key in chunks:
self._remove(chunk_key, force=True)
raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If self._remove raises an exception during cleanup (e.g., due to a temporary network issue or store error), the loop will be interrupted, leaving the remaining chunks orphaned in the store. Wrapping the individual chunk deletion in a try...except block ensures that we attempt to clean up as many chunks as possible.

            except BaseException:
                for chunk_key in chunks:
                    try:
                        self._remove(chunk_key, force=True)
                    except Exception:
                        pass
                raise

@github-actions github-actions Bot added documentation Improvements or additions to documentation run-ci Installation Tests labels Jul 10, 2026
@ykwd
ykwd requested a review from Aionw July 10, 2026 03:32
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment thread mooncake-wheel/mooncake/weight_store/model_keyspace.py
@ykwd

ykwd commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

The CI failed for the non-consistent docs, could you take a look? Thanks

@xianzhiT

Copy link
Copy Markdown
Author

The CI failed for the non-consistent docs, could you take a look? Thanks
Thanks for flagging this, i will fix it.

@stmatengss

Copy link
Copy Markdown
Collaborator

@qoder Review it

@qoderai

qoderai Bot commented Jul 27, 2026

Copy link
Copy Markdown

I've finished a pass over PR #2829. Here’s a concise summary of my review:

Strengths

  • Clear separation between CLI, keyspace layout, and ModelFileCacheClient logic.
  • Good validation around checkpoint_ids and safetensors index files.
  • Helpful progress logging for long-running imports.
  • Unit tests cover a range of manifest and failure-mode behaviors.

Issues / Risks

  1. mooncake-wheel/mooncake/weight_store/model.py:115 – Catalog index updates and manifest existence checks are non-atomic. Concurrent imports can race, leading to duplicate manifests for a checkpoint_id and lost entries in weight/index/models or weight/index/model/{sha256}.
  2. mooncake-wheel/mooncake/weight_store/model.py:256materialize_file writes directly to the final output path and only verifies size/SHA-256 afterwards. Interruptions or integrity failures can leave partially written or corrupted files at the target path.
  3. mooncake-wheel/mooncake/weight_store/model.py:362_put_file_chunks cleanup on failure will stop on the first _remove error, leaving any remaining chunk objects orphaned in the store.
  4. mooncake-wheel/mooncake/weight_store/model.py:319_read_index performs an extra _exists check before store.get, adding unnecessary round-trips and latency for list/index operations.
  5. mooncake-wheel/mooncake/weight_store/cli.py:181_connect_store treats any setup result not equal to 0 as failure. If setup returns None on success (which other code paths accept), the CLI will incorrectly abort.
  6. docs/source/getting_started/examples/sglang-integration/weight-file-cache-control-plane.md:194 – The MiniMax smoke script kills processes by broad pattern match (e.g., mooncake_master, /tmp/storage_node.py). If run outside a tightly scoped test container, it can terminate unrelated processes.

Recommended tests / follow-ups

  • Add concurrency tests that run multiple imports/deletes in parallel to surface index races and checkpoint_id conflicts.
  • Add a test that simulates a failing _remove during _put_file_chunks to define partial-cleanup behavior.
  • Add an integration-style test for materialize_file that covers interruption or integrity-mismatch scenarios and clarifies expected behavior for the output file.
  • Add a CLI test where MooncakeDistributedStore.setup returns None to confirm it’s treated as success.
  • If you keep the MiniMax smoke script, consider a stronger doc warning or guard so it’s clearly constrained to isolated test environments.

🤖 Generated by QoderView workflow run

@xianzhiT
xianzhiT force-pushed the feature/file-level-weight-cache-v2 branch from 184294f to 10fee62 Compare July 28, 2026 08:19
@xianzhiT
xianzhiT force-pushed the feature/file-level-weight-cache-v2 branch from 10fee62 to 1721c66 Compare July 28, 2026 12:48

@Aionw Aionw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of the latest head. These four findings are new and intentionally exclude issues already covered by existing review threads.

Comment thread mooncake-wheel/mooncake/weight_store/model.py
Comment thread mooncake-wheel/mooncake/weight_store/model.py
try:
self._remove(chunk_key, force=True)
except BaseException:
pass

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve keys whose cleanup fails. Continuing through all removals is useful, but swallowing a removal failure loses the only cleanup record for that partial file: the current file is never appended to records, so the FAILED manifest and a later delete_model() cannot find the surviving chunk. I reproduced an orphan that remains even after deleting the failed checkpoint. Please retain every partial chunk key in a cleanup journal/failed record until removal succeeds.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Partial chunk keys are now preserved in a FAILED record until cleanup succeeds, so delete_model() can reliably reclaim any survivors after a failed import.

Comment thread mooncake-wheel/mooncake/weight_store/model.py
Resolve all 5 review comments from Aionw on PR kvcache-ai#2829:

- F1 (model.py): verify chunks BEFORE publishing the manifest, so a
  READY status is never visible for unverified data.
- F2 (model.py): persist per-file chunk_size in the manifest; readers
  refuse to guess a multi-chunk layout when the field is absent (0=legacy).
- F3 (model.py): on a failed import, record already-written chunk keys in
  a journal so a later delete can reclaim them; nothing is silently leaked.
- F4 (model.py): remove the manifest LAST during delete (de-index first,
  then chunks, then manifest), making delete crash-safe and retryable.
- F5 (model_keyspace.py): add isinstance + length bound (<=255) to id
  validation in addition to the existing charset regex.

Also add regression tests covering crash-recovery, write-once stores,
and chunk_size round-trip (14 new cases in test_weight_model_cache.py).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation Installation run-ci Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants