Unverified Commit 68ac71c4 authored by Biswa Panda's avatar Biswa Panda Committed by GitHub
Browse files

feat: portable dynamo build (#1215)

parent 0594235b
......@@ -196,7 +196,7 @@ func RetrieveDynamoGraphConfigurationFile(ctx context.Context, url string) (*byt
}
// Extract the YAML file
yamlFileName := "bento.yaml"
yamlFileName := "dynamo.yaml"
yamlContent, err := archive.ExtractFileFromTar(tarData, yamlFileName)
if err != nil {
return nil, err
......
# Use ARG to allow base image to be specified at build time
ARG BASE_IMAGE=__BASE_IMAGE__
FROM ${BASE_IMAGE}
# Build arguments for user configuration
ARG USER_ID=1024
ARG GROUP_ID=1024
ARG USERNAME=dynamo
ARG GROUPNAME=dynamo
ARG HOME_DIR=/home/${USERNAME}
# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV PATH="${HOME_DIR}/.local/bin:$PATH"
ENV PYTHONPATH="${HOME_DIR}/app:$PYTHONPATH"
# Create group and user
RUN if [ "$(id -u)" != "0" ]; then \
echo "Using sudo for user/group creation"; \
sudo groupadd --gid ${GROUP_ID} ${GROUPNAME} \
&& sudo useradd --uid ${USER_ID} --gid ${GROUP_ID} --create-home --shell /bin/bash ${USERNAME} \
&& sudo mkdir -p ${HOME_DIR}/app \
&& sudo mkdir -p ${HOME_DIR}/.local/bin \
&& sudo mkdir -p ${HOME_DIR}/.cache/pip \
&& sudo chown -R ${USERNAME}:${GROUPNAME} ${HOME_DIR}; \
else \
echo "Running as root, no sudo needed"; \
groupadd --gid ${GROUP_ID} ${GROUPNAME} \
&& useradd --uid ${USER_ID} --gid ${GROUP_ID} --create-home --shell /bin/bash ${USERNAME} \
&& mkdir -p ${HOME_DIR}/app \
&& mkdir -p ${HOME_DIR}/.local/bin \
&& mkdir -p ${HOME_DIR}/.cache/pip \
&& chown -R ${USERNAME}:${GROUPNAME} ${HOME_DIR}; \
fi
# Switch to non-root user
USER ${USERNAME}
WORKDIR ${HOME_DIR}/app
# Copy application code
COPY --chown=${USERNAME}:${GROUPNAME} . .
RUN chmod +x ${HOME_DIR}/app
This diff is collapsed.
......@@ -22,10 +22,11 @@ import importlib.metadata
import typer
from rich.console import Console
from dynamo.sdk.cli.build import build
from dynamo.sdk.cli.deployment import app as deployment_app
from dynamo.sdk.cli.deployment import deploy
from dynamo.sdk.cli.env import env
from dynamo.sdk.cli.pipeline import build, get
from dynamo.sdk.cli.pipeline import get
from dynamo.sdk.cli.run import run
from dynamo.sdk.cli.serve import serve
......
......@@ -140,7 +140,7 @@ def _handle_deploy_create(
# TODO: hardcoding this is a hack to get the services for the deployment
# we should find a better way to do this once build is finished/generic
configure_target_environment(TargetEnum.BENTO)
configure_target_environment(TargetEnum.DYNAMO)
entry_service = load_entry_service(pipeline)
deployment_manager = get_deployment_manager(target, endpoint)
......
......@@ -164,7 +164,7 @@ def serve(
sys.path.insert(0, working_dir_str)
svc = find_and_load_service(dynamo_pipeline, working_dir=working_dir)
logger.info(f"Loaded service: {svc.name}")
logger.debug(f"Loaded service: {svc.name}")
logger.debug("Dependencies: %s", [dep.on.name for dep in svc.dependencies.values()])
LinkedServices.remove_unused_edges()
......
......@@ -363,11 +363,7 @@ def resolve_service_config(
def configure_target_environment(target: TargetEnum):
from dynamo.sdk.core.lib import set_target
if target == TargetEnum.BENTO:
from dynamo.sdk.core.runner.bentoml import BentoDeploymentTarget
target = BentoDeploymentTarget()
elif target == TargetEnum.DYNAMO:
if target == TargetEnum.DYNAMO:
from dynamo.sdk.core.runner.dynamo import LocalDeploymentTarget
target = LocalDeploymentTarget()
......@@ -393,7 +389,7 @@ def is_local_planner_enabled(svc: Any, service_configs: dict) -> bool:
planners = [
node
for node in nodes
if node.config.get("dynamo", {}).get("component_type") == ComponentType.PLANNER
if node.config.dynamo.component_type == ComponentType.PLANNER
]
if len(planners) > 1:
......@@ -429,7 +425,7 @@ def raise_local_planner_warning(svc: Any, service_configs: dict) -> None:
nodes.append(svc)
worker_names = ("PrefillWorker", "VllmWorker")
worker_counts_greater_than_one = [
node.config.get("workers", 1) > 1 for node in nodes if node.name in worker_names
node.config.workers > 1 for node in nodes if node.name in worker_names
]
if any(worker_counts_greater_than_one) and not no_op:
......
......@@ -14,15 +14,15 @@
# limitations under the License.
# Modifications Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES
import logging
import os
from typing import Any, Callable, Dict, Optional, Type, TypeVar, Union
from typing import Any, Callable, Optional, Type, TypeVar
from fastapi import FastAPI
from dynamo.sdk.core.protocol.interface import (
DependencyInterface,
DeploymentTarget,
DynamoConfig,
ServiceConfig,
ServiceInterface,
)
......@@ -33,6 +33,7 @@ G = TypeVar("G", bound=Callable[..., Any])
# this should be set to a concrete implementation of the DeploymentTarget interface
_target: DeploymentTarget
logger = logging.getLogger(__name__)
DYNAMO_IMAGE = os.getenv("DYNAMO_IMAGE", "dynamo:latest-vllm")
......@@ -49,36 +50,25 @@ def get_target() -> DeploymentTarget:
return _target
# TODO: dynamo_component
def service(
inner: Optional[Type[G]] = None,
/,
*,
dynamo: Optional[Union[Dict[str, Any], DynamoConfig]] = None,
app: Optional[FastAPI] = None,
system_app: Optional[FastAPI] = None,
**kwargs: Any,
) -> Any:
"""Service decorator that's adapter-agnostic"""
config = ServiceConfig(kwargs)
# Parse dict into DynamoConfig object
dynamo_config: Optional[DynamoConfig] = None
if dynamo is not None:
if isinstance(dynamo, dict):
dynamo_config = DynamoConfig(**dynamo)
else:
dynamo_config = dynamo
assert isinstance(dynamo_config, DynamoConfig)
config = ServiceConfig(**kwargs)
logger.info(f"inner: {inner} config: {config}")
def decorator(inner: Type[G]) -> ServiceInterface[G]:
provider = get_target()
if inner is not None:
dynamo_config.name = inner.__name__
config.dynamo.name = inner.__name__
return provider.create_service(
service_cls=inner,
config=config,
dynamo_config=dynamo_config,
app=app,
system_app=system_app,
**kwargs,
......
......@@ -97,12 +97,16 @@ class DeploymentStatus(str, Enum):
@dataclass
class ScalingPolicy:
"""Scaling policy."""
policy: str
parameters: t.Dict[str, t.Union[int, float, str]] = field(default_factory=dict)
@dataclass
class Env:
"""Environment variable."""
name: str
value: str = ""
......
......@@ -16,17 +16,39 @@
from abc import ABC, abstractmethod
from collections import defaultdict
from dataclasses import dataclass
from enum import Enum, auto
from typing import Any, Dict, Generic, List, Optional, Set, Tuple, Type, TypeVar
from fastapi import FastAPI
from pydantic import BaseModel
from dynamo.sdk.core.protocol.deployment import Env
T = TypeVar("T", bound=object)
class LeaseConfig(BaseModel):
"""Configuration for custom dynamo leases"""
ttl: int = 1 # seconds
class ComponentType:
"""Types of Dynamo components"""
PLANNER = "planner"
class DynamoConfig(BaseModel):
"""Configuration for Dynamo components"""
enabled: bool = True
name: str | None = None
namespace: str | None = None
custom_lease: LeaseConfig | None = None
component_type: str | None = None # Indicates if this is a meta/system component
class DynamoTransport(Enum):
"""Transport types supported by Dynamo services"""
......@@ -34,10 +56,23 @@ class DynamoTransport(Enum):
HTTP = auto()
class ServiceConfig(Dict[str, Any]):
class ResourceConfig(BaseModel):
"""Configuration for Dynamo resources"""
cpu: int = 1
memory: str = "100Mi"
gpu: str = "0"
class ServiceConfig(BaseModel):
"""Base service configuration that can be extended by adapters"""
pass
dynamo: DynamoConfig
resource: ResourceConfig = ResourceConfig()
workers: int = 1
image: str | None = None
envs: List[Env] | None = None
labels: Dict[str, str] | None = None
class DynamoEndpointInterface(ABC):
......@@ -157,30 +192,6 @@ class ServiceInterface(Generic[T], ABC):
raise NotImplementedError()
@dataclass
class LeaseConfig:
"""Configuration for custom dynamo leases"""
ttl: int = 1 # seconds
class ComponentType:
"""Types of Dynamo components"""
PLANNER = "planner"
@dataclass
class DynamoConfig:
"""Configuration for Dynamo components"""
enabled: bool = True
name: str | None = None
namespace: str | None = None
custom_lease: LeaseConfig | None = None
component_type: str | None = None # Indicates if this is a meta/system component
class DeploymentTarget(ABC):
"""Interface for service provider implementations"""
......@@ -189,7 +200,6 @@ class DeploymentTarget(ABC):
self,
service_cls: Type[T],
config: ServiceConfig,
dynamo_config: Optional[DynamoConfig] = None,
app: Optional[FastAPI] = None,
**kwargs,
) -> ServiceInterface[T]:
......
......@@ -19,7 +19,6 @@ import logging
import os
import shlex
import sys
from dataclasses import asdict
from typing import Any, Dict, List, Optional, Set, Type, TypeVar
import psutil
......@@ -33,7 +32,6 @@ from dynamo.sdk.core.protocol.deployment import Env
from dynamo.sdk.core.protocol.interface import (
DependencyInterface,
DeploymentTarget,
DynamoConfig,
DynamoEndpointInterface,
DynamoTransport,
LinkedServices,
......@@ -71,20 +69,15 @@ class LocalService(ServiceMixin, ServiceInterface[T]):
self,
inner_cls: Type[T],
config: ServiceConfig,
dynamo_config: Optional[DynamoConfig] = None,
watcher: Optional[Watcher] = None,
socket: Optional[CircusSocket] = None,
app: Optional[FastAPI] = None,
system_app: Optional[FastAPI] = None,
):
self._inner_cls = inner_cls
self._config = config
name = inner_cls.__name__
self._dynamo_config = dynamo_config or DynamoConfig(
name=name, namespace="default"
)
# Add the dynamo config to the service config
self._config["dynamo"] = asdict(self._dynamo_config)
self._config = config
self._watcher = watcher
self._socket = socket
self.app = app or FastAPI(title=name)
......@@ -120,7 +113,7 @@ class LocalService(ServiceMixin, ServiceInterface[T]):
@property
def envs(self) -> List[Env]:
return self._config.get("envs", [])
return self._config.envs or []
@property
def inner(self) -> Type[T]:
......@@ -148,7 +141,7 @@ class LocalService(ServiceMixin, ServiceInterface[T]):
del self._dependencies[dep_key]
def dynamo_address(self) -> tuple[str, str]:
return (self._dynamo_config.namespace, self._dynamo_config.name)
return (self._config.dynamo.namespace, self._config.dynamo.name)
@property
def dependencies(self) -> dict[str, "DependencyInterface"]:
......@@ -217,7 +210,6 @@ class LocalDeploymentTarget(DeploymentTarget):
self,
service_cls: Type[T],
config: ServiceConfig,
dynamo_config: Optional[DynamoConfig] = None,
app: Optional[FastAPI] = None,
system_app: Optional[FastAPI] = None,
**kwargs,
......@@ -261,7 +253,6 @@ class LocalDeploymentTarget(DeploymentTarget):
return LocalService(
inner_cls=service_cls,
config=config,
dynamo_config=dynamo_config,
watcher=watcher,
socket=socket,
)
......
......@@ -208,7 +208,7 @@ def _get_dir_size(path: str) -> int:
def load_entry_service(
pipeline_tag: str, build_dir: str = "~/bentoml/bentos"
pipeline_tag: str, build_dir: str = "~/.dynamo/packages"
) -> Service:
"""
Given a built pipeline tag (e.g. frontend:2uk2fwzvqsswvs7t), load the entry service as a deployment Service instance.
......@@ -220,7 +220,7 @@ def load_entry_service(
if not os.path.isdir(graph_dir):
raise FileNotFoundError(f"Pipeline directory not found: {graph_dir}")
config_path = os.path.join(graph_dir, "bento.yaml")
config_path = os.path.join(graph_dir, "dynamo.yaml")
if not os.path.isfile(config_path):
raise FileNotFoundError(
f"Pipeline config (bento.yaml) not found in {graph_dir}"
......
......@@ -23,14 +23,12 @@ pytestmark = pytest.mark.pre_merge
@pytest.fixture(scope="module", autouse=True)
def setup_and_teardown():
configure_target_environment(TargetEnum.BENTO)
yield
configure_target_environment(TargetEnum.DYNAMO)
yield
def test_gpu_resources(setup_and_teardown):
"""Test resource configurations"""
from _bentoml_sdk import Service as BentoService
from dynamo.sdk import service
......@@ -42,7 +40,4 @@ def test_gpu_resources(setup_and_teardown):
def __init__(self) -> None:
pass
svc: BentoService = MyService.get_bentoml_service() # type: ignore
assert svc.config["resources"]["cpu"] == "2"
assert svc.config["resources"]["gpu"] == "1"
assert svc.config["resources"]["memory"] == "4Gi"
assert MyService.config is not None # type: ignore
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment