08 — Distributed Inference in vLLM & SGLang¶
Why this file: Serving a model larger than one GPU (or wanting lower latency than one GPU allows) means TP/PP inside the inference engine. But the tradeoffs at serving-time are the opposite of training: batch is small, decode is memory-bound, and every all-reduce eats latency you can’t hide. Understanding when TP pays and when it destroys goodput separates people who can run vLLM from people who can architect inference clusters.
Reading:
vLLM distributed serving docs: https://docs.vllm.ai/en/latest/serving/distributed_serving.html
SGLang server args & TP docs: https://docs.sglang.ai/backend/server_arguments.html
Aleksa Gordić, Inside vLLM (deep dive) — has excellent TP + scheduler sections.
kipply, Transformer Inference Arithmetic (2022) — the batch-size vs bandwidth math that underlies everything here.
The Serving TP Question¶
Training TP: activation-heavy, always compute-bound at prefill-size batches, all-reduce cost dominated by GEMM cost → TP=8 within-node scales at ~85-95% efficiency.
Serving TP at decode: each rank does 1/N of the GEMM (linear in FLOPs saved) and still pays 2 all-reduces per block (independent of TP degree in bytes moved). At batch=1 decode, the GEMM is already memory-bound and tiny — you’re spending time on 128×H all-reduces, not on 128×H·H matmuls. Scaling efficiency at low batch: 40-70% typical.
Rule of thumb (memorize this):
Regime |
TP scaling efficiency |
Why |
|---|---|---|
Prefill (compute-bound) |
85-95% within-node |
GEMMs dominate, all-reduces overlap-ish |
Decode, high batch (32+) |
70-85% |
GEMMs still meaningful, some overlap |
Decode, batch=1 |
40-65% |
All-reduce latency dominates; NVLink helps a lot |
Cross-node TP |
Don’t. |
IB is 10x slower than NVLink → all-reduce longer than block itself |
The corollary: TP is a capacity decision more than a latency decision. You use TP=2/4/8 to fit a model that doesn’t fit in one GPU. You don’t use TP for latency until you’ve saturated everything else — quantization, better kernels, larger batch.
When TP Actually Pays for Latency¶
Batch-1 decode of a 70B model in bf16 on ONE H100 (80 GB):
Weights alone: 140 GB → doesn’t fit. FP8: 70 GB → fits with room. INT4: 35 GB → plenty of room.
If fp16 required: TP=2 (2× H100, 160 GB total) is mandatory just to hold weights.
Decode speed on TP=2: theoretical
2 × HBM_BW / (params_per_gpu × bytes)=2 × 3.35 TB/s / (35 GB × 2 bytes)≈ 96 tok/s per user (fp16). Real: ~70-85 tok/s after all-reduce overhead.Decode on TP=4: theoretical 190 tok/s, real ~130-150. Efficiency dropped from 100%→~75%.
Decode on TP=8: theoretical 380 tok/s, real ~200-260. Efficiency ~60-70%.
The scaling starts to break down because the all-reduce time (fixed per block) becomes a bigger fraction of the shrinking per-rank compute time. The right answer for 70B latency is usually TP=2 or TP=4 within a node, not TP=8.
Configuring TP in vLLM¶
vllm serve meta-llama/Llama-3.3-70B-Instruct \
--tensor-parallel-size 4 \
--pipeline-parallel-size 1 \
--max-model-len 2412.19437 \
--gpu-memory-utilization 0.90 \
--enable-prefix-caching \
--kv-cache-dtype fp8
Key knobs:
--tensor-parallel-size(TP): must divide num-attention-heads and num-kv-heads (for GQA models). Llama-3-70B has 8 KV heads → max TP=8 without KV-head replication.--pipeline-parallel-size(PP): use only if you need cross-node. Adds latency.--enable-expert-parallel(EP): for MoE models. TP × EP × PP × DP must equal world size.--distributed-executor-backend {ray,mp}:rayfor multi-node,mp(multiprocessing) for single-node.
Multi-node vLLM:
# On head node:
ray start --head
# On worker nodes:
ray start --address='<head_ip>:6379'
# Then serve with --tensor-parallel-size 8 --pipeline-parallel-size 2 for 16 GPUs across 2 nodes
Configuring TP in SGLang¶
python -m sglang.launch_server \
--model-path meta-llama/Llama-3.3-70B-Instruct \
--tp 4 \
--dp 1 \
--context-length 2412.19437 \
--mem-fraction-static 0.85 \
--enable-torch-compile \
--kv-cache-dtype fp8_e5m2
SGLang-specific:
--dpruns multiple full replicas (data-parallel serving) — use for throughput scaling when a single replica is fast enough.--tp × --dp= world size.--attention-backend {flashinfer,triton,torch_native}— flashinfer is default, fastest for paged KV.RadixAttention prefix cache is on by default — this is often the biggest single win over vLLM’s older implementations, especially for your agentic workloads.
PP for Serving: Almost Always the Wrong Answer¶
PP saves you from TP’s all-reduce cost by turning it into a single send/recv per stage. Sounds great. It’s a throughput win at high concurrency (pipelining microbatches through stages) but a latency disaster at low concurrency (each token waits for p stage passes).
Serving PP math: with PP=p stages, decode latency per token ≈ p × per-stage compute + p × comm. With TP=p, latency per token ≈ per-block compute + per-block all-reduce. TP wins on latency for interactive; PP only wins when:
You literally can’t fit even TP=8 in one node (e.g., serving DeepSeek-R1’s 671B params).
Cross-node is unavoidable → PP over IB, TP inside NVLink domain.
Standard wide-model recipe: TP=8 within each node, PP=N across N nodes. That’s how you serve a 405B model on 2 nodes (TP=8, PP=2).
Scaling Efficiency: How to Measure Honestly¶
Don’t trust marketing numbers. Run these yourself:
# Baseline: single GPU with the largest quant that fits
vllm bench serve --model llama-8b-fp16 --num-prompts 500 --request-rate 5
# TP=2 same request rate:
vllm bench serve --model llama-70b-fp16-tp2 --num-prompts 500 --request-rate 5
Metrics to record:
TTFT p50/p95/p99 (dominated by prefill and queue)
ITL p50/p95/p99 (inter-token latency — dominated by decode)
Throughput in output tokens/sec
Goodput (throughput within SLO; e.g., “tokens/sec where ITL_p99 < 100ms”)
GPU util per rank — if TP scales badly, you’ll see all-reduce waits on the Nsight timeline.
Report a scaling table. Format:
Config |
Prefill throughput |
Decode throughput |
ITL p95 |
TTFT p95 |
Scaling |
|---|---|---|---|---|---|
8B fp16 TP=1 |
X tok/s |
Y tok/s |
Z ms |
W ms |
1.0 |
70B fp16 TP=4 |
… |
… |
… |
… |
(measured/ideal) |
70B fp16 TP=8 |
… |
… |
… |
… |
… |
Bottleneck Diagnosis Playbook¶
“My TP=8 isn’t 8x faster than TP=1” — well, no, and here’s how to figure out why:
Roofline the block. For your model, batch, seq: compute FLOPs per block, bytes moved per block. Compute FLOPs available and bandwidth available across N GPUs. Predict per-block time. If measured >> predicted → check step 2.
Nsight Systems, look at NCCL rows. Are they solid and short (good) or long and gap-filled (bad)? If NCCL time > compute time, you’re comm-bound → reduce TP degree or increase batch.
Check kernel launch overhead. vLLM captures CUDA graphs for common decode shapes; if you’re running with
--enforce-eager, latency is 2-5x worse. Never benchmark eager unless debugging.Check GPU-GPU topology.
nvidia-smi topo -mshould show NV12 (fully NVLinked) between all TP ranks. Anything less (SYS = through PCIe, NODE = same NUMA but no NVLink) → move ranks or accept the cost.PCIe fallback. If NVLink Bridge is dead or unpaired GPUs, NCCL silently falls back to PCIe P2P (or worse, staging through host memory). Your “TP=2 is only 1.3x” mystery lives here.
The AMDA-Overlap Trick (Advanced)¶
Recent vLLM/SGLang optimizations overlap the second half of the all-reduce with the beginning of the next GEMM. This is only ~10-15% win on serving but is the state of the art. Ampere had AsyncTP; Hopper has better via TMA async loads. You don’t need to implement this, but knowing it exists explains why bleeding-edge engine versions get faster without hardware changes.
CUTLASS has explicit async-tp GEMM kernels. Some vLLM builds enable them via VLLM_USE_ASYNC_TP=1 — test if your workload benefits.
Data Parallelism at Serving Time¶
DP for serving means running N independent replicas of the same model, load-balanced by a router. This is:
The default throughput scaling strategy for models that fit in one GPU.
Combined with TP:
TP × DP = world size. Example: 8 GPUs, 70B model. Either TP=8 (1 replica, ~500 tok/s per user) or TP=4 × DP=2 (2 replicas, ~350 tok/s each, but 2 concurrent users at that rate → much better throughput). Choose based on SLO.SGLang exposes this cleanly via
--dp. vLLM’s approach is typically “run multiple engine instances behind Ray Serve / K8s.”
Standard decision tree for a fixed hardware budget:
Fits in 1 GPU at your quant + KV budget? → DP-only, N replicas.
Fits in TP=2? Prefer TP=2 × DP=(N/2) over TP=N single replica for throughput.
Doesn’t fit even in one node? → TP-max within node, PP across nodes.
The Zoho Angle¶
Your on-prem CRM customers get e.g. 4×L40S (48 GB each) or 8×H100. The napkin math you must produce in an hour:
Customer: “We have 8×H100 SXM, want to serve a 70B chat model to ~200 seats.”
Your derivation:
70B in fp8 KV-fp8: 70 GB weights + KV that grows. Fits in 1 H100 for very small context, but not with realistic 8k context × concurrent users.
TP=2 with FP8: weights 35 GB/rank, ~45 GB KV budget/rank → ~200k tokens of KV total. At 8k avg per user → ~25 concurrent decode slots per replica.
8 GPUs / TP=2 = 4 replicas. Total ~100 concurrent decode slots. At 5-8s active session per user × 200 seats × business-day duty cycle → fine, with prefix caching handling the system-prompt load.
Fallback: TP=4 for lower latency if p99 ITL SLO is strict → 2 replicas × ~50 slots = ~100 slots, similar throughput but ~30% lower per-user tok/s.
This is exactly the exercise you’ll do 50 times in your career if this becomes your specialty. Write the template once, tune it forever.
Exercises¶
The scaling table. On rented 8×H100, serve Llama-3-70B in fp8 with TP={1,2,4,8}. Measure the table above. Explain each row.
TP vs DP tradeoff. Same 8 GPUs. Compare TP=8/DP=1 vs TP=4/DP=2 vs TP=2/DP=4 for 100 concurrent users, ShareGPT trace. Which config maximizes goodput at ITL_p95<150ms?
PP disaster. Run TP=1/PP=8 vs TP=8/PP=1 for a 70B. Confirm PP-only is a latency disaster. Measure how bad.
Cross-node TP disaster. If you have 2 nodes with 4 H100s each and IB, try TP=8 (cross-node). Compare to TP=4/PP=2. Feel the pain that motivates the “never cross-node TP” rule.
Links¶
vLLM distributed docs: https://docs.vllm.ai/en/latest/serving/distributed_serving.html
SGLang docs: https://docs.sglang.ai/
Aleksa Gordić Inside vLLM: https://www.aleksagordic.com/blog/vllm
kipply Transformer Inference Arithmetic: https://kipp.ly/transformer-inference-arithmetic/
Next: file 09 splits prefill and decode onto different GPU pools — disaggregated serving (DistServe, Mooncake). This is the frontier of production LLM infra as of 2025-2026.