Repository navigation
Let a ModelCache download only the files a model needs - #578
Draft
dennis-upbound wants to merge 9 commits into
Draft
dennis-upbound wants to merge 9 commits into
dennis-upbound wants to merge 9 commits into
Conversation
A ModelReplica that references a ModelCache can't tell from the cache whether the model is readable on its cluster, or how to read it. It works out the claim name itself, with a copy of the code that names the claim, and mounts it whether or not the hydration Job has finished. Give each cluster entry a mount: the volumes, volume mounts and environment that read the model there. The cache sets it only while the cluster's phase is Ready, so a replica can copy it into its engine pods and wait until it appears. Describe the existing message field, which says why a cluster isn't Ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Dennis Ramdass <dennis@upbound.io>
A ModelCache reports each cluster's phase, but not how to read the model there, nor why a cluster is stuck. When a claim waited on a node the cloud had replaced, the cache said Pending for 40 minutes, and finding the cause took someone with access to the workload cluster. Write each cluster's mount once its phase is Ready: the claim as a volume at /mnt/models, and HF_HUB_CACHE so an engine finds the model by its repo ID. These are the volume, mount and variable replicas already use, so pods built from them don't change. Write a message for every other phase. A Pending cluster names the claim it waits on, and the node the scheduler chose for it. A Hydrating cluster names the repo and claim. A Failed cluster gives the Job, how many attempts failed, and the Job's own message. A missing token names the Secret and key. Towards modelplaneai#494. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Dennis Ramdass <dennis@upbound.io>
A replica that references a ModelCache started its engines as soon as it was scheduled. On a fresh cluster the hydration Job was still writing the cache, so the first engine found no model there and downloaded all 464 GB itself, in parallel with the Job. Require the ModelCache, and read how to mount it from the entry its status publishes for the replica's cluster. Until that entry has a mount, a new replica composes nothing and its ModelAccepted condition says why, using the cache's own message: hydrating, a claim waiting on a node, a failed Job, a cluster the cache doesn't stage to, or no cache at all. A replica whose engines already run doesn't wait. Until its cache publishes a mount, it keeps the cache volume, volume mount and HF_HUB_CACHE its workload already has, read from the workload it last composed, or no cache if it has none, and warns. So a cache that stops being Ready or is deleted doesn't take a serving model down, and adding a cache or switching to another doesn't restart the engines until the new cache is Ready. MX_MODEL_REVISION and the IPC_LOCK capability follow the mount the engines use. The mount is the volume, mount and variable replicas used before, so running engines don't restart on upgrade. Fixes modelplaneai#495. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Dennis Ramdass <dennis@upbound.io>
A replica now holds its engines until its cache publishes a mount, but the docs didn't say so, or where to read why a replica is waiting. Say how a replica waits for its cache, the messages it and the cache report while it waits, how soon it notices the cache is ready, and that a running replica keeps the cache it mounts until the new one is ready. Add the design for the mount each cache publishes, the replica's wait for it, and the messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Dennis Ramdass <dennis@upbound.io>
|
Docs preview: https://modelplane-docs-pr-578.vercel.app (ready once the site's Content workflow finishes) |
dennis-upbound
force-pushed
the
dennis/modelcache-file-filters
branch
from
October 26, 2026 20:59
c3bb955 to
8ed4ce1
Compare
Unit tests show what the functions compose, but not that a replica really holds its engines until the hydration Job has staged the model, or that the pod mounts what the cache publishes. Add an end-to-end test that binds the cache's claim to a hostPath volume only after the replica exists. It checks that the replica waits while the claim is Pending, then that the engine mounts the published claim, sets HF_HUB_CACHE, and reads the staged snapshot. Each run uses a new hostPath, so a previous run's completion marker can't make the Job skip. The local pool has one fake GPU, which mock-demo holds, so the test scales mock-demo to zero while it runs. It runs only with E2E_MODEL_CACHE=1, after the rest of the suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Dennis Ramdass <dennis@upbound.io>
The hydration Job downloads every file in a HuggingFace repository. Many repositories publish the same weights more than once, such as a consolidated checkpoint beside the sharded safetensors, or one copy per format. Mistral-Small-3.2-24B-Instruct-2506 holds about 90 GiB, and vLLM can serve it from the 45 GiB in Mistral's format, so the cache needs twice the storage and twice the download time. Add include and exclude to spec.huggingFace. Each pattern goes to hf download as --include or --exclude, quoted for the shell. sizeGiB is still required. The end-to-end test filters its tiny repository to its config and tokenizer files, and checks that the cache staged none of the weights the filters leave out. The cache downloads once, so an edit to the source after that never reached the cache, and an edit during the download failed on the Job's immutable template. Make repo, revision, include and exclude immutable with CEL transition rules, so the API server rejects the edit and says to create a new ModelCache. repo and revision were editable before; this is a deliberate API change. Towards modelplaneai#457. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Dennis Ramdass <dennis@upbound.io>
The hydration Job runs hf download in a shell. The include and exclude patterns were quoted, but the repo and revision went into the command as written, so a revision with a shell character split the command or ran a second one. Quote both with shlex.quote. A repo ID or revision made of letters, digits, dots, dashes and slashes comes out unchanged, so the Job of an existing cache doesn't change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Dennis Ramdass <dennis@upbound.io>
The hydration Job is composed without the Delete management policy, so its completed pod is cleaned up by the Job's own TTL once the cache is Ready. That also kept the Job when the ModelCache was deleted before the download finished. The Job and the claim are named from the cache's namespace and name, so a cache created again under the same name got a claim of the same name, and the old Job downloaded into it. A cache whose include and exclude left out the weights went Ready holding every weight file of the repository, and a cache recreated with another revision got the old revision. Compose a hydrating Job with every management policy, so deleting the cache deletes the Job. Once the cluster is Ready, compose the Job without Delete until its Object reflects that, then drop it. Dropping it then orphans the Job to its TTL, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Dennis Ramdass <dennis@upbound.io>
A user running a cached model wanted its startup not to depend on HuggingFace being reachable, and found no documented way to do it. The docs also told users to size a cache to the model, though the cache downloads every file in the repository. Show include and exclude with a repository that publishes its weights in two formats, with the vLLM flags that load that format, and say to size the cache to the files it downloads. Say the source can't change once the cache exists, because a change would mean downloading again and restarting every engine that reads it. To change it, create a cache under a new name and point the deployment at it. A cache recreated under its old name gets its old claim's name, and can't stage while engines still mount that claim. Say how to start an engine without contacting HuggingFace: set HF_HUB_OFFLINE=1 in the engine's env, and make sure the cache contains every file the engine reads. Modelplane doesn't set it, because it breaks an engine that fetches a second repository. Add the file filters and the immutable source to the mount design. Fixes modelplaneai#530. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Dennis Ramdass <dennis@upbound.io>
dennis-upbound
force-pushed
the
dennis/modelcache-file-filters
branch
from
October 27, 2026 03:59
8ed4ce1 to
65f1777
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description of your changes
Fixes #530.
A
ModelCachedownloads every file in a HuggingFace repository. Many repositories publish the same weights more than once, as a consolidated checkpoint beside the sharded safetensors or one copy per format.mistralai/Mistral-Small-3.2-24B-Instruct-2506holds about 90 GiB, and vLLM can serve it from the 45 GiB in Mistral's format, which leaves the cache with twice the storage and download time it needs (#457). The docs also don't say how to start a cached model's engine when HuggingFace isn't reachable (#530).This builds on #574 and should merge after it. It's based on
main, so GitHub shows #574's commits here too until #574 merges.spec.huggingFacegets optionalincludeandexcludelists of glob patterns. The hydration Job passes each tohf downloadas--includeor--exclude, quoted for the shell. The Job now quotesrepoandrevisiontoo. A repo ID or revision made of letters, digits, dots, dashes and slashes comes out unchanged, so an existing cache's Job doesn't change.Deleting a cache before its download finished left the hydration Job on the cluster, because the Job is composed without the
Deletemanagement policy so that its own TTL can clean up its completed pod. A cache created again under the same name gets a claim of the same name, and the old Job downloaded into it. In a live run, a cache whose filters left out the weights went Ready with every weight file of the repository staged. This happens onmaintoo, without filters: a cache recreated with anotherrevisiongets the old revision. A hydrating Job now has every management policy, so deleting the cache deletes it. Once the cluster isReadythe Job is composed withoutDeleteuntil its Object reflects that, then dropped. The TTL cleanup works as before.This is a breaking change:
repo,revision,includeandexcludecan't change once the cache exists. The cache downloads once. An edit afterwards did nothing, and an edit during the download failed on the Job's immutable template. CEL transition rules onspec.huggingFacenow reject the edit with "Create a new ModelCache to change spec.huggingFace.." I checked the rules withkubectl replace --dry-run=serveragainst a 1.34 kube-apiserver. Changing, adding or removing any of the four is rejected. The same spec, asizeGiBorauthSecretchange, a metadata update and a status update are accepted.The docs show the filters and say to size the cache to the files it downloads. To start an engine without contacting HuggingFace, they say to set
HF_HUB_OFFLINE=1in the engine'senvand to make sure the cache contains every file the engine reads. Modelplane still doesn't set the variable itself, because it breaks engines that fetch a second repository. I checked the documented setup with the hydration Job's exact command. With no network, a staged model loads by repo ID withHF_HUB_OFFLINE=1. Without it, the load retries huggingface.co for 93 seconds first.Decisions. The source is immutable because it can't change in place without downloading again and restarting every engine that reads the cache. Creating a new cache under a new name is explicit, and makes the swap a deliberate rollout. The engines keep the old cache until the new one is ready, then restart onto it once. The docs say not to recreate a cache under its old name while engines mount it, because the new cache gets the old claim's name and can't stage until those engines stop.
sizeGiBstays required, so this is towards #457, not a fix.Towards #457.
compose-model-cachehas new unit tests for the filters, the quoting, and the two steps that drop a finished Job. The end-to-end test from #574 now filters its repository and checks that the cache staged none of the weights the filters leave out. It passed on kind, and so did a manual check that the API server rejects an edit to the source. The Job deletion change has unit tests but hasn't run live yet.An
includethat matches no file still marks the cacheReadywith an empty snapshot. That's a follow-up.I have:
nix flake check(or./nix.sh flake check) and made sure it passes.git commit -s.🤖 Generated with Claude Code