为什么生成一个 token,要把权重从内存搬到计算单元旁边?

从矩阵乘法、GPU 内存层级和 Transformer decode 流程解释,为什么大模型推理不能直接用显存里的权重原地计算。

为什么生成一个 token,要把权重从内存搬到计算单元旁边?

有一句话很容易让人困惑:

每生成 1 个 token,都要把大量权重从内存搬到计算单元附近。

第一反应通常是:权重不是已经在内存里了吗?为什么还要搬?计算单元不能直接用内存里的权重算吗?

这篇把这个问题拆开讲清楚。核心结论是:

内存负责存数据,计算单元负责做乘加。
权重即使已经在 GPU 显存里,也必须经过缓存、片上存储和寄存器,才能变成计算单元真正能用的操作数。

所以这里的“搬”,通常不是指从硬盘重新加载模型,也不是每个 token 都从 CPU 内存复制到 GPU。更准确地说,它指的是:

GPU 显存里的权重,沿着 GPU 内部的数据通路,被流式送到离 Tensor Core / CUDA Core 更近的位置。

1. 先把“直接用内存”这句话拆开

在程序员的抽象里,我们经常写:

y = x * w

看起来 w 在内存里,计算语句直接用了它。

但硬件不是这样工作的。乘法器不能隔空读取一个很远的存储阵列。它真正能吃进去的是已经到达计算单元附近的值。一次计算大致会经历:

从内存地址发起读取

经过内存控制器、片上互连和缓存

进入更近的缓存或寄存器

作为乘法/加法单元的输入

写回结果

编程模型可以让你感觉“直接访问内存”,但物理实现一定有数据移动。

更直白地说:

内存是仓库。
计算单元是机器。
权重是仓库里的原料。

机器要加工原料,原料必须送到机器旁边。

机器不能真的在仓库里原地加工,除非你的硬件就是专门做“近存计算”或“存内计算”的结构。主流 GPU 仍然是存储和计算分离的结构。

2. 先补一点矩阵基础

Transformer 里面最重的操作不是神秘魔法,而是大量矩阵乘法。

先看一个很小的例子。假设当前 token 的隐藏状态是一个向量:

h = [4, 5]

一层模型里有一个权重矩阵:

W = [
  [ 2, -1],
  [ 0,  3]
]

计算 h × W 时,可以理解成“拿 h 去和权重矩阵的每一列做乘法再求和”:

第一列:4 * 2  + 5 * 0  = 8
第二列:4 * -1 + 5 * 3  = 11

输出:[8, 11]

真实大模型里,h 不再只有 2 个数,可能是几千个数;W 也不再是 2 乘 2,可能是几千乘几千,甚至更多。每一层都有多个这样的权重矩阵。

所以模型生成 token 时,硬件一直在做类似的事:

读取当前激活值
读取一块权重
做大量乘法和加法
写出新的激活值
进入下一层

这个“读取一块权重”,就是本文讨论的数据搬运。

3. GPU 里大概有哪几层?

不同 GPU 的细节不完全一样,但理解大模型推理,可以先用这个简化层级:

flowchart TB
  SSD[硬盘 / SSD] --> DRAM[CPU 内存 DRAM]
  DRAM --> HBM[GPU 显存 HBM / GDDR]
  HBM --> L2[GPU L2 Cache]
  L2 --> SMEM[SM 内部 L1 / Shared Memory]
  SMEM --> REG[寄存器 / Tensor Core 操作数片段]
  REG --> CORE[CUDA Core / Tensor Core]

越往下,越靠近计算单元:

容量越来越小
速度越来越快
访问能耗通常越来越低
能被计算单元直接使用的程度越来越高

粗略看,每层的职责是:

硬盘 / SSD:
  长期保存模型文件。推理时不能靠它逐 token 读取,太慢。

CPU 内存 DRAM:
  可以暂存模型文件、tokenizer、运行时数据,但 GPU 计算不能靠它实时喂权重。

GPU 显存 HBM / GDDR:
  推理时模型权重主要放在这里。容量大,带宽高,但仍然离计算单元有距离。

L2 Cache:
  GPU 芯片上的共享缓存,帮忙减少重复访问显存。

L1 / Shared Memory:
  每个 SM 附近的小容量高速存储,常用于矩阵乘法的分块暂存。

寄存器:
  最贴近指令执行的位置。算术指令和 Tensor Core 最终消费的是这里或等价的操作数片段。

CUDA Core / Tensor Core:
  真正执行乘法、加法、矩阵乘加的计算单元。

这里最重要的一点是:

模型权重通常可以常驻 GPU 显存,
但不能整体常驻 Tensor Core 旁边。

显存可能有几十 GB、上百 GB;片上缓存和寄存器的容量小得多。一个 70B dense 模型,如果用 FP16,仅权重数量级就是:

70B * 2 bytes = 140GB

这类权重规模不可能全部塞进片上缓存,更不可能全部塞进寄存器。

4. 为什么不能把权重一直放在计算单元旁边?

因为近处的位置太贵、太小。

计算单元旁边的存储就像厨房案板旁的小碗,访问很快,但只能放当前要处理的一点食材。显存像大仓库,能放很多,但离锅更远。

所以矩阵乘法通常不会把整个大矩阵搬进来,而是切块:

大权重矩阵
  ↓ 切成 tile
一小块权重
  ↓ 搬到更近的缓存 / shared memory / 寄存器
Tensor Core 做矩阵乘加

换下一块权重

这就是 tiling / blocking 的直觉。

如果没有这种分块搬运,Tensor Core 的算力会很难吃满。原因不是它不会算,而是数据供应跟不上。

5. 生成一个 token 时,模型到底跑了什么?

以标准 Transformer decode 为例,生成一个 token 大致是:

flowchart TB
  A[当前 token / 当前 hidden state] --> B[第 1 层 Transformer]
  B --> C[第 2 层 Transformer]
  C --> D[...]
  D --> E[最后一层 Transformer]
  E --> F[输出 logits]
  F --> G[采样或选择下一个 token]

每一层里面又有多个重权重矩阵。简化看,一层里常见的权重包括:

Attention 部分:
  Wq, Wk, Wv, Wo

MLP 部分:
  Wup, Wgate, Wdown

当前 hidden state 进入这一层后,会和这些权重矩阵相乘:

hidden × Wq
hidden × Wk
hidden × Wv
hidden × Wo
hidden × Wup
hidden × Wgate
hidden × Wdown

如果模型有 80 层,这个过程就从第 1 层重复到第 80 层。生成下一个 token 以后,再把新 token 接回上下文,继续跑下一轮。

这就是自回归生成:

生成第 1 个 token:跑一遍模型
生成第 2 个 token:再跑一遍模型
生成第 3 个 token:再跑一遍模型
...

所以“每生成 1 个 token 都要搬大量权重”,不是说每一步都重新加载模型文件,而是说:

每一步 decode 都要让大量层权重重新参与前向计算。
权重虽然在显存里,但参与计算时仍然要一块块流向计算单元附近。

6. 为什么 decode 阶段特别容易被显存带宽卡住?

大模型推理通常分两段:

Prefill:
  处理用户输入的 prompt,一次算很多 token。

Decode:
  一个或少量 token 一步步生成。

Prefill 阶段,一次输入很多 token。权重被读出来以后,可以同时服务一批 token 的矩阵计算,计算量比较容易摊开。

Decode 阶段,如果 batch 很小,每一步只生成一个或少量 token。大量权重仍然要被读出来,但能摊到的 token 数很少。

于是 decode 经常更像这样:

读很多权重
算一点当前 token
再读很多权重
再算一点当前 token

这就容易变成 memory-bound:

计算单元理论上还能算更多,
但显存带宽把权重送不过来。

用一个数量级例子看。一个 70B dense 模型 FP16 权重大约 140GB。如果每生成一个 token 时大量权重都要被扫过,生成 20 token/s 的权重读取压力就是:

140GB * 20 = 2800GB/s = 2.8TB/s

这只是非常粗糙的下限直觉,还没有完整计算 KV cache、中间激活、多卡通信、kernel 调度等开销。实际系统会通过 batch、量化、缓存复用、并行切分等办法缓解,但这个估算能解释为什么 decode 对显存带宽这么敏感。

7. KV cache 是不是能避免搬权重?

KV cache 解决的是另一个问题。

没有 KV cache 时,每生成一个新 token,都可能要重新计算所有历史 token 在每一层里的 Key 和 Value。这样历史越长,重复计算越多。

有 KV cache 后:

历史 token 的 K/V 保存下来
新 token 只计算自己的 Q/K/V
Attention 时读取历史 K/V

它减少了历史 token 的重复计算,但没有取消当前 token 经过所有层的需求。

也就是说:

KV cache 主要减少重复算历史上下文。
权重读取压力仍然存在。

而且 KV cache 自己也占显存。上下文越长,KV cache 越大,decode 时还要读取历史 KV。长上下文推理里,瓶颈就不只是权重,还会叠加 KV cache 的读写压力。

8. “直接在内存里算”有没有可能?

有研究方向,常见名字包括:

Near-memory computing
In-memory computing
Processing-in-memory

这些方向的目标就是把一部分计算能力放到内存附近,甚至放进存储阵列里,减少数据来回移动。

但主流 GPU 的基本结构仍然是:

大容量显存负责存
片上缓存负责中转和复用
寄存器保存即将计算的操作数
CUDA Core / Tensor Core 负责算

这套结构通用、成熟、可编程、吞吐高。代价就是:数据必须移动。

所以在今天的大模型推理工程里,优化重点经常围绕这些问题展开:

量化:
  权重变小,搬得更少。

Batching:
  一次读出的权重服务更多 token / 请求,提高复用。

FlashAttention:
  让 attention 的读写更少、更连续,减少中间结果落显存。

Tensor parallel / pipeline parallel:
  把权重和计算拆到多张卡上,但会引入通信成本。

Speculative decoding:
  减少大模型实际执行 decode 的次数。

KV cache 管理:
  让长上下文下的缓存占用和读写更可控。

这些优化的共同背景都是:算力很贵,但数据移动也很贵,而且在 decode 阶段经常更关键。

9. 把那句话翻译成更准确的话

原句:

每生成 1 个 token,都要把大量权重从内存搬到计算单元附近。

可以翻译成更精确的版本:

在自回归 decode 中,每生成一个 token,模型都要执行一次前向传播。
这次前向传播会让各层的大量权重矩阵参与计算。
这些权重通常已经在 GPU 显存里,但 Tensor Core / CUDA Core 不能直接对远处的显存阵列做乘加。
硬件必须把权重按块从显存流经缓存、shared memory 和寄存器,送到计算单元可消费的位置。
由于权重巨大、片上存储很小,decode 的速度常常受显存带宽和数据复用效率限制。

10. 自检

如果你能回答下面几个问题,这句话基本就理解到位了:

1. “权重已经在显存里”和“权重已经在计算单元旁边”是不是一回事?
2. Tensor Core 真正消费的是显存地址,还是已经加载到近处的操作数?
3. 为什么缓存和寄存器不能把整个 70B 模型都放下?
4. Prefill 为什么比小 batch decode 更容易摊薄权重读取?
5. KV cache 省掉的是历史 token 的重复计算,还是当前 token 的权重读取?

最短答案是:

不能直接用内存里的权重算,因为内存不是乘法器。
计算发生在计算单元里,而计算单元只能高速消费已经被送到附近的数据。
大模型权重太大,近处放不下,所以每个 token 的 decode 都要不断把权重按块搬过来再计算。