Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any

from nemo_deployments_plugin.backends.labels import docker_volume_name
Expand All @@ -16,17 +17,70 @@ class DeploymentConfigError(ValueError):
"""Invalid deployment config for docker backend."""


@dataclass(frozen=True)
class DockerDeploymentPlan:
"""The docker backend's view of a (possibly multi-container) DeploymentConfig.

- ``init_containers`` run to completion, in order, before the group starts.
- ``primary`` is the server container; it keeps the canonical deployment
container name, publishes host ports, and owns any GPUs.
- ``sidecars`` share the primary's network namespace (``network=container:``)
and volumes; they do not publish host ports or own GPUs.
"""

primary: Container
init_containers: list[Container] = field(default_factory=list)
sidecars: list[Container] = field(default_factory=list)

@property
def is_multi_container(self) -> bool:
"""True when the plan has any init containers or sidecars beyond the primary."""
return bool(self.init_containers or self.sidecars)


def parse_docker_backend_config(backend_config: dict[str, Any]) -> DockerDeploymentConfig:
docker_section = backend_config.get("docker") or {}
return DockerDeploymentConfig.model_validate(docker_section)


def build_docker_plan(config: DeploymentConfig) -> DockerDeploymentPlan:
"""Split a DeploymentConfig into a docker orchestration plan.

The first container is the primary/server; any additional containers are
sidecars sharing the primary's network namespace and volumes (e.g. the LoRA
adapters sidecar). Init containers run to completion first.

Raises DeploymentConfigError for shapes the docker backend cannot honor.
"""
if not config.containers:
raise DeploymentConfigError("docker backend requires at least one container")

primary = config.containers[0]
sidecars = list(config.containers[1:])

# Sidecars share the primary's netns, so they cannot publish their own host
# ports. (The primary owns the published ports for the whole group.)
for sidecar in sidecars:
if sidecar.ports:
raise DeploymentConfigError(
f"docker sidecar container '{sidecar.name}' may not declare ports; "
"it shares the primary container's network namespace"
)

return DockerDeploymentPlan(
primary=primary,
init_containers=list(config.init_containers),
sidecars=sidecars,
)


def validate_config_for_docker(config: DeploymentConfig) -> Container:
if config.init_containers:
raise DeploymentConfigError("init_containers are not supported by the docker backend in v1")
if len(config.containers) != 1:
raise DeploymentConfigError(f"docker backend v1 supports exactly one container; got {len(config.containers)}")
return config.containers[0]
"""Back-compat single-container accessor: returns the primary container.

Retained for callers/tests that only need the primary; use
:func:`build_docker_plan` for multi-container orchestration.
"""
return build_docker_plan(config).primary


def restart_policy_kwargs(restart_policy: RestartPolicy, backoff_limit: int) -> dict[str, Any]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
VOLUME_WORKSPACE_LABEL = "nemo.nvidia.com/volume-workspace"
VOLUME_NAME_LABEL = "nemo.nvidia.com/volume-name"
BACKOFF_LIMIT_LABEL = "nemo.nvidia.com/backoff-limit"
# Role of a docker container within a multi-container deployment group
# (e.g. "server" or a sidecar container name). Used to discover companion
# containers (LoRA adapters sidecar) that share the primary server container.
CONTAINER_ROLE_LABEL = "nemo.nvidia.com/container-role"

# The primary container in a deployment group keeps the canonical deployment
# name (``container_name``); companions derive their names from it plus role.
CONTAINER_ROLE_SERVER = "server"


def deployment_key(workspace: str, name: str) -> str:
Expand All @@ -33,10 +41,24 @@ def deployment_key(workspace: str, name: str) -> str:


def container_name(workspace: str, deployment_name: str) -> str:
"""Docker container name for a deployment (``dep-`` prefix, hashed identity)."""
"""Docker container name for a deployment (``dep-`` prefix, hashed identity).

This is the *primary* (server) container of a deployment group. Companion
containers (init/sidecar) derive their names from it via
:func:`companion_container_name`.
"""
return k8s_deployment_resource_name(workspace, deployment_name)


def companion_container_name(workspace: str, deployment_name: str, role: str) -> str:
"""Docker container name for a companion (sidecar/init) container in a group.

Derived from the primary container name so the whole group shares the
``dep-<hash>`` stem and is easy to discover/tear down together.
"""
return f"{container_name(workspace, deployment_name)}-{role}"


def docker_volume_name(workspace: str, volume_name: str) -> str:
"""Docker volume name for a deployment volume (``dep-vol-`` prefix, hashed identity)."""
return k8s_volume_resource_name(workspace, volume_name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from typing import Any

from nemo_deployments_plugin.entities import Container, DeploymentConfig
from nemo_deployments_plugin.entities import Container, ContainerPort, DeploymentConfig, VolumeMount
from nemo_deployments_plugin.types import RestartPolicy


Expand All @@ -27,6 +27,49 @@ def sample_config(*, restart_policy: RestartPolicy = "Always") -> DeploymentConf
)


def lora_config(*, restart_policy: RestartPolicy = "Always") -> DeploymentConfig:
"""A LoRA-shaped multi-container config: init + server + adapters sidecar.

Mirrors what the models compiler emits for docker + LoRA (server publishes
:8000, adapters sidecar shares the server netns and the scratch volume).
"""
return DeploymentConfig(
name="cfg1",
workspace="default",
initContainers=[
Container(
name="lora-cache-init",
image="docker.io/library/busybox:latest",
command=["sh", "-c", "mkdir -p /scratch/loras && chmod -R 777 /scratch/loras"],
volumeMounts=[VolumeMount(name="scratch", mountPath="/scratch")],
)
],
containers=[
Container(
name="server",
image="vllm/vllm-openai:v0.22.1",
command=["vllm", "serve"],
args=["/model-store"],
ports=[ContainerPort(name="http", containerPort=8000)],
volumeMounts=[
VolumeMount(name="weights", mountPath="/model-store", readOnly=True),
VolumeMount(name="scratch", mountPath="/scratch"),
],
),
Container(
name="lora-adapters",
image="my-registry/nmp-api:local",
command=["nemo", "services", "run", "--sidecars", "adapters"],
volumeMounts=[
VolumeMount(name="weights", mountPath="/model-store", readOnly=True),
VolumeMount(name="scratch", mountPath="/scratch"),
],
),
],
restart_policy=restart_policy, # ty: ignore[unknown-argument]
)


def container_attrs(*, status: str = "running", exit_code: int = 0) -> dict[str, Any]:
del status
return {
Expand Down
Loading