如果你曾观察过 LLM 生成长响应的过程,并纳闷为什么在初始提示词闪现后,生成速度似乎变得极其缓慢,那么你正在实时观察一个硬件瓶颈。大多数开发者会归咎于他们的 Python 代码、框架或模型本身的庞大。他们分析函数性能、更换优化器、并试图在预处理中节省几毫秒。但这些都无法解决根本问题。速度限制不在你的软件里,而是在硅片里。

每一次大语言模型推理任务都取决于服务器中 GPU 的两个物理特性:它计算数字的速度有多快,以及它将这些数字移动到计算位置的速度有多快。

计算很廉价,移动数据却很贵

GPU 厂商热衷于谈论计算能力。每秒数万亿次的浮点运算。这些数字令人惊叹。但计算只是故事的一半。另一半是内存带宽,即数据从高带宽内存传输到进行实际算术运算的计算核心的速度。

LLM 的运行速度不会超过这两个环节中较弱的那一个。想象一个拥有 20 名顶级厨师的商业厨房。烤箱很热,刀很锋利,每位厨师都准备就绪。但食材配送只能靠自行车,一次一篮。厨房停滞了。增加厨师解决不了问题,购买更快的烤箱也解决不了问题。瓶颈在于道路。

在现代数据中心 GPU 中,算术单元非常强大,以至于它们经常完成计算后就处于闲置状态,在等待权重和激活值通过内存流转时白白消耗周期。这种不平衡不是你代码中的 Bug,而是芯片构建方式的物理现实。内存带宽的发展未能跟上原始计算能力的步伐,而 LLM 对这种不平衡尤为“残酷”,因为它们的前向传播在生成每一个输出 Token 时都需要触及每一个参数。

为什么提示词阶段感觉很快,而生成阶段感觉很慢

LLM 推理分为两个截然不同的阶段,它们对硬件施加的压力完全不同。

Prefill(预填充) 发生在你的提示词首次进入模型时。所有的 Token 一起到达。GPU 可以利用大规模的矩阵-矩阵乘法并行处理它们。数千个算术单元同时工作,工作负载保持密集。这个阶段是计算密集型的(compute-bound)。你在开始时看到的突然爆发的速度?那就是 GPU 正在发挥其设计初衷。

Decode(解码) 则是痛苦的开始。当模型生成下一个 Token 时,它是逐个生成的。这个阶段依赖于矩阵-向量运算,而这种运算仅使用了 GPU 并行能力的极小一部分。更糟糕的是,每一个新 Token 都会迫使 GPU 从内存中重新加载整个模型权重。算术单元渴望工作,结果却只能等待。Decode 是内存密集型的(memory-bound)。GPU 实际上变成了一个昂贵的交通控制器,在数学引擎“冷却”时,不断地在内存总线上搬运参数。这就是为什么即使初始提示词分析感觉是瞬间完成的,一个百词左右的响应仍可能需要 10 秒钟。

KV cache 让情况变得更有趣。在 Decode 过程中,模型会存储每个先前 Token 的 Key 和 Value 张量,这样它就不必从头开始重新计算 Attention。该缓存随序列长度增长,且存储在内存中。因此,现在 GPU 不仅要重新加载权重,还要在每一次前向传播中读写不断扩大的缓存。计算核心几乎没怎么出力,而内存总线却在为两者“满头大汗”。

对抗“内存墙”

工程师们开发了一系列技术来减少必须移动的数据量,或者至少分摊移动数据的成本。

Batching(批处理) 是最直接的方法。如果一个用户的请求迫使从内存加载全量权重,那么同时处理 8 个或 16 个请求可以让 GPU 将该负载分摊到所有请求上。权重只需读取一次,即可在批次中的每个序列中重复使用。在生产环境中,复杂的调度系统会动态地对请求进行分组,有时被称为连续批处理(continuous batching)或在途批处理(in-flight batching),从而使 GPU 极少停顿。这就像是一辆大巴车与 16 辆行驶在同一路线上的私家车之间的区别。

量化 (Quantization) 直接针对带宽问题。模型权重通常以 16 位浮点格式存储。通过将其压缩为 8 位甚至 4 位整数,你实际上将通过总线传输的数据量减少了一半或更多。模型仍需要足够的精度来产生连贯的输出,但现代训练后量化 (post-training quantization) 方法可以在不破坏质量的情况下大幅缩小模型的内存占用。传输中的数据越少,在内存控制器处等待的时间就越短。

FlashAttention 重构了注意力机制,将中间结果保留在 GPU 快速的片上内存中。标准的注意力机制必须将庞大的注意力矩阵写入缓慢的外部内存,然后再将其读回。FlashAttention 将计算分解为适合 SRAM 的较小分块 (tiles),在片上执行 softmax 和缩放步骤,并且仅将最终输出写回高带宽内存。它通过增加少量的计算量,换取了极大幅度减少的主存往返次数,这几乎总是一个明智的选择。

PagedAttention 解决了另一种类型的内存浪费。在解码过程中,KV 缓存 (KV cache) 的增长是不可预测的。传统系统为每个序列分配固定且连续的内存块,随着某些序列提前结束而其他序列扩展,这会留下巨大的内存空隙。PagedAttention 借鉴了操作系统中的虚拟内存概念。它将 KV 缓存条目存储在固定大小的块中,这些块可以非连续地分配,并通过间接表进行映射。这防止了内存闲置在已预留但半空的缓冲区中,并允许更大的批处理大小 (batch sizes),从而通过让内存总线忙于处理有用工作而非碎片化开销,提升了整体吞吐量。

转变问题视角

当延迟激增时,太多的团队会问是否应该切换到更小的模型或重写其推理服务器。这些问题很重要,但它们是次要的。第一个问题应该是关于硬件本身的:你的 GPU 实际上是在忙于计算,还是在等待数据(处于数据饥饿状态)?

查看你的利用率指标。将内存带宽饱和度与 GPU 计算占用率 (compute occupancy) 进行分析。如果你在解码期间看到高内存争用和低算术强度 (arithmetic intensity),那么你面临的不是模型架构问题,而是物理问题。解决方案不会来自于更整洁的 Python 代码,而将来自于更激进的批处理、通过量化权重以更快地挤过“管道”、重构注意力机制以保持在片上,以及管理 KV 缓存以便在不耗尽空间的情况下容纳更大的批次。

一旦你通过这个视角看待推理,优化就变得机械化了。你不再去追逐“模型智能降低了速度”这种迷思,而是开始做出基于硬件实际交付能力的工程决策。这正是区分能够扩展的生产系统与仅仅能运行的系统之间的转变。