为什么生成一个 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 都要不断把权重按块搬过来再计算。