Skip to content

feat(nvsnap): download each model once per cluster into a shared volume for Helm functions - #2106

Open
balajinvda wants to merge 11 commits into
nvsnap/helm-electionfrom
nvsnap/helm-artifacts
Open

balajinvda wants to merge 11 commits into
nvsnap/helm-electionfrom
nvsnap/helm-artifacts

Conversation

@balajinvda

Copy link
Copy Markdown
Contributor

Why

Customers deploy Helm charts of N identical GPU workers, and every worker
downloads and compiles on its own. The gate-and-promote election in #2104
cannot serve multi-node instances (LWS, StatefulSet groups, Dynamo podgangs):
holding any member deadlocks the group. The invariant we want is simpler
and storage-independent: the model is downloaded once per cluster and every
pod attaches it; compile caches are shared the same way. Design and the
decisions taken with the product owner: docs/proposals/helm-shared-model-volume.md.

What changed

  • internal/modelid: identity and landing volume for any chart. Identity as
    a URI (hf://, ngc://, s3://, nim://) from engine args (including
    positional vllm serve <x> and $VAR expansion), engine env, download
    init containers (NGC CLI, hf download, S3, KServe storage-initializer),
    NIM images, and LeaderWorkerSet workers via the leader template. Landing
    volume kind decides substitutability; a customer's PVC, hostPath or image
    is left alone. Tests from the prd11 NVCF function, the Dynamo sample,
    KServe, NIM and the upstream LWS example.
  • internal/modelvolume + webhook: one download Job per identity per
    cluster (nvsnap-model-dl-<key>, idempotent create, so no election), built
    from the chart's own download init wrapped to touch a marker, or from
    hf download on the engine image with credentials forwarded. Every
    workload pod is a reader: its download init becomes a wait for the marker
    (deadline, then self-download), the engine is started offline when it used
    to download itself. RWX mode mounts the shared claim; Block mode (NVMesh)
    gives readers a hostPath landing with HostToContainer propagation.
  • Agent ModelVolumeController: on Job success labels the retained PV
    complete and releases the claim (NVMesh refuses read-only attaches while
    a read-write attachment exists, including a Succeeded pod's); for pending
    readers on its node, once the volume is detached, mints the read-only PV
    and claim in the reader's namespace, attaches through a mount-holder,
    bind-mounts under a dedicated Bidirectional hostPath and un-pends. Binds
    are verified against the mount table and the volume handle, and redone
    when missing or stale.
  • Storage profile modelVolume: {mode, storageClass, size}; block by
    default on shared-volume strategies, rwx must be declared. Helm
    agent.modelVolume.{enabled,waitDeadline}, agent.hostPaths.nvsnapModels;
    agent RBAC gains PVC update/patch and pods patch.

Customer Release Notes

Helm chart functions download each model once per cluster; every other
worker, later version, scale-up and redeploy attaches the downloaded volume.
No chart change. Requires NVMesh or a distributed filesystem.

Plan Summary

Helm: new values (off by default), one extra Bidirectional hostPath on the
agent DaemonSet, ClusterRole verbs. No new controllers or CRDs.

Usage

helm upgrade nvsnap deploy/helm/nvsnap --set agent.modelVolume.enabled=true
kubectl get jobs,pvc -A -l nvsnap.io/model
kubectl get pods -A -l nvsnap.io/model-pending=true

Testing

dev1, 2026-09-26, NVMesh, stock vllm-workers chart (Qwen2.5-32B TP=4,
two replicas on two nodes, no nvsnap markers):

first deploy    one Job; 65 GB downloaded once (354 s); both readers un-pended +400 s;
                both Ready +591 s; 0 downloads in the pods; serve
reinstall       both un-pended +12 s, both Ready +136 s
already present both Ready +205 s
cold            325 s

Unit: identity forms and classifier; Job spec derivation and idempotence;
webhook matrix (Block and RWX readers, injected download, complete state,
customer volumes left alone); controller completion, release, detach gate,
per-namespace read-only minting, rebind on missing or stale mounts, no
un-pend without a mount. All mutation-checked as described in the commits.

Notes

Stacked on #2104 (base nvsnap/helm-election). Open items in the design
doc: compile caches on block storage (cache-only capture), cacheMode and
the Lustre flock check, retention including unbinding when the last reader
leaves a node and holder cleanup, and the RWX path needs a cluster with a
distributed filesystem for e2e (dev1 has none).

References

Relates to #2099

Related Pull Requests

#2104, #2101

Dependencies

None

First step of docs/proposals/helm-shared-model-volume.md. For any GPU pod at
admission, internal/modelid answers which artifact the pod will download,
as a URI shared by every chart, version and namespace that names it, and
where the bytes land and what backs that path today.

Identity sources, as the field uses them: engine args (--model,
--model-path, --model=, positional `vllm serve <x>`, --revision), engine
env (HF_MODEL_ID, MODEL_ID, MODEL_PATH, with $VAR expansion from the
container's own env), download init containers (NGC CLI with
NGC_MODEL_NAME, huggingface-cli download with --local-dir, aws s3 sync,
KServe storage-initializer args), NIM images with NIM_MODEL_PROFILE, and
for LeaderWorkerSet workers that name no model the leader template of
their group. URIs: hf://org/repo[@rev], ngc://org/team/model:ver,
s3://bucket/key, nim://image@profile, path:///abs for pre-filled paths.

The landing volume is the mount at or above the download destination in
the main container: emptyDir or rootfs are substitutable, a customer's
PVC, hostPath or other volume means sharing is already solved and the pod
is left alone. KServe pvc:// is likewise not a downloader.

Tests are built from real specs: the NVCF Helm function observed on prd11
(NGC init download, positional MODEL_PATH), the Dynamo operator sample,
SGLang, NIM, KServe, huggingface-cli and S3 inits, the upstream LWS vLLM
example, and non-downloaders (Dynamo frontend, Ray worker, etcd).
Mutation-checked: PVC marked substitutable, revision split dropped and
env expansion dropped each turn tests red.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
Step 2 of docs/proposals/helm-shared-model-volume.md. For a GPU pod whose
model identity resolves (internal/modelid), the webhook replaces the
volume the download lands in with the per-identity model volume and
turns the download into a write-once step; no pod is gated.

internal/modelvolume names the claim per identity (nvsnap-model-<key>),
creates the writer claim (ReadWriteMany on a distributed filesystem,
ReadWriteOnce on NVMesh), looks the identity up cluster-wide and records
completion as a label. The webhook elects the writer with the existing
Lease elector keyed by identity; the writer's landing emptyDir becomes
the claim, its engine mounts the model read-only, and its download init
is wrapped to touch <volume>/.nvsnap-complete on success. Readers on a
distributed filesystem mount the same claim and their init waits for
the marker, downloading themselves after the deadline (decided: always
fall back). Readers on NVMesh keep their emptyDir, are labelled pending
and wait for the agent to bind the completed volume in (step 3). Charts
whose engine downloads itself get an injected huggingface-cli init that
lands the model in the volume, with the engine started offline and the
registry credentials forwarded. Compile caches are redirected into
<volume>/.nvsnap/cache/<engine key> on a distributed filesystem and into
the local cachedir on NVMesh; the model env of the cachedir template is
left alone because the model lives in the landing volume now.

Tests use the prd11 function shape and a stock vLLM Deployment: writer
on Block, pending reader on Block, reader on RWX, injected init with
forwarded token and offline engine, completed volume skipping the
election, and the customer-PVC / no-GPU / no-model pods left alone.
Mutation-checked: read-only mount dropped, reader fallback dropped,
Block reader substituting the volume, and writer marker dropped each
turn tests red.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
…Mesh

Step 3 of docs/proposals/helm-shared-model-volume.md. The agent gains a
model volume controller. For writers, on any node: when the download init
the webhook named on the pod exits 0, the writer claim is labelled
complete and, on block storage, SharedVolumePromoter.MintReadOnly exposes
it as a read-only claim (primary PV retained, secondary static PV with the
namespace-rewritten NVMesh handle, pre-bound claim). The writer claim
stays because the writer still runs on it and later writers must find it
complete. For pending readers on this node: once the identity is complete
the read-only claim is minted in the reader's namespace, attached to the
node through a mount-holder pod, bind-mounted read-only onto the reader's
hostPath landing under the Bidirectional overlays root
(<overlay-root>/models/<key>), and the pod is un-pended. The writer touched
the marker inside the volume, so the reader's wait init sees it as soon as
the bind lands. Every agent watches writers (idempotent marking and
minting); only the reader's own agent binds.

The webhook gives Block-mode readers a hostPath landing with
HostToContainer propagation on the engine and init mounts, and names the
writer's download init for the agent. Storage profiles gain
`modelVolume: {mode, storageClass, size}`; block is the default for
shared-volume strategies, rwx must be declared. Agent flags
--model-volume and --model-volume-wait-deadline, Helm
agent.modelVolume.{enabled,waitDeadline}, and pods patch in the agent
ClusterRole.

Tests with fake clients: writer completion marks and mints the read-only
PV and claim with the handle rewritten and the primary retained; a
pending reader on its node gets the claim in its own namespace, one
attach, one bind at the hostPath, and is un-pended, a second reader of
the same identity reuses the bind, readers on other nodes are ignored.
Mutation-checked: node filter dropped, completion on any exit code,
binding before completion, and un-pend dropped each turn tests red.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
…b in huggingface_hub 1.x

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
The first cluster run of the model volume on NVMesh exposed a constraint
the design had missed: a volume attached read-write by a running pod
cannot be attached read-only anywhere else (AttachVolume failed on the
read-only PV while the writer pod held the primary). The L2 promote never
met it because it deletes the writer claim before readers attach. So the
download step cannot live inside a pod that goes on to serve.

The download is now a Job per identity per cluster, nvsnap-model-dl-<key>,
created idempotently by the webhook on first sight: the chart's own
download init (image, command, env, pull secrets, tolerations) wrapped to
touch the marker, or `hf download` on the engine image when the engine
fetches the model itself. Its exit releases the volume. Every workload pod
is a reader; there is no writer pod and no Lease election on this path,
because Job create is atomic. The agent completes the identity when the
Job succeeds and mints the read-only claim on block storage; readers are
bound as before. The design doc records the constraint and the reason.

Tests: Job spec derived from the NGC init and from a stock vLLM pod (hf
download, forwarded token, claim mounted at the init's path), idempotent
create, no Job once the volume is complete, controller completes on Job
success only. Mutation-checked: Job never created, Job created when
complete, and completion on a running Job each turn tests red.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
…detaches

A Succeeded pod keeps its volumes attached. On NVMesh that let the
read-only attach succeed only on the Job's own node (dev1 2026-09-26);
every other node failed until the pod was deleted. The Job now carries
ttlSecondsAfterFinished=30; completion state lives on the claim label.
MarkComplete tolerates the update race between agents by re-reading.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
…a dedicated host root

Two findings from the second dev1 run of the model volume on NVMesh.

The retained volume must have no read-write attachment before any node can
attach it read-only: with the Job's claim still bound, the read-only attach
succeeded only on the Job's own node and failed everywhere else. On block
storage MarkComplete now labels the retained PV with identity and
completion, sets Retain, and deletes the download claim, the same release
the L2 promote performs; Lookup reads completion from the PV; readers'
read-only claims are minted from that PV, and only after no
VolumeAttachment references it (Detached). RWX mode keeps the shared claim
and labels it as before.

The bind root moves out of the overlays root: the overlay sweeper removes
entries it does not own and deleted the model binds under it. Completed
model volumes are bound under a dedicated Bidirectional hostPath
(agent.hostPaths.nvsnapModels, default /var/lib/containerd/nvsnap-models),
mounted at the same path in the agent and on the host.

Tests: block completion releases the claim and labels the PV, RWX labels
the claim, the reader is not served while a VolumeAttachment exists and is
served once it is gone (mutation-checked: dropping the detach gate turns
the test red), the read-only PV carries the reader namespace's handle.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
…on dev1

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
…d when stale

The controller kept "bound" in memory. After the volume for an identity
was replaced (and after a manual unmount during cleanup on dev1) it
un-pended readers without a bind and they waited on an empty hostPath.
The mount table is now the truth: a reader is served only when the
device mounted at the bind target belongs to the identity's primary PV
(NVMesh device csi-id against the volume handle); a missing mount is
bound again, a mount of a replaced volume is unbound and redone, and a
bind that leaves nothing mounted does not un-pend the pod.

Tests: rebinding after an unmount, replacing a stale bind, device/handle
matching. Mutation-checked: dropping the stale unbind and the missing
mount check each turn tests red.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
@balajinvda
balajinvda requested a review from a team as a code owner September 26, 2026 17:02
@balajinvda
balajinvda requested a review from vrv3814 September 26, 2026 17:02
@coderabbitai

coderabbitai Bot commented Sep 26, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • main

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: NVIDIA/nvcf/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 976005a2-be79-4250-8743-224a222f416a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant