Skip to content

feat: add the OCI image source to ModelArtifact - #666

Merged
thxCode merged 8 commits into
mainfrom
feat/model-image-source
Sep 27, 2026
Merged

thxCode merged 8 commits into
mainfrom
feat/model-image-source

Conversation

@thxCode

@thxCode thxCode commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

What type of PR is this?

/kind enhancement
/kind api-change
/area worker
/area testing

What this PR does / why we need it:

Adds an image source to ModelArtifact: weights identified by a digest-pinned OCI image
reference (registry/repository@sha256:<64 hex>) and delivered by mounting a Kubernetes image
volume — kubelet pulls the pinned image on the node that needs it, and nothing enters the
model-manager plugin's cache chain.

  • Admission accepts the member behind a version capability gate: the union names image,
    the reference must pin a digest (a mutable tag would let one artifact deliver different weights
    on different pulls), patterns are refused because an image is mounted whole, and creation is
    refused on an apiserver below the ImageVolume default-on floor (1.35), where the volume field
    would be dropped silently. The gate reads the startup version snapshot through a swappable
    function; an unreadable version refuses. ModelPrefetch refuses an image artifact: there is
    nothing to warm.
  • The artifact reconciler resolves an image source with no network: Resolved=True on the
    first pass with a claim-shaped status — no revision, no manifest digest (the pinned reference is
    the whole identity; the KV identity falls back to the artifact UID), no status.nodes, no
    revalidation.
  • Both consumers render the delivery: a ModelDeployment and an Instance mount one image
    volume, read-only, at /var/lib/gpustack/model — no cache volume, no download environment, no
    --revision, no ephemeral-storage raise — and status.model.delivery reports Image. An
    Instance pinned to a node checks that node's kubelet (≥ 1.35) and containerd (≥ 2.1) before
    rendering. The soft placement preference names the nodes whose Node.status.images list the
    reference, hostname-sorted and capped, never a filter.
  • Docs: a new page owns the source (digest contract, the build recipe measured byte-equal to
    its hub commit, version floors, double storage 1.79×/2.00×/2.26× and why the CRI size must not
    be used for capacity, the three image-GC rules, registry mirrors); model-artifact.md, the docs
    index and the skill routing tables follow. No new Setting.
  • e2e: case-112 proves the cold-mount path on a live cluster — the tag refusal, the
    claim-shaped resolution, and an Instance reading the image's fixture byte for byte — with the
    fixture generated and pushed into an in-cluster registry over the node's loopback, so no
    containerd configuration is touched.

Deviation from the adjudicated draft, recorded in the spec: AC13's preference ordering is
hostname-sorted, name-only — Node.status.images carries no referenced-now or last-used signal,
so the digest preference's serving-now ordering has nothing to sort on. Weight, cap and softness
stand.

Which issue(s) this PR links to:

None

Special notes for your reviewer:

  • The chart matrix was run locally against all seven node images (v1.23.17 → v1.35.5, each
    installing gpustack/gpustack-operator:chart-test built from this branch; v1.33.12's first two
    attempts failed on Docker Hub anonymous rate limiting — the chart's mirrored dependency images
    pulled from docker.io — and passed with the images preloaded into the nodes, zero docker.io
    pull events in the final run).
  • make generate was verified in a clean checkout: zero diff.

Does this PR introduce a user-facing change?

ModelArtifact gains an `image` source: weights delivered as a digest-pinned OCI image reference
through a Kubernetes image volume, on Kubernetes 1.35+ with containerd 2.1+. Admission refuses
tag references and clusters below the version floor; an Instance pinned to a node that cannot
run image volumes waits and names the node.

A fourth source union member, Image, carrying a digest-pinned OCI image
reference. The digest is the artifact's whole identity: it pins the image's
bytes, not the hub manifest digest, and the operator never reads the
registry, so status.resolved stays claim-shaped. Delivery mounts the image
root read-only through a Kubernetes image volume (apiserver and kubelet
>= 1.35, containerd >= 2.1), outside the node cache plugin chain.

ModelDeploymentModelDelivery gains the Image value for the consumer status.

Signed-off-by: thxCode <thxcode0824@gmail.com>
Admission accepts the image member: exactly-one names it, the reference
must pin a digest (a tag is mutable, so one artifact could deliver
different weights on different pulls), patterns are refused because an
image is mounted whole, and creation is refused on an apiserver below the
ImageVolume default-on floor (1.35), where the volume field would be
dropped silently. The gate reads the startup version snapshot through a
swappable function because the snapshot's Configure ignores later calls,
and an unreadable version refuses. ModelPrefetch refuses an image
artifact: it never enters the node cache, so there is nothing to warm.

Mutation-validated: the floor check, the gate, the digest case rule and
the prefetch refusal each shown able to go red.

Signed-off-by: thxCode <thxcode0824@gmail.com>
An image artifact resolves claim-shaped: no network, no nodes aggregation,
the pinned reference as the whole identity, and a KV identity that falls
back to the artifact UID. Both consumers render the weights as one image
volume mounted read-only at the model path - no cache, no download
environment, no revision argument - and the status names the Image
delivery. An Instance pinned to a node checks that node's kubelet and
containerd against the feature floors before rendering, and the soft
placement preference names the nodes whose status.images lists the
reference, hostname-sorted and capped.

Mutation-validated: the render branch, the node pre-check and the
preference match each shown able to go red.

Signed-off-by: thxCode <thxcode0824@gmail.com>
One page owns the image source: the digest contract and what the operator
does not promise, the build recipe measured byte-equal to its hub commit,
image-volume delivery on both consumers, the version floors with the PSA
correction, the double-storage accounting and why the CRI size must not
be used for capacity, the three image-GC rules, and registry mirrors.

model-artifact.md gains the union member, the delivery table column, the
status wording and the requirements line, linking the new page; the docs
index and the docs skill's routing tables follow.

Signed-off-by: thxCode <thxcode0824@gmail.com>
The admission and renderer halves of the image source landed, but the
artifact reconciler's source switch still answered UnsupportedSource: an
image artifact resolved nowhere, so no consumer could ever mount it.
reconcileImage mirrors a claim: the resolution is the pinned spec being
stored, with no registry read, no revalidation and no nodes aggregation.
Found by case-112 on a live cluster, mutation-validated.

Signed-off-by: thxCode <thxcode0824@gmail.com>
Signed-off-by: thxCode <thxcode0824@gmail.com>
The five draft questions were adjudicated on 2026-09-27 and the AC13
ordering wording records the implementation (hostname-sorted, name-only:
Node.status.images carries no referenced-now signal). Found during the
live e2e: the artifact reconciler's own source switch still answered
UnsupportedSource; reconcileImage closed it.

Signed-off-by: thxCode <thxcode0824@gmail.com>
@gpustack-code-review

gpustack-code-review Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

🔍 OpenCodeReview found 3 issue(s) in this PR.

  • ✅ Successfully posted inline: 0 comment(s)
  • 📋 Routed to summary by policy: 3 comment(s)

test · low

📄 pkg/worker/webhooks/worker/model_prefetch.go (L112-L116)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category test)

Reminder per repo rules (not a claim about this PR's contents, since generated and test files are excluded from review): this webhook change must be accompanied by the regenerated zz_generated.webhooks.go output (make generate) and a regression test in the same PR. Likewise, the controller changes in pkg/worker/controllers/worker (image delivery rendering, runtime floor check, placement preference) must ship same-PR regression tests.


documentation · low

📄 api/worker/v1alpha1/model_artifact.go (L110-L110)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category documentation)

Reminder (per project rules, not a claim about presence/absence): this API change (new optional Image source member and the ModelDeploymentModelDeliveryImage constant) is additive and backward compatible — no renames, type changes, or removed markers — but it must ship with regenerated make generate output (zz_generated deepcopy/CRD manifests), which is excluded from this review. Please confirm it was regenerated in this PR.


test · low

📄 pkg/worker/webhooks/worker/model_artifact.go (L284-L284)

⚠️ GitHub could not post this as an inline comment: Routed to summary (severity low · category test)

Reminder (per project rules, not a claim about presence/absence): the new validation paths here — digest pinning (missing @, malformed digest, empty/whitespace reference), pattern refusal on image sources, and the version-gate refusal — are new bug surface; a regression test covering them should ship in this same PR. Also, the webhook change must be accompanied by the regenerated make generate output (zz_generated.webhooks.go), which is excluded from this review.

Comment thread docs/reference/model-artifact.md Outdated
Stating the floors in the requirements bullet repeated what
model-image-source.md owns; the bullet keeps one clause and the link.

Signed-off-by: thxCode <thxcode0824@gmail.com>
@thxCode
thxCode merged commit ad48449 into main Sep 27, 2026
11 checks passed
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