Skip to content

feat(models): support LoRA deployments on the docker backend - #870

Merged
benmccown merged 2 commits into
mainfrom
docker-lora-support
Jul 23, 2026
Merged

feat(models): support LoRA deployments on the docker backend#870
benmccown merged 2 commits into
mainfrom
docker-lora-support

Conversation

@benmccown

@benmccown benmccown commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

The deployments-plugin docker backend was single-container: it rejected init_containers and multi-container DeploymentConfigs, so LoRA deployments (server + adapters sidecar + lora-cache-init) could not run there. The models controller fast-failed docker + LoRA with a "single-container" ERROR and told users to deploy on kubernetes.

This adds targeted multi-container orchestration to the docker backend so it can run the LoRA shape the models compiler already emits, then lifts the guardrail.

Changes

  • backends/docker/containers.py — replace the single-container validator with build_docker_plan(), which splits a DeploymentConfig into init containers, a primary (server) container, and sidecars. Rejects sidecars that declare host ports (they share the primary's netns).
  • backends/docker/backend.py
    • create_deployment: run init containers to completion in order → primary (publishes host ports, owns GPUs) → sidecars that join the primary's network namespace (network=container:<server>) and share volumes, with no ports/GPUs of their own. Group image pulls reuse the existing _pull_image (NGC auth) and fall back to a locally present image (e.g. the local-only nmp-api adapters sidecar) instead of failing.
    • read_status: gate READY on sidecar health (a stopped sidecar keeps the deployment STARTING; its Always restart policy recreates it).
    • delete_deployment: tear down the whole labeled container group (primary + sidecars), not just the primary.
  • backends/labels.py — add companion_container_name and a container-role label so group members are discoverable.
  • models deployments_plugin/backend.py — remove the docker + LoRA fast-fail guardrail.

Tests

New/updated unit tests (deployments-plugin: 302 passed; models + reconciler suites green; lint clean):

  • test_containers.py — plan builder splits init/primary/sidecars; rejects sidecar ports / empty config.
  • docker backend mocked — multi-container create runs init→server→sidecar with network=container:<server> and no sidecar ports; init non-zero fails before the server starts; delete removes the whole group; pull-fallback (local image present vs absent).
  • Updated the two tests that asserted the old guardrail (docker + LoRA now creates substrate; generalized the reconciler backend-error-persistence test).

Manual verification (dev pod, docker backend)

docker vLLM + LoRA (Qwen3-1.7B + linear-algebra adapter):

  • deploy accepted (no guardrail) → lora-cache-init runs to completion → server + lora-adapters sidecar come up as a group (sidecar shares the server netns) → READY.
  • sidecar loads default--linear-algebra into vLLM over shared localhost; provider /v1/models lists base + adapter.
  • gateway chat-completion works for both the base model and the adapter.
  • delete tears down the full group and frees the GPU.

Notes

  • Builds on fix(models): make docker weights volume writable for the HF weight-puller #853 (docker weights-volume puller fix), now merged to main.
  • Branch name is a leftover convention; the change is the docker multi-container LoRA support above.
  • NIM + LoRA on docker should work through the same multi-container path but was not separately exercised in the pod.
  • Local sidecar image requirement: the LoRA adapters sidecar runs the nmp-api image. On the docker backend this image must already be present on the host (built/loaded locally, e.g. nmp-api:local) — it is a local-only tag and not pullable from a registry. The pull step tolerates this via a local-copy fallback, but a fresh host without the image will fail at sidecar start. This is inherent to the local docker dev/test flow (k8s pulls the platform image normally).

Review feedback addressed

Follow-up to the review on PR #870 (tylersbray non-blocking approval + CodeRabbit nits):

  • _sidecars_healthy now derives the expected sidecar set from the DeploymentConfig, so a removed (not just exited) sidecar is detected as not-ready; single-container deployments short-circuit the containers.list entirely.
  • Narrowed the image-pull local-fallback except to docker ImageNotFound/NotFound so unexpected client errors aren't masked.
  • Added a known-limitation comment on the sidecar network=container:<server> netns coupling across a server recreate.
  • Added a comment that init containers are intentionally CPU-only.
  • Strengthened the docker-LoRA backend test to assert the full multi-container contract (server + adapters sidecar, sidecar has no ports/GPU, server owns port + GPU, lora-cache-init present).
  • Added read_status tests for sidecar readiness gating: running sidecar → READY; stopped sidecar → STARTING; removed sidecar → STARTING.
  • Deferred (follow-up): gating READY on the sidecar having actually loaded the adapter into vLLM (a sidecar readiness probe / "loaded" marker), rather than just the process running.

Summary by CodeRabbit

  • New Features
    • Docker deployments now support a multi-container setup: init containers, a primary server, and sidecars.
    • Sidecars run as companions to the server (shared networking; they can’t publish host ports).
    • Deployment readiness now also checks that expected sidecars are present and running.
  • Bug Fixes
    • Image pull failures fall back to locally available images when possible; otherwise deployments fail cleanly.
    • Init failures or sidecar startup failures prevent partial deployments.
    • Deleting a deployment now removes the entire container group, not just the primary container.
    • Docker-based LoRA deployments are no longer rejected during model deployment creation.

@github-actions github-actions Bot added the feat label Jul 23, 2026
The deployments-plugin docker backend was single-container: it rejected
init_containers and multi-container DeploymentConfigs, so LoRA deployments
(server + adapters sidecar + lora-cache-init) could not run and the models
controller fast-failed docker + LoRA with a 'single-container' ERROR.

Add targeted multi-container orchestration to the docker backend so it can run
the LoRA shape the models compiler already emits, then lift the guardrail:

- containers.py: replace the single-container validator with build_docker_plan,
  which splits a DeploymentConfig into init containers, a primary (server)
  container, and sidecars. Rejects sidecars that declare host ports.
- backend.py create_deployment: run init containers to completion in order,
  then the primary (publishes host ports, owns GPUs), then sidecars that join
  the primary's network namespace (network=container:<server>) and share
  volumes, with no ports/GPUs of their own. Image pulls tolerate local-only
  images (e.g. the nmp-api sidecar) by falling back to a locally present copy.
- backend.py read_status: gate READY on sidecar health (a stopped sidecar keeps
  the deployment in STARTING; its Always restart policy recreates it).
- backend.py delete_deployment: tear down the whole labeled container group
  (primary + sidecars), not just the primary.
- labels.py: add companion_container_name and a container-role label so group
  members are discoverable.
- models deployments_plugin/backend.py: remove the docker + LoRA fast-fail.

Verified in a docker-backend dev deployment: vLLM + LoRA reaches READY, the
adapters sidecar loads the adapter into vLLM over the shared network namespace,
and gateway chat-completion works for both the base model and the adapter;
delete tears down the full group.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
@benmccown
benmccown force-pushed the docker-lora-support branch from 4d13fcb to 034c3d8 Compare July 23, 2026 19:17
@benmccown
benmccown marked this pull request as ready for review July 23, 2026 19:27
@benmccown
benmccown requested review from a team as code owners July 23, 2026 19:27
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 75581452-35eb-44ca-8a84-01c9e4279372

📥 Commits

Reviewing files that changed from the base of the PR and between 034c3d8 and 97fd6a8.

📒 Files selected for processing (4)
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/backend.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/containers.py
  • plugins/nemo-deployments/tests/unit/backends/docker/test_backend_mocked.py
  • services/core/models/tests/unit/controllers/backends/deployments_plugin/test_backend.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • services/core/models/tests/unit/controllers/backends/deployments_plugin/test_backend.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/containers.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/backend.py

📝 Walkthrough

Walkthrough

Docker deployments now support grouped init, server, and sidecar containers. Startup, image handling, readiness, rollback, and deletion operate across the group. Docker LoRA deployments proceed through model deployment creation instead of failing validation.

Changes

Docker multi-container deployment

Layer / File(s) Summary
Deployment plan and container roles
plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/containers.py, plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/labels.py, plugins/nemo-deployments/tests/unit/backends/docker/*
Configurations are split into primary, init, and sidecar containers; sidecars cannot publish ports, and role labels identify grouped containers.
Grouped image pulling and startup
plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/backend.py, plugins/nemo-deployments/tests/unit/backends/docker/*
All images are handled with local fallback; init containers run first, followed by the server and shared-network sidecars, with rollback and failure-path coverage.
Grouped readiness and deletion
plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/backend.py, plugins/nemo-deployments/tests/unit/backends/docker/test_backend_mocked.py
Always-restarting deployments require healthy sidecars, and deletion removes the complete container group.

LoRA deployment integration

Layer / File(s) Summary
Docker LoRA deployment creation
services/core/models/src/.../deployments_plugin/backend.py, services/core/models/tests/unit/controllers/backends/deployments_plugin/test_backend.py
Docker LoRA configurations proceed through substrate and deployment configuration creation with init and sidecar containers.
Generic backend error persistence
services/core/models/tests/unit/controllers/test_deployment_reconciler.py
Backend-provided error messages are persisted unchanged.

Sequence Diagram(s)

sequenceDiagram
  participant ModelDeploymentController
  participant DockerDeploymentBackend
  participant DockerDeploymentPlan
  participant DockerEngine
  ModelDeploymentController->>DockerDeploymentBackend: create_deployment(config)
  DockerDeploymentBackend->>DockerDeploymentPlan: build_docker_plan(config)
  DockerDeploymentBackend->>DockerEngine: pull all group images
  DockerDeploymentBackend->>DockerEngine: run init containers sequentially
  DockerDeploymentBackend->>DockerEngine: start server container
  DockerDeploymentBackend->>DockerEngine: start sidecars in server network
  DockerDeploymentBackend->>DockerEngine: check sidecar readiness
Loading

Possibly related PRs

Suggested labels: test

Suggested reviewers: tylersbray, anubhutivyas, crookedstorm

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.65% 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 The title accurately summarizes the main change: enabling LoRA deployments on the Docker backend.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docker-lora-support

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.

🧹 Nitpick comments (2)
services/core/models/tests/unit/controllers/backends/deployments_plugin/test_backend.py (1)

179-182: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the LoRA container shape, not just the server deployment.

This only proves that my-dep-server was created. A regression that drops the adapters sidecar—or its shared-network, role, port, or GPU settings—would still pass. Inspect the created server configuration and assert the multi-container LoRA contract.

🤖 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/tests/unit/controllers/backends/deployments_plugin/test_backend.py`
around lines 179 - 182, Strengthen the test around the created “my-dep-server”
Deployment by inspecting its server configuration and asserting the complete
multi-container LoRA contract, including the adapters sidecar, shared network,
container role, port, and GPU settings. Keep the existing creation assertion,
but verify the relevant configuration fields rather than only the deployment
name.
plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/backend.py (1)

544-578: 🚀 Performance & Scalability | 🔵 Trivial

Extra containers.list on every readiness poll. _sidecars_healthy runs a labeled containers.list for every ready Always deployment, including single-container ones with no sidecars — added daemon load on the reconciler's hot path. Consider gating the enumeration behind a cheap signal (e.g. a per-deployment "has sidecars" marker/label set at create time) so single-container deployments skip the query.

🤖 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
`@plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/backend.py`
around lines 544 - 578, The _sidecars_healthy method performs a Docker
containers.list query on every readiness poll, even for deployments without
sidecars. Add and maintain a cheap per-deployment sidecar-presence signal, such
as a label set during creation, and have _sidecars_healthy return the existing
healthy result without enumerating containers when that signal indicates no
sidecars; preserve the current labeled lookup and health checks for deployments
marked as having sidecars.
🤖 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
`@plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/backend.py`:
- Around line 544-578: The _sidecars_healthy method performs a Docker
containers.list query on every readiness poll, even for deployments without
sidecars. Add and maintain a cheap per-deployment sidecar-presence signal, such
as a label set during creation, and have _sidecars_healthy return the existing
healthy result without enumerating containers when that signal indicates no
sidecars; preserve the current labeled lookup and health checks for deployments
marked as having sidecars.

In
`@services/core/models/tests/unit/controllers/backends/deployments_plugin/test_backend.py`:
- Around line 179-182: Strengthen the test around the created “my-dep-server”
Deployment by inspecting its server configuration and asserting the complete
multi-container LoRA contract, including the adapters sidecar, shared network,
container role, port, and GPU settings. Keep the existing creation assertion,
but verify the relevant configuration fields rather than only the deployment
name.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8ec38fb6-ef60-4987-a76b-bd1a1639108b

📥 Commits

Reviewing files that changed from the base of the PR and between 840f160 and 034c3d8.

📒 Files selected for processing (9)
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/backend.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/docker/containers.py
  • plugins/nemo-deployments/src/nemo_deployments_plugin/backends/labels.py
  • plugins/nemo-deployments/tests/unit/backends/docker/docker_helpers.py
  • plugins/nemo-deployments/tests/unit/backends/docker/test_backend_mocked.py
  • plugins/nemo-deployments/tests/unit/backends/docker/test_containers.py
  • services/core/models/src/nmp/core/models/controllers/backends/deployments_plugin/backend.py
  • services/core/models/tests/unit/controllers/backends/deployments_plugin/test_backend.py
  • services/core/models/tests/unit/controllers/test_deployment_reconciler.py
💤 Files with no reviewable changes (1)
  • services/core/models/src/nmp/core/models/controllers/backends/deployments_plugin/backend.py

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 27140/34858 77.9% 62.1%
Integration Tests 15925/33570 47.4% 19.9%

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

Non-blocking approval, thanks for tackling this. The comments in the main body of the code are really helpful. GPU accounting looks good. New test coverage looks good despite the nit for one more and I appreciate the lora fixture.

Review follow-ups for the docker multi-container LoRA support:

- _sidecars_healthy now derives the expected sidecar set from the
  DeploymentConfig, so a *removed* (not just exited) sidecar is detected as
  not-ready. Deployments with no sidecars short-circuit and skip the Docker
  containers.list on the readiness hot path.
- Narrow the image-pull local-fallback except to docker ImageNotFound/NotFound
  so unexpected client errors are not masked as 'image present locally'.
- Document the sidecar network=container:<server> netns coupling as a known
  limitation across a server-container recreate.
- Note that init containers are intentionally CPU-only (no device_requests).
- Strengthen the docker LoRA backend test to assert the full multi-container
  contract (server + adapters sidecar; sidecar has no ports/GPU; server owns
  the port and GPU; lora-cache-init init present).
- Add read_status tests for sidecar readiness gating: running sidecar -> READY,
  stopped sidecar -> STARTING, removed sidecar -> STARTING.

Signed-off-by: Ben McCown <bmccown@nvidia.com>
@benmccown
benmccown added this pull request to the merge queue Jul 23, 2026
Merged via the queue into main with commit 34408a0 Jul 23, 2026
60 checks passed
@benmccown
benmccown deleted the docker-lora-support branch July 23, 2026 21:36
AnuradhaKaruppiah pushed a commit to AnuradhaKaruppiah/nemo-platform that referenced this pull request Jul 24, 2026
…NeMo#870)

* feat(models): support LoRA deployments on the docker backend

The deployments-plugin docker backend was single-container: it rejected
init_containers and multi-container DeploymentConfigs, so LoRA deployments
(server + adapters sidecar + lora-cache-init) could not run and the models
controller fast-failed docker + LoRA with a 'single-container' ERROR.

Add targeted multi-container orchestration to the docker backend so it can run
the LoRA shape the models compiler already emits, then lift the guardrail:

- containers.py: replace the single-container validator with build_docker_plan,
  which splits a DeploymentConfig into init containers, a primary (server)
  container, and sidecars. Rejects sidecars that declare host ports.
- backend.py create_deployment: run init containers to completion in order,
  then the primary (publishes host ports, owns GPUs), then sidecars that join
  the primary's network namespace (network=container:<server>) and share
  volumes, with no ports/GPUs of their own. Image pulls tolerate local-only
  images (e.g. the nmp-api sidecar) by falling back to a locally present copy.
- backend.py read_status: gate READY on sidecar health (a stopped sidecar keeps
  the deployment in STARTING; its Always restart policy recreates it).
- backend.py delete_deployment: tear down the whole labeled container group
  (primary + sidecars), not just the primary.
- labels.py: add companion_container_name and a container-role label so group
  members are discoverable.
- models deployments_plugin/backend.py: remove the docker + LoRA fast-fail.

Verified in a docker-backend dev deployment: vLLM + LoRA reaches READY, the
adapters sidecar loads the adapter into vLLM over the shared network namespace,
and gateway chat-completion works for both the base model and the adapter;
delete tears down the full group.

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

* fix(models): address docker LoRA review feedback

Review follow-ups for the docker multi-container LoRA support:

- _sidecars_healthy now derives the expected sidecar set from the
  DeploymentConfig, so a *removed* (not just exited) sidecar is detected as
  not-ready. Deployments with no sidecars short-circuit and skip the Docker
  containers.list on the readiness hot path.
- Narrow the image-pull local-fallback except to docker ImageNotFound/NotFound
  so unexpected client errors are not masked as 'image present locally'.
- Document the sidecar network=container:<server> netns coupling as a known
  limitation across a server-container recreate.
- Note that init containers are intentionally CPU-only (no device_requests).
- Strengthen the docker LoRA backend test to assert the full multi-container
  contract (server + adapters sidecar; sidecar has no ports/GPU; server owns
  the port and GPU; lora-cache-init init present).
- Add read_status tests for sidecar readiness gating: running sidecar -> READY,
  stopped sidecar -> STARTING, removed sidecar -> STARTING.

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

---------

Signed-off-by: Ben McCown <bmccown@nvidia.com>
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