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
6 changes: 3 additions & 3 deletions mkdocs/docs/concepts/services.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Below is an example for running `Qwen/Qwen3.8-Flash-Next` on `H200`:
resources:
cpu: 4
router:
type: sglang
type: smg

- replicas: 2
commands:
Expand Down Expand Up @@ -363,7 +363,7 @@ Below is an example for running `zai-org/GLM-4.5-Air-FP8` on `H200`:
resources:
cpu: 4
router:
type: sglang
type: smg

- replicas: 4
commands:
Expand Down Expand Up @@ -537,7 +537,7 @@ groups:
resources:
cpu: 4..
router:
type: sglang
type: smg

- replicas: 2
commands:
Expand Down
2 changes: 1 addition & 1 deletion mkdocs/docs/concepts/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ and the [NCCL/RCCL tests](../examples/clusters/nccl-rccl-tests.md) example for r
master node.

> In a service, worker registration happens automatically when the router group sets
> [`router`](services.md#router) to `type: sglang`.
> [`router`](services.md#router) to `type: smg`.

<div editor-title=".dstack.yml">

Expand Down
6 changes: 3 additions & 3 deletions mkdocs/docs/examples/inference/sglang.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ To run SGLang with [PD disaggregation](https://docs.sglang.io/advanced_features/
resources:
cpu: 4
router:
type: sglang
type: smg

- replicas: 1..4
scaling:
Expand Down Expand Up @@ -240,7 +240,7 @@ To run SGLang with [PD disaggregation](https://docs.sglang.io/advanced_features/
--port 8000 \
--prefill-policy cache_aware
router:
type: sglang
type: smg
resources:
cpu: 4

Expand Down Expand Up @@ -321,7 +321,7 @@ To run SGLang with [PD disaggregation](https://docs.sglang.io/advanced_features/
resources:
cpu: 4..
router:
type: sglang
type: smg

- replicas: 1..2
scaling:
Expand Down
2 changes: 1 addition & 1 deletion mkdocs/docs/examples/inference/vllm.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ groups:
--port 8000 \
--prefill-policy cache_aware
router:
type: sglang
type: smg
resources:
cpu: 4

Expand Down
8 changes: 6 additions & 2 deletions mkdocs/docs/reference/dstack.yml/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,12 @@ The `service` configuration type allows running [services](../../concepts/servic
#SCHEMA# dstack._internal.core.models.configurations.ReplicaGroup
overrides:
show_root_heading: false
type:
required: true

##### `groups[n].router` { #router data-toc-label="router" }

#SCHEMA# dstack._internal.core.models.routers.ReplicaGroupRouterConfig
overrides:
show_root_heading: false

### `retry`

Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/core/models/configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,7 +1066,7 @@ class ReplicaGroup(CoreModel):
router: Annotated[
Optional[ReplicaGroupRouterConfig],
Field(
description="When set, replicas in this group run the in-service HTTP router (e.g. SGLang).",
description="When set, replicas in this group run the in-service HTTP router (e.g. SMG)",
),
] = None

Expand Down
43 changes: 34 additions & 9 deletions src/dstack/_internal/core/models/routers.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
from enum import Enum
from typing import Literal
from typing import Any

from pydantic import Field
from pydantic import Field, field_serializer, field_validator
from typing_extensions import Annotated

from dstack._internal.core.models.common import CoreModel


class RouterType(str, Enum):
SGLANG = "sglang"
SMG = "smg"
DYNAMO = "dynamo"


# The value `RouterType.SMG` had before the rename
_LEGACY_SMG_VALUE = "sglang"


class ReplicaGroupRouterConfig(CoreModel):
type: Annotated[
Literal["sglang", "dynamo"],
RouterType,
Field(
description=(
"The router implementation for this replica group. "
"`sglang` runs the SGLang router and dstack syncs worker URLs to it. "
"`dynamo` runs the NVIDIA Dynamo frontend, which discovers workers "
"itself via etcd/NATS."
"The router implementation for this replica group."
" `smg` runs the Shepherd Model Gateway (SMG) and dstack syncs worker URLs to it."
" `dynamo` runs the NVIDIA Dynamo frontend, which discovers workers"
" itself via etcd/NATS"
),
),
] = "sglang"
] = RouterType.SMG

@field_validator("type", mode="before")
@classmethod
def _accept_legacy_smg_value(cls, v: Any) -> Any:
# Stored run specs and older clients' requests use the legacy value
if v == _LEGACY_SMG_VALUE:
return RouterType.SMG
return v

@field_serializer("type", when_used="json")
def _serialize_type(self, v: RouterType) -> str:
# Servers and clients < 0.22.4 reject `smg`, so keep sending and storing the legacy value
# for one minor release. This covers client requests, server responses, and the run specs
# stored in the DB, which older server replicas read during a rolling deployment.
# TODO(0.23.0): Drop this serializer. When doing so, patch run specs in server responses
# back to the legacy value for clients < 0.22.4 (see `server/compatibility/runs.py`),
# since 0.23.* servers still support 0.22.* clients. Keep `_accept_legacy_smg_value`:
# run specs stored with the legacy value stay in the DB.
if v == RouterType.SMG:
return _LEGACY_SMG_VALUE
return v.value
14 changes: 7 additions & 7 deletions src/dstack/_internal/proxy/gateway/services/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,8 +349,8 @@ async def get_nginx_service_config(
) -> ServiceConfig:
limit_req_zones: list[LimitReqZoneConfig] = []
locations: list[LocationConfig] = []
sglang_limits: dict[str, LimitReqConfig] = {}
sglang_prefix_lengths: dict[str, int] = {} # Track prefix lengths for most-specific selection
router_limits: dict[str, LimitReqConfig] = {}
router_prefix_lengths: dict[str, int] = {} # Track prefix lengths for most-specific selection

for i, rate_limit in enumerate(service.rate_limits):
zone_name = f"{i}.{service.domain_safe}"
Expand All @@ -368,13 +368,13 @@ async def get_nginx_service_config(
if rate_limit.prefix == path or path.startswith(rate_limit.prefix):
# Use the longest prefix if multiple prefixes match the same path
current_prefix_len = len(rate_limit.prefix)
if path not in sglang_limits or current_prefix_len > sglang_prefix_lengths.get(
if path not in router_limits or current_prefix_len > router_prefix_lengths.get(
path, 0
):
sglang_limits[path] = LimitReqConfig(
router_limits[path] = LimitReqConfig(
zone=zone_name, burst=rate_limit.burst
)
sglang_prefix_lengths[path] = current_prefix_len
router_prefix_lengths[path] = current_prefix_len
else:
locations.append(
LocationConfig(
Expand All @@ -388,10 +388,10 @@ async def get_nginx_service_config(
for path in ROUTER_WHITELISTED_PATHS:
# Use prefix match for paths that end with a slash and exact match for paths that don't
if path.endswith("/"):
locations.append(LocationConfig(prefix=path, limit_req=sglang_limits.get(path)))
locations.append(LocationConfig(prefix=path, limit_req=router_limits.get(path)))
else:
locations.append(
LocationConfig(prefix=f"= {path}", limit_req=sglang_limits.get(path))
LocationConfig(prefix=f"= {path}", limit_req=router_limits.get(path))
)

# Don't auto-add / location for router-based services (catch-all 403 handles it)
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/proxy/lib/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
DEFAULT_SERVICE_READ_TIMEOUT = 300

# Inference endpoints exposed by the in-replica HTTP router. Applies to both
# SGLang's router and Dynamo's `dynamo.frontend` — they share the
# SMG and Dynamo's `dynamo.frontend` — they share the
# OpenAI-compatible endpoint surface.
ROUTER_WHITELISTED_PATHS: tuple[str, ...] = (
"/generate",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ class _StartupContext:
"""Dynamo-specific env (e.g. DSTACK_ROUTER_INTERNAL_IP) computed from the
router replica's state. Passed through to RunnerClient.submit_job, which
merges it into a deep-copied job_spec.env so the shared job_spec is not
mutated. None for SGLang services, non-router runs, and the router
mutated. None for SMG services, non-router runs, and the router
replica itself."""


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
from dstack._internal.server.services.logging import fmt
from dstack._internal.server.services.pipelines import PipelineHinterProtocol
from dstack._internal.server.services.runs.router_worker_sync import (
run_model_has_sglang_router_replica_group,
run_model_has_smg_router_replica_group,
sync_router_workers_for_run_model,
)
from dstack._internal.server.utils import tracing
Expand Down Expand Up @@ -220,7 +220,7 @@ async def process(self, item: ServiceRouterWorkerSyncPipelineItem) -> None:
run_model.deleted
or run_model.status.is_finished()
or run_model.status != RunStatus.RUNNING
or not run_model_has_sglang_router_replica_group(run_model)
or not run_model_has_smg_router_replica_group(run_model)
):
early_cleanup_update_map: _SyncRowUpdateMap = {"deleted": True}
set_processed_update_map_fields(early_cleanup_update_map)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ def _probes(self) -> list[ProbeSpec]:
# workers), and they don't receive traffic directly.
return []
# The router answers chat completions only once it has workers, which it gets
# regardless of its own readiness. For SGLang routers, dstack registers workers
# regardless of its own readiness. For SMG routers, dstack registers workers
# via `ServiceRouterWorkerSyncWorker`, which doesn't check router readiness.
# Dynamo workers register themselves with the router via etcd/NATS.
return [_openai_model_probe_spec(model.name, model.prefix)]
Expand Down
2 changes: 1 addition & 1 deletion src/dstack/_internal/server/services/runs/replicas.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def get_router_env_for_job(
router_group = get_router_replica_group(run_spec)
if router_group is None or router_group.name is None:
return None
# DSTACK_ROUTER_INTERNAL_IP is Dynamo-specific. SGLang workers
# DSTACK_ROUTER_INTERNAL_IP is Dynamo-specific. SMG workers
# are registered via the worker-sync pipeline (ServiceRouterWorkerSyncModel)
if router_group.router is None or router_group.router.type != RouterType.DYNAMO:
return None
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Reconcile SGLang router /workers with dstack's ready worker replicas (async, SSH-tunneled)."""
"""Reconcile SMG /workers with dstack's ready worker replicas (async, SSH-tunneled)."""

import asyncio
import logging
Expand Down Expand Up @@ -42,14 +42,14 @@
from dstack._internal.utils.logging import get_logger

from .replicas import job_belongs_to_group
from .service_router_worker_sync import run_spec_has_sglang_router_replica_group
from .service_router_worker_sync import run_spec_has_smg_router_replica_group

logger = get_logger(__name__)


def run_model_has_sglang_router_replica_group(run_model: RunModel) -> bool:
def run_model_has_smg_router_replica_group(run_model: RunModel) -> bool:
run_spec = get_run_spec(run_model)
return run_spec_has_sglang_router_replica_group(run_spec)
return run_spec_has_smg_router_replica_group(run_spec)


async def sync_router_workers_for_run_model(run_model: RunModel) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ def _reactivate_sync_row_update_map(*, now: datetime) -> _SyncRowUpdateMap:
}


def run_spec_has_sglang_router_replica_group(run_spec: RunSpec) -> bool:
def run_spec_has_smg_router_replica_group(run_spec: RunSpec) -> bool:
if run_spec.configuration.type != "service":
return False
cfg = run_spec.configuration
if not isinstance(cfg, ServiceConfiguration):
return False
return any(
g.router is not None and g.router.type == RouterType.SGLANG for g in cfg.replica_groups
g.router is not None and g.router.type == RouterType.SMG for g in cfg.replica_groups
)


Expand All @@ -48,7 +48,7 @@ async def ensure_service_router_worker_sync_row(
run_model: RunModel,
run_spec: RunSpec,
) -> None:
if not run_spec_has_sglang_router_replica_group(run_spec):
if not run_spec_has_smg_router_replica_group(run_spec):
return
res = await session.execute(
select(ServiceRouterWorkerSyncModel).where(
Expand Down
8 changes: 4 additions & 4 deletions src/tests/_internal/core/models/test_configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
parse_run_configuration,
)
from dstack._internal.core.models.resources import Range, ResourcesSpec
from dstack._internal.core.models.routers import ReplicaGroupRouterConfig
from dstack._internal.core.models.routers import ReplicaGroupRouterConfig, RouterType


class TestParseConfiguration:
Expand Down Expand Up @@ -154,8 +154,8 @@ def test_replica_group_router(self):
{
"name": "router",
"count": 1,
"commands": ["sglang serve"],
"router": {"type": "sglang"},
"commands": ["smg launch"],
"router": {"type": "smg"},
},
{"name": "worker", "count": 2, "commands": ["worker"]},
],
Expand All @@ -165,7 +165,7 @@ def test_replica_group_router(self):
assert parsed.groups is not None
router_g = next(g for g in parsed.groups if g.name == "router")
assert isinstance(router_g.router, ReplicaGroupRouterConfig)
assert router_g.router.type == "sglang"
assert router_g.router.type == RouterType.SMG

def test_spot_policy_set_at_both_service_and_group_rejected(self):
with pytest.raises(
Expand Down
30 changes: 30 additions & 0 deletions src/tests/_internal/core/models/test_routers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pytest

from dstack._internal.core.models.routers import ReplicaGroupRouterConfig, RouterType


class TestReplicaGroupRouterConfig:
def test_accepts_legacy_smg_value(self):
router = ReplicaGroupRouterConfig.model_validate({"type": "sglang"})
assert router.type == RouterType.SMG

def test_accepts_legacy_smg_value_from_json(self):
router = ReplicaGroupRouterConfig.model_validate_json('{"type": "sglang"}')
assert router.type == RouterType.SMG

@pytest.mark.parametrize(
("router_type", "expected"),
[
pytest.param(RouterType.SMG, "sglang", id="smg"),
pytest.param(RouterType.DYNAMO, "dynamo", id="dynamo"),
],
)
def test_serializes_smg_to_legacy_value_in_json(self, router_type: RouterType, expected: str):
router = ReplicaGroupRouterConfig(type=router_type)
assert router.model_dump(mode="json") == {"type": expected}
assert router.model_dump_json() == f'{{"type":"{expected}"}}'
assert ReplicaGroupRouterConfig.model_validate_json(router.model_dump_json()) == router

def test_keeps_enum_in_python_dump(self):
router = ReplicaGroupRouterConfig(type=RouterType.SMG)
assert router.model_dump() == {"type": RouterType.SMG}
8 changes: 4 additions & 4 deletions src/tests/_internal/core/models/test_runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def test_run_get_error_returns_none_for_specific_reasons():
def _service_run_spec_dict(router_type=None, retry=None, top_level_extras=None):
"""Build a minimal RunSpec dict for a service.

`router_type`: None | "sglang" | "dynamo" — controls whether/how the
`router_type`: None | "smg" | "dynamo" — controls whether/how the
second replica group has a router field.
`retry`: optional dict passed as `profile.retry`.
`top_level_extras`: optional dict merged into the service configuration.
Expand Down Expand Up @@ -177,12 +177,12 @@ def test_dynamo_router_without_retry_is_accepted(self):
# Should not raise:
RunSpec.model_validate(spec)

def test_sglang_router_with_retry_is_accepted(self):
def test_smg_router_with_retry_is_accepted(self):
spec = _service_run_spec_dict(
router_type="sglang",
router_type="smg",
retry={"on_events": ["error"]},
)
# SGLang services are unaffected by the validator.
# SMG services are unaffected by the validator.
RunSpec.model_validate(spec)

def test_service_without_router_with_retry_is_accepted(self):
Expand Down
Loading
Loading