Model Deployment Scaling Benchmark
On one node with eight H100 GPUs, ModelDeploymentScaler increased serving replica counts
under live traffic. Four vLLM configurations completed every measured request. SGLang failed under
sustained load, and a separate scale-out check recorded request failures at the Router.
The measurements demonstrate that ModelDeploymentScaler observes load changes and drives
ModelDeployment replicas updates promptly. End-to-end scale-out landing time in production
varies with deployment-environment factors outside the scaler’s control: model weight loading,
engine startup and compute-graph compilation, and accelerator resource availability or queueing.
Contents
- Summary
- Environment
- Configuration
- Suite 1: Scaling comparison
- Verification
- Benefits
- Trade-offs
- Limitations
Summary
A prefill/decode (PD) pair separates prompt processing from token generation.
An encode/prefill/decode (EPD) deployment adds a separate image encoder.
The text configurations compared Proportional, TokenThroughput and Predictive scaling.
The multimodal configuration used Predictive.
Model Deployment Scaling
defines the algorithms and replica guards.
Four vLLM configurations completed every measured request. The separate lifecycle check lost requests during scale-out, so these results do not establish uninterrupted serving. Its scale-down and post-retirement phases completed without request failures.
Environment
| Item | Captured value |
|---|---|
| Cluster | Kubernetes v1.35.7; one CPU node and one GPU node |
| Hardware | 8 × NVIDIA H100 80 GB SXM GPUs; 128 vCPUs and 1600 GB host memory |
| Text model | Qwen/Qwen3-8B, artifact qwen3-8b, revision b968826d9c46dd6066d109eabc6255188de91218 |
| Multimodal model | Qwen/Qwen2.5-VL-7B-Instruct, artifact qwen2-5-vl-7b-instruct, revision cc594898137f460bfe9f0759e9844b3ce807cfb5 |
| Model delivery | Prefetched node cache, mounted at /models |
| vLLM image | gpustack/runner:cuda13.0-vllm0.30.0 |
| SGLang image | gpustack/runner:cuda12.9-sglang0.5.18 |
| Router | llm-d-router with cluster-internal Service endpoints |
| Load driver | In-cluster default/bench-driver; the separate lifecycle check used case129-probe |
| GPU allocation | bench-gpu InstanceType; one GPU per role replica, tensor parallelism (TP) = 1 |
| KV transfer | Default TCP path between prefill and decode |
Requests went directly from the driver to the Router’s cluster-internal endpoint.
No kubectl port-forward carried inference traffic. vLLM roles set
VLLM_HTTP_TIMEOUT_KEEP_ALIVE=3600; the separate lifecycle check still observed scale-out failures.
Configuration
Each role started with one replica and had bounds of one to two.
The scaler used Active mode and a 10 s sampling interval.
| Configuration | Model | Algorithm | Capacity settings |
|---|---|---|---|
vllm-pd-proportional |
Qwen3-8B | Proportional |
RunningRequests, target 3 per role, tolerance 0.1 |
vllm-pd-token-throughput |
Qwen3-8B | TokenThroughput |
Prefill 1200 tok/s; decode 300 tok/s per replica |
vllm-pd-predictive |
Qwen3-8B | Predictive |
Same fallback targets; forecast capacity 2000 tok/s per role |
sglang-pd-token-throughput |
Qwen3-8B | TokenThroughput |
Prefill 1200 tok/s; decode 300 tok/s per replica |
vllm-epd-predictive |
Qwen2.5-VL-7B-Instruct | Predictive |
Same fallback and forecast capacities; encode coupled to prefill |
Both predictive configurations used a 30 s horizon, two minimum observations, qLevel: "0.5"
and qTrend: "0.3". Token-based configurations retained the default 180 s stable and 60 s panic
windows, panic threshold 2, and latency targets of 500 ms and 50 ms.
Each configuration preferred a prefill/decode ratio of 1.
The EPD configuration also preferred an encode/prefill ratio of 1.
Only target was set, so these were preferences rather than fixed ratio bands.
The observed EPD maxima were one encode, two prefill and two decode replicas.
The vllm-pd-proportional scaler took this form.
apiVersion: worker.gpustack.ai/v1
kind: ModelDeploymentScaler
metadata:
name: autoscale-vllm-pd
spec:
targetRef:
name: autoscaling-vllm-pd
mode: Active # commits the recommendation to the target
sampling:
intervalSeconds: 10 # sampling cadence
roles:
- name: prefill
minReplicas: 1
maxReplicas: 2
- name: decode
minReplicas: 1
maxReplicas: 2
ratios:
- numeratorRole: prefill
denominatorRole: decode
target: "1" # preferred ratio, not a fixed band
algorithm:
type: Proportional
proportional:
metric: RunningRequests # requests per ready replica
tolerance: "0.1" # deadband around the target
roleTargets:
- name: prefill
target: "3" # requests per ready replica
- name: decode
target: "3" # requests per ready replica
# The token throughput configurations replaced the proportional block.
# type: TokenThroughput
# tokenThroughput:
# prefillTargetTokensPerSecond: "1200"
# decodeTargetTokensPerSecond: "300"
# The predictive configurations kept those token targets and added a forecast.
# type: Predictive
# predictive:
# tokenThroughput:
# prefillTargetTokensPerSecond: "1200"
# decodeTargetTokensPerSecond: "300"
# horizonSeconds: 30
# minPoints: 2
# qLevel: "0.5"
# qTrend: "0.3"
# prefillTokenCapacityPerReplica: "2000"
# decodeTokenCapacityPerReplica: "2000"The target ModelDeployment manifest is in
Scaling Walkthrough
.
Parallel schedule
The two waves shared an eight-GPU ceiling. Allocations describe maximum capacity reserved for each concurrent sequence; they do not assert that eight GPUs served throughout every phase.
| Wave | First sequence | Second sequence | GPU ceiling |
|---|---|---|---|
| 1 | vLLM PD: Proportional, reset, then Predictive; up to 4 GPUs |
SGLang PD: TokenThroughput, release, then a separate vLLM PD TokenThroughput deployment; up to 4 GPUs |
4 + 4 = 8 |
| 2 | vLLM EPD Predictive; up to 6 GPUs |
Single-role live lifecycle check; up to 2 GPUs | 6 + 2 = 8 |
The admission check ran once before Wave 1. It created its own targets and did not overlap either wave. Between configurations that shared a deployment, the runner deleted the scaler, waited for binding release, then restored one replica per role.
Workload
Text configurations used a continuous streaming client with a target input of 1024 tokens and up to 256 output tokens. Phase durations are requested load durations; recorded wall time includes completion of outstanding requests.
| Phase | Duration (s) | Concurrency | Multimodal requests |
|---|---|---|---|
| Baseline | 45 | 2 | 9 |
| Ramp | 75 | 24 | 180 |
| Sustain | 75 | 24 | 180 |
| Cooldown | 90 | 2 | 18 |
The multimodal client used those fixed request counts, a 560-pixel generated image,
shared_image: false, a target text length of 1024 tokens and up to 256 output tokens.
Its phases were request-count bounded, so their wall times differ from the text load durations.
Time to first token (TTFT) measures delay before the first streamed token. Inter-token latency (ITL) measures gaps between streamed tokens. Latency percentiles use successful requests and the client’s percentile calculation. Output throughput divides successful output tokens by the phase’s recorded wall time. Failed requests remain in the request and error columns.
Suite 1: Scaling comparison
The charts show TTFT p50 and output throughput per phase for the three vLLM PD configurations.
The lines are, in order, vllm-pd-proportional, vllm-pd-token-throughput and vllm-pd-predictive.
Rendering diagram…
Mermaid source
xychart-beta
title "TTFT p50 by phase"
x-axis ["baseline", "ramp", "sustain", "cooldown"]
y-axis "TTFT p50 (ms)" 0 --> 3000
line [298.564, 2540.853, 506.734, 188.231]
line [284.481, 2325.222, 480.418, 182.044]
line [164.044, 467.906, 447.864, 60.883]
Rendering diagram…
Mermaid source
xychart-beta
title "Output throughput by phase"
x-axis ["baseline", "ramp", "sustain", "cooldown"]
y-axis "Output (tok/s)" 0 --> 2500
line [258.325, 1117.481, 2044.782, 274.402]
line [256.117, 1230.879, 2054.687, 276.51]
line [282.371, 2109.251, 2128.27, 288.484]
Both ramp and sustain run at concurrency 24. In ramp, vllm-pd-proportional and
vllm-pd-token-throughput exceed 2300 ms TTFT p50 and cap near single-replica throughput
(1117–1231 tok/s) while the second replica starts. Once that replica is Ready in sustain,
TTFT p50 drops below 510 ms and output throughput nearly doubles above 2040 tok/s at the same
concurrency, while vllm-pd-predictive enters ramp already scaled and holds below 470 ms.
vLLM proportional PD
Configuration: vllm-pd-proportional.
| Phase | Requests | Errors | TTFT p50 (ms) | TTFT p99 (ms) | ITL p50 (ms) | ITL p99 (ms) | Output (tok/s) |
|---|---|---|---|---|---|---|---|
| baseline | 46 | 0 | 298.564 | 408.832 | 6.412 | 11.227 | 258.325 |
| ramp | 346 | 0 | 2540.853 | 5648.778 | 9.662 | 18.393 | 1117.481 |
| sustain | 615 | 0 | 506.734 | 2704.049 | 9.525 | 17.441 | 2044.782 |
| cooldown | 98 | 0 | 188.231 | 396.106 | 6.445 | 10.531 | 274.402 |
vLLM token throughput PD
Configuration: vllm-pd-token-throughput.
| Phase | Requests | Errors | TTFT p50 (ms) | TTFT p99 (ms) | ITL p50 (ms) | ITL p99 (ms) | Output (tok/s) |
|---|---|---|---|---|---|---|---|
| baseline | 46 | 0 | 284.481 | 389.265 | 6.527 | 10.844 | 256.117 |
| ramp | 378 | 0 | 2325.222 | 5384.105 | 9.683 | 17.682 | 1230.879 |
| sustain | 617 | 0 | 480.418 | 4020.449 | 9.463 | 16.921 | 2054.687 |
| cooldown | 98 | 0 | 182.044 | 403.547 | 6.446 | 10.502 | 276.51 |
vLLM predictive PD
Configuration: vllm-pd-predictive.
| Phase | Requests | Errors | TTFT p50 (ms) | TTFT p99 (ms) | ITL p50 (ms) | ITL p99 (ms) | Output (tok/s) |
|---|---|---|---|---|---|---|---|
| baseline | 51 | 0 | 164.044 | 303.918 | 6.364 | 10.52 | 282.371 |
| ramp | 635 | 0 | 467.906 | 2099.983 | 9.333 | 16.619 | 2109.251 |
| sustain | 641 | 0 | 447.864 | 2909.804 | 9.316 | 16.683 | 2128.27 |
| cooldown | 103 | 0 | 60.883 | 364.602 | 6.356 | 10.485 | 288.484 |
SGLang token throughput PD
Configuration: sglang-pd-token-throughput.
| Phase | Requests | Errors | TTFT p50 (ms) | TTFT p99 (ms) | ITL p50 (ms) | ITL p99 (ms) | Output (tok/s) |
|---|---|---|---|---|---|---|---|
| baseline | 50 | 0 | 165.869 | 338.252 | 6.513 | 25.694 | 273.838 |
| ramp | 738 | 3 | 254.654 | 12393.727 | 7.15 | 28.832 | 2215.393 |
| sustain | 2899 | 2534 | 803.553 | 3629.195 | 6.969 | 27.752 | 1174.755 |
| cooldown | 1921 | 1884 | 189.41 | 256.882 | 6.426 | 25.2 | 105.225 |
vLLM predictive EPD
Configuration: vllm-epd-predictive.
| Phase | Requests | Errors | TTFT p50 (ms) | TTFT p99 (ms) | ITL p50 (ms) | ITL p99 (ms) | Output (tok/s) |
|---|---|---|---|---|---|---|---|
| baseline | 9 | 0 | 277.229 | 2025.801 | 6.051 | 10.031 | 201.984 |
| ramp | 180 | 0 | 1437.248 | 4623.104 | 9.457 | 18.319 | 1407.333 |
| sustain | 180 | 0 | 479.805 | 2502.224 | 7.527 | 16.049 | 1951.521 |
| cooldown | 18 | 0 | 208.498 | 266.302 | 5.968 | 10.166 | 266.625 |
The SGLang failure signature was Failed to get kvcache from prefill instance, it might be dead.
Its error-heavy phases do not establish healthy serving capacity. The recorded investigation
associated the failures with the documented TCP port exhaustion
limitation. The TIME-WAIT reuse setting
needs
kubelet configuration before use; a successful rerun with that setting was not captured.
Verification
Scale-out timing
Scale-out lead measures the interval from ramp start to the first sampled desired count above one. It uses deployment status, so it does not measure when a newly added replica first became Ready or served a request. A count already above one at ramp start is reported as zero with an explicit flag.
| Configuration | Lead (s) | Above one before ramp | Maximum desired counts | Maximum Ready counts |
|---|---|---|---|---|
vllm-pd-proportional |
30 | No | Prefill 2, decode 2 | Prefill 2, decode 2 |
vllm-pd-token-throughput |
0 | Yes | Prefill 2, decode 2 | Prefill 2, decode 2 |
vllm-pd-predictive |
0 | Yes | Prefill 2, decode 2 | Prefill 2, decode 2 |
sglang-pd-token-throughput |
0 | Yes | Prefill 2, decode 2 | Prefill 2, decode 2 |
vllm-epd-predictive |
0 | Yes | Encode 1, prefill 2, decode 2 | Encode 1, prefill 2, decode 2 |
Both token throughput and predictive configurations scaled during baseline. The zero lead therefore does not isolate a forecasting benefit. The recorded maxima summarize each role across samples; they are not a synchronized GPU utilization trace.
Retirement
The separate single-role lifecycle check confirmed a new Ready replica served requests, then reduced
the desired count from two to one. It observed Disqualified and Draining, followed by one Ready survivor.
Its scale-down and post-retirement phases completed with zero failed requests.
| Lifecycle phase | Requests | Errors | Observation |
|---|---|---|---|
Scale-out (up) |
13073 | 7 | Router returned HTTP 503 with connection termination |
Two replicas (two) |
14810 | 0 | Expanded deployment served requests |
Scale-down (down) |
1880 | 0 | Retirement observed while traffic continued |
One replica (one) |
942 | 0 | One Ready survivor served requests |
These values come from the standalone lifecycle rerun. The earlier check embedded in Wave 2 also failed during scale-out, with 3 failures among 10579 requests. Admission passed all 45 checks; the standalone lifecycle check passed 10 checks and failed its all-phases completion check.
The benchmark configuration timelines sampled Disqualified for the proportional and token-based
PD configurations. Predictive PD also sampled Admitted and Aborted.
The EPD timeline captured no retirement state. These sets do not establish complete retirement sequences.
The retirement protocol
uses Draining followed by Deleting
before release completes. Kubernetes can display a deleting Pod as Terminating.
Terminating is not a retirement state. The captures do not include every transition, and zero client
errors in the two scale-down phases do not prove every in-flight request survives every retirement.
A later runtime-acceptance run scaled a vLLM multi-Server deployment by hand through one to two
to one replicas. The Router’s worker set followed every edit, the pinned round_robin policy
split sixteen requests eight and eight across the two workers at two replicas, both workers
stayed healthy, and the deployment still served after the scale-back. The run completed 21
checks with zero failures.
A second run retired one of two replica groups of a vLLM External DP deployment at data parallelism 2. The Router’s worker set held every member throughout — two workers for one group of two, four for two groups of two, and two again after the scale-down.
The endpoint withdrawal reached the Router in 21 seconds, and the retired members were deleted 70 seconds after the patch, past the 60-second disaggregated handoff grace window, which the retirement honored. Eight checks passed; the one failing row had timed the endpoint withdrawal against that window, a measurement defect in the case rather than a product finding.
A third run drove a SGLang PD deployment of one prefill and one decode replica with the
scaler active: 29 checks passed with zero failures, warnings or blocked rows. The deployment
published each role’s group independently, the four content classes served through the
sglang-gateway, the thinking switch held on the arithmetic, Chinese and JSON requests, and the
Mooncake transfer over the direct leg moved 54 MB across six transfers with zero failures.
The scaler read the engine’s waiting-request counter during a transfer and recorded 24 waiting requests on the decode role; under load the decode role grew to two replicas, and both roles returned to their floor of one with no retirement outstanding.
The three runs cover one node, one shape each and one recorded cycle. They do not establish multi-node behavior or a general scaling claim.
Benefits
Predictive PD had lower ramp median TTFT than proportional PD: 467.906 versus 2540.853 ms. Its ramp throughput was 2109.251 versus 1117.481 tok/s. It entered the ramp with extra replicas already requested, so this comparison includes different capacity at ramp start.
Trade-offs
Cooldown lasted 90 s for text configurations, while the stored downward stabilization window was 300 s. Several final snapshots still requested two replicas per role. Cooldown completion therefore does not establish a full return to the initial allocation.
Limitations
The measurements cover one node, two models and one recorded comparison per configuration, with a separate SGLang retry. They do not establish multi-node performance or general algorithm rankings. The SGLang table uses the retry’s complete phase artifacts; its earlier interrupted run is excluded.
The text and multimodal clients perform different work and use different phase stopping rules. Their throughput values cannot establish a serving-mode speedup. Image tags identify the configured builds; the capture does not establish identical installed engine package versions across configurations.
Zero errors in the four vLLM benchmark configurations describe their measured requests. The independent scale-out failures and incomplete retirement traces prevent an availability guarantee. The Scaling Walkthrough distinguishes configuration examples from captured lifecycle evidence.