Modal 如何为万亿参数编码智能体服务万亿级 token
How to serve trillions of tokens for trillion-parameter coding agents
Modal 分享了为 Moonshot AI 的 Kimi K2.6 提供编码智能体推理服务的性能优化方法,单副本每用户性能提升 2.8 倍、跨用户提升 5.6 倍,其中一个服务每天处理数千亿 token。
Modal 公开了为 Kimi K2.6 编码智能体做推理服务的完整优化路径,从单副本调优到多副本路由都可迁移。
软件正在吞噬世界,而智能体正在吞噬软件工程。软件工程师必须理解智能体软件——如今他们最重要的工具——以及智能体软件的智能核心如何通过大型生成式语言模型的推理来工作,这不仅是因为工程师需要理解和控制他们的工具,至少还因为推理将消耗比计算机所有其他用途更多的计算能力并产生更多收益。
编码智能体的推理服务的核心事实是,它们必须在极高的相对和绝对性能下运行。
所谓相对性能,我们指的是其使用硬件峰值速率或“光速”的很大一部分。所谓绝对性能,我们指的是该峰值速率的规模以及每个请求所完成的工作量都很大。当代的矩阵数学加速器,如Tensor Cores,运行在每秒千万亿次浮点运算的规模上。具有足够智能来自动化软件开发的大型生成序列模型拥有数万亿个浮点参数,即使仅服务单个请求,每个参数每秒也必须被访问多次。
由于这些要求,目前经济可行的编码智能体推理服务只有在足以摊销硬件和工程成本的规模下运营才可行——大致是数万亿输入和输出token的规模。
我们已经做到了这一点,并愿意分享我们的方法。
在Modal,我们运营着多个此类规模的编码智能体推理服务,并与许多同样如此做的客户合作。您可以通过像OpenRouter或Vercel AI Gateway这样的推理路由平台间接使用我们的服务,或直接通过我们的Shared Endpoints使用。
在这篇博客文章中,我们将介绍我们在为编码智能体提供Moonshot AI的Kimi K2.6模型推理时如何优化推理性能。尽管这个模型按该领域的标准来说已经“老”了(字面意义上数百天前发布!),但序列建模、硬件和扩展的基本原理变化足够缓慢,以至于核心故事和许多细节与我们为更新模型所做的工作相匹配,这些模型在智能和成本性能上已取代K2.6,例如Kimi K3。
我们的优化使我们能够将推理副本的每副本性能提升:每个用户2.8倍,副本上跨用户5.6倍:
此图表将单个用户的体验(每用户每秒解码token数,即交互性)作为x轴,与整个系统的成本性能(每GPU每分钟总token数,即token吞吐量)作为y轴相关联,每个点表示并发用户数。
更直观地说,这就是一个极其昂贵、用户体验如右图所示的服务与一个价格有竞争力、用户体验如左图所示的服务之间的区别:
然后我们将这些单容器副本扩展为部署和服务。其中一个特定服务每天处理数千亿token,总计达数万亿:
下面,我们旨在让这种性能工程对广大软件工程受众来说易于理解。通过分享我们以及我们的客户如何能够运营这些服务,我们希望这能让你也能做到同样的事——也许是通过在 Modal 上部署一个 Dedicated Endpoint。
首先,理解工作负载。
我们将其分为两部分:理解对每个请求的响应进行推断的序列模型,以及理解跨请求的工作负载结构。
最先进的编码智能体由万亿参数的神经序列模型支持,这些模型并行处理输入并顺序推断输出。
当代编码智能体由 unicode 序列的概率生成模型驱动,这些模型主要经由无监督掩码序列预测进行预训练,并主要通过对输出软件正确性的强化进行后训练。就像编译器的解析器一样,它们不作用于原始字符串,而是作用于分词后的序列,因此我们将其输入和输出称为 token。因为归根结底,我们是在猜测输出 token 应该是什么,所以这被称为 推理。如果你更喜欢演绎,那就继续用数据库和操作系统吧。
如今底层的序列模型是混合注意力、专家混合的 Transformer 神经网络。这些网络既执行序列中每个 token 的计算,也执行序列中跨 token 的计算。
注意力已演变为跨 token 计算的通用术语。专家混合指的是动态路由的块稀疏矩阵乘法,它承担了大部分每 token 计算。这些计算迭代地更新网络的内部表示,即潜在表示。
要深入了解这些序列模型内部发生了什么,请参阅 Anthropic 的 “A Mathematical Framework for Transformer Circuits”(2021 年,但至今仍未被超越)。
通过此类神经网络的一次前向传播,会为每个序列位置产生大量内部状态以及序列中下一个(些)token 的概率分布。因为我们基于自己的输出(“auto”)进行预测(“regress”),所以这是自回归序列建模。
为了响应客户端请求,我们通常像这样将多次前向传播串联起来:
前向传播代价高昂,因此我们希望尽可能摊销这项工作。每 token 计算中的大部分工作通过将多个序列批处理在一起来摊销。跨 token 计算中的大部分工作通过缓存内部状态来摊销。由于历史原因,这被称为键值缓存(KV cache 或简称 KV),尽管像 Kimi 这样的当代模型并没有区分键和值。你可以在 Kipply 的精彩博文 “Transformer Inference Arithmetic”(2022 年,但至今仍未被超越)中阅读更多关于“餐巾纸数学”的内容。
当前向传播处理请求的输入 token 时,我们称之为 prefill,因为它在“预填充”KV 缓存。当前向传播产生响应的输出 token 时,我们称之为 decode,因为我们正在将模型对过去状态的“编码”“解码”为预测的未来。那同时做这两者的前向传播呢?是啊,我们也不喜欢这套术语。
Prefill 性能主要通过完成一个请求所有 prefill 的延迟来衡量,即首 token 时间(TTFT)。Decode 性能主要通过之后输出 token 的生成速率来衡量,即每秒输出 token 数(TPS)。两者都可以在客户端或服务端测量,这造成了无尽的混淆。
本文所涉及的特定序列模型是 Moonshot AI 的 Kimi K2.6。该模型用大约一万亿个数字(矩阵中的权重)来参数化其矩阵乘法,其中大部分以四位整数(INT4)存储。
然而,我们使用四位浮点数(FP4)来服务该模型。四位只能给出十六个不同的值,因此你还需要一种微缩放格式来独立地缩放张量内的各个块。我们选择了 NVFP4 微缩放格式,它在 Blackwell 流式多处理器架构 GPU(如 B200 和 B300)的 Tensor Core 中具有 petaFLOP/s 级别的原生硬件支持。由于我们在计算供应受限的时期运营动态 GPU 集群,我们准备让部署同时运行在 B200 和 B300 GPU 上。以下结果均针对 B200 GPU;B300 基本相似,但由于其拥有更多可用于缓存的高带宽内存(HBM),因此以更高的请求并发度运行。
我们选择 SGLang 推理引擎作为基础。我们发现了若干通过修补该引擎来提升性能的机会。作为 SGLang 项目的贡献者,我们将这些补丁上游化,并在下文中进行了描述和链接。
为了优化用户体验和性价比,你必须理解这些序列在请求之间的结构。
当你以朴素方式在编码智能体流量上服务此类模型时,会得到糟糕的结果。
该图表表明,在超过 6 个并发用户后,吞吐量和交互性会迅速崩溃。此外,即使在该峰值之前,交互性也低于用户预期,系统效率也低于可接受水平。
因此,从这里开始,你需要提高交互性和吞吐量,以便为用户提供更好的结果,同时降低自身成本。要做到这一点,你需要比仅仅“输入 token 和输出 token”更深入地理解此工作负载中的序列。
输出 token 的单个请求是在“会话”中创建的:用户、生成模型和工具调用迭代地链接在一起,构建起一座输入序列之塔,随时间累积上下文——以及价值。这种意义构建、信息发现和理解过程的迭代性,在我们看来是序列建模和顺序行动本质的基础,因此我们预计这种模式将远远比“编码智能体”更持久。
具体来说,单个会话大致如下所示:
也就是说,每一轮 T 的输入序列(绿色)是截至 T 的整个会话历史(深绿色),加上一些新内容(浅绿色)。这有两个关键后果。
首先,这意味着请求的输入序列相对于其输出序列天然就很长(如上图粉色部分)——第 T 轮的输入中包含 T-1 个过去的输出序列,而 T 是几十。在我们用于优化并在生产环境中服务的核心工作负载中,这个比例是 200:1;请求包含大约 100k 个输入 token,并产生大约 500 个输出 token。这意味着处理的大部分 token 将是输入 token(只需查看你的编码代理软件中的 token 使用量数字即可)。
其次,这意味着输入序列与之前处理过的输入序列——即第 1 轮到第 T-1 轮的输入——高度重叠。这意味着在服务第 T 轮的过程中,第 1 轮中的 token 被处理了 T 次。这使得缓存绝对至关重要——我们可以避免线性扩展的重新计算以节省工作量,但会引入必须管理的线性扩展状态,并且该状态有其自身的性能特征。权衡这一取舍是我们在本文中要解决的核心工程问题。
带着对工作负载的这一认识,我们转向优化。
然后,优化单个副本。
要优化性能,先构建一个可运行的系统,找出瓶颈,然后消除它。根据需要重复,直到获胜。
尽管我们的最终目标是优化整个服务,但我们将该问题分解为两个更简单的问题:先优化单个副本,然后从一个副本扩展到多个副本。
我们进一步将单副本性能问题拆分为两个子问题:首先最大化交互性,然后在不损失交互性的前提下最大化吞吐量。
交互性主要影响请求延迟。请求延迟和吞吐量通过并发(即进行中的请求数量)相互作用,这是对利特尔法则的一种重新排列:
我们在延迟、并发和吞吐量方面的关键瓶颈始于GPU HBM。
我们在延迟方面的关键瓶颈是解码期间的HBM 带宽。我们通过跨 GPU 并行化矩阵乘法(张量并行,TP)以及应用自定义的DFlash 推测解码来消除它——每次内存加载做更多计算,即使这些计算可能并不需要。
这通过 HBM 容量造成了并发瓶颈:我们能在缓存中保留多少工作,而该缓存的加载速度比我们直接重新计算结果更快。我们通过清理 HBM 中的中间结果、将中间结果量化为更低的浮点精度,以及使用HiCache将缓存层次扩展到 CPU RAM 来消除它。我们使用缓存命中率(CHR)作为缓存改进的针对性指标。对于大多数编码代理工作负载,1 到 2 个 9 的 CHR 是非常可行的。
我们从最大化交互性开始。
提高交互性会提升单个用户所感知的系统性能。我们选择首先处理这一点。我们做出这一选择有几个原因。
首先也是最简单的一点,我们发现编码代理用户喜欢并且愿意为更快到达的 token 支付更多费用,因此高交互性是构建我们的用户和客户用户所期望的服务的关键。
其次,交互性特别适合通过推测解码来改进。因为它是一种简单的、基于学习的技术,其性能收益会随计算量和数据规模而扩展:机器学习著名的“苦涩教训”,在 ML 系统的性能工程中再次应验。而且我们知道如何扩展训练。
这种“交互性最大化”的选择带来了两个额外好处,一个在运维层面,另一个在吞吐量层面,而这两点尤其突出,因为我们运营着一个动态、自动扩缩容的机群,包含数千块 GPU。
交互性最大化的副本更小,因此更易于服务。
将多个处理器一起使用需要互连网络(interconnect)进行通信。对于 Nvidia GPU 而言,延迟最低、带宽最高的互连是 NVLink。NVLink 在某个规模的“域”内的一组处理器之间运行。
单个主机操作系统最多可支持 8 块 GPU 的 NVLink 域。目前普遍可用的最大 NVLink 域包含 72 个加速器(位于多节点IMEX 域中)。使用更多加速器将需要更慢的互连(IB/RoCE,或者更糟的标准以太网)。这意味着,为了实现最大交互性,我们不应期望每个副本使用超过 72 个加速器——通信开销几乎肯定会压过任何单请求延迟上的收益。
但这并不意味着我们必须使用 72 个加速器。
看看SemiAnalysis 针对同一 NVFP4 Kimi K2.6 模型的 InferenceX 基准测试的这些结果,它们展示了多种部署下每 GPU 吞吐量随交互性变化的曲线,并标注了 GPU 数量:
最高交互性是由每个副本仅 8 块 GPU 的部署实现的。此外,这种交互性是在每 GPU 吞吐量相当的情况下实现的,这意味着通过选择更小的域,我们并没有明显放弃峰值吞吐量的成本性能(在我们交互性约束的前提下)。
为了让图表清晰易读,我们只选取了与我们的部署最相似的一小部分部署,但这一规律在 InferenceX 基准测试中的更多加速器类型和更多模型上都成立(可在此处探索)。一般来说,仅用四块或八块 GPU,就能在每 GPU 吞吐量相当的情况下实现最高交互性。然后,你可以通过扩缩更小的副本来达到相同的总吞吐量。核心 Modal 无服务器平台使这种扩缩容既高效又可靠。
这是运维上的巨大胜利。更小、更简单的单元更易于扩缩容。八块 GPU 可以由单个主机操作系统内核驱动。而一个 NVL72 域则由九个这样的子系统组成,共享一个地址空间(是的,你应该感到不寒而栗)。可用性受限,合同周期长且不灵活。
另一方面,八 GPU 的 Blackwell 系统足够标准化,可以通过按需和竞价市场获得,这使得处理可变负载更具成本效益。此外,包含一、二或四块 GPU 的副本可以打包在单台物理八 GPU 机器内——这台机器已经拥有启动另一个副本所需的所有资源(模型权重、JIT 产物)。
当然,随着计算供给和用户需求的变化,我们会乐于重新审视这一选择。
提高交互性会间接提高吞吐量。
通过降低单个请求的延迟,我们释放出资源用于新请求,从而间接提高了吞吐量。
智能体编码工作负载在每个会话中大致是“闭环”的。会话几乎总是由用户编写的 token、工具调用响应和模型输出构成的链条。因此,会话中的下一个请求几乎总是在上一个响应完成生成后的一段时间才到达。于是,会话的下一个请求会在推理系统之外潜伏一段时间——对于工具调用,为几十毫秒到几秒,尾部可达数分钟;对于用户响应,为几秒到几分钟,尾部可达数小时甚至更久。
在这段时间内,其他请求可以在同一节点上被处理。当活跃容量有足够的负载时,总有请求等待节点处理。当活跃负载有足够的容量时,总有节点可以映射请求。这两点都由我们的快速自动扩缩容系统保证。我们将在扩展到多个副本的章节中进一步讨论请求路由。
使用自定义的推测解码,在每次遇到交互性瓶颈时完成更多工作。
交互性衡量的是每个用户每秒的输出 token 数。朴素地说,像 Transformer 这样的自回归序列模型是顺序生成这些 token 的。Amdahl 那令人心碎的定律再次应验。
每生成一个 token,都必须从GPU HBM 加载数 GB 甚至更多的模型权重和 KV 缓存到流式多处理器 L1 缓存,这通常比实际计算单个下一个 token 的 KV 状态和输出耗时更长。这就在内存带宽上造成了瓶颈。并行有助于创造更多带宽,但这对于逐 token 计算比跨 token 计算更有用,而后者在长序列中会成为瓶颈,正如在编码智能体工作负载中所观察到的那样。
因此,我们转而通过应用推测解码来提升这一瓶颈。
从根本上说,推测解码做出的权衡与处理器中的推测执行相同:当由于操作之间的串行依赖而存在空闲的操作带宽时,你可以利用该带宽来运行可能最终用不上的操作。如果你能高概率地猜中将被使用的操作,有效操作吞吐量就会提高,而关键就在于以尽可能少的工作提高这一概率。
对于自回归序列模型推理,每次迭代运行更多操作的“诀窍”是使用另一个更快的语言模型(“推测器”或“草稿模型”)来猜测接下来的几个 token 是什么,然后与服务模型(“目标”或“验证器”)并行验证这些猜测。
与推测执行一样,这种加速不会改变程序行为,即目标序列模型的概率分布。
正如我们在发布 Qwen 3.5 和 3.6 模型家族推测器的博客文章中所描述的,推测解码带来的收益很大——是整数倍的提升,而非仅仅百分之几十。
反直觉的是,平均而言,要产出一个能预测输出中接下来四个、八个甚至更多 token 的推测器相当容易,尤其是在编码智能体工作负载下。大致来说,原因有两个:目标模型为推测器的成功创造了条件,而且大多数 token 并不需要动用目标模型的全部智能。
推测器可以复用目标模型的工作成果。
首先,目标语言模型在前向传播过程中已经为序列生成了极其有用的表示——从每个 token 的静态嵌入开始,模型的每一层都逐步丰富这一表示,直到最后的“语言建模头”层将该表示转化为下一个 token 上的分布。更妙的是,这些表示已经存储在 KV 缓存中。像 DFlash(以及 DSpark 等衍生架构)这样的最先进推测器架构将这些状态复用为输入,因此它们可以比目标模型小(且快)几个数量级:站在巨人的肩膀上,指向它们下一步可能去往的方向。
token 序列具有重复性且信息密度低。
考虑以下编码智能体输出示例:
任何使用过近期模型的人都能很好地猜出 You’re absolutely 之后是什么(它绝不会是 wrong)。而且这段引文来自之前的用户输入,所以一旦引号打开,接下来的 token 就变得高度可预测。
再深入一层,考虑这个序列在按照模型“聊天模板”中的特殊控制 token 格式化之后是什么样子:
这个序列具有大量无需高智能即可生成的结构。当然,该结构内部的细节对正确性仍然重要,因此目标模型的能力依然关键!
那么,目标模型的大部分容量很可能都用于丰富这些 token 的表示,以便用于预测 许多步之后 的 token。如果你已经知道接下来几个 token 是什么,你就可以并行计算它们的表示。
这并非什么怪癖或取巧手段:相对于传统的循环神经网络,同时提供并行和串行前向传播是 现代序列模型的一项基本特性。它同时存在于“经典”Transformer 和线性/混合注意力模型中,因此我们可以预期它会持续存在。
出于这个原因以及其他原因,我们 在推测解码上投入了大量精力,我们也建议你这样做。
自定义推测器可以大幅提高接受长度。
最快的推测器不仅要训练来预测目标模型的一般行为,还要预测其在特定数据集上的行为。由于它们很小,其建模容量有限,而你希望只把这种容量用于生产环境中实际会发生的情况。给 ML 爱好者的说明:推测器的损失是相对于目标模型的 Kullback-Leibler 散度,这会鼓励模式寻求(mode-seeking),而非模式覆盖(mode-covering)。
但正如一般的神经网络一样,我们的实验表明,最好从强大的基础开始,然后让推测器适应特定任务——也就是微调。因此,我们首先在通用数据混合上为 Kimi K2.6 训练了一个 DFlash 推测器,然后在目标模型输出的编码轨迹上对其进行微调。之后,当处理生产流量时,草稿模型可以在目标模型的输出上持续训练。
在处理实时流量时,我们遇到了一个问题:将 token 映射为字符串再重新分词并不是恒等映射,因为分词从根本上说是一种糟糕的 hack。但典型的日志记录(例如 HTTP 请求的日志)操作的是字符串,而不是 token。因此,我们修改了 SGLang,使其通过 sglext 输出原始 token id,并将这项工作贡献到了上游。
在具有代表性的轨迹上,微调使我们的接受长度从每步 5.00 个 token 提升到 5.84 个 token,增量加速为 20%。
张量并行是实现最大交互性的最佳并行策略。
给一个缓慢的任务增加更多工程师会让它耗时更长,但计算机没有这种弱点——前提是你正确地并行化工作并分片数据。
序列模型推理的主要并行策略将工作拆分到:
- 单个请求内,跨模型前向传播(预填充-解码分离),
- 单次模型前向传播内,跨层(流水线并行),
- 一批请求内,跨序列(数据并行),
- 单个序列内,跨 token(上下文并行),
- 单个模型层内,跨矩阵乘法(专家并行),以及
- 单次矩阵乘法内,跨行/列(张量并行)。
在这些选择中,只有上下文并行、专家并行和张量并行在单个请求内拆分工作,因此能直接提升交互性。张量并行(TP)是最低层级的并行化——除了内核执行内部的并行化,后者数量庞大但不在本文范围内(我们已在别处分享了部分相关工作)。这意味着 TP 优化能更好地与其他策略组合,因此是一个很好的首要目标。
更详细地说:张量并行将矩阵乘法的输入接收进来,并把输出处理工作拆分到并行工作节点上,因此可以分片该处理所需的矩阵数据,也就是模型权重。更多内容请参见Megatron 论文(2019 年,但至今仍未被超越)。
尽管有这一第一性原理的论证,我们仍然研究了多种其他并行策略,因为 1)交互性可能间接受到其他方面优化的影响,2)你永远不知道自己不知道什么。然而,我们发现张量并行就是交互性最大化所需的全部™。例如,我们发现数据并行注意力允许我们通过分片 KV 缓存实现更高的并发,但延迟更差。事实上,它差到导致每 GPU 的整体吞吐量下降,尽管并发度提高了。
除了选择并行策略,你还需要选择并行工作节点的数量。对于在 B200 GPU 上运行、序列可能包含数十万 token 的 Kimi K2.6 模型,可行的配置是四 GPU(TP4)和八 GPU(TP8)。
这里做个快速估算:一块 B200 有 180 GB 的 HBM,而 Kimi K2.6 有 595 GB 的权重(超过一万亿,每个权重一个 nybble)。把权重溢出到 CPU RAM 或磁盘会严重破坏延迟,所以 TP1 和 TP2 都不可行。用四块或八块 GPU 来分片权重,我们大约有 125 或 845 GB 用于 KV。每个 KV 条目有 576 个元素,以两字节 BF16 格式存储,共有 61 层,每层对每个 token 都有自己的 KV 条目,因此单个 token 消耗约 72 KB = 576×2×61 字节。这样在 TP4 下大约能容纳五十万个 token 的 KV,在 TP8 下大约三百万个——硬件翻倍,缓存容量六倍。
| 配置 | 总 HBM | HBM 减去权重 | 每 GPU 约 KV 大小 | 约 KV 容量 |
|---|---|---|---|---|
| TP1 | 180 GB | -415 GB | - | - |
| TP2 | 360 GB | -235 GB | - | - |
| TP4 | 720 GB | 125 GB | 31.3 GB | 0.45 Mtokens |
| TP8 | 1.44 TB | 845 GB | 105.6 GB | 3.0 Mtokens |
这让 TP8 看起来相当有吸引力。然而,我们发现在目标工作负载上,以及在与我们交互性目标兼容的并发量下,TP8 仅运行 prefill 时每 GPU 吞吐量大致与 TP4 同时运行 prefill 和 decode 相同——这是一个偏向 TP8 的不公平比较,而它还是输了。
然而,选择 TP4 让我们在 KV 缓存容量上受到极大限制。
因此,从这里开始,我们转向缓解这一限制的策略。
我们通过更好的 KV 缓存解除了吞吐量上的并发瓶颈。
在每请求约 10 万最大输入 token 且运行 TP4 的情况下,单个副本上只能调度大约 4 个用户的对话而不至于拖垮交互性。超过这个数,缓存命中率(CHR)骤降,交互性/吞吐量因长输入被重新计算而崩溃。重新计算远比从 HBM 加载它们的 KV 条目慢得多。于是我们着手为 KV 创造更多空间。
对 HBM 来一场近藤麻理惠式的整理。
最直接的优化是找出浪费的 HBM,把它还给 KV 缓存。
我们查看了 SGLang 中 DFlash 草稿模型架构的实现,注意到它占用了必要 HBM 用量的两倍。
具体来说,用作草稿模型输入的目标模型中间结果首先被收集为指针列表,然后在前向传播结束时复制到连续内存中。我们重写了它,改为预先分配该连续内存作为缓冲区,并在前向传播期间将中间结果推入其中,将 HBM 的峰值负载减半。而且我们没有改变基于追加的逻辑,这要归功于一点 Python 魔法。我们在这个 PR 中将改动上游到了 SGLang。
用量化释放 HBM。
释放空间的第二种最简单方法是在所有地方使用更低精度的浮点数。
但与投机解码或减少浪费不同,降低精度并非免费的午餐。模型输出可能发生巨大变化,而且通常对应用结果不利。你可以通过我们 LLM Engineer’s Almanac 中的可视化工具直观感受块量化技术的影响(示例如下;左侧为块量化,右侧为原始)。
能够自信地做出原则上是有损、但不会影响目标应用结果的改动至关重要——这也是定制、自托管推理应用相对于通用、多租户模型 API 提供商的一项差异化能力。
和往常一样,推测解码是较容易处理的情况,因此量化草稿模型是一个轻松的收益。模型的目标结果——解码速度——会平滑下降,不像智能那样,而草稿模型的正确性除了性能之外不会影响应用结果。我们在这个 PR 中向上游为 SGLang 的 DFlash 推测器架构添加了 FP8 支持。
但不可避免地存在一些会影响应用结果的诱人优化,这就是为什么我们大力投入建设评估推理服务器建模能力的能力(更多内容即将推出!)。我们也非常欣赏并支持像 Moonshot 的Kimi Vendor Verifier 这样的举措,它们帮助模型使用者持续评估质量。评估,评估,评估!
基于我们的评估,我们发现可以将模型的 KV 缓存从 BF16 量化为 FP8。这使缓存容量(以 token 计)翻倍。此外,大多数专家 matmul 已经采用 NVFP4,这是目前具有原生硬件支持的最紧凑格式(目前如此!)。但并非每个 token 都会激活的关键“共享”专家。我们发现可以将共享专家从 FP8 量化为 NVFP4,从而为缓存释放额外的 HBM。在我们的评估中,这两项更改都没有显著降低模型质量(相对于运行间非确定性)。
用 HiCache 扩展 KV 缓存容量
最后,如果缓存更大,即使这意味着它更慢呢?
到目前为止,我们只考虑了用 GPU HBM 来存储 KV。这意味着我们在处理输入序列时只有两个选择:要么“把它保存在 GPU 上超快、超昂贵的存储中”,要么“把它扔进垃圾桶”。当我们需要服务工作负载的 KV 缓存大小超过 HBM 所能容纳的容量时,由于缓存命中率(CHR)下降,副本性能会急剧下降:
这就是为什么所有好的缓存都是多层的!缓存的每一层都会在负载增加时带来另一个更平缓的 CHR 下降台阶。SGLang 的 HiCache 通过添加“L2”和“L3”缓存层级来扩展 KV 缓存容量,允许将 KV 存储在主机内存(L2)和分布式存储(L3)中。
更高的缓存层级仍然更慢(否则我们就会直接把它们用作更低层级了!),因此它们很容易损害延迟,并可能损害吞吐量。仅使用基于 CPU RAM 的 L2 缓存就让我们获益良多。即便如此,我们基本上只用它来处理过载。
也就是说,如果没有 HiCache,当并发量超过支持峰值性能的水平时,性能会迅速下降,这阻止了我们尝试在该峰值下提供服务。有了它,当单个副本的负载短暂超过峰值时,副本行为会更加平滑。
副本并不总是恰好具有目标请求负载,因为上游用户/代理行为存在非确定性,并且请求会在副本之间进行路由,我们接下来会考虑这一点。
最后,扩展到许多副本。
在优化了单副本性能之后,我们扩展到了更大的部署——毕竟付出了这么多努力,我们想要服务的可远不止六个并发用户!在高层次上,我们通过在 Modal Server 后面提供一个自动扩缩容的推理引擎副本池来实现这一点。
因为我们核心平台的自动扩缩容基础设施处理了所有困扰自动扩缩容、并将其纠缠在意大利面条式 Kubernetes YAML 中的典型问题——决定何时扩缩容、获取资源、启动主机环境、快速设置副本、从故障中恢复、提供可观测性、释放资源——所以我们的工作基本上全部集中在路由层。
路由“很简单”,除非副本有状态,而 KV 缓存给副本增加了状态。幸运的是,这是良性的状态,一种临时缓存:它对于应用程序的正确性并非必需,并且可以在未命中时轻松重新计算。但重新计算会带来性能损失,因此路由成为性能优化的重要部分。
我们观察到两个性能问题,促使我们更仔细地审视路由:
- 排队请求以及尾部首 token 时间(TTFT)和端到端(e2e)延迟反复出现尖峰。
- 单副本吞吐量低于预期。
这两个问题的根本原因是“令人遗憾的冷预填充”——输入序列与我们之前见过的序列有重叠,但我们最终重新计算了整个 KV。而这的根本原因是我们最初的无状态路由算法对请求的放置效率低下,这既受会话内并发的影响,也受运气不佳的哈希影响。
基于这项工作,我们更新了路由层,以支持有状态且感知 KV 缓存的路由算法。如果你有兴趣在你的 Modal Servers 中使用它,请联系我们。
我们从无状态的“会话亲和性”路由开始。
默认情况下,Modal Servers 对所有请求使用均匀随机路由。为了让某些请求“粘”到特定容器,客户端可以提供请求头 Modal-Session-Id。然后将其哈希并映射到某个副本,大致如下:
尽管并不完全是图中的环形哈希——我们使用一致性哈希,以便在副本数量或身份发生变化时获得更好的行为。详情请参见此代码示例。
因此,Modal 上推理服务的编码代理客户端可以在同一代理会话中创建并复用会话 ID,将请求映射到已经见过其先前输入的副本,从而可能将其 KV 表示缓存在内存中,进而带来更好的性能——通常如此。
规模无法让你免于“运气不佳”的哈希。
无状态、均匀随机的会话路由算法在并发用户数量很大、且对并发会话数变化的容忍度较高时,能够相当好地实现均衡,但在低并发下对严格容忍度存在一些问题——而这正是高交互性编码代理推理所处的确切场景。
简略说一下其中的数学原理。使用任何均匀随机哈希算法时,每个服务器的会话数分布是二项分布,其中 N 等于总会话数,p 等于服务器数量的倒数。二项分布会迅速收敛为泊松分布,其速率参数等于 Np, 即会话数除以服务器数。聚焦于稳态动态,得益于自动扩缩容,我们可以将其视为固定值并等于目标并发数。这很好!但泊松分布的方差等于这个固定的速率参数,这意味着每个副本会话数的离散程度不会随着规模增大而减小。它保持固定,从而带来可预测的尾部行为。
具体来说:如果你的目标是每个副本五个会话,并且有五十个副本服务 250 个会话,那么均匀随机路由算法会产生一个服务 ≤1 个会话的副本,概率约为 4%,这表现为聚合效率降低。它还会产生一个至少服务 12 个会话的副本,概率约为 0.5%,这表现为尾部延迟。这些比率与规模无关。
因此,这些路由算法只有在负载离散程度可容忍的情况下才能工作——而编码智能体工作负载并非如此。
而且实际情况比模型预测的还要糟糕。与模型的偏差(偏差总是存在!)会导致并发数出现额外方差。我们直接观察到了这一点。方差常常是均值的数倍,并带有非常重的右尾,导致偶尔出现非常高的延迟。参见下面的每副本负载观测结果(来自一个目标设为五个并发请求、以提高交互性的部署)。
这就是我们观察到的尾部 TTFT 延迟和吞吐量不足的根本原因。
我们重写了路由系统,以更好地处理编码智能体工作负载。
最终的路由器系统实现了显著低于泊松分布的负载离散度(方差低于均值的一半)。下面展示了一个样本负载分布,同样来自一个目标为五个并发请求的部署。
为了实现这一点,我们调查了尾部延迟和负载过度离散的原因,并添加了新的路由算法来逐一解决。
- 会话发送多个并发请求使其副本过载。通过将这些“厚会话”拆分到多个副本来解决。
- 每个会话的工作量不均匀。通过让路由感知负载来解决——这也减少了上述的“不幸哈希”。
- 在扩容期间,我们重新平衡了太多会话。通过优先将新会话映射到新副本来解决。
通过拆分并发会话来修复热点副本。
过度离散和尾部延迟的最大单一原因,是我们将带 ID 的会话建模为“闭环”(即每个会话一次一个请求)的假设被违反。
在我们的系统中,客户端控制会话 ID,因此无法阻止客户端使用同一会话 ID 提交多个并发请求。而且如果会话 ID 总是映射到同一个容器,那么每个容器的并发请求数就不再受限。一个副本会变“热”,负载非常高,即使整体负载并未增加。
典型的编码代理会话,即使有子代理,也不需要跨并发请求共享会话 ID,因为典型会话一次只进行一轮。但在某些情况下,多个并发输入序列会共享一个前缀。当编码代理会话是树状结构而非链式结构时,就会出现这种情况——比如当你使用 /btw 时。
下面是一份跨多个副本、按会话 ID 统计的请求数时间点采样,其中请求数最多的会话用红色标出。副本 6 上最大的会话,其并发请求数超出目标负载好几倍。这可不太妙!
当存在这种“thicc 会话”时,试图保持完美局部性所带来的性能反而比复制部分缓存、把并发工作分散到多个副本上更差。为了解决这个问题,我们的路由器会有意将高并发会话拆分到多个容器中。也就是说,在把某个会话发送到其分配的某个副本之前,我们会检查该会话的负载阈值。一旦超过阈值,我们就把这个请求发送到另一个副本。
通过细粒度的负载感知会话放置,修复不走运的路由和不均匀的会话。
如上所述,均匀随机算法会以固定概率产生“不走运”的容器/用户。
更糟的是,上述模型假设请求处理时间是负载的固定函数。但更多的工作意味着处理在途工作需要更长时间,因此负载较高的服务器上的请求耗时更长,其负载升高的时间也更长——尾部比模型中的更厚。
除此之外,它还假设各会话所需的工作量相等。但有些编码代理会话很长,其中的请求包含数十万个 token,而另一些则很短,只有几千个 token。
为了避免这种情况,我们根据更细粒度的基于负载的信号来把新会话分配到副本,例如运行中的请求(而不仅仅是已分配的会话!)和 KV 利用率。在会话放置之后,我们仍然保持会话亲和性,以保留 CHR。
在扩容时尽量减少缓存迁移。
在负载增加期间,我们需要增加副本数量。现有副本对已在途的会话拥有热缓存,因此我们更希望继续把这些会话路由到那里。
但使用 rendezvous 哈希时,改变容器集合会导致在途会话重新平衡,而不仅仅是新会话。这并非完全无序——使用一致性哈希类算法的要点在于,当你新增一个目标时,只需重新路由约 1/N 的会话即可实现平衡。但即便如此,也需要大量 KV 重算,并导致某些会话变慢。
这最终成为另一种形式的负载感知路由。尤其是在负载增加期间,新会话通常会定期创建,而将更多此类会话映射到负载较低的副本上,会使它们优先落到较新的副本上:那些副本尚未积累任何负载!当新会话的速率与新副本的速率不匹配时,通常仍需要进行一些平衡。我们再次通过负载感知来解决这个问题,这次是在会话重新分配中,而不仅仅是会话/请求分配。
部署并享受吧。
在路由方面完成所有这些改动后,我们的多副本部署行为与从单副本结果外推所预期的表现更加接近。TTFT 明显更加稳定,即便部署规模扩大,每副本吞吐量也始终更贴近单副本性能。
综合来看,这些优化使我们能够以每天数千亿 token 的规模运行多个 Kimi K2.6 推理服务,并且其交互性和性价比都显著优于我们的基线和其他方案。
此后,我们对多个其他模型重复了这一基本流程——速度快得多,因为我们更有经验,也因为我们构建了可复用的工具和基础设施!这包括 Moonshot 更新的 Kimi K3 模型。我们多次在竞争激烈的 OpenRouter 市场上承载了 Kimi-K3 的大部分 token 流量,该市场会根据供应商所提供的推理服务质量将需求路由给供应商。
“科学有时是个骗子。”
在整个工作过程中,我们花在理解基准测试和工作负载上的时间,几乎与优化服务本身一样多。基准测试很难!
我们关心的几乎每一个指标都是系统和我们所输入的工作负载两者的函数。即使系统没有任何变化,改变数据也可能改变我们观察到的结果。
对此最简单的答案就是始终在同一份数据上进行基准测试,并且让这份数据与生产环境的工作负载完全匹配。但生产数据是敏感的,这限制了访问。而且生产数据具有变异性,既体现在请求之间,也体现在时间维度上;要做好性能工程,关键是要理解系统在特定场景下的行为,而不仅仅是总体表现。
因此,我们有时也希望在生产环境常规行为模式之外运行“受控实验”,以便 1)构建理论,2)清晰地隔离并衡量性能干预措施的影响。举个例子,前面已经提到,我们以仅 prefill 模式运行 TP8,以清楚地证明它在我们的场景中不如 TP4。可以将其类比为科学家不仅通过观察自然行为来研究系统,还通过在受控实验室环境中进行干预来研究系统。
数据依赖性显现出来的几个案例:
- TPM / GPU 取决于输出长度——在闭环设置中,许多“典型”请求可能会在生成一个长序列所需的时间内完成。
- 投机接受长度取决于其所评估的数据——代码的接受长度大约可以是散文的 2 倍。
- CHR 取决于单个轨迹——诸如事后编辑消息这类对缓存不友好的模式,可能会让缓存改进看起来无效。
然后呢?
这项工作始于为某个客户针对特定模型优化编码智能体工作负载。但我们也研究过各种推理工作负载,比如对高延迟/吞吐量敏感的分析处理(很快会有更多介绍)。我们持续与一些将推理部署到生产环境的全球领先公司紧密合作,我们也非常希望与你合作!请在此联系我们。
正如本文中性能讨论的通用性所表明的那样,这项工作很容易推广到支持其他客户,也适用于服务其他模型。它还推动了我们推理服务和评估栈的长期改进,包括我们自己的评估平台、新的路由系统以及更好的基准测试技术,我们很快会对此进行更多讨论。
你可能已经注意到,我们非常喜欢我们的核心自动扩缩容云基础设施平台,我们认为正是它使本文中许多底层工程选择成为可能。
这就是这篇文章存在的原因——如果我们真的相信我们的平台是运行高性能推理的最佳选择,为什么要隐藏推理性能的“alpha”?这也是我们致力于开源的原因。我们不仅将推理引擎方面的工作上游化,还发布了代码和配置,作为 Modal Auto Endpoints 的后备来源。现在就用 modal endpoint create 启动一个 Kimi K2.6 的专用端点,你将能够查看我们的设置——或者根据自己的目的进行修改。
最后,如果你读到了这里,我们敢打赌你对推动推理性能的前沿既感兴趣又有能力。如果你想和我们一起做这件事,请查看 modal.jobs!我们很乐意听到对推理感兴趣的系统工程师以及推理专家的声音。
致谢
如果没有像 Moonshot AI 这样的开放权重模型提供商、像 SGLang 这样的开源推理引擎,以及整个分享自己的工作供他人继续构建的研究人员和工程师社区的出色工作,这项工作就不可能实现。
来源:Modal Blog · modal.com