文章

通过多节点DeepSeek部署实现更低延迟和更高吞吐量

闪亮的鲸鱼,提及DeepSeek

在大多数系统中,延迟和吞吐量往往是相互矛盾的目标,需要在设计和部署期间进行权衡。例如,在密集的大型语言模型中,增加批处理大小可以提高吞吐量,但也会增加延迟;在单台机器中增加张量并行性可以减少延迟,但会减少副本数量,从而降低吞吐量。

像DeepSeek-V3/R1这样的专家混合(MoE)模型最近展示了出色的模型能力和操作效率。例如,DeepSeek-V3/R1模型总共有6710亿个参数,但每个标记在推理期间只使用37亿个参数。这种模型架构对推理系统既带来了挑战,也带来了机会。

本文展示了,与传统系统相反,MoE模型如DeepSeek-V3/R1在大多数场景下,利用更多GPU进行多节点部署时可以同时实现更高的吞吐量和更低的延迟


部署架构

由于该模型包含大量小型专家,部署必须分布在多个设备上。我们考虑了在单个节点上具有8xH200 GPU的单节点部署和具有8xH100 GPU的多节点部署。

这两种部署架构都利用了数据并行性,并通过我们的内部请求调度程序进行协调。数据并行实现涉及启动多个推理引擎实例,每个实例独立操作以服务和维护请求。请求调度程序通过GRPC与引擎交互,负责尽可能均匀地分布请求,同时促进KV重用,将部分匹配前缀的请求发送到包含缓存的服务器。引擎实例不跨越多个节点。它们可以选择性地使用张量并行性来将注意力分割到多个设备。实例通过在单节点情况下使用NVLink或者在多节点情况下使用InfiniBand进行互连,调度并收集专家。

单节点部署配置在小批量情况下提供了优越的延迟性能;然而,在负载增加条件下,性能迅速下降。

为了部署服务引擎,我们为每个节点启动一个pod,承载多个引擎实例。PyTorch负责设置分布式通信并协商NVSHMEM初始化。对于通信,我们依赖于早前博客文章中描述的自定义CUDA内核。这两个部署的实现几乎是相同的,模型根据实现专家并行性的结构选择要使用的正确内核。


并行化技术

在深入进行性能比较之前,需要了解使像DeepSeek-V3/R1这样的庞大MoE模型能够部署的关键并行化策略。

张量并行

在LLM推理中,张量并行( TP )通常用于减少每个GPU的内存使用和计算,从而减少延迟。通常,我们可以沿着行或列维度分割注意力和MLP层中的线性投影,并沿着注意力头维度分割注意力操作。

在TP的帮助下,Llama-3架构没有GPU间的线性投影和注意力操作的重复计算,这是理想的分片方法。然而,在DeepSeek-V3/R1模型中,TP无法实现这一点。

DeepSeek-V3/R1模型使用多潜在注意力(MLA)。MLA层首先使用线性投影kv_a_proj计算潜在向量,然后使用另一个线性投影kv_b_proj将其转换为每个注意力头的空间。由于所有注意力头共享相同的潜在向量,TP无法分割潜在向量,因此所有TP等级需要复制kv_a_projkv_b_proj的参数和计算。类似地,由于MLA将潜在向量存储在KV缓存中,每个TP等级存储KV缓存的相同副本。

尽管MLA中存在一些重复,张量并行仍然在计算需求减少中提供了部分优势,因此在需要高输出速度的场景中非常有价值。

专家并行

DeepSeek-V3/R1模型用MoE层替代了MLP层。MoE层有256个路由专家和一个共享专家。每个标记被分派到8个不同的路由专家进行计算,结果被加权和。每个标记还在共享专家中进行计算,并将结果添加到来自路由专家的结果中。

专家并行 (EP) 被视为MoE层的典型分片方法,每个GPU负责管理256 / EP路由专家,同时保持共享专家的副本。与TP相比,EP的优势在于可以在更多GPU之间分配计算,降低每个GPU的计算和内存使用。

进行专家计算之前,所有GPU需要执行AllToAll通信以将标记发送到相应专家所在的GPU;完成专家计算后需要另一次AllToAll通信以从各种GPU收集计算结果并进行加权和。我们使用NVSHMEM实现了这两个AllToAll通信内核的优化版本,调度和组合。在我们之前的一篇博客文章中,我们详细介绍了实现过程,并在GitHub上将我们的内核开源

数据并行

通过EP,我们可以在128个甚至更多GPU间分发MoE计算。然而,MLA计算不能使用EP进行分割。这时我们可以引入数据并行(DP)。每个DP组都有完整的MLA层副本。每个DP组接受不同的输入并独立执行MLA层计算。

MLA层的DP和TP可以结合使用,一个DP组可以分割成多个TP等级。MoE层的EP可以与MLA层的DP/TP结合。EP = DP * TP。例如,在16台机器上,EP128 DP32 TP4表示将路由专家分配到128个GPU上,每4个GPU组成一个DP组,总共有32个独立DP组。


单节点与多节点

DeepSeek的6710亿个参数超过了单台8-GPU H100机器(80 GB * 8)的内存容量,但一台8-GPU H200机器可以完全容纳整个模型(141 GB * 8)。使用EP8 DP8 TP1配置,模型使用每个GPU约100 GB的内存,留给KV缓存和其他中间结果的内存约40 GB。每个标记占用70272字节的KV缓存。假设每个请求有5000个标记,每个GPU可以容纳大约100个请求。

我们希望了解单节点和多节点部署在不同配置下的性能差异。我们使用了一台H200机器进行单节点部署,最多使用16台H100机器进行多节点部署。对于每种部署环境,我们使用了TP 1,2,4,8的组合和每个GPU批量大小为1,2,4,8,16,32,64,128。我们假设每个请求有5000个标记的KV缓存长度。我们还假设多标记预测(MTP)预测1个额外标记(即每个请求的查询长度为2),保守地假设接受率为60%。下图展示了不同配置的吞吐量和输出速度。

横轴代表每个请求的输出速度,以标记/秒为单位。纵轴使用对数刻度表示每台机器的吞吐量,单位为标记/秒。我们用不同颜色的线标记了每个EP配置的帕累托前沿。

在极高输出速度要求的场景中,使用单节点EP8 DP1 TP8批量大小为1的配置可以实现超过100标记/秒的输出速度,但吞吐量极低,相当于输出速度。在这个场景中,整个批次只有2个标记,可以最多分派给2*8=16个专家,激活最多57亿个参数。

在80-40标记/秒的输出速度范围内,随着吞吐量增加,输出速度显著下降。相比之下,EP128在相同输出速度下的吞吐量约为单节点部署的5倍。

这种现象可以通过检查单节点部署的行为来解释:增加批量大小直接与激活专家数量增加相关。当批量大小为1时,每个GPU激活的专家平均数是2 * 8 / 8 = 2。当批次足够大时,所有专家被激活,这意味着每个GPU激活256 / 8 = 32个专家。激活更多专家意味着GPU需要从内存中读取更多参数,显著增加了内存带宽压力。由于大型语言模型的解码阶段通常受内存带宽而非计算性能的限制,增加单节点部署中的批量大小显著降低了输出速度。

对比四种多节点部署配置(EP16,EP32,EP64和EP128)显示,更高的EP值将帕累托前沿转向同时提高吞吐量和输出速度

使用更高的EP数意味着每个GPU分配的专家更少。例如,EP128意味着每个GPU负责256 / 128 = 2个专家,因此内存带宽压力显著减少。换句话说,通过使用更大的EP数,我们有效地获得了更多的内存带宽。当每个GPU的批量小于64时,增加批量大小对专家计算速度没有显著影响,因为增加输入数量不会显著增加内存带宽压力。因此,我们观察到使用EP128时,增加批量大小对输出速度的影响不大。

有趣的是,在较大批量(每个GPU 64个请求)情况下,我们观察到一个新的现象:单节点部署吞吐量略高于多节点部署。部分原因是节点内NVLink的带宽高于节点间InfiniBand。另一个原因则是由于实现的限制。我们将在稍后详细分析这种现象。

由于内存容量限制,EP8 DP8 TP1配置不能达到每GPU批量大小128,因此多节点部署在追求更高吞吐量的场景中仍是更好的选择。


计算与通信重叠

如上关于专家并行性简要介绍中所述,MoE层通信期间GPU处于空闲状态。为减少浪费和降低延迟,我们需要找到数据无关的计算任务来填补这段空闲时间。

上图的上半部分显示了一个层的计算流程。MoE计算依赖于调度,下一层的计算依赖于组合的结果。

我们将共享专家放置在每个GPU上。这样,共享专家计算不需要AllToAll通信。因此,在调度发送之后,我们可以立即执行共享专家计算,然后等待调度接收完成。我们称这种重叠方案为"调度重叠"。

调度重叠提供了简单的实现和广泛的适用性。这项技术隐藏了在所有EP大小和批量大小上的共享专家计算时间。

为了进一步增加计算和通信重叠,我们使用DeepSeek技术报告中提到的微批处理来破除数据依赖。如图下半部分所示,我们将一个变换器层的计算分成5个阶段:

  • 阶段1:输入归一化,QKV投影,附加KV,BMM

  • 阶段2:BMM,注意力,O投影,后归一化,分门别类

  • 阶段3:调度发送,共享专家

  • 阶段4:调度接收,MoE,组合发送

  • 阶段5:组合接收

在前3个密集变换器层,我们使用整个批次。在接下来的58个MoE变换器层,我们将批次均匀分成两个微批次。两个微批次交替执行,相隔3个阶段。由于这两个微批没有数据依赖性,我们可以在调度发送和组合发送后切换到另一个微批次的计算。


延迟分解

接下来,我们通过实验比较了重叠的效果,以及比较了单节点部署EP8和多节点部署EP128的性能差异。为了便于比较,我们在以下实验中使用了H100 GPU。我们使用了TP1,每个GPU的批量大小为128,每个请求的查询长度为2,KV缓存长度为5000。

上图显示了一个MoE变换器层上消耗的总时间和不同类型内核的延迟比例。除了调度、组合和GroupGEMM之外,其他内核的执行时间在EP8、EP128无重叠和EP128调度重叠系列中应该是相等的,因为批次大小相同。

重叠

首先比较三种重叠方法的效果。无重叠总共花费2667微秒,调度重叠花费2651微秒,节省16微秒,仅0.6%。微批显示出非常显著的改进,总耗时为1896微秒,速度提高了29%。调度时间减少了从593微秒到367微秒,组合从1012微秒减少到237微秒。

注意,对于计算内核,将大小为128的批次拆分成两个大小为64的批次增加了总执行时间。因此,虽然通信时间减少了1001微秒,总时间只减少了771微秒。我们将在下面的Roofline模型部分用屋顶线模型解释原因。

因为这个原因,微批处理并不总能改善性能。

上图显示了Microbatch相对于DispatchOverlap批量大小为4-128的性能改进。当批量小于32时,Microbatch性能降低5%-40%。当批量大于或等于32时,Microbatch可以提高性能10%-35%。

EP8与EP128

让我们回到前面的图中并比较EP8和EP128微批。EP8总共花费1802微秒,略低于EP128的1896微秒。除了上述Microbatch导致的内核执行时间增加外,主要差异在于用于MoE计算的GroupGEMM和两个通信内核——调度和组合。

EP8的GroupGEMM耗时555微秒,而EP128的GroupGEMM耗时270微秒,减少了一半。这是多节点部署的核心优势。

不幸的是,通信耗时增加了213微秒,大大抵消了GroupGEMM的优势。在我们单独的性能测试中,我们发现它们只能达到Infiniband带宽的一半。我们将继续优化我们的通信内核。

另一个执行显著滞后的内核是GEMM。Microbatch增加了GEMM 95微秒。我们将在下面的Roofline章节中更深入地分析GEMM。我们相信当前GEMM实现尚未达到最佳性能。

屋顶线

屋顶线模型是分析内核性能的良好工具。其水平轴是算术强度,即FLOP与内存I/O字节的比率。可以直接从内核语义中计算水平轴值。纵轴代表实现的性能,通过将FLOP除以基准延迟计算。

内核性能的理论上限由GPU规格直接确定。H100的FP8峰值性能为1979 TFLOP/s,在屋顶线模型中表示为水平线。H100的内存带宽为3.35 TB/s,表示为通过原点的线的斜率。这两条线为计算受限和内存受限内核提供了性能限制。

下面,我们讨论GroupGEMM和GEMM内核的性能。

GroupGEMM

MoE中的GroupGEMM内核执行以下计算:共有g个组,第i个组有m_i个标记,进行矩阵乘法[m_i, k] x [k, n] -> [m_i, n]。在性能测试中,我们假设每个组的标记数量相同,记为m_i = m。然后,GroupGEMM的FLOP计数为2 * g * m * k * n,内存I/O字节为g * (m * k + n * k + m * n)

在DeepSeek-V3/R1模型中,有256个专家,每个标记被分配给8个专家进行计算。假设批次大小为128,查询长度为2,使用EP128 DP128配置,每个专家平均接收的标记数(即m)为128 * 2 * 8 * 128 / 256 = 1024。类似地,我们可以计算其他配置和批量大小的m

我们使用DeepGEMM的GroupGEMM实现进行性能测试。测试点覆盖了与TP1和批量大小1-128组合的EP8、EP16、EP32、EP64、EP128配置。

上图显示了不同EP配置下GroupGEMM的屋顶线模型。不同EP对应不同的组数。图中展示了几乎重叠的性能线,显示GroupGEMM性能主要由总标记数量(表示为g * m)决定。

星号标记代表与每个EP配置每GPU批次大小为128的对应数据点。比较这些星号数据点,我们可以看到,随着EP的增加(并且DP同步增加),每个专家的标记数m也增加。在EP8时,m=128,而在EP128时,m=2048

随着m增加,算术强度也增加。在大多数配置中,GroupGEMM受限于内存带宽,因此,增加m提高了性能。

GEMM

GEMM内核对应于模型中的线性投影,例如Q/K/V/O投影。对于矩阵乘法[m, k] x [k, n] -> [m, n],FLOP计数为2 * m * k * n,内存I/O字节为m * k + n * k + m * n。我们也可以测试批量大小1-128的延迟。

上图显示了不同EP配置下GEMM的屋顶线模型。我们可以看到,GEMM性能受限于内存带宽。随着批量增加,算术强度也增加,从而改善性能。

微批处理

使用微批处理时,我们将批次均匀分成两部分。从上面两张图来看,m变为m/2时,矩阵乘法效率降低。因此,执行两个大小为m/2的矩阵乘法所花时间比执行一个大小为m的矩阵乘法要长。

多标记预测

在整篇文章中,我们假设使用多标记预测(MTP)进行推测解码。MTP将每个请求的查询长度从1变为2。对于矩阵乘法,这相当于将m变为m*2,从而提高矩阵乘法效率。另一方面,如果我们为MLA绘制屋顶线模型,我们会发现增加查询长度显著提高了MLA内核效率。

因此,使用MTP在模型效率中起着重要作用。

实现与优化

在本节中,我们将介绍我们DeepSeek-V3/R1模型的一些实现和优化细节。

量化

DeepSeek-V3/R1在FP8上进行本土训练,采用每块量化方案,权重静态量化,激活进行动态量化。与静态按通道或按矩阵计算缩放因子不同,缩放因子是在128元素向量上计算的,对于矩阵则基于128x128单元块进行计算,从而限制量化导致的精度下降。

在Perplexity,我们依赖于CUDA和Triton内核的混合,支持推理,其中CUDA用于最性能敏感且不经常修改的内核(如注意力和GEMM),Triton实现广泛的激活、归一化和实用内核。Triton使我们能够快速适应内核到块量化方案。

对于线性和MoE层,我们混合了深度GEMM内核和我们自己的Triton GEMM内核,因为我们注意到对于某些矩阵维度和较低的批次大小,Split-K能够提供更低的延迟。如果未量化层执行(M, K) x (K, N)乘法,则需要(M x ceil_div(K, 128)) x (ceil_div(K, 128), ceil_div(N, 128))缩放因子用于块量化。对于块量化,激活的缩放因子是在飞行中计算的,而不是预先校准的。由于激活缩放因子仅沿K维聚合而不是沿M维,因此内核只需轻微更改即可支持方案。

DeepSeek-V3/R1使用的SiLU激活功能进行了大量更改以支持CUDA图形、块量化和动态路由标记数。块量化可能会引入水平归约的问题,但是内核已经在它们的隐藏维度沿方向将激活分块成1024元素内块。在一个块内,待量化张量被进一步分块成128块以计算最大绝对值,Triton生成了高效的跨warp最大归约,加上极少的开销。

为了在CUDA图形下支持MoE路由,内核必须意识到路由信息,即指示每个专家的标记数量,而不是根据为容纳标记数上限分配的缓冲区大小安排工作。我们无法基于输入张量维度划分问题,因此发起了一定数量的持久内核,这些内核读取路由信息以确定填充了多少标记并动态地分配激活处理的工作。

我们已经向FlashInfer项目上游提交了一些我们的内核,未来我们将开源更多代码。

MLA层

我们使用FlashInfer进行MLA计算。FlashInfer支持灵活的页表设置和极高的性能。

我们将q_a_projkv_a_proj融合到单个qkv_a_proj中。延迟从15.4微秒 + 14.8微秒=30.2微秒减少到16.7微秒。

我们将kv_b_proj分解为两个矩阵,k_b_projv_b_proj。我们为与这两个矩阵相关的计算编写了一个FP8块量化的BMM内核。

CUDA图

CUDA图能显著减少内核启动开销,这对性能至关重要。我们为每个批量大小创建了一个CUDA图。

在开发我们的AllToAll内核之前,我们使用torch.all_to_all_single()进行AllToAll通信。这个操作需要所有GPU使用相同的批量大小。然而,不同的DP组可能运行不同的批量大小。

为了确保all_to_all_single()能兼容不同DP组使用不同的批量大小,我们首先在每次模型运行之前使用allreduce()操作获取所有DP组中的最大批量大小。然后,我们让所有DP组使用这个批量大小运行。

虽然这种方法确保我们能使用CUDA图,但它有三个缺点。首先,它需要额外进行allreduce()操作。其次,批量较小的DP组被迫填充。第三,它使我们的实现代码复杂。

在实现我们自己的AllToAll内核之后,我们不再要求所有GPU使用相同的批量大小。因此,我们不再需要执行额外的allreduce()操作或填充批量大小。

MoE路由器

MoE路由器在Triton中实现,依赖标准库修改的排序并记录排序元素的索引。该实现共享给所有MoE模型,因为Mixtral路由是DeepSeek路由的特殊情况,其中Top-K组与所有专家组相同。稀疏内核直接使用Top-K索引和分数,而依赖于all-to-all的密集调度/组合方案则需要按专家聚合路由信息,而不是基于每标记的基础。

未来工作

在未来的工作中,我们计划进一步优化DeepSeek模型的性能。

最重要的下一个优化是预填充分解。DeepSeek-V3/R1模型的预填充阶段和解码阶段有非常不同的计算特性。两者都可以使用不同的优化策略和部署方案。

对于MLA层,在解码阶段,我们使用矩阵吸收减少MLA计算的FLOP计数。在预填充阶段,首先将潜在向量投影到K/V空间,然后以多头注意力(MHA)形式计算,会有更好的表现。

如果预填充和解码在同一个GPU上运行,为了减少预填充对解码输出速度的影响,我们通常使用分块预填充将查询划分为多个块进行预填充。由于KV缓存存储潜在向量,转换MLA为MHA形式变得困难。

对于MoE层,在解码阶段,我们尽可能使用大的EP和DP以增加每个专家的输入标记数,从而提高GroupGEMM性能。在预填充阶段,因为标记数量已经足够大,GroupGEMM已经计算受限。因此,对于预填充,我们可以使用较小的EP和DP。

如果预填充和解码在同一个GPU上运行,只要任何DP组正在进行预填充,所有GPU上的MoE层延迟将增加,显著影响解码输出速度。

除了预填充分解,我们还计划优化以下方面:

  • AllToAll性能:我们的AllToAll内核当前只能达到Infiniband带宽的1/3。我们将继续优化这个内核。

  • EAGLE风格的推测性解码:在上面的数据中,我们假设使用推测性解码预测1个标记。EAGLE可以使用树形结构预测多个标记,提高接受长度,可以显著增加输出速度。

  • GEMM内核:在早先展示的屋顶线模型中,我们可以发现GEMM内核的效率仍然远低于理论极限。我们将继续优化这个内核。

  • GB200 NVL72:在NVIDIA最新的GB200 NVL72方案中,72个Blackwell GPU通过高速NVLink连接。对于MoE架构模型,这是一个非常大的机会和挑战。

结论

DeepSeek MoE模型的多节点部署实现了密集的LLM通常不可能实现的目标:同时提高吞吐量和延迟。通过在更多GPU之间分配专家,我们降低了每设备内存带宽压力,从而实现了更快的处理速度和更高的系统吞吐量。我们的实验显示EP128配置在同等输出速度下的吞吐量比单节点部署高达5倍。

计算与通信重叠技术如微批处理显著降低了多节点通信开销,我们的实现显示40%的加速。我们的自定义AllToAll通信内核和优化内核实现使得671亿参数模型的部署更加高效。

随着MoE架构的普及,这些部署策略为高效扩展此类模型提供了宝贵的见解。

参考文献

有兴趣塑造我们API平台的未来吗?我们正在招聘。

加入我们的开发者社区,掌握最新版本、功能和更新。

有兴趣塑造我们API平台的未来吗?我们正在招聘。

加入我们的开发者社区,掌握最新版本、功能和更新。

有兴趣塑造我们API平台的未来吗?我们正在招聘。

加入我们的开发者社区,掌握最新版本、功能和更新。