← 返回资讯

SGLang 多卡部署:张量并行、流水并行与专家并行怎么配

2026-09-15

机器批下来了,卡插好了,下一步就是写启动命令。这时候最常见的做法是把卡数直接填进 --tp,起来了就算成功——能起来确实说明权重放得下,但它不说明这是这批卡的正确用法。张量并行、流水并行、专家并行、数据并行在 SGLang 里是四个互相独立的旋钮,各自切的东西不一样、各自解决的瓶颈不一样,也各自有一套硬约束。填错的后果通常不是报错,而是你花了四张卡的钱拿到比两张卡好不了多少的吞吐。

下面按 SGLang 官方文档的口径来说:参数名、默认值、约束条件全部照文档原文,文档没写的地方我会说清楚是文档没写。如果你还不清楚 SGLang 这个引擎本身的定位和它的核心机制,可以先看SGLang 是什么那篇,再回来看怎么把它铺到多卡上。

一、四种切法,切的根本不是同一个东西

先把参数名对齐。SGLang 的这几个并行度参数都有长短两种写法,文档里两种都在用:

  • 张量并行:--tensor-parallel-size / --tp-size,命令行示例里常简写成 --tp,默认值 1
  • 流水并行:--pipeline-parallel-size / --pp-size,默认值 1
  • 专家并行:--expert-parallel-size / --ep-size / --ep,默认值 1
  • 数据并行:--data-parallel-size / --dp-size,示例里写作 --dp,默认值 1

名字像,做的事完全不同。张量并行切的是单个层内部的权重矩阵:一层的权重被横着分到多张卡上,每张卡算自己那一片,算完必须同步一次才能进下一层。所以 TP 的通信是逐层发生的,频率高、对互联带宽敏感。文档里对它的定位说得很直接:TP 是节点内扩展的常规做法,但在多节点部署时经常撞上通信瓶颈。

流水并行切的是层。模型按层切成若干 stage,每个 stage 放一批层,只在 stage 的边界上做跨节点通信。文档给的判断是:正因为通信只发生在边界,PP 比一个很大的 TP 更容易做到计算与通信的重叠,因此也是提升吞吐的一条可行路线。

专家并行切的是 MoE 的专家权重。它只在混合专家模型上有意义:把专家权重分散到多张卡上,token 在运行时被动态路由到各自对应的专家那里去。文档对它的定位是解决显存瓶颈,让大规模 MoE 模型能被有效地铺开。

数据并行不切模型,它复制模型。每个副本都是完整的一份,请求被分到不同副本上跑。文档在调优页里的口径很明确:数据并行更适合追求吞吐,显存够的时候优先考虑它;同时建议用 SGLang Model Gateway(原 Router)来做数据并行,而不是直接用 dp_size 参数。

除了这四个,参数表里还有两条另外的线——--attention-context-parallel-size / --attn-cp-size--dcp-size / --decode-context-parallel-size(后者对应解码阶段的上下文并行,MLA 类模型用)。它们解决的是序列维度的问题,不在本文这条主线上,知道有这么两个开关就行。

还有一个开箱就可能撞上的坑值得先记住:开 TP 时如果报 “peer access is not supported between these two devices”,文档给的处理是在启动命令上加 --enable-p2p-check。这个开关的说明是「为 GPU 访问启用 P2P 检查,否则默认允许 p2p 访问」。

二、先确认瓶颈,再选切法

并行方案不该从卡数出发,该从瓶颈出发。这几种切法各自的适用信号是可以分清的:

单卡装不下权重 → 先在节点内用 TP。这是 TP 最本职的用途,它同时也把单层的算力摊开了。代价是每层一次同步,TP 规模越大、同步占比越高。

跨节点了,而 TP 的通信开销开始难看 → 考虑 PP。PP 只在 stage 边界通信这个性质,正是为跨节点场景准备的。

超长上下文的首字延迟难看 → PP 加上分块预填充。文档把这件事讲得很清楚:KV 缓存技术能省掉重复计算,但省不掉超长序列本身那个昂贵的 TTFT;而在分块预填充下,同一个请求的输入 token 可以被切成多个不超过块大小的 chunk,同一请求的不同 chunk 由不同节点同时处理,从而把处理过程并行化、压低 TTFT。

模型是 MoE,显存卡在专家权重上 → EP。

显存有余但吞吐不够 → DP,或者上 Model Gateway。

至于显存规划,这里只讲方法,不给容量数字——容量数字换一张卡、换一个量化格式、换一档并行度就全变了,抄别人的数没有意义。文档给了一个可以直接用的分解式:总内存占用 = 模型权重 + KV 缓存池 + CUDA graph 缓冲 + 激活;而 --mem-fraction-static 控制的是前两项,它的定义就是 (模型权重 + KV 缓存池) / GPU 内存容量。

所以规划动作是这样的:起服务前不用去猜,起服务时看日志。服务就绪前会打出一行包含 max_total_num_tokenschunked_prefill_sizemax_prefill_tokensmax_running_requestscontext_lenavailable_gpu_mem 的记录,文档让你看的就是 available_gpu_mem 这个值——它给了一个留给激活的余量区间,落在区间里说明配得合适,留得太多就把 --mem-fraction-static 调高把内存还给 KV 池,留得太少就调低以免后面 OOM。另一条更粗暴的路子文档也写了:以 0.01 为步长往上加,直到你的负载开始 OOM,再退回来。具体该留多少请对照文档当前版本给的那个区间,别记我这句话里的任何数。关于容量怎么自己算,显存需求估算那篇讲的是同一套方法。

这里有一条容易忘的联动:并行度一改,这个静态比例就得重算。TP 规模变了,单卡上的权重份额变了;PP 规模变了,单卡上的层数变了;CUDA graph 的缓冲也跟着批大小和并行度动。文档在讲 --cuda-graph-max-bs-decode 时专门提了一句,CUDA graph 吃更多内存,调大它可能需要同时把 --mem-fraction-static 调小。多卡调参最常见的返工就是只拧了一个旋钮。

三、流水并行的真正敌人是气泡

PP 听起来简单,实际难点全在气泡上,而文档把气泡的成因写得相当透。

固定大小的分块预填充会在流水线里造成气泡,PP 规模越大越明显。原因不是块切得不匀,而是即使每个块的大小完全相同,模型的运行时间也不是均匀的——这是 Transformer 结构本身带来的:前缀序列越长,同样大小的那个块跑得越慢。于是各个 stage 之间出现错位,错位又会传播到下一个 stage,PP rank 越高、被拖得越厉害,扩展效率就这么掉下去了。

SGLang 给的解法是动态分块:--enable-dynamic-chunking。它的机制是用一个拟合出来的函数预测下一个块该多大,目标是让每个块的执行时间对齐,判据写成式子就是

Runtime(L + 下一个块的大小) - Runtime(L) = Runtime(初始块大小)

其中 L 是前缀序列长度。文档说他们通过对不同输入长度的请求做 profiling,把累计运行时间建模成序列长度的二次函数,再由这个模型解出任意前缀长度下的最优下一块大小。因为注意力的计算复杂度随 L 增长,所以随着 L 变大,下一个块会被逐步调小,这样才能让各 stage 的块执行时间保持对齐。有一个实现细节值得记:调度器不直接用预测出来的原始值,而是向下对齐到 max(--page-size, 64) 的整数倍,为的是 KV 缓存的内存管理效率和硬件执行效率。

开了动态分块之后,--chunked-prefill-size 的语义变了:它不再是固定块大小,而是初始块大小。文档提醒这时候要把它设得比原来那个固定值明显大一些,否则块的数量会太多。

调节力度的是一个环境变量 SGLANG_DYNAMIC_CHUNKING_SMOOTH_FACTOR。它的语义边界很清楚:设成 0,块大小不再动态调整,等价于退回传统的固定块;设成 1,严格按那个二次模型的预测走;中间值越大越激进——可能性能更好,但块大小变化也更剧烈,序列末尾的块可能变得很小反而拖慢,总块数也更多。文档推荐的是一个偏中间的区间,具体数值以文档当前版本为准,它是按实验结论给的,会随版本动。

文档给的调优顺序是三步,这个顺序本身比任何一个具体值都值钱:第一步,先针对你的目标 PP 规模,迭代找出最优的固定块大小作为基线——不同 PP 规模、不同输入长度的最优值不一样,必须按自己手上的资源迭代;第二步,把动态分块的初始块设成这个基线的若干倍,目的是减少总块数、避免尾部的小块把硬件利用率拉下来(文档也说了,动态预测器会保证后续块不低于初始块的四分之一);第三步,再调平滑因子。文档里那个案例研究正好是这个流程的示范:同样的输入长度、同一批硬件,两个不同的 MoE 模型算出来的最优固定块大小并不相同,最优平滑因子也不相同。这就是为什么这类值不能抄。

还有两个 PP 侧的旋钮:--pp-max-micro-batch-size 控制流水并行里的最大 micro batch 大小,--pp-async-batch-depth 控制异步批深度。以及一条挺实用的层分区技巧:当层数不能被 PP 规模整除时,环境变量 SGLANG_PP_LAYER_PARTITION 可以指定每个 rank 拿多少层,文档建议把较大的那一份放在更高的 PP rank 上——高 rank 在等前一级结果的时候本来是闲着的,多给它几层反而能提高利用率、减少气泡。

顺便说一句它的实现,因为这决定了 PP 在这里值不值得认真用。SGLang 的 PP 走的是 micro-batching 事件循环加非阻塞异步 P2P:调度器发送时不等传输完成,先拿到一个 P2PWork 句柄,真正的同步被推迟到提交通信的那一步,CPU 因此能在数据还在路上的时候去调度下一个批次或处理元数据;另外除了作为同步流的 default_stream,还有专门的 forward_streamcopy_stream 分别跑前向计算和设备到主机的拷贝,为的就是重叠。这不是个凑数的特性。

但也要把文档的克制照抄过来:动态分块目前被明确标为实验特性,需要一定量的调参实验,并且可能并不适合所有负载;而「流水并行与 PD 分离结合的最佳实践」这一节,文档里写的是「待补充」。也就是说这条路还在铺,你踩上去要有心理准备。

四、专家并行:把显存问题换成了通信问题

EP 只对 MoE 有意义,而且它的复杂度不在「怎么开」,在「开了之后 token 怎么跨卡流动」。文档把 EP 的价值说成三件事:借助优化过的 all-to-all 通信和分组矩阵乘(grouped GEMM),降低延迟、提升吞吐、减少 GPU 空转。反过来说,专家权重一分散,每一层都要把 token 按路由结果发到对应的卡、算完再收回来,这就是 all-to-all,EP 的成败主要取决于这一步

所以 SGLang 把这一步做成了可选后端,两个参数分别管两件事:

  • --moe-a2a-backend:选 all-to-all 通信的后端,默认 none。选项是 nonedeepepmooncakenixlmoriascend_fuseepflashinfermegamoepplx
  • --moe-runner-backend:选 MoE 计算(分组 GEMM)的后端,默认 auto

默认的 none 值得单独说一句,因为它不是「没实现」。文档的说明是:none 对 EP 关闭 all-to-all,改用 All-Reduce 或 All-Gather 来做 token 分发,适用场景是 EP 与 TP 混合的部署。而 auto 这个 runner 默认值的行为是按模型架构、硬件、量化方案和运行时条件自动选最合适的后端,文档把它定位成通用部署下不需要人工干预的选择。

后端各有硬约束,这几条是选型时真正会卡住你的:

  • DeepEP、Mooncake、NIXL-EP、ascend_fuseeppplx、MORI 目前都只支持 ep_size = tp_size 的情形。
  • 想做 EP 与 TP 混合(也就是 ep_size < tp_size),只有 none 后端支持
  • pplx 还额外要求开 --enable-dp-attention 且至少有两个 DP 组,否则它的 AllToAll 构造不起来。
  • MORI 目前只支持 normal 模式。

模式这件事就是 --deepep-mode,取值 normallow_latencyauto,默认 auto。语义很直白:normal 面向预填充负载,优化吞吐;low_latency 面向解码负载,优化延迟并兼容 CUDA graph。文档推荐设 auto 让它在运行时自动切换,显式设成另外两个主要是为了调试和开发。

MoE 的第二个真问题是负载不均。 路由是模型自己决定的,热门专家所在的那张卡会被压满,其他卡在等它。SGLang 集成了 DeepSeek 的 EPLB(专家并行负载均衡器)来处理这件事,机制是分析专家激活的统计数据,算出一个更优的专家排布,通过放置或复制专家来压低各 GPU 利用率之间的方差。开关是 --enable-eplb,配套的旋钮有 --eplb-algorithm、周期性再均衡的迭代间隔、每次前向再均衡的层数、触发再均衡的最低 GPU 平均利用率阈值,以及 --ep-num-redundant-experts(分配多少个冗余专家)、--ep-dispatch-algorithm(为冗余专家选 rank 的算法)、--init-expert-location(专家的初始位置)。文档给的使用建议是:把批大小加大,让激活统计稳定下来,再配周期性再均衡去适应负载的漂移。

观测入口也有:--expert-distribution-recorder-mode 与配套的缓冲区大小控制专家分布记录器,--expert-balancedness-report-mode 决定均衡度往哪儿报,取值是 offserver_logprometheusboth。想判断 EPLB 有没有用上,看的就是这里,别凭感觉。

第三件事是重叠。 all-to-all 的通信延迟能不能藏到计算后面,直接决定 EP 的效率。SGLang 给了两个层次的开关:--enable-two-batch-overlap(TBO,把请求拆成 micro batch,让注意力计算与 dispatch/combine 交错,执行图里的 yield 点就是为了让出时机做重叠)和 --enable-single-batch-overlap(SBO,通过 dispatcher 的 hook 机制在单个批次内部做重叠,比如把共享专家的计算和通信叠起来)。

还有两个跟并行度直接打架的参数要知道:--moe-dense-tp-size 单独设 MoE 模型里 dense MLP 层的 TP 规模,文档说它的用途是——TP 规模很大时,MLP 层的权重维度会小于 GEMM 支持的最小维度从而报错,这时候用它给 dense 层单独设一个较小的 TP。以及 --enable-dp-attention:注意力走数据并行、FFN 走张量并行,文档明确写了 dp size 应当等于 tp size,且当前支持的是 DeepSeek-V2 与 Qwen 2/3 的 MoE 模型;配套还有 --enable-dp-lm-head,让词表在注意力 TP 组内并行,避免跨 DP 组做 all-gather。

关于多种并行方式之间的取舍与通用原理,多卡并行推理那篇讲的是不限引擎的那一层,这里是 SGLang 的具体落法。

五、量化与并行度是互相牵着的

很多人把量化当成独立的一步:先决定并行度,再看要不要量化。实际顺序应该反过来,因为量化直接改了权重占的那一块,也就直接改了你需要几张卡。

先记两条文档明说的纪律:为了性能、易用性和便利性,离线量化优于在线量化;以及如果用的是预量化模型,不要同时加 --quantization 去开在线量化——离线量化过的模型,量化方法会从 Hugging Face 或 msModelSlim 的 config 里解析出来,不需要你重复指定。文档还专门提了一句,量化后的模型必须跑基准验证,防止异常的量化损失回归。

量化和并行度的相互影响有三个方向:

一是往好的方向。 权重占用降下来,同一张卡上留给 KV 缓存池的空间就多了,--mem-fraction-static 能往上提,并发上限跟着涨;有时候量化一上,原本需要跨卡的模型就不用跨了,TP 直接省掉一档——这比任何调参都划算。KV 缓存自己也能量化,--kv-cache-dtype 支持 fp8_e4m3fp8_e5m2

二是往受限的方向。 某些量化路径会把你锁进特定的后端组合。最典型的是 nvfp4_online:文档写得很死,只有 --moe-runner-backend flashinfer_trtllmflashinfer_trtllm_routed 被支持,省略这个参数时 SGLang 会自己选 flashinfer_trtllm。它对 TP 的支持是明确的——激活的 per-token scale 在每个 TP rank 本地计算,而在线权重量化仍然用加载时那个张量的 per-tensor scale。但同一段还说了,FlashInfer TRTLLM 的 MoE 后端会关掉共享专家融合,于是在线量化只作用于被路由的那些专家,共享专家保持检查点里的精度。这类约束你必须在定并行方案之前看清楚,否则会出现「量化选好了,发现跟计划用的 a2a 后端合不上」。文档给的这个示例命令,本身就是把量化和两种并行度写在一起的:

python3 -m sglang.launch_server \
    --model-path Qwen/Qwen3-30B-A3B-Instruct-2507 \
    --tp-size 2 \
    --ep-size 2 \
    --quantization nvfp4_online \
    --port 30000 --host 0.0.0.0

三是平台差异。 量化文档里那张平台兼容表是按 NVIDIA / AMD / 昇腾 NPU 分栏的,不同架构能走的量化方法不一样,而 a2a 后端同样分平台(mori 对 ROCm、ascend_fuseep 对昇腾、pplx 只在特定架构上、flashinfer_trtllm 系列面向较新的 NVIDIA 架构)。换句话说,你的卡决定了量化与 EP 后端的可选集合,而不是反过来。真要在异构环境里做方案,先把这两张表按自己的硬件各筛一遍,剩下的交集才是可选项。至于量化格式本身怎么挑,量化精度选择那篇讲得更细。

六、一个能照着做的递进顺序

把上面的东西压成一条动作线,也是我建议的加卡顺序:

  1. 先确认单卡真的装不下。 装得下就别上多卡,TP 的通信开销是白付的。
  2. 单节点内先加 TP。 遇到 peer access 报错加 --enable-p2p-check。起来之后立刻按启动日志校 --mem-fraction-static
  3. 单节点不够再跨节点。 多机的三个参数是 --dist-init-addr(也叫 --nccl-init-addr,形如主机名或 IP 加端口)、--nnodes--node-rank。文档给的跨节点 TP 示例是这样的,两个节点各起一条命令、只有 --node-rank 不同:
# Node 0
python -m sglang.launch_server \
  --model-path meta-llama/Meta-Llama-3-8B-Instruct \
  --tp 4 \
  --dist-init-addr sgl-dev-0:50000 \
  --nnodes 2 \
  --node-rank 0

# Node 1
python -m sglang.launch_server \
  --model-path meta-llama/Meta-Llama-3-8B-Instruct \
  --tp 4 \
  --dist-init-addr sgl-dev-0:50000 \
  --nnodes 2 \
  --node-rank 1

如果遇到死锁,文档建议试着加 --disable-cuda-graph。另外容器和 Kubernetes 环境要记得配共享内存——进程间通信用的就是它,Docker 侧是 --shm-size,K8s 侧要改 /dev/shm 的大小。这一条漏了会表现成莫名其妙的启动失败。

  1. 跨节点之后再评估 PP。 判据是你的通信开销占比和输入长度:超长输入、TTFT 是痛点,PP 加分块预填充这条路值得走;否则先把 TP 调稳。
  2. MoE 模型再上 EP,并且先按 ep_size = tp_size 那条约束反推你能选哪个 a2a 后端。
  3. 显存有余而吞吐不够,才轮到 DP,或者按文档建议上 Model Gateway。
  4. 每改一次并行度,重跑一遍单机那套调参。 --mem-fraction-static--chunked-prefill-size--max-running-requests--cuda-graph-max-bs-decode 全都要重新看。单机这套旋钮的含义和顺序在 SGLang 部署 OpenAI 兼容服务那篇里讲得更细,多卡只是在它外面套了一层。

按症状找旋钮的话,可以记这么几条对应关系:预填充阶段 OOM → 调小 --chunked-prefill-size;解码阶段 OOM → 调小 --max-running-requests;两头都不稳 → 调小 --mem-fraction-static,代价是并发上限和峰值吞吐都降;KV 池利用率长期上不去而队列里又有请求 → 服务端收请求太保守,往下调 --schedule-conservativeness;频繁看到 KV 池满、请求被 retract 的告警 → 往上调它;共享前缀多 → 试 --schedule-policy lpm。这些都是文档里的口径,不是我编的经验法则。

七、加卡不总是线性收益

最后说几句可能不太好听的。

每一种并行都是在用一种开销换另一种开销。 TP 换来的是单卡能装下、单层算得快,付出的是每层一次同步——卡越多,通信在总时间里的占比越高,到某个点之后再加卡就只是在加通信。PP 换来的是跨节点通信少、超长输入的 TTFT 低,付出的是气泡,而文档已经写明了 PP 规模越大气泡越严重,这就是为什么它要专门做动态分块去救。EP 换来的是 MoE 的专家权重铺得开,付出的是 all-to-all 和负载不均——所以才需要一整套后端选择加 EPLB 加重叠开关。每加一档并行度,你都是在把一个问题换成另一个问题,而不是消灭它。

所以先确认瓶颈在哪儿再动手。 如果你的瓶颈其实是客户端喂得不够快(调优文档里那个 #queue-req 长期为 0 的信号),加卡一分钱都不值;如果瓶颈是 KV 池太小导致并发上不去,先量化、先把静态比例调对,可能比加卡有效;如果瓶颈是解码延迟而你的请求彼此毫无前缀重叠,那这些并行参数都帮不上,得回去看负载形状本身。

还有两个诚实的信号要说清楚。 动态分块在文档里是实验特性,明说了需要调参且不一定适合所有负载;流水并行与 PD 分离结合的最佳实践那一节,文档写的是待补充。这两条说明 SGLang 的多卡这条线还在快速演进,任何参数组合的最优值都会随版本漂移。

也得说清楚我的边界:我没有这些多卡集群,本文所有参数、默认值、约束条件都是照 SGLang 官方文档的原文来的,不是实测结论。 文档没覆盖的地方我没有替它补——比如具体某个模型在某种卡上该配几档并行、静态内存比例该填多少、块大小的最优值是多少,文档给的是方法和迭代步骤,不是答案,因为这些值本来就跟模型结构、卡型、互联方式和你的输入长度分布绑在一起。真要定方案,就按第六节那条顺序一步步来,每一步都用启动日志和 decode 统计那两行数据说话。抄别人的启动命令是这件事上最省时间也最贵的做法。

算完账发现自建推理不划算?

先用托管端点把业务跑起来,量上来了再回头算自建的平衡点。

去试用

这个页面有问题?

提交时会附带当前页面地址和浏览器信息,帮助我们定位问题。不填联系方式即为匿名。