Skip to content

Commit 34baf13

Browse files
Bihan  RanaBihan  Rana
authored andcommitted
Resolve review comments
1 parent 0f8f1b6 commit 34baf13

10 files changed

Lines changed: 62 additions & 84 deletions

File tree

docs/blog/posts/pd-disaggregation.md

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,12 @@ For inference, `dstack` provides a [services](../../docs/concepts/services.md) a
2626

2727
> If you’re new to Prefill–Decode disaggregation, see the official [SGLang docs](https://docs.sglang.io/advanced_features/pd_disaggregation.html).
2828
29+
!!! note "Deprecation notice"
30+
Configuring the SGLang router in a gateway is deprecated and will be disallowed in a future release. To run router and workers as separate replica groups, see [SGLang PD disaggregation (router as replica group)](https://dstack.ai/examples/inference/sglang/#pd-disaggregation).
31+
2932
## Services
3033

31-
With `dstack` `0.20.17`, you can define a service with separate replica groups for Router, Prefill and Decode workers and run PD disaggregated Inference.
34+
With `dstack` `0.20.10`, you can define a service with separate replica groups for Prefill and Decode workers and enable PD disaggregation directly in the `router` configuration.
3235

3336
<div editor-title="glm45air.dstack.yml">
3437

@@ -43,21 +46,6 @@ env:
4346
image: lmsysorg/sglang:latest
4447

4548
replicas:
46-
- count: 1
47-
# For now replica group with router must have count: 1
48-
commands:
49-
- pip install sglang_router
50-
- |
51-
python -m sglang_router.launch_router \
52-
--host 0.0.0.0 \
53-
--port 8000 \
54-
--pd-disaggregation \
55-
--prefill-policy cache_aware
56-
router:
57-
type: sglang
58-
resources:
59-
cpu: 4
60-
6149
- count: 1..4
6250
scaling:
6351
metric: rps
@@ -67,7 +55,7 @@ replicas:
6755
python -m sglang.launch_server \
6856
--model-path $MODEL_ID \
6957
--disaggregation-mode prefill \
70-
--disaggregation-transfer-backend nixl \
58+
--disaggregation-transfer-backend mooncake \
7159
--host 0.0.0.0 \
7260
--port 8000 \
7361
--disaggregation-bootstrap-port 8998
@@ -83,7 +71,7 @@ replicas:
8371
python -m sglang.launch_server \
8472
--model-path $MODEL_ID \
8573
--disaggregation-mode decode \
86-
--disaggregation-transfer-backend nixl \
74+
--disaggregation-transfer-backend mooncake \
8775
--host 0.0.0.0 \
8876
--port 8000
8977
resources:
@@ -94,8 +82,12 @@ model: zai-org/GLM-4.5-Air-FP8
9482

9583
probes:
9684
- type: http
97-
url: /health
85+
url: /health_generate
9886
interval: 15s
87+
88+
router:
89+
type: sglang
90+
pd_disaggregation: true
9991
```
10092
10193
</div>
@@ -111,33 +103,32 @@ $ dstack apply -f glm45air.dstack.yml
111103

112104
</div>
113105

114-
### SSH fleet
106+
### Gateway
115107

116-
Create an [SSH fleet](https://dstack.ai/docs/concepts/fleets/#apply-a-configuration) that includes one CPU host for the router and one or more GPU hosts for the workers. Make sure the CPU and GPU hosts are in the same network.
108+
Just like `dstack` relies on the SGLang router for cache-aware routing, Prefill–Decode disaggregation also requires a [gateway](../../docs/concepts/gateways.md#sglang) configured with the SGLang router.
117109

118-
<div editor-title="pd-fleet.dstack.yml">
110+
<div editor-title="gateway-sglang.dstack.yml">
119111

120112
```yaml
121-
type: fleet
122-
name: pd-disagg
123-
124-
placement: cluster
125-
126-
ssh_config:
127-
user: ubuntu
128-
identity_file: ~/.ssh/id_rsa
129-
hosts:
130-
- 89.169.108.16 # CPU Host (router)
131-
- 89.169.123.100 # GPU Host (prefill/decode workers)
132-
- 89.169.110.65 # GPU Host (prefill/decode workers)
113+
type: gateway
114+
name: inference-gateway
115+
116+
backends: [kubernetes]
117+
region: any
118+
119+
domain: example.com
120+
121+
router:
122+
type: sglang
123+
policy: cache_aware
133124
```
134125
135126
</div>
136127
137128
## Limitations
138-
* The router replica group is currently limited to `count: 1` (no HA yet). Support for multiple router replicas for HA is planned.
139-
* Prefill–Decode disaggregation is currently available with the SGLang backend (Nvidia-dynamo and vLLM support is coming).
129+
* Because the SGLang router requires all workers to be on the same network, and `dstack` currently runs the router inside the gateway, the gateway and the service must be running in the same cluster.
140130
* Autoscaling supports RPS as the metric for now; TTFT and ITL metrics are planned next.
131+
* Prefill–Decode disaggregation is currently available with the SGLang backend (vLLM support is coming).
141132

142133
With native support for inference and now Prefill–Decode disaggregation, `dstack` makes it easier to run high-throughput, low-latency model serving across GPU clouds, and Kubernetes or bare-metal clusters.
143134

docs/docs/concepts/gateways.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ If you configure the `sglang` router, [services](../concepts/services.md) can ru
9999
To run services with PD disaggregation see [SGLang PD disaggregation](https://dstack.ai/examples/inference/sglang/#pd-disaggregation).
100100

101101
!!! note "Deprecation"
102-
Configuring the SGLang router in a gateway will be deprecated in a future release.
102+
Configuring the SGLang router in a gateway is deprecated and will be disallowed in a future release.
103103

104104
??? info "Policy"
105105
The `policy` property allows you to configure the routing policy:

examples/inference/sglang/README.md

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,6 @@ replicas:
179179

180180
port: 8000
181181
model: zai-org/GLM-4.5-Air-FP8
182-
# SSH fleet containing both router (CPU) and workers (GPU).
183-
fleets: [pd-disagg]
184182

185183
# Custom probe is required for PD disaggregation.
186184
probes:
@@ -193,31 +191,15 @@ probes:
193191
194192
Currently, auto-scaling only supports `rps` as the metric. TTFT and ITL metrics are coming soon.
195193

196-
#### SSH fleet
194+
#### Fleet
197195

198-
Create an [SSH fleet](https://dstack.ai/docs/concepts/fleets/#apply-a-configuration) that includes one CPU host for the router and one or more GPU hosts for the workers. Make sure the CPU and GPU hosts are in the same network.
196+
Create a [fleet](https://dstack.ai/docs/concepts/fleets/) that can provision both a CPU node (for the router replica group) and GPU nodes (for the prefill/decode replica groups).
197+
You can create an SSH fleet, elastic Cloud fleet (nodes: 0..) or kubernetes cluster. Just don't specify any resource constraints in the fleet, and dstack will automatically provision the correct instances (both CPU and GPU, in the same fleet) based on the resources specified in replicas in the run configuration.
199198

200-
<div editor-title="pd-fleet.dstack.yml">
201-
202-
```yaml
203-
type: fleet
204-
name: pd-disagg
205-
206-
placement: cluster
207-
208-
ssh_config:
209-
user: ubuntu
210-
identity_file: ~/.ssh/id_rsa
211-
hosts:
212-
- 89.169.108.16 # CPU Host (router)
213-
- 89.169.123.100 # GPU Host (prefill/decode workers)
214-
- 89.169.110.65 # GPU Host (prefill/decode workers)
215-
```
216-
217-
</div>
199+
The only requirement is that the router and worker replicas run in the same network. In practice, this typically means using a single fleet where the backend and region are the same or using `placement: cluster` if the backend supports it.
218200

219201
!!! note "Gateway-based routing (deprecated)"
220-
If you create a gateway with the [`sglang` router](https://dstack.ai/docs/concepts/gateways/#sglang), you can also run SGLang with PD disaggregation. This method will be deprecated in the future in favor of running the router as a replica.
202+
If you create a gateway with the [`sglang` router](https://dstack.ai/docs/concepts/gateways/#sglang), you can also run SGLang with PD disaggregation. This method is deprecated and will be disallowed in a future release in favor of running the router as a replica.
221203

222204
## Source code
223205

examples/inference/sglang/pd.dstack.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,6 @@ replicas:
5656
port: 8000
5757
model: zai-org/GLM-4.5-Air-FP8
5858

59-
# SSH fleet containing both router (CPU) and workers (GPU).
60-
# Router and workers must be in the same fleet (same network).
61-
fleets: [pd-disagg]
62-
6359
probes:
6460
- type: http
6561
url: /health

src/dstack/_internal/proxy/gateway/resources/nginx/service.jinja2

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ server {
4747
}
4848
{% endfor %}
4949

50-
{# For SGLang router: block all requests except whitelisted locations added dynamically above #}
51-
{% if router is not none and router.type == "sglang" %}
50+
{# For router services: block all requests except whitelisted locations added dynamically above #}
51+
{% if has_router_replica or (router is not none and router.type == "sglang") %}
5252
location / {
5353
return 403;
5454
}

src/dstack/_internal/proxy/gateway/services/registry.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,9 @@ async def get_nginx_service_config(
342342
) -> ServiceConfig:
343343
limit_req_zones: list[LimitReqZoneConfig] = []
344344
locations: list[LocationConfig] = []
345-
is_sglang = service.router and service.router.type == RouterType.SGLANG
345+
is_sglang = (
346+
service.router is not None and service.router.type == RouterType.SGLANG
347+
) or service.has_router_replica
346348
sglang_limits: dict[str, LimitReqConfig] = {}
347349
sglang_prefix_lengths: dict[str, int] = {} # Track prefix lengths for most-specific selection
348350

src/dstack/_internal/server/services/proxy/repo.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ async def get_service(self, project_name: str, run_name: str) -> Optional[Servic
7575
(g for g in run_spec.configuration.replica_groups if g.router is not None),
7676
None,
7777
)
78+
has_router_replica = router_group is not None
7879
router = run_spec.configuration.router
7980
replicas = []
8081
for job in jobs:
@@ -139,6 +140,7 @@ async def get_service(self, project_name: str, run_name: str) -> Optional[Servic
139140
client_max_body_size=DEFAULT_SERVICE_CLIENT_MAX_BODY_SIZE,
140141
strip_prefix=run_spec.configuration.strip_prefix,
141142
replicas=tuple(replicas),
143+
has_router_replica=has_router_replica,
142144
router=router,
143145
)
144146

src/dstack/_internal/server/services/proxy/services/service_proxy.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ async def proxy(
4141
if not service.strip_prefix:
4242
path = concat_url_path(request.scope.get("root_path", "/"), request.url.path)
4343

44-
if service.router is not None and service.router.type == RouterType.SGLANG:
44+
if (
45+
service.router is not None and service.router.type == RouterType.SGLANG
46+
) or service.has_router_replica:
4547
path_for_match = path if path.startswith("/") else f"/{path}"
4648
if not _is_whitelisted_path(path_for_match, SGLANG_WHITELISTED_PATHS):
4749
raise ProxyError("Path is not allowed for this service", status.HTTP_403_FORBIDDEN)

src/dstack/_internal/server/services/runs/spec.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,21 @@ def _check_can_update_configuration(
199199
raise ServerClientError(
200200
f"Configuration type changed from {current.type} to {new.type}, cannot update"
201201
)
202+
203+
if isinstance(current, ServiceConfiguration) and isinstance(new, ServiceConfiguration):
204+
current_router_group = next(
205+
(g for g in current.replica_groups if g.router is not None), None
206+
)
207+
new_router_group = next((g for g in new.replica_groups if g.router is not None), None)
208+
current_router_group_name = (
209+
current_router_group.name if current_router_group is not None else None
210+
)
211+
new_router_group_name = new_router_group.name if new_router_group is not None else None
212+
if current_router_group_name != new_router_group_name:
213+
raise ServerClientError(
214+
"Cannot update router replica groups in-place (adding/removing `router` or changing "
215+
"which replica group is the router is not supported). Stop the run and apply again."
216+
)
202217
updatable_fields = _CONF_UPDATABLE_FIELDS + _TYPE_SPECIFIC_CONF_UPDATABLE_FIELDS.get(
203218
new.type, []
204219
)

src/dstack/_internal/server/services/services/__init__.py

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -275,24 +275,12 @@ def _build_service_router_config(
275275
service_configuration: ServiceConfiguration,
276276
) -> Optional[AnyServiceRouterConfig]:
277277
"""
278-
Router metadata to store on the gateway proxy for this service (`service.router`).
279-
280-
A replica-group `router:` does **not** depend on the gateway having its own global SGLang
281-
router block—the router runs on service replicas. When the gateway has no global SGLang
282-
config but a replica group does declare `router:`, we still return a default
283-
`SGLangServiceRouterConfig` so nginx/proxy code can treat the service as SGLang and apply path
284-
rules. When the gateway *does* have global SGLang, we merge gateway policy with service-level
285-
`configuration.router` overrides as before.
278+
Build router config from gateway (type, policy) + service (pd_disaggregation, policy override).
279+
Service's policy overrides gateway's if present. Keeps backward compat: SGLang enabled
280+
automatically when gateway has it configured.
286281
"""
287-
has_replica_group_router = any(
288-
g.router is not None for g in service_configuration.replica_groups
289-
)
290282
if not _gateway_has_sglang_router(gateway_configuration):
291-
if not has_replica_group_router:
292-
return None
293-
# In later releases we will deprecate service-level and gateway-level router
294-
# configuration and return `ReplicaGroupRouterConfig` here instead.
295-
return SGLangServiceRouterConfig()
283+
return None
296284

297285
gateway_router = gateway_configuration.router
298286
assert gateway_router is not None # ensured by _gateway_has_sglang_router

0 commit comments

Comments
 (0)