Skip to content

feat(models): vllm k8s support - #305

Merged
benmccown merged 16 commits into
mainfrom
vllm-k8s-support/bmccown
Jun 24, 2026
Merged

feat(models): vllm k8s support#305
benmccown merged 16 commits into
mainfrom
vllm-k8s-support/bmccown

Conversation

@benmccown

@benmccown benmccown commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

vLLM deployments on the Kubernetes backend (no operator) + reconciler refactor

Summary

Adds first-class vLLM support to the models-controller Kubernetes backend by
emitting native Kubernetes objects directly (PVC / weight-puller Job /
Deployment / Service) — no k8s-nim-operator required for the vLLM path. As part
of this, the previously monolithic K8sNimOperatorServiceBackend is split into a
small reconciler hierarchy so the operator path (NIM) and the direct-emission
path (vLLM) live in separate, focused units behind a common interface.

The NIM-on-operator path is preserved and behavior-neutral throughout.

Linear: AIRCORE-694

Motivation

The k8s backend previously only spoke NIMService / NIMCache CRs and delegated
all reconciliation to the in-cluster k8s-nim-operator. To run open models via
vLLM we need to stand up the serving stack ourselves. Rather than bolt vLLM logic
onto the operator backend, the engine paths are separated cleanly.

What's in this PR

vLLM on k8s (native objects, no operator)

  • New vllm_k8s_compiler.py: engine-agnostic compilers for the PVC, weight-puller
    Job, Deployment, and Service.
  • Staged, controller-gated rollout driven from the status path:
    • P0 (create): emit the PVC + weight-puller Job only (no serving objects yet,
      so the controller can gate on weight readiness).
    • P3 (status, once the puller Job succeeds): delete the completed puller Job
      to release its ReadWriteOnce volume, then emit the serving Deployment + Service
      with ownerReferences so a later delete cascades the PVC/Service.
  • vLLM pods run as the image's vllm user (uid 2000 / gid 0) to avoid the torch
    getpwuid crash; the puller writes weights as the same uid/gid.
  • In-cluster weight pull from the Files service via a cluster-routable HF endpoint
    (apis/files/v2/hf), not the local-service localhost URL the puller can't reach.
  • Re-pull policy on update: weights are only re-pulled when the model source
    (name/revision) changes; otherwise the Deployment is patched in place and the
    owned PVC/Job survive.
  • LoRA support: a cache-init container + adapters sidecar wired into the Deployment
    when LoRA is enabled.
  • generic engine is explicitly rejected on the k8s backend (clear error).

Reconciler refactor

  • New reconcilers/ package:
    • base.pyBaseReconciler ABC, the ResolvedDeployment input dataclass, and
      the shared, engine-agnostic Kubernetes status helpers (pod-log fetch, crash-loop
      detection, pod-status drill-down, pending-timeout/crash-loop error builders) +
      the idempotent, 404-tolerant single-object delete.
    • nim_operator.pyNimOperatorReconciler: emits NIMService / NIMCache CRs
      and projects status from the operator-reported NIMService.status.
    • k8s.pyK8sReconciler: the direct-emission vLLM reconciler described above.
  • K8sNimOperatorServiceBackend is now a thin coordinator: it owns the
    nemo_platform SDK and API-object work, resolves everything a reconciler needs
    into a ResolvedDeployment, selects the reconciler by config.engine, and
    delegates. Delete asks both reconcilers (delete has no engine context) and
    aggregates results; list unions both paths for orphan reconciliation.

Behavior changes worth calling out

  • No more NIM-operator CRD validation at startup. The backend no longer fails
    fast when the k8s-nim-operator CRDs are absent, so a cluster without the
    operator can run vLLM-only deployments. Missing NIM CRDs now surface lazily only
    if a NIM deployment is actually created (delete/list tolerate their absence).
  • Status requires a config. get_model_deployment_status takes the engine from
    the config (same selection as create/update). When the controller can't supply a
    config for a cycle (e.g. a transient config-fetch failure), the backend returns
    UNKNOWN instead of probing the cluster to guess the engine; the controller
    retries on the next poll and escalates to ERROR after its existing retry budget.

Testing

  • services/core/models unit suite: passing (added coverage for the vLLM
    compiler, the k8s backend phases/teardown/list/delete aggregation, the
    config-less UNKNOWN path, and the new config fields).
  • ruff (lint + format), ty, and pre-commit all pass on the changed files.
  • The vLLM path was validated end-to-end on a live cluster: a real Qwen3-1.7B vLLM
    deployment pulled weights, reached READY, and served inference (non-streaming and
    streaming) through the inference gateway.

Review guide

This PR does two things at once: it adds the vLLM-on-k8s path and refactors
the k8s backend into reconcilers
. Reading the files in dependency order (shared
pieces → reconcilers → coordinator → tests) makes both much easier to follow than
reading the raw diff top-to-bottom.

Note on the diff: review against the branch's merge-base. A raw origin/main..HEAD
diff also shows an unrelated prompts/prompt-service removal — that's already on
main, not this branch's work.

Suggested reading order

1. Engine plumbing (shared, small) — start here

  • controllers/backends/engine.py (+55) — engine discriminants (nim / vllm /
    generic), config_engine(), and resolve_health_path(). This is the
    vocabulary the rest of the PR dispatches on.
  • controllers/backends/vllm_compiler.py (moved from docker/, ~178 lines) — the
    backend-agnostic vLLM compiler (image, vllm serve args, env). Relocated so both
    the docker and k8s backends share it; the diff is mostly the move + import fixups.
    Compare with docker/creation_reconciler.py to confirm the docker path is
    unchanged behaviorally.

2. The vLLM k8s object compiler

  • controllers/backends/k8s_nim_operator/vllm_k8s_compiler.py (new, 428) — pure
    functions that build the PVC, weight-puller Job, Deployment, Service. No
    control flow, no API calls — easiest place to verify the actual k8s objects
    (volumes, securityContext uid/gid, /dev/shm, probes, ownerRefs, labels).
    Paired tests: tests/unit/controllers/backends/test_vllm_k8s_compiler.py (+307).

3. The reconciler split (the heart of the refactor)

Read base → the two implementations → the coordinator.

  • .../reconcilers/base.py (new, 448) — BaseReconciler ABC, the
    ResolvedDeployment input dataclass (what the coordinator pre-resolves and
    hands down), and the shared engine-agnostic status helpers (pod-log fetch,
    crash-loop detection, pod-status drill-down, pending-timeout/crash-loop error
    builders) + the idempotent, 404-tolerant single-object delete.
  • .../reconcilers/nim_operator.py (new, 466) — NimOperatorReconciler. This is
    the previous operator logic, moved verbatim (NIMService/NIMCache CRUD, status
    from NIMService.status). The cheapest way to review: diff its method bodies
    against the old backend.py and confirm they're unchanged apart from taking a
    ResolvedDeployment.
  • .../reconcilers/k8s.py (new, 600) — K8sReconciler, the new vLLM logic and the
    most important file to review carefully (see "Where to focus" below).
  • .../k8s_nim_operator/backend.py (919 deletions, now 377) — the coordinator. The
    big deletion is code moving into the reconcilers. New responsibilities:
    _resolve() (build ResolvedDeployment), _select_reconciler(engine),
    delegation in create/update/status, delete that calls both reconcilers and
    aggregates
    , and list that unions both paths.

4. Config + tests

  • .../k8s_nim_operator/config.py (+57) — new vLLM-related fields (image/tag,
    service account, uid/gid defaults, /dev/shm size limit). Tests:
    tests/unit/controllers/test_backend_config_fields.py.
  • tests/unit/controllers/test_k8s_nim_operator_backend.py (+681) — the bulk of the
    test churn. Note the helpers near the top: _sync_reconcilers() (propagates the
    per-test mock clients onto the reconcilers the backend built at init),
    _StatusHelperReconciler (exercises the BaseReconciler status helpers directly),
    and _nim_config() / _vllm_config().

Where to focus your attention (the subtle bits)

These are the parts most worth scrutiny — they encode hard-won operational behavior:

  1. The staged P0 → P3 rollout in k8s.py (create + get_status +
    _create_vllm_serving_objects). Create emits only the PVC + puller Job;
    the serving Deployment/Service are created later from the status path once the
    puller Job succeeds. This is intentional weight-readiness gating.

  2. RWO volume hand-off (_delete_puller_job + _create_vllm_serving_objects).
    The completed puller Job is deleted before the serving Deployment is created so
    its pod releases the ReadWriteOnce volume (avoids a Multi-Attach error if the
    server schedules on another node). Note the "still terminating → defer" return,
    and the Job-absent + PVC-present → resume P3 (not LOST) branch in get_status.

  3. ownerReferences — PVC and Service are owned by the Deployment so a single
    Deployment delete cascades the rest.

  4. Update re-pull policy (update + _existing_model_source) — weights re-pull
    only when the model-source annotation changes; unchanged source patches in place.

  5. Engine-agnostic teardown (backend.delete_model_deployment
    both reconcilers → aggregate). Delete has no engine context (also used for orphan
    reconciliation), so it attempts both paths, is 404-tolerant per-resource, and
    surfaces real failures as ERROR rather than falsely reporting DELETED. The
    forbidden-CR-doesn't-block-vLLM-cleanup case is covered by tests.

  6. uid/gid 2000/0 for vLLM pods + puller (config.py defaults, used in
    vllm_k8s_compiler.py). Avoids the torch getpwuid crash; there's a FUTURE note
    in the compiler flagging that a NIM raw-object path must pass its own uid/gid.

Behavior changes to sanity-check (not just refactor)

  • No startup CRD validation. _validate_nim_operator_crds is gone — a cluster
    without k8s-nim-operator can run vLLM-only. Confirm you're comfortable that
    missing NIM CRDs now surface lazily (on NIM create) rather than at boot.
  • Status without a config → UNKNOWN. get_model_deployment_status no longer
    probes the cluster to guess the engine when config is None; it returns UNKNOWN
    and lets the controller retry/escalate. See the short branch at the top of that
    method and deployment_reconciler.py's existing _handle_unknown_status.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Kubernetes raw-object emission for vLLM with staged weights pulling (PVC + pull Job) before creating serving resources.
    • Added engine-aware lifecycle dispatch for Kubernetes NIM Operator backends, including LoRA sidecar support where applicable.
  • Bug Fixes

    • Improved deployment creation/status flows by using full deployment context consistently across backends.
    • Enhanced Kubernetes NIM Operator delete/status handling with better pending-timeout and pod crash diagnostics.
  • Documentation

    • Updated NIM Operator config defaults (vLLM security context, images, service account, shared memory) and BusyBox image reference.
    • Updated the default Hugging Face model puller image to a dynamically qualified value.

@benmccown benmccown self-assigned this Jun 12, 2026
@github-actions github-actions Bot added the feat label Jun 12, 2026
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 20908/27478 76.1% 61.2%
Integration Tests 12109/26247 46.1% 19.5%

@benmccown
benmccown marked this pull request as ready for review June 15, 2026 20:26
@benmccown
benmccown requested review from a team as code owners June 15, 2026 20:26
@benmccown
benmccown force-pushed the vllm-k8s-support/bmccown branch from d78cd03 to 29d3f93 Compare June 15, 2026 20:29
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Refactors all backends to accept ModelContext instead of separate deployment/config/entity parameters. Introduces shared engine.py module for engine dispatch. Redesigns K8s backend as delegating dispatcher to two engine-specific reconcilers: NimOperatorReconciler for NIMService/NIMCache CRDs and K8sReconciler for phased direct vLLM k8s object emission (PVC→puller Job→Deployment+Service). Adds StatusProjector for shared pod diagnostics and ResourceDeleter for idempotent deletion. Extends K8sNimOperatorConfig with vLLM-specific fields. Refactors test suites with helper functions for reconciler syncing and status projection testing.

Changes

ModelContext interface refactor across backends

Layer / File(s) Summary
Engine constants and health-path dispatch
services/core/models/src/nmp/core/models/controllers/backends/engine.py, ...docker/creation_reconciler.py, ...backends/vllm_compiler.py
New engine.py provides ENGINE_NIM/VLLM/GENERIC constants, ENGINE_LABEL, HEALTH_PATH_LABEL, ENGINE_HEALTH_PATHS mapping, config_engine(), and resolve_health_path(). Docker reconciler drops local copies and imports from shared module. vllm_compiler docstring updated to backend-agnostic scope.
ServiceBackend interface and implementation refactor
...backends/backends.py, ...docker/backend.py, ...backends/none_backend.py, ...controllers/deployment_reconciler.py, services/core/inference-gateway/tests/integration/conftest.py, services/core/models/tests/integration/conftest.py
All backends refactored to accept ctx: ModelContext instead of (deployment, config, model_entity) for create_model_deployment, update_model_deployment, get_model_deployment_status. Implementations extract parameters from ctx. deployment_reconciler and mock backends updated to pass full context.

Kubernetes reconciler architecture: base classes and helpers

Layer / File(s) Summary
ResolvedDeployment dataclass and Reconciler interface
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/base.py
ResolvedDeployment packages pre-resolved deployment inputs (config, view, k8s resource names, weight source, model identity, files HF endpoint). Reconciler ABC defines async lifecycle contract (create, update, get_status, delete, list_managed_deployment_names).
StatusProjector and pod diagnostics
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/status_projector.py
Centralizes deployment/pod/event status projection: host URL generation, best-effort pod log fetching with truncation, most-recent pod discovery, pending-timeout and crash-loop error builders with formatted messages and error details, pod-status projection with event-message mapping and restart-count aggregation.
ResourceDeleter: idempotent 404-tolerant deletion
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/resource_deleter.py
Wraps delete operations with 404 tolerance, non-stack-trace error logging, and error aggregation. Handles both typed and dynamic client exceptions. Special RBAC message handling.

Kubernetes reconciler implementations

Layer / File(s) Summary
vllm_k8s_compiler: Kubernetes object compilation
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/vllm_k8s_compiler.py
Pure compiler emitting PVC, weight-puller Job, serving Deployment, ClusterIP Service with standardized management labels, engine/health-path annotations, conditional GPU resources, conditional pod security context, probe wiring from startup_grace_seconds, dshm/scratch volumes with optional size limits, and optional init/sidecar containers.
NimOperatorReconciler: NIMService/NIMCache CRD delegation
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/nim_operator.py
Manages NIMService/NIMCache CRs via dynamic client. Conditionally ensures NIMCache for FILES_SERVICE weights. Treats ConflictError as already-initiated. Maps NIMService.status.state to deployment status (readyREADY, notready→pod projection, failedERROR, missing→LOST). Lists managed deployments with ForbiddenError tolerance.
K8sReconciler: phased vLLM direct-emission
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/k8s.py
Stages creation (PVC→puller Job with 409 tolerance, then Deployment+Service after Job succeeds). Update via model-source drift detection on Job annotation; re-pulls on change. get_status phase-checks: Deployment existence drives readiness; queries Job (with log retrieval on failure, creates serving objects on success, LOST on missing Job+PVC). LoRA support via init container and sidecar. PVC ownerReference patching. Aggregated deletion of all resources.

K8s backend and configuration

Layer / File(s) Summary
K8sNimOperatorConfig vLLM-specific fields
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/config.py
Added default_vllm_user_id, default_vllm_group_id, default_vllm_image, default_vllm_image_tag, service_account_name, default_shared_memory_size_limit. Expanded documentation distinguishing NIM/operator vs vLLM security context expectations. Updated busybox_image to fully-qualified docker.io/library/busybox.
K8sNimOperatorServiceBackend engine-based reconciler dispatch
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py
Refactored into resolve-and-delegate: constructs both reconcilers with shared StatusProjector and ResourceDeleter. Adds _resolve() to build ResolvedDeployment and _remote_files_hf_url() for cluster-routable Files endpoint. create/update/get_status dispatch by engine selection. get_status requires config (returns UNKNOWN if missing), enforces pending-timeout with pod diagnostics. delete and list aggregate both reconcilers.

Testing, configuration, and documentation

Layer / File(s) Summary
vllm_k8s_compiler and config field unit tests
services/core/models/tests/unit/controllers/backends/test_vllm_k8s_compiler.py, test_backend_config_fields.py, test_vllm_compiler.py
New vllm_k8s_compiler tests cover naming helpers, common labels, PVC/Job/Deployment/Service compilation with storage classes, GPU resources, security contexts, probes, volumes, init/sidecar containers. Config field tests validate vLLM-on-k8s defaults/overrides. vllm_compiler test import updated to backend-agnostic path.
Docker backend test refactoring to ModelContext
services/core/models/tests/unit/controllers/test_docker_backend.py
drive_creation_to_completion() and all test methods updated to use ModelContext(model_deployment=..., model_deployment_config=..., model_entity=...) instead of positional arguments across creation, status polling, port-forwarding, DonD/DinD, GPU allocation/release, multi-LLM, SFT, tool-call plugins, stepped pipeline, and concurrent deployment scenarios.
K8s backend and reconciler test refactoring
services/core/models/tests/unit/controllers/test_k8s_nim_operator_backend.py, test_deployment_reconciler.py, test_backend_registry.py
Adds _sync_reconcilers(), _nim_config(), _status_helper_reconciler() helpers. Patches moved to reconciler modules. NIM CRD tests sync reconcilers before operations. Status tests pass explicit config or None, verify UNKNOWN behavior. Pod-status helpers migrated to StatusProjector. Extensive vLLM coverage (phased create/status/update, LoRA sidecars, LOST state, host_url mapping, re-pull on drift, union listing). _validate_nim_operator_crds mock removed.
Configuration defaults and documentation
services/core/models/src/nmp/core/models/config.py, docs/set-up/config-reference.mdx
ModelsConfig huggingface_model_puller default computed dynamically via get_qualified_image("nmp-api") instead of pinned nvcr.io tag. Docs extended with vLLM-on-k8s config reference (user_id/group_id, image/tag, service_account_name, shared_memory_size_limit). busybox reference updated to docker.io/library/busybox. Docker backend puller documented as my-registry/nmp-api:local.

Sequence Diagram(s)

sequenceDiagram
  participant DeploymentReconciler
  participant K8sBackend as K8sNimOperatorServiceBackend
  participant K8sReconciler
  participant K8s as Kubernetes API
  
  DeploymentReconciler->>K8sBackend: get_model_deployment_status(ctx: ModelContext)
  K8sBackend->>K8sBackend: _resolve() → ResolvedDeployment
  K8sBackend->>K8sBackend: _select_reconciler(engine=vllm)
  K8sBackend->>K8sReconciler: get_status(resolved)
  
  alt Serving Deployment absent, puller Job running
    K8sReconciler->>K8s: read Job status
    K8sReconciler-->>K8sBackend: PENDING
  else Puller Job succeeded
    K8sReconciler->>K8s: delete Job (release RWO)
    K8sReconciler->>K8s: create Deployment + Service
    K8sReconciler-->>K8sBackend: PENDING (server startup)
  else Serving Deployment ready
    K8sReconciler->>K8s: check pod replicas
    K8sReconciler-->>K8sBackend: READY
  end
  
  K8sBackend-->>DeploymentReconciler: DeploymentStatusUpdate
Loading

Possibly related PRs

  • NVIDIA-NeMo/nemo-platform#232: Parallel engine-aware dispatch refactoring in DockerServiceBackend; this PR extends the pattern to k8s with reconciler architecture.

Suggested reviewers

  • mckornfield
  • soluwalana
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 77.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title directly describes main change: adding vLLM Kubernetes support. It's specific, concise, and captures the primary feature being delivered.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vllm-k8s-support/bmccown

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py (1)

264-328: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pending timeout uses hardcoded _nim_reconciler instead of selected reconciler.

Line 315-316 calls self._nim_reconciler._find_pod_name and _build_pending_timeout_error even for vLLM deployments. Since both methods are inherited from BaseReconciler, it works, but should use the already-selected reconciler for clarity and correctness if the methods are ever overridden.

                 if elapsed >= self._backend_config.pending_timeout_seconds:
-                    pod_name = self._nim_reconciler._find_pod_name(resource_name)
-                    return self._nim_reconciler._build_pending_timeout_error(resource_name, elapsed, pod_name)
+                    pod_name = reconciler._find_pod_name(resource_name)
+                    return reconciler._build_pending_timeout_error(resource_name, elapsed, pod_name)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py`
around lines 264 - 328, In the get_model_deployment_status method, the PENDING
timeout handling at lines 315-316 incorrectly uses the hardcoded
self._nim_reconciler instead of using the already-selected reconciler variable.
Replace the two method calls (self._nim_reconciler._find_pod_name and
self._nim_reconciler._build_pending_timeout_error) with calls to the reconciler
variable that was selected earlier in the method based on the deployment's
engine type. This ensures consistency and correctness if reconciler
implementations override these methods.
🧹 Nitpick comments (1)
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py (1)

221-237: 💤 Low value

Missing return type annotation on _select_reconciler.

-    def _select_reconciler(self, engine: str):
+    def _select_reconciler(self, engine: str) -> Optional[BaseReconciler]:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py`
around lines 221 - 237, Add a return type annotation to the `_select_reconciler`
method. Since the method can return either a reconciler object
(self._k8s_reconciler or self._nim_reconciler) or None, use an Optional type
annotation that properly reflects the possible return types of the reconciler
instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/core/models/src/nmp/core/models/controllers/backends/engine.py`:
- Around line 52-55: The `resolve_health_path` function returns the
user-supplied `health_check_path` attribute without normalizing it, which can
result in paths without a leading slash (like `health` instead of `/health`)
that produce invalid downstream probe URLs. Normalize the `explicit_path` before
returning it by ensuring it has a leading slash - you can do this by checking if
the path starts with `/` and prepending it if it doesn't, or by using a string
formatting approach that guarantees the correct format.

---

Outside diff comments:
In
`@services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py`:
- Around line 264-328: In the get_model_deployment_status method, the PENDING
timeout handling at lines 315-316 incorrectly uses the hardcoded
self._nim_reconciler instead of using the already-selected reconciler variable.
Replace the two method calls (self._nim_reconciler._find_pod_name and
self._nim_reconciler._build_pending_timeout_error) with calls to the reconciler
variable that was selected earlier in the method based on the deployment's
engine type. This ensures consistency and correctness if reconciler
implementations override these methods.

---

Nitpick comments:
In
`@services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py`:
- Around line 221-237: Add a return type annotation to the `_select_reconciler`
method. Since the method can return either a reconciler object
(self._k8s_reconciler or self._nim_reconciler) or None, use an Optional type
annotation that properly reflects the possible return types of the reconciler
instances.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2dc7131b-6abd-4bfc-b17e-3be3cb08c715

📥 Commits

Reviewing files that changed from the base of the PR and between 1c9528b and d78cd03.

📒 Files selected for processing (20)
  • services/core/models/src/nmp/core/models/controllers/backends/backends.py
  • services/core/models/src/nmp/core/models/controllers/backends/docker/backend.py
  • services/core/models/src/nmp/core/models/controllers/backends/docker/creation_reconciler.py
  • services/core/models/src/nmp/core/models/controllers/backends/engine.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/config.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/base.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/k8s.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/nim_operator.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/vllm_k8s_compiler.py
  • services/core/models/src/nmp/core/models/controllers/backends/none_backend.py
  • services/core/models/src/nmp/core/models/controllers/backends/vllm_compiler.py
  • services/core/models/src/nmp/core/models/controllers/deployment_reconciler.py
  • services/core/models/tests/integration/test_models.py
  • services/core/models/tests/unit/controllers/backends/test_vllm_compiler.py
  • services/core/models/tests/unit/controllers/backends/test_vllm_k8s_compiler.py
  • services/core/models/tests/unit/controllers/test_backend_config_fields.py
  • services/core/models/tests/unit/controllers/test_backend_registry.py
  • services/core/models/tests/unit/controllers/test_deployment_reconciler.py
  • services/core/models/tests/unit/controllers/test_k8s_nim_operator_backend.py
💤 Files with no reviewable changes (2)
  • services/core/models/tests/unit/controllers/test_backend_registry.py
  • services/core/models/tests/integration/test_models.py

@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (1)
services/core/inference-gateway/tests/integration/conftest.py (1)

110-112: ⚡ Quick win

Use concrete types for the updated status signature.

This changed method uses Any for new params; switch to concrete types to match the backend contract and catch signature drift in tests.

Suggested patch
-from typing import Any, Generator
+from typing import Any, Generator, Optional
+from nemo_platform.types.inference.model_deployment import ModelDeployment
+from nemo_platform.types.inference.model_deployment_config import ModelDeploymentConfig
+from nemo_platform.types.models.model_entity import ModelEntity
@@
-    async def get_model_deployment_status(
-        self, deployment: Any, config: Any = None, model_entity: Any = None
-    ) -> DeploymentStatusUpdate:
+    async def get_model_deployment_status(
+        self,
+        deployment: ModelDeployment,
+        config: Optional[ModelDeploymentConfig] = None,
+        model_entity: Optional[ModelEntity] = None,
+    ) -> DeploymentStatusUpdate:

As per coding guidelines, "**/*.py`: Always prefer concrete type hints over string-based ones in Python code; do not import types under TYPE_CHECKING, instead import types as regular imports when possible".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/core/inference-gateway/tests/integration/conftest.py` around lines
110 - 112, The get_model_deployment_status method in the mock class uses `Any`
type hints for the deployment, config, and model_entity parameters instead of
concrete types. Replace these `Any` type annotations with the appropriate
concrete types that match the actual backend contract for this method. Import
any required types as regular imports (not under TYPE_CHECKING) at the top of
the file to ensure the signature accurately reflects the backend implementation
and enables catching any unintended signature drift during testing.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@services/core/inference-gateway/tests/integration/conftest.py`:
- Around line 110-112: The get_model_deployment_status method in the mock class
uses `Any` type hints for the deployment, config, and model_entity parameters
instead of concrete types. Replace these `Any` type annotations with the
appropriate concrete types that match the actual backend contract for this
method. Import any required types as regular imports (not under TYPE_CHECKING)
at the top of the file to ensure the signature accurately reflects the backend
implementation and enables catching any unintended signature drift during
testing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 071e1488-5ec9-4939-b31f-9b89d9763dd1

📥 Commits

Reviewing files that changed from the base of the PR and between 808d978 and b9e5fdc.

📒 Files selected for processing (4)
  • docs/set-up/config-reference.mdx
  • services/core/inference-gateway/tests/integration/conftest.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/config.py
  • services/core/models/tests/integration/conftest.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/set-up/config-reference.mdx

@mckornfield mckornfield 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.

well that was fun

Comment thread services/core/models/src/nmp/core/models/controllers/deployment_reconciler.py Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py (1)

360-365: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Isolate each reconciler delete.

If the NIM delete raises, the vLLM delete is never attempted. That breaks the stated “one reconciler’s failure never aborts the other” behavior.

Proposed fix
-        for result in (
-            await self._nim_reconciler.delete(workspace, name),
-            await self._k8s_reconciler.delete(workspace, name),
-        ):
+        for path, reconciler in (("nim", self._nim_reconciler), ("vllm", self._k8s_reconciler)):
+            try:
+                result = await reconciler.delete(workspace, name)
+            except Exception as e:
+                logger.warning(
+                    "Delete path failed",
+                    extra={"workspace": workspace, "deployment_name": name, "path": path, "error": str(e)},
+                )
+                errors.append(f"{path} delete failed: {type(e).__name__}: {e}")
+                continue
             if result.status == "ERROR" and result.error_details:
                 errors.extend(result.error_details.get("errors", []))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py`
around lines 360 - 365, The current code structure uses tuple unpacking where
both self._nim_reconciler.delete(workspace, name) and
self._k8s_reconciler.delete(workspace, name) are awaited together, which means
if the NIM reconciler delete raises an exception, the K8S reconciler delete will
never execute. Isolate each reconciler delete operation by executing them
separately with individual try-except blocks around each delete call, ensuring
that an exception from one reconciler does not prevent the other from attempting
to delete.
🧹 Nitpick comments (1)
services/core/inference-gateway/tests/integration/conftest.py (1)

90-102: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use ModelContext instead of Any for mock backend context methods.

These signatures should stay concrete to preserve contract checks.

Proposed change
-    async def create_model_deployment(self, ctx: Any) -> DeploymentStatusUpdate:
+    async def create_model_deployment(self, ctx: ModelContext) -> DeploymentStatusUpdate:
         """Record call and return configured response."""
         self.create_calls.append((ctx.model_deployment, ctx.model_deployment_config, ctx.model_entity))
         return self.create_response

-    async def update_model_deployment(self, ctx: Any) -> DeploymentStatusUpdate:
+    async def update_model_deployment(self, ctx: ModelContext) -> DeploymentStatusUpdate:
         """Record call and return configured response."""
         self.update_calls.append((ctx.model_deployment, ctx.model_deployment_config, ctx.model_entity))
         return self.create_response

-    async def get_model_deployment_status(self, ctx: Any) -> DeploymentStatusUpdate:
+    async def get_model_deployment_status(self, ctx: ModelContext) -> DeploymentStatusUpdate:
         """Record call and return configured response."""
         self.status_calls.append(ctx.model_deployment)
         return self.status_response

As per coding guidelines, **/*.py: Always prefer concrete type hints over string-based ones in Python code; do not import types under TYPE_CHECKING, instead import types as regular imports when possible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/core/inference-gateway/tests/integration/conftest.py` around lines
90 - 102, Replace the generic `Any` type hint with the concrete `ModelContext`
type hint in the ctx parameter for all three mock backend methods:
create_model_deployment, update_model_deployment, and
get_model_deployment_status. This preserves the contract checks and follows the
coding guideline to prefer concrete type hints over generic ones.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/resource_deleter.py`:
- Around line 39-47: The exception handling in the resource deletion logic does
not catch all possible failures from the Kubernetes client, allowing unhandled
exceptions to escape and violate the documented contract. Replace the specific
`except k8s_dynamic_exceptions.ForbiddenError as e:` handler with a general
`except Exception as e:` catch-all to ensure all exceptions (including any
unexpected failures) are passed to the _classify_delete_error method for proper
error classification and return, rather than propagating unhandled to abort
cleanup.

In
`@services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/status_projector.py`:
- Around line 120-154: The error messages in build_pending_timeout_error and
build_crash_loop_error methods hardcode the string "NIM" in their status
messages, but since this projector is shared by both NIM and vLLM backends, vLLM
failures will display misleading diagnostics. Replace the hardcoded "NIM"
references with engine-neutral language or a dynamic reference that represents
the actual backend type being used, ensuring both NIM and vLLM deployments
display accurate status messages.

---

Outside diff comments:
In
`@services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py`:
- Around line 360-365: The current code structure uses tuple unpacking where
both self._nim_reconciler.delete(workspace, name) and
self._k8s_reconciler.delete(workspace, name) are awaited together, which means
if the NIM reconciler delete raises an exception, the K8S reconciler delete will
never execute. Isolate each reconciler delete operation by executing them
separately with individual try-except blocks around each delete call, ensuring
that an exception from one reconciler does not prevent the other from attempting
to delete.

---

Nitpick comments:
In `@services/core/inference-gateway/tests/integration/conftest.py`:
- Around line 90-102: Replace the generic `Any` type hint with the concrete
`ModelContext` type hint in the ctx parameter for all three mock backend
methods: create_model_deployment, update_model_deployment, and
get_model_deployment_status. This preserves the contract checks and follows the
coding guideline to prefer concrete type hints over generic ones.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: b11748a1-b7a8-48d7-965e-d40528a8bb88

📥 Commits

Reviewing files that changed from the base of the PR and between 694cbd6 and 4eec3f9.

📒 Files selected for processing (17)
  • services/core/inference-gateway/tests/integration/conftest.py
  • services/core/models/src/nmp/core/models/controllers/backends/backends.py
  • services/core/models/src/nmp/core/models/controllers/backends/docker/backend.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/backend.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/base.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/k8s.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/nim_operator.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/resource_deleter.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/status_projector.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/vllm_k8s_compiler.py
  • services/core/models/src/nmp/core/models/controllers/backends/none_backend.py
  • services/core/models/src/nmp/core/models/controllers/deployment_reconciler.py
  • services/core/models/tests/integration/conftest.py
  • services/core/models/tests/unit/controllers/backends/test_vllm_k8s_compiler.py
  • services/core/models/tests/unit/controllers/test_deployment_reconciler.py
  • services/core/models/tests/unit/controllers/test_docker_backend.py
  • services/core/models/tests/unit/controllers/test_k8s_nim_operator_backend.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/nim_operator.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/reconcilers/k8s.py
  • services/core/models/src/nmp/core/models/controllers/backends/k8s_nim_operator/vllm_k8s_compiler.py
  • services/core/models/tests/unit/controllers/test_k8s_nim_operator_backend.py

@benmccown
benmccown requested a review from mckornfield June 23, 2026 22:01

@mckornfield mckornfield 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.

fancy pr is fancy

benmccown added 14 commits June 24, 2026 16:09
Signed-off-by: Ben McCown <bmccown@nvidia.com>
Fixes found validating the k8s vLLM path on a real cluster:
- model-source and health-path are stored as annotations, not labels
  (their values contain '/', invalid for k8s label values).
- puller Job runs the image entrypoint via container args, not command
  (command would override the entrypoint and exec the first token).
- puller HF_ENDPOINT resolves the cluster-routable Files URL (service
  discovery / base_url), bypassing the in-process local-service routing
  that returns localhost (unreachable from the puller pod).
- release the RWO weights volume before serving: delete the completed
  puller Job at P3 so its pod releases the volume attachment, avoiding a
  Multi-Attach error when the server pod schedules on another node. Only
  on the success path; a failed Job is kept for status/log reporting.
- status reads the Deployment first (source of truth once created); a
  missing Job with the PVC still present resumes P3 instead of reporting
  LOST (prevents a re-pull loop via drift recovery).
- pod securityContext: do not force runAsUser on the server pod (images
  like vLLM lack a passwd entry for uid 1000 -> getpwuid crash); the
  puller keeps fsGroup so it can write the freshly-provisioned PVC.
- orphan listing tolerates NIM CRD 403 quietly on the vLLM-only path.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
Teardown deletes every resource a deployment could own, by name, across
both the operator path (NIMService/NIMCache CRs) and the directly-emitted
vLLM path (Deployment/Service/Job/PVC). delete_model_deployment has only
workspace/name (no engine/config -- it is also driven by orphan
reconciliation), so attempting all resource types by name is the correct,
idempotent, self-healing teardown.

Previously a single delete failure (notably a 403 deleting a NIMService
when the ServiceAccount lacked that RBAC on the vLLM-only path) raised
out of the routine and aborted the whole teardown, leaving the deployment
stuck in DELETING and re-erroring every reconcile.

Now each delete is independent and 404-tolerant ('already gone' is
success); non-404 failures are logged concisely (no stack trace) and
aggregated, so one resource's failure never blocks the others, and the
result is ERROR (visible/stuck) rather than a false DELETED that would
orphan cluster resources.

The platform Helm chart's models ServiceAccount must grant get/list/
watch/delete on apps.nvidia.com nimservices/nimcaches (in addition to the
core/apps/batch resources) so engine-agnostic teardown does not 403.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
The vLLM puller + server pods previously inherited the NIM-oriented default
securityContext (or none). Running the server as an arbitrary uid 1000 made
torch/inductor crash at startup (getpass.getuser -> pwd.getpwuid: 'uid not
found: 1000') because the vllm/vllm-openai image has no /etc/passwd entry for
1000, and running unset defaulted to root.

Inspecting the image, its provisioned non-root user is 'vllm' (uid 2000, gid 0)
with a real passwd entry. Add dedicated default_vllm_user_id (2000) /
default_vllm_group_id (0) config and apply them to both the puller and the
server: the server runs as a non-root user that resolves cleanly, and the
puller writes the weights under the same uid/gid so the server can read them.
Operators can override via config; we do not hardcode root.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
… raw-object migration

When NIM is migrated off k8s-nim-operator onto the shared raw-object
compilers (vllm_k8s_compiler), the securityContext uid/gid params are
engine-specific and must not be shared: vLLM uses 2000/0 (its image's
'vllm' user, which has an /etc/passwd entry), while NIM images expect the
operator's historical 1000/2000. Reusing default_vllm_user_id for NIM (or
hardcoding either in the compiler) would reintroduce the getpwuid crash or
break NIM. Add a FUTURE note in the compiler module docstring and
cross-references on the NIM uid/gid config fields and the vLLM puller call
site so a future implementer (possibly not us) does not hit this.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
Extract the Kubernetes reconciliation logic out of the monolithic
K8sNimOperatorServiceBackend into a reconciler hierarchy under
reconcilers/:

* BaseReconciler (base.py): the reconciler interface plus the shared,
  engine-agnostic Kubernetes status helpers (pod log fetch, crash-loop
  detection, pod-status drill-down, pending-timeout/crash-loop error
  builders) and the idempotent 404-tolerant single-object delete.
* NimOperatorReconciler (nim_operator.py): emits NIMService / NIMCache
  CRs for the in-cluster operator to reconcile.
* K8sReconciler (k8s.py): emits native Kubernetes objects directly
  (PVC / weight-puller Job / Deployment / Service) for the vLLM engine
  and drives the staged rollout itself.

The ServiceBackend now owns only the nemo_platform SDK and API-object
work: it resolves all reconciler inputs into a ResolvedDeployment,
selects the reconciler by config.engine, and delegates. Delete asks both
reconcilers (no engine context) and aggregates; list unions both.

This is behavior-neutral: the NIM operator path is moved verbatim and
the vLLM path is unchanged. Tests are retargeted at the reconciler
modules/instances; the shared status-helper unit tests now exercise
BaseReconciler directly.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
Remove _validate_nim_operator_crds and its call site so the k8s backend
no longer fails fast when the k8s-nim-operator CRDs are absent. A cluster
without the operator can now run vLLM-only deployments (vLLM emits native
Kubernetes objects and needs no operator); missing NIM CRDs surface
lazily when a NIM deployment is actually created.

Drop the associated test mocks.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
get_model_deployment_status now requires a config (engine is taken from
it, same selection as create/update). When the controller can't supply a
config this cycle (e.g. a transient config-fetch failure), the backend
returns UNKNOWN rather than probing the cluster to guess the engine. The
controller's existing _handle_unknown_status retries on the next poll
(which normally has a config) and escalates to ERROR after its retry
budget -- so we get the ultimate-timeout behavior for free.

Removes the get_status_orphan entry point from all three reconcilers and
the _vllm_objects_exist probe from the status path (the probe is still
used internally by the vLLM update re-pull check).

Signed-off-by: Ben McCown <bmccown@nvidia.com>
Drop the refactor-provenance note ("moved verbatim from the previous
monolithic K8sNimOperatorServiceBackend") from both reconciler module
docstrings. In k8s.py it read as if the file were the NIM-operator
implementation; replace with a concise statement of the actual contract
(inputs pre-resolved on ResolvedDeployment; reconciler talks only to
Kubernetes).

Signed-off-by: Ben McCown <bmccown@nvidia.com>
Run generate-config-docs to pick up the new K8sNimOperatorConfig vLLM
fields (default_vllm_user_id/group_id, default_vllm_image/_tag,
service_account_name, default_shared_memory_size_limit). Fixes the
lint-config-reference-docs and lint-web-sdk CI jobs, which both diff
regenerated artifacts against the tree.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
…ault

The integration MockServiceBackend.get_model_deployment_status kept the
old single-arg signature (deployment), but the controller now threads
(deployment, config, model_entity) through. The resulting TypeError was
swallowed by the reconciler's per-deployment except, so the status call
was never recorded and test_controller_polls_pending_deployment failed
with assert 0 == 1. Update both the models and inference-gateway
integration mock backends to match the ServiceBackend ABC signature.

Also qualify the k8s backend busybox_image default
(busybox -> docker.io/library/busybox) so the LoRA cache-init container
resolves on container runtimes that enforce fully-qualified image names
(short names fail there); regenerate the config reference doc.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
Default the model-weights puller image to the platform nmp-api image
(resolved via get_qualified_image("nmp-api"), so registry/tag follow
platform config and remain overridable), replacing the bespoke
nds-v2-huggingface-cli image. The nmp-api image already ships the
huggingface_hub CLI, so the k8s weight-puller Job overrides the
container entrypoint to `hf` (command=["hf"]) and runs
`hf download <repo> --local-dir /model-store [...]`.

Regenerate the config reference doc for the new default.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
Composition over inheritance for the k8s reconcilers (review #2/#3):
* Extract StatusProjector (pod-status projection, crash-loop/pending-timeout
  error builders, host URL) and ResourceDeleter (idempotent 404-tolerant
  delete) as standalone collaborators.
* Reconciler is now a pure interface (the 5 verbs); NimOperatorReconciler and
  K8sReconciler compose the projector + deleter instead of inheriting them.
  The backend builds both collaborators in init() and injects them.

Thread the reconcile context through the backend interface (review #19):
* create/update/get_model_deployment_status now take a single
  ctx: ModelContext instead of (deployment, config, model_entity); applied
  across the ServiceBackend ABC and the docker / none / k8s backends, the
  deployment reconciler call sites, and the test mocks. delete stays
  (workspace, name).

Fixes + nits:
* Harden NIMService status read against a null status/state (review #15):
  (nim_status.get("state") or "").lower() can no longer raise.
* Convert nim_operator logging to structured extra={} (review #13); avoid the
  reserved LogRecord 'name' key (use resource_name / deployment_name).
* Flatten the Files-service create/update branches into a guard-clause helper
  (review #14).
* compile_puller_job: rename args -> container_args (review #17).
* Reconciler nits: import the vllm_k8s_compiler module under its full name
  (review #7), reflow the P3 (a)/(b) comment (review #8), quote values in the
  model-source error (review #9), drop the _ = image_pull_secrets dance
  (review #12), name the event-message cap MAX_EVENT_MESSAGE_CHARS (review #6),
  and document the _select_reconciler None contract (review #16).

Signed-off-by: Ben McCown <bmccown@nvidia.com>
The vLLM k8s Deployment stamped the resolved health path as the
nmp.nvidia.com/health-path annotation on both the Deployment metadata and
the pod template. Nothing ever read it back: the status path recomputes the
health path via resolve_health_path(engine, view) each cycle, and the probes
consume health_path directly. It was leftover write-only state from an earlier
iteration where status recovered the path from the annotation.

Remove the HEALTH_PATH_ANNOTATION constant and both write sites; the probes are
unaffected. Drop the two test assertions on the annotation (the health_path ->
probe behavior is already covered). Unrelated to the docker path's
HEALTH_PATH_LABEL, which is genuinely read and is left untouched.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
delete_one() promised that any non-404 failure is returned as a short error
string so the caller can aggregate it, but only ApiException / NotFoundError /
ForbiddenError were caught. Any other failure (transport/connection error,
dynamic API error, ...) would escape and abort the caller's per-resource delete
loop partway -- leaving later resources unattempted and risking a false DELETED.

Replace the ForbiddenError-specific handler with a catch-all except Exception so
every non-404 failure is routed through _classify_delete_error and returned, not
raised. Add unit tests for delete_one covering 404 success (typed + dynamic),
403 -> classified, and an unexpected exception -> classified (not raised).

Signed-off-by: Ben McCown <bmccown@nvidia.com>
Signed-off-by: Ben McCown <bmccown@nvidia.com>
@benmccown
benmccown force-pushed the vllm-k8s-support/bmccown branch from bde648d to d04a705 Compare June 24, 2026 22:11
@benmccown
benmccown enabled auto-merge June 24, 2026 22:13
@benmccown
benmccown added this pull request to the merge queue Jun 24, 2026
Merged via the queue into main with commit 9133d7c Jun 24, 2026
53 checks passed
@benmccown
benmccown deleted the vllm-k8s-support/bmccown branch June 24, 2026 22:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants