Skip to content

feat(nvsnap): elect one downloader per cache hash at admission - #2104

Open
balajinvda wants to merge 12 commits into
nvsnap/helm-chart-supportfrom
nvsnap/helm-election
Open

balajinvda wants to merge 12 commits into
nvsnap/helm-chart-supportfrom
nvsnap/helm-election

Conversation

@balajinvda

@balajinvda balajinvda commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Why

Customers predominantly deploy Helm charts, and a chart creates N identical
model workers. Today each one downloads and initialises on its own: the pod
template cannot name a capture hash, restore-from: auto never made a pod a
capture source, and the watcher's hash never matched admission's. Measured
on dev1 with a TinyLlama TP=2 Deployment, both replicas were admitted "no
capture for hash; admitting pod unchanged" and the scale-up pod cold-started.

The goal, in two sentences: if the hash exists, the pods that would download
the model get the cache volume instead. If it does not, exactly one of them
downloads and the rest get the volume once it exists.

What changed

The webhook decides per pod, with no template change.

  • Classifier: a pod is a downloader when it requests a GPU and its main
    container names a model (--model/--model-path in one arg or as
    Dynamo's separate list items, --model=, HF_MODEL_ID/MODEL_ID/
    NIM_MODEL_NAME, or a NIM image). Frontends, routers, etcd, nats are
    left alone. Covers stock vLLM/SGLang and vLLM/SGLang under Dynamo.
  • Hash once: composed from the incoming spec, stamped on the pod
    (annotation full, label short). The watcher and orchestrator capture
    under the stamped hash instead of recomposing from the live pod.
  • Promoted capture exists: cachedir restore decoration.
  • Otherwise elect: Lease nvsnap-capture-<hash> created with an id the
    webhook mints (pods have no UID at CREATE admission). The one admission
    that succeeds is the leader and gets the capture decoration plus the
    capture label. Every other pod is a follower: restore decoration against
    the deterministic rox-<hash> claim, a schedulingGate (no node, no GPU
    held), a gated label.
  • nvsnap-server releases the gates when the agent posts pvc-state=ready
    and deletes the Lease. On failed, a leader pod that is gone or
    terminated, or a Lease past its deadline, the reconciler evicts gated
    followers that have a controller so they are recreated and re-elected.
  • Followers need ReadOnlyMany storage; on per-pod-clone storage they start
    cold. Any error fails open.
  • Off by default: agent.election.enabled, agent.election.deadline
    (60m). Server RBAC gains leases get/list/watch.

Design: docs/proposals/helm-chart-cache-election.md.

Claims across namespaces

NVCF runs every chart in its own namespace and a PVC is namespaced. The
promoter gains EnsureClaim(hash, ns): shared-volume mints one more
secondary static PV per consumer namespace with the CSI handle rewritten for
it, pre-bound to a rox-<hash> claim there (NVCA's pattern, zero copy);
snapshot-clone with ReadOnlyMany re-exposes the promote's snapshot handle in
the target namespace through a pre-provisioned VolumeSnapshotContent +
VolumeSnapshot and clones it; per-pod clone reports unsupported and the pod
stays cold. Mount mints on a miss; the promote mints for every namespace
that already holds stamped pods before publishing ready. Delete reaps by
label. Agent RBAC gains volumesnapshotcontents.

Customer Release Notes

Helm chart functions with several identical GPU model workers download and
initialise the model once; the other workers start from that worker's
cache, and a redeploy or scale-up of the same chart starts every worker
from the cache. Requires a ReadOnlyMany storage class for the cache.

Plan Summary

Helm: new agent.election values (off by default), --election flags on
the agent DaemonSet, server ClusterRole leases verbs. No new objects.

Usage

helm upgrade nvsnap deploy/helm/nvsnap --set agent.election.enabled=true
kubectl get lease -n nvsnap-system -l nvsnap.io/kind=capture-election
kubectl get pods -l nvsnap.io/gated=true -A

Testing

dev1, 2026-09-25, stock TinyLlama TP=2 Deployment with no nvsnap markers,
agent v0.2.75-election2, server v0.0.32-election2, NVMesh:

install replicas=2      leader + gated follower decided in the same second
leader Ready            70s
followers released      +124s (server: "capture promoted; followers released" released=1)
follower Ready          53s after release, other node, 0 downloads, serves " Paris."
scale to 3              third pod role=restore, no election, Ready 53s
uninstall + reinstall   both role=restore, Ready 50s
cold, same node         94s

Cross-namespace, capture only in nvsnap-system, same stock chart in
nvsnap-xns with two replicas admitted in the same second:

webhook   "L2 claim minted in restore namespace" then both "restoring"
claim     nvsnap-xns/rox-<hash> Bound to a per-namespace PV, handle rewritten for nvsnap-xns
pods      both role=restore, Ready 60s, 0 downloads, serve " Paris."

Dynamo sample (frontend + prefill + decode, Qwen3-0.6B): both workers hash
the same, prefill leader, decode follower, frontend ignored. The gang did
not schedule under kai-scheduler (see Notes).

Unit: model-identity forms and the classifier; Lease election (one leader
of four, uid-less pods, error surfacing); webhook matrix (frontend ignored,
leader, gated follower with wait/seed/prewarm inits, cold fallbacks,
restore without election, unbound capture left alone, explicit
restore-from bypass, metadata maps bootstrapped once); server release,
eviction of owned pods only, leader liveness; orchestrator and watcher
honouring the stamped hash. Mutation-checked: dropping the gate, the
leader label, the single bootstrap, the restore-before-elect branch and
the stamped-hash override each turned tests red after compiling.

Notes

Stacked on #2101 (base is nvsnap/helm-chart-support); merge that first.

Disaggregated workers: Dynamo prefill and decode workers differ by a role
flag and the etcd/NATS/DYN_* wiring. The hash leaves those out
(stripRoleFlags), so both roles share one leader and one rox; compile
caches recompile into the per-pod writable shadow where they differ. Flags
that change the download (--model, --revision, --tokenizer) stay in.

Gang scheduling: Grove/KAI place a DynamoGraphDeployment as one podgang, so
a follower that is unschedulable until rox-<hash> exists holds the leader
too and nothing downloads. Grove also rewrites schedulingGates. Options
and a recommendation are on #2099; this PR keeps stock charts working and
does not change behaviour for gang-scheduled workers beyond the shared hash.

Restore-namespace NetworkPolicy: the chart's policy allows egress only to
nvsnap-server and selects every pod, so a namespace without other egress
allows loses DNS. NVCF function namespaces carry NVCA's allows; noted in the
design doc for a follow-up.

References

Relates to #2099

Related Pull Requests

#2101

Dependencies

None

A Helm chart creates N identical model workers, and today each one
downloads and initialises on its own: the pod template cannot name a
capture hash, `restore-from: auto` never made a pod a capture source,
and the watcher's hash never matched admission's (resolved image digest
and injected env on one side, spec tag and original env on the other).
Measured on dev1 with a TinyLlama TP=2 Deployment: both replicas were
admitted "no capture for hash; admitting pod unchanged", nothing was
captured, the scale-up pod cold-started.

The webhook now decides per pod, with no template change. A pod is a
downloader when it requests a GPU and its main container names a model
(`--model`/`--model-path` as one arg or as Dynamo's separate list items,
`--model=`, HF_MODEL_ID/MODEL_ID/NIM_MODEL_NAME, or a NIM image);
frontends, routers, etcd and nats are left alone. The webhook composes
the hash once and stamps it on the pod (annotation full, label short);
the watcher and orchestrator capture under the stamped hash instead of
recomposing. If a promoted cachedir capture exists the pod restores.
Otherwise the webhook creates Lease nvsnap-capture-<hash> with the pod
UID as holder: the one admission that succeeds is the leader and gets
the capture decoration plus the capture label; every other pod is a
follower and gets the restore decoration against the deterministic
rox-<hash> claim, a schedulingGate (no node, no GPU held) and a gated
label. Any error fails open and admits the pod unchanged.

nvsnap-server already receives the promote state from the agent. On
`ready` it drops the gate on every gated pod of the hash and deletes the
Lease; on `failed`, when the leader pod is gone or terminated, or when
the Lease deadline passes, the reconciler evicts the gated followers
that have a controller so they are recreated and re-elected. Pod
volumes are immutable, so recreation is the only way a follower whose
claim will never bind can start.

Followers need ReadOnlyMany storage; on per-pod-clone storage the L2
backend cannot name the claim ahead of the promote and followers start
cold. Off by default: `agent.election.enabled`, `agent.election.deadline`
(60m). Server RBAC gains leases get/list/watch.

Tests: model-identity forms and the downloader classifier; Lease
election (one leader of four, error surfacing); the webhook matrix
(ignored frontend, leader, gated follower with the wait/seed/prewarm
inits, cold fallbacks, restore without election, unbound capture left
alone, explicit restore-from bypass, metadata maps bootstrapped once);
server release, eviction of owned pods only, and leader liveness; the
orchestrator and watcher honouring the stamped hash. Mutation-checked:
dropping the gate, the leader label, the single bootstrap, the
restore-before-elect branch, and the stamped-hash override each turned
tests red after compiling.

Design: docs/proposals/helm-chart-cache-election.md.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
The first cluster run of the election admitted both chart pods unchanged
with "election: pod has no UID at admission". A mutating webhook sees a
CREATE before the API server assigns the UID (and, for generateName
pods, the name), so nothing on the pod could serve as the Lease holder.
The unit fixture carried a UID and hid this.

The elector now mints its own id (UUID, seam for tests), uses it as the
Lease holder and returns it; the webhook stamps it on the leader as
nvsnap.io/election-id, and the server's leader-liveness check matches
that annotation instead of the UID. Fixtures drop the UID so they match
what admission actually sees.

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 25, 2026 15:03
@balajinvda
balajinvda requested a review from estroz September 25, 2026 15:03
@coderabbitai

coderabbitai Bot commented Sep 25, 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: ee8dfbd1-5577-4c57-b703-7859e5622a0a

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.

Dynamo prefill and decode workers run the same image and download the
same model, but their args differ by a role flag and their env by the
Dynamo, etcd and NATS wiring, so they hashed apart and each role elected
its own leader and downloaded once more. The hash now drops the role
flags (--is-prefill-worker, --is-decode-worker, --disaggregation-*,
--kv-transfer-config) in both the one-token-per-item and the shell-string
arg forms, and the DYN_/DYNAMO_ env prefixes plus ETCD_ENDPOINTS,
NATS_SERVER and NATS_URL. Flags that change the download (--model,
--revision, --tokenizer, quantization) stay in, because the model tree
mounts read-only and a pod restoring from a tree missing its files would
fail. Compile caches live in the per-pod writable shadow, so a role that
needs different kernels recompiles into it.

Tests: vLLM, SGLang and shell-string prefill/decode pairs hash equal;
different models, revisions and cache env still differ; the stripper's
token handling including a dangling value flag. Mutation-checked: hashing
the raw args again turns the test red.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
The first Dynamo operator run on dev1 still elected two leaders: the
prefill worker alone carries --kv-events-config, and the operator
injects GROVE_* gang-scheduling env whose values name the component and
the pod index. Neither changes what a worker downloads. Test built from
the live pod specs: prefill equals decode, and two replicas of one
component (different GROVE_PCLQ_POD_INDEX) equal each other.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
NVCF runs each chart in its own namespace, but the promoted rox claim
existed only in the capture's namespace, so a restore anywhere else
missed and started cold. Promoter gains EnsureClaim(hash, ns), the shape
NVCA uses for one model volume across tenant namespaces: shared-volume
mints one more secondary static PV per consumer namespace with the CSI
handle rewritten for it, pre-bound to a rox claim there (zero copy);
snapshot-clone with ReadOnlyMany re-exposes the promote's snapshot
handle in the target namespace through a pre-provisioned
VolumeSnapshotContent + VolumeSnapshot pair and clones from it; per-pod
clone storage reports ErrUnsupported and the pod stays cold.

Mount mints the claim on a miss and retries, covering restores admitted
after the promote in any namespace. The promote mints the claim in every
namespace that already holds pods stamped with the hash before it
publishes ready, so gated followers find it bound on release. Everything
minted carries nvsnap.io/hash-short and nvsnap.io/namespace and Delete
reaps it. Agent RBAC gains volumesnapshotcontents.

Tests with fake clients: per-namespace PV shape, handle rewrite, labels,
idempotence, a second namespace, not-promoted, Delete reaping across
namespaces, Mount minting on a miss, and the snapshot pre-provisioning
chain with restore size; per-pod clone unsupported. Mutation-checked:
Mount without the mint and a PV without labels each turn tests red.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
Two pods of one chart admitted in the same second both ran EnsureClaim
for nvsnap-xns on dev1; the second's post-create label Update hit "the
object has been modified" and the webhook failed open, so that pod
started cold. The per-namespace secondary PV is now created with its
hash and namespace labels; nothing in EnsureClaim updates anything, so
concurrent callers converge through AlreadyExists. Test rejects every PV
update and runs EnsureClaim three times.

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
The election was verified against charts that lived only in a scratch
directory, so nobody else could rerun the test. deploy/k8s/charts/vllm-workers
is a plain vLLM Deployment with N identical GPU workers and no nvsnap
labels or annotations (model, tensor parallelism, replicas, image, pull
secret and excluded nodes are values). scripts/test-election-e2e.sh
installs it, checks one leader and gated followers, waits for the release
after the promote, checks the follower came up with no downloads and
serves, prints the cold and warm engine phases from the vLLM logs, then
uninstalls and reinstalls expecting every pod to restore.

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

Co-Authored-By: Balaji Ganesan <bganesan@nvidia.com>
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