🦙 第2章 部署 Llama 2 大模型
📄 一句话概括

把 Llama 2 跑起来之前,有三笔账要先算清:显存、内存、磁盘。 算错任何一笔,后面都会在最难受的时候卡住——要么下完了发现装不进显卡, 要么跑起来了但一长上下文就 OOM。 这一节把三笔账算成表,再说环境怎么搭,以及那些「版本不匹配」的坑长什么样。

📝 本章和前面几章的关系

上一章讲的是模型里面怎么算,这一章开始动手。 13 课 服务器工作原理 里讲的是通用的机器与内存, 这一节讲的是显存这笔特殊的账——它比内存贵得多,也不能换页。

一、硬件:先算显存,再谈别的

显存要装三样东西:权重、KV 缓存、激活与框架开销。三样里只有第一样是固定的,后两样跟着你的用法变。 权重最好算:参数量乘字节数。67 亿参数按 fp16 存是两个字节一个,约 12.6 GiB;按 int8 存是 6.3 GiB; 按 int4 存是 3.1 GiB。同一份权重,换一种存法体积就差整数倍。 KV 缓存是长上下文时突然爆显存的原因。 每生成一个 token,每层每个头都要存一份键和一份值,供后面所有 token 来查。 公式是「2 × 层数 × KV 头数 × 每头维度 × 序列长度 × 字节数」。

7B 在 4096 上下文下这块是 2 GiB,8k 就是 4 GiB。 70B 反而只有 1.25 GiB——因为它用了分组查询注意力,64 个查询头共用 8 组键值,这块直接省了八倍。 第三样最容易漏:中间激活一到两 GiB,框架自己的显存池一到两 GiB,CUDA 上下文每张卡还要占几百兆。 很多人算完前两样觉得刚好够,一跑就 OOM,差的就是这几项。 三样加起来,一张 24 GB 的卡:7B 的 fp16 能跑(16.6 GiB),13B 的 fp16 差一点(29.3 GiB), 70B 的 int4 也不够(35.3 GiB)。这就是为什么大家做实验都挤在 7B 上。

二、内存与磁盘:两个常被忽略的数

内存要能装下权重。 用映射方式加载时物理内存不是立刻占满,但只要开始算,用到的页就会被读进来,最终还是要十几 GB。 8 GB 内存的机器跑 7B 会一直换页,慢到不能用。 磁盘要留两倍。下载一份权重、解压或转换成另一种格式又一份,中间还可能留缓存。 HF 的缓存目录默认在用户主目录下,很多人下到一半发现 C 盘满了。 下载时间也别忽略。7B 的 fp16 权重十三点几 GiB,一百兆宽带要下小半个小时,十兆宽带要三个小时。 这一步的坑不在慢,在断了之后能不能续。

三、环境:版本对齐比装什么都重要

三样东西的版本必须对齐:显卡驱动、CUDA 运行时、PyTorch 编译时用的 CUDA 版本。 驱动向下兼容,但 torch 装错了 CUDA 版本,会报「CUDA 不可用」而不是报错装错了。 判断装对没有,跑一行就够:torch.cuda.is_available() 返回真, 并且 torch.version.cuda 与 nvidia-smi 右上角那个版本对得上。这两个数不一致,就是装错了。 显卡算力也要看。太老的卡不支持 fp16 加速,跑起来比 CPU 还慢;有些量化格式要求特定的指令集。 查算力看显卡型号后面那个数字,比如 8.6、8.9。

Python 环境建议用虚拟环境隔离,不要往系统 Python 里装。 理由是这一行的依赖更新太快,两个项目要的 torch 版本经常打架。 还有一条经验:先在 CPU 上把流程跑通,再上 GPU。加载、分词、拼提示这些步骤在 CPU 上一样能验, 报错信息也更清楚。直接在 GPU 上调,一个显存不足的报错会把真正的原因盖掉。

四、动手 Demo:三笔显存账算成表,看一张 24 GB 卡能跑什么

这一节的演示全是算的,不需要显卡。 按 Llama 2 三档模型的规格,算四种精度下的权重、三种上下文长度下的 KV 缓存,再加上激活与框架开销, 最后给出一张「什么配置能不能跑」的判定表。顺带算一下不同带宽下下载要多久。

GB = 1024.0 ** 3

# 每档模型:(名字, 参数量, 层数, KV 头数, 每头维度)
SIZES = [(u'Llama 2 7B',  6.74e9, 32, 32, 128),
         (u'Llama 2 13B', 13.0e9, 40, 40, 128),
         (u'Llama 2 70B', 68.9e9, 80,  8, 128)]

# 权重:参数量乘字节数
for nm, n, _L, _h, _d in SIZES:
    print(nm, n * 2 / GB)          # fp16

# KV 缓存:2 x 层数 x KV头数 x 每头维度 x 序列长度 x 字节数
kv = 2 * L * h * d * seq * 2 / GB

# 合计:权重 + 4k 缓存 + 2 GiB 的激活与框架开销
tot = n * b / GB + kv + 2.0

实跑输出:

--------------------------------------------------------------------
显存到底要多少:权重、缓存、激活三笔账
--------------------------------------------------------------------


  ① 第一笔:权重
  --------------------------------------------------------------------
| 模型 | 参数 | fp32 | fp16 | int8 | int4 |
|---|---:|---:|---:|---:|---:|
| Llama 2 7B | 67.4 亿 | 25.1 GiB | 12.6 GiB | 6.3 GiB | 3.1 GiB |
| Llama 2 13B | 130.0 亿 | 48.4 GiB | 24.2 GiB | 12.1 GiB | 6.1 GiB |
| Llama 2 70B | 689.0 亿 | 256.7 GiB | 128.3 GiB | 64.2 GiB | 32.1 GiB |

  这笔账最好算:参数量乘字节数。同一个模型,换一种存法,体积就差整数倍。
  ! 注意 int4 那列是 0.5 字节——不是真的每个参数半个字节,是两个人挤一个字节。
  所以 int4 的模型读出来要先解包,比 fp16 多一步。

  ② 第二笔:KV 缓存
  --------------------------------------------------------------------
  每生成一个 token,每层每个头都要把它的键和值存下来,供后面的 token 查。
  这就是为什么长上下文推理会突然爆显存。
| 模型 | 1k 上下文 | 4k 上下文 | 8k 上下文 |
|---|---:|---:|---:|
| Llama 2 7B | 0.50 GiB | 2.00 GiB | 4.00 GiB |
| Llama 2 13B | 0.78 GiB | 3.12 GiB | 6.25 GiB |
| Llama 2 70B | 0.31 GiB | 1.25 GiB | 2.50 GiB |

  公式是「2 × 层数 × KV 头数 × 每头维度 × 序列长度 × 字节数」,最前面那个 2 是键和值各存一份。
  ! 70B 那一行反而比 7B 小,因为它用了分组查询注意力:64 个查询头只共用 8 组键值。
  这不是省显存的巧合,是专门为省这块设计的。

  ③ 第三笔:激活与框架开销
  --------------------------------------------------------------------
| 项 | 量级 | 说明 |
|---|---|---|
| 推理时的中间激活 | 1 到 2 GiB | 跟批大小和序列长度走 |
| 框架自身的显存池 | 1 到 2 GiB | PyTorch 的缓存分配器会先占一块 |
| CUDA 上下文 | 0.3 到 0.6 GiB | 每张卡一份,跑空模型也要占 |

  这一笔最容易漏。很多人算完权重加缓存觉得刚好够,一跑就 OOM,差的就是这几项。

  ④ 合起来看:一张 24 GB 卡能跑什么
  --------------------------------------------------------------------
| 模型 | 精度 | 权重 | 4k 缓存 | 合计 | 24 GB 卡 |
|---|---:|---:|---:|---:|---|
| Llama 2 7B | fp32 | 25.1 | 2.0 | 29.1 | 32 GB 能跑 |
| Llama 2 7B | fp16 | 12.6 | 2.0 | 16.6 | 能跑 |
| Llama 2 7B | int8 | 6.3 | 2.0 | 10.3 | 能跑 |
| Llama 2 7B | int4 | 3.1 | 2.0 | 7.1 | 能跑 |
| Llama 2 13B | fp32 | 48.4 | 3.1 | 53.6 | 要 80 GB |
| Llama 2 13B | fp16 | 24.2 | 3.1 | 29.3 | 32 GB 能跑 |
| Llama 2 13B | int8 | 12.1 | 3.1 | 17.2 | 能跑 |
| Llama 2 13B | int4 | 6.1 | 3.1 | 11.2 | 能跑 |
| Llama 2 70B | fp32 | 256.7 | 1.2 | 259.9 | 单卡放不下 |
| Llama 2 70B | fp16 | 128.3 | 1.2 | 131.6 | 单卡放不下 |
| Llama 2 70B | int8 | 64.2 | 1.2 | 67.4 | 要 80 GB |
| Llama 2 70B | int4 | 32.1 | 1.2 | 35.3 | 要 80 GB |

  合计 = 权重 + 4k 上下文的 KV 缓存 + 2 GiB 的激活与框架开销。
  ! 这张表解释了两件事:为什么大家挤在 7B 上做实验(fp16 刚好装得下),
  以及为什么 70B 必须量化或者上多卡。

  ⑤ 微调比推理更吃显存
  --------------------------------------------------------------------
| 场景 | 要存什么 | 相对权重的倍数 |
|---|---|---|
| 推理 | 权重 + 缓存 + 激活 | 1 份 |
| 全参微调 | 权重 + 梯度 + 优化器状态 + 激活 | 约 16 份 |
| LoRA 微调 | 权重 + 两个小矩阵 + 激活 | 约 2 到 3 份 |

  全参微调那 16 倍是这么来的:fp16 权重 2 字节、梯度 2 字节、AdamW 的一阶矩和二阶矩各 4 字节,
  加起来 12 字节,是权重本身的 6 倍;再把中间激活算进去,实际能到十几倍。
  ! 所以 7B 全参微调要 100 GB 级别的显存,而 LoRA 只要十几 GB——这就是第 3 章要讲的那件事。

  ⑥ 下载要多久
  --------------------------------------------------------------------
| 权重体积 | 10 Mbps | 100 Mbps | 1000 Mbps |
|---|---|---|---|
| 13.5 GiB | 184 分钟 | 18 分钟 | 2 分钟 |
| 26.0 GiB | 355 分钟 | 35 分钟 | 4 分钟 |
| 130.0 GiB | 1775 分钟 | 177 分钟 | 18 分钟 |

  这张表是算的,不是测的:体积除以带宽,换算成分钟。
  实际会更慢,因为对面限速、断点续传、校验都要时间。
  ! 7B 的 fp16 权重十几 GB,一百兆宽带要下小半个小时。
  这一步的坑不在慢,在断了之后得能续——所以下载脚本都支持断点续传。

第一张表看权重:7B 从 fp32 的 25.1 GiB 一路降到 int4 的 3.1 GiB,每一步都是减半。 int4 那一列是 0.5 字节,不是每个参数半个字节,是两个参数挤一个字节—— 所以 int4 的模型读出来要先解包。 第二张表是这一节最有信息量的:7B 的 4k 缓存 2 GiB,70B 的只有 1.25 GiB。 大模型反而更省,因为它用了分组查询注意力。这不是巧合,是专门为省这块设计的。 第四张表把三笔账加起来,判定很直接:24 GB 卡上,7B 的 fp16 能跑,13B 的 fp16 就差那 5 GiB, 得降到 int8;70B 无论怎么量化单卡都放不下。 第五张表解释了一件很多人不知道的事: 微调比推理吃显存得多。全参微调要存梯度(2 字节)和优化器状态(一阶矩、二阶矩各 4 字节), 加起来是权重本身的六倍,再算上激活能到十几倍。 所以 7B 全参微调要 100 GB 级别的显存,而 LoRA 只要十几 GB。 这个差距不是调参调出来的,是「要不要存梯度和优化器状态」决定的——第 3 章会细说。 最后那张下载表是纯算的:体积除以带宽。实际会比这个慢,因为对面限速、断点续传、校验都要时间。 凡是这种「理论值」,正文里都会标清楚是算的还是测的。

30 秒实验

  1. 把 KV 缓存那段里的序列长度从 4096 改成 16384,重算 7B 的缓存。 再看第四张表:24 GB 卡上还跑得动吗?这就是长上下文为什么贵。
  2. 把 70B 的 KV 头数从 8 改成 64(相当于不用分组查询注意力),算出来的缓存是多少? 再对比 7B 那一行,你就能看出 GQA 到底省了多少。
  3. 把「激活与框架开销」从 2 GiB 改成 4 GiB(相当于批大小翻倍), 看哪些配置从「能跑」变成「不能跑」。

常见误区

  1. 只按权重大小买显卡。权重只是三笔账里的一笔,KV 缓存和激活能让一张「刚好够」的卡直接 OOM。 买之前按「权重乘一点五」估,比较稳。
  2. 以为显存和内存可以互换。显存不能换页,不够就是不够,系统不会帮你把一部分挪到硬盘上。 内存不够还能撑,显存不够直接报错。
  3. 以为 CUDA 版本越高越好。 torch 是用某个 CUDA 版本编译的,装了个不匹配的驱动,它会静默回落到 CPU,你还以为在跑 GPU。 一定要看 torch.cuda.is_available()。
自测三问(点开对答案) 1. 为什么 70B 的 KV 缓存反而比 7B 小? —— 因为 70B 用了分组查询注意力:64 个查询头共用 8 组键值,KV 头数从 64 降到 8。 缓存公式里乘的是 KV 头数,所以直接省了八倍。7B 是 32 个头全存,没有这个优化。 2. 为什么全参微调要十几倍显存,而 LoRA 只要两三倍? —— 全参微调要为每个参数存梯度(2 字节)和优化器状态(AdamW 的一阶矩、二阶矩各 4 字节), 加起来 12 字节,是权重本身的六倍,再算激活就到十几倍。 LoRA 只训两个极小的矩阵,主权重不动,所以只多一点点。 3. 「先在 CPU 上跑通再上 GPU」,具体能验什么? —— 能验加载、分词、拼提示、后处理这些不依赖显卡的环节,还能验配置文件读对没有。 这些环节的报错信息在 CPU 上很明确,在 GPU 上常被显存报错盖住。

账算完了,下一节把权重和代码真的拉下来——这一步的坑比想象中多。

相关笔记