如果你手里只有一张 4GB 显存的旧卡,看到“70B 大模型”这个词,第一反应多半是:跑不了。AirLLM 这类项目做的事情,就是把这个结论往回掰一点——它不承诺快,只承诺“能跑通”。核心方式是分层推理:把大模型按 Transformer 的层拆开,一次只调度一小部分到 GPU,用完立刻让位给下一层。这样显存占用从“整个模型”降成“一层模型”,4GB 卡也就有了参与 70B 模型的可能。这篇文章我会从显存账目、原理、代码、踩坑四个角度,把它讲透。适合那些手头只有消费级显卡、想低成本验证开源大模型效果的人,也适合想理解模型内部结构的开发者。
1. 先算清一笔账:70B 模型为什么跑不动
1.1 显存不只是被“权重”吃掉的
很多人以为 70B 模型跑不动,纯粹因为权重太大,其实只对了一半。推理时的显存占用大致由三块组成:权重、KV Cache、中间激活。
权重最容易理解。70B 参数如果按 FP16 存储,每个参数占 2 字节,那就是 140GB。如果量化到 4bit,每个参数占 0.5 字节,也要 35GB 左右。随便一张 24GB 的显卡,连 4bit 版本都塞不下,更别说 4GB 卡。
KV Cache 是第二块。Transformer 解码时,需要把历史上所有 token 的 Key 和 Value 缓存下来,避免每步都重新计算。它的大致公式是:
KV Cache = 2(K和V两组) × 层数 × KV头数 × 每头维度 × 序列长度 × 批次大小 × 每个元素字节数拿一个常见的 70B 配置来估:80 层、8 个 KV 头、每个头 128 维、序列长度 2048、batch size 为 1、FP16,KV Cache 大约是 640MB。这已经算比较克制的量,如果序列拉长到 32K,KV Cache 会涨到 10GB 以上。
中间激活更隐蔽。每一层做完 attention 和前馈网络后,都会产生一组临时张量,这些张量在反传时很有用,但推理时往往也是一次性的。它受 batch size 影响很大,batch 一旦变大,激活值会成倍增加,很多时候 OOM 爆发都源于这里。
1.2 常规优化方案为什么都救不了 4GB 卡
市面上解决显存不足的思路大概分四类:量化、剪枝蒸馏、CPU offload、多卡并行。
量化是把权重从 FP16 压到 INT8 或 4bit,显存需求能缩小 4 到 8 倍。问题是 70B 模型即使压到 4bit,仍然需要 35GB 左右,和 4GB 差了一个数量级。
CPU offload 的思路是把部分权重放在内存里,GPU 用到哪块就现场拷贝哪一块。看起来可行,但传统实现往往以算子或整块模型为单位做调度,前端一个算子刚拷完,另一个算子又触发下一次拷贝,PCIe 带宽很容易被碎片化请求打满,实际吞吐惨不忍睹。
多卡并行是很多实验室方案,但先不提多张卡的成本,消费级主板上要凑齐足够显存本身就是另一笔预算。租云 GPU 倒是省心,却意味着每次实验都要联网传数据、按小时计费,不太适合只想本地验证效果的人。
所以 AirLLM 选择了一个反常规的思路:既然单张卡放不下整个模型,那就干脆按“层”为单位搬运。每一层计算完就释放,再拉下一层进来,把显存峰值压到“一层模型”量级。简单粗暴,但对 4GB 卡的场景确实有效。
2. AirLLM 的分层推理到底做了什么
2.1 核心操作:以“层”为单位调度
先回顾一下 Transformer 的结构。一个典型的大模型可以看成三段:最前面的 embedding、中间重复堆叠的几十上百个 decoder layer、最后的输出层(lm_head)。真正跑前向传播时,数据是依次穿过这些层的。
AirLLM 的切入点就在这里:它不尝试将整个计算图一次性摆在 GPU 上,而是把模型拆成一个个层单元。推理时,GPU 先加载第 0 层,把输入张量算成第 1 层的输入,然后立刻释放第 0 层的权重,再加载第 1 层。整个过程像流水线上的工人,每人只看自己工位那一份零件,绝不把整条生产线都堆在面前。
这样做的好处是显存占用不再和“模型总参数”挂钩,而是和“单层参数”挂钩。假设 70B 模型有 80 层,平均每层约 0.875B 参数,FP16 下就是大约 1.75GB。再加上该层计算时需要的激活和部分缓存,4GB 显存刚好有一战之力。当然,不同模型层数和维度不同,这只是用于理解量级的粗算。
2.2 为什么峰值显存能降到“单层级别”
关键在于释放动作做得很彻底。常规 PyTorch 代码里,即使你用with torch.no_grad()包裹,模型层权重大概率还是常驻显存;就算用.to("cpu")把某个模块挪走,CUDA 缓存可能还会占着显存不还。AirLLM 这类方案会主动控制每个层的前向过程,算完当前层后把该层的张量释放,再进入下一层。
你可能想问:既然每层都要从 CPU 拉到 GPU,那和 CPU offload 有什么区别?区别主要在调度粒度。很多 offload 框架是按“算子”管理的,一个Linear算完就松手,下一个Attention又立刻申请显存,CUDA 缓存反复波动;AirLLM 的粒度是完整的一层 Transformer,数据流更规整,也更方便预判“这一层到底需要多少显存”。
另外,KV Cache 也不是简单堆在显存里。序列变长后,它同样是显存大户,所以这类实现会针对解码阶段做缓存管理,必要时把缓存也降级到内存。这样即便上下文长度增加,4GB 卡也不会一上来就爆。
2.3 代价都付在哪里:速度确实是个问题
天下没有免费的午餐。AirLLM 最明显的代价是慢。
每次生成一个新 token,模型都需要对所有层做一次完整的前向计算。如果模型权重都在 CPU/磁盘,那么每个 token 都要把整个模型逐层搬一遍。你大致算一下:PCIe 3.0 x16 的实际带宽差不多 10GB/s 上下,70B 模型 FP16 权重 140GB,理论上全部搬一轮就需要十几秒,这还没算计算时间和层间调度开销。
所以它更适合离线批处理这种对延迟不敏感的场景,不适合做在线对话和高并发 API。如果有人告诉你 4GB 卡跑 70B 模型能流畅对话,那大概率是在调低预期:不是那种 GPT 一样的秒回体验,而是真正意义上的“能出结果”。
3. 动手实操:从安装到跑通一次推理
3.1 硬件与环境准备
我实际测试时用的是一张老款 4GB 显存显卡,系统是 Linux。4GB 显存是本方案的下限,但 CPU 内存建议给足,原因后面会讲。硬盘也要预留空间,因为 70B 模型原始权重按 FP16 存储至少需要 140GB;如果只是试跑 7B 模型,也需要大约 14GB。
软件环境上,主要依赖 Python 3.8 以上、PyTorch、Transformers 和相关依赖。安装过程没什么特殊的,先确认驱动正常:
nvidia-smi python -c "import torch; print(torch.cuda.is_available())"只要这两步返回正常,就说明 CUDA 环境基本就绪。之后从项目文档找到安装命令,一般就是 pip 安装对应包。我在安装时习惯单独建一个虚拟环境,避免污染其他项目依赖,这一点在反复试不同模型时尤其有用。
3.2 最小推理代码,可以直接抄
假设我已经把模型权重下载到本地目录,推理脚本大概长这样:
import torch from transformers import AutoTokenizer from airllm import AutoModel MODEL_PATH = "/data/models/llama-2-7b-hf" model = AutoModel.from_pretrained(MODEL_PATH) tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH) prompt = "请用一句话解释什么是递归。" inputs = tokenizer(prompt, return_tensors="pt").to("cuda:0") with torch.no_grad(): output_ids = model.generate( **inputs, max_new_tokens=64, temperature=0.7, do_sample=True, ) print(tokenizer.decode(output_ids[0], skip_special_tokens=True))代码量很少,和普通 Transformers 用法几乎一样。需要提醒的是,不同版本 API 细节可能有差异,实际使用前以官方 README 为准。我踩过的一个小坑是 tokenizer 也要手动加载,有些示例只加载了模型,结果打印出来的全是 token id,排查半天才发现 tokenizer 没接上。
3.3 首次加载与参数选择
首次加载模型时,代码往往需要把原始权重做一次整理或索引,因此耗时会长一些,这不代表卡死。我建议第一次跑的时候把日志打到终端,看到每层加载进度在跳动,心里就有底了。
接下来是参数选择。max_new_tokens直接影响可接受等待时间:如果你生成 512 个 token,而每个 token 要花几秒,那一次生成就是几十分钟。所以首次验证时建议把max_new_tokens压到 32 或 64,先确认链路通不通,再逐步放长。
batch_size更不要轻易加大。分层推理下,一个 batch 的中间激活和 KV Cache 都会线性增长,4GB 显存很快就会到极限。我一般固定 batch size 为 1,需要批量处理文本时就改成循环,逐条喂给模型。
4. 实操中的坑:显存、内存与速度
4.1 OOM 时按顺序检查什么
显存不够是最常见的错误,而且错误信息不一定直接写着 CUDA Out of Memory。我自己遇到最多的是两类:一类是某个 layer 加载时 OOM,另一类是生成到一半时 OOM,因为 KV Cache 在变长。
遇到 OOM,我建议按这个顺序排查:
- 看当前显存占用:用
watch -n 0.5 nvidia-smi观察有没有其他进程占着显存,比如没关干净的桌面、其他 Jupyter 内核。 - 看激活值增长:
max_new_tokens调小,batch size 拉回 1,先排除最简单变量。 - 看模型层权重是否真被释放:有些库版本对层间释放做得不彻底,必要时升级或换分支版本。
- 看序列长度是不是太长:长文本会把 KV Cache 顶到爆,可以加上
max_input_length之类的限制。
另外,不要同时开着多个生成任务,这相当于把 4GB 显存做了二次划分,每一份都不够用。
4.2 内存不足比显存不足更隐蔽
我一开始只盯显存,结果遇到一种奇怪的现象:程序没有报 CUDA 错误,但运行几分钟后进程被直接杀掉,像是被外力终结。查了系统日志才发现,是因为 CPU 内存耗尽,触发了系统的 OOM killer。
这其实是分层推理的一个隐形前提:模型权重在 GPU 之外时,总得有地方放。如果拿 7B 模型测试,FP16 权重 14GB,CPU 内存至少要有 20GB 以上的余量;到了 70B,内存占用就是 140GB 量级,普通电脑根本扛不住。
所以不要把注意力全放在显卡上。跑大模型前先看内存:
free -h如果内存紧张,可以增加 swap 空间,扛过权重换入换出的高峰,但速度会进一步下降。想省内存,可以考虑用支持 4bit 权重的模型格式,把 70B 权重从 140GB 压到 35GB 左右,CPU 内存要求会低很多。
4.3 速度预期:别拿它和在线服务比
关于速度,群里不少人抱怨“怎么这么慢,是不是哪里设置错了”。我自己的判断是:慢是常态,不慢才不正常。在消费级 PCIe 环境下跑 7B 模型,每生成一个 token 需要把十几 GB 权重过一遍,再怎么优化也很难做到跟原生推理一样的速度。
如果只是测试模型是否正常,建议用短输出任务;如果需要批量生成,最好夜间挂着跑,别盯着进度条。还有一个技巧:尽量用稍大一点的 batch 换取整体吞吐,但这很考验显存,需要你拿显存监控一点点试探。
我这里再强调一次,AirLLM 的价值在于“用最低门槛跑通”,而不是“跑得飞快”。调整好这个预期,很多问题就不会被误判成故障。
5. 这个方案适合谁,不适合谁
5.1 推荐场景:低成本验证与模型解剖
我在测试时发现,分层推理有一个附带好处:模型每一层的输入输出边界非常清晰。研究某个 attention 层发生什么、中间隐藏状态长什么样,比常规加载整个模型更直观。你可以像外科手术一样单独盯着某几层看,而不用关心整张显卡是否装得下。
另外,如果你只是想回答“这个开源模型到底行不行”,手上又暂时借不到高端显卡,AirLLM 是很好的验证手段。下载一个 7B 或 13B 模型,用统一 prompt 跑几条输出,判断风格和效果,基本够用。
它也很适合教学场景。讲 Transformer 结构时,用分层推理工具可以实时展示数据经过每一层后的变化,学生对“什么是 layer”“为什么需要很多层”会有更具体的感受。
5.2 不建议的场景:在线服务与高并发
生产环境不建议用这种方案。在线问答、客服机器人、API 后端,这些场景对时延要求很高,分层推理动辄数秒到数十秒才能生成一个 token,用户根本等不起。哪怕只是 8B 模型的分层推理,在高并发下也会立刻放大带宽瓶颈。
还有一类场景是持续的大 prompt。每次生成前,你要对 prompt 做前向计算,prompt 越长,这一遍就越慢。如果用户动不动粘几千字进去,同样是灾难。想跑大上下文,更好的办法是换用支持更长序列的高显存卡或服务化框架。
总的来说,AirLLM 解决的是“有没有”的问题,而不是“好不好”的问题。
5.3 可以叠加的后续玩法
分层推理并不排斥量化,两者其实可以叠加。把模型先转成 4bit 或更低精度格式,权重缩小后,每次层间搬运的数据量更少,PCIe 带宽压力会大幅缓解,生成速度也能明显改善。显存占用更小,CPU 内存要求也更低。
另一个思路是配合投机解码。用一个很小的草稿模型生成多个候选 token,再由大模型一次性验证,减少大模型逐层调度的次数。这是更深层的优化方向,但实施门槛较高,适合已经跑通基础流程后继续研究的玩家。
也有人会在层与层之间插入自定义逻辑,比如提取中间特征、修改 hidden state、替换注意力输出。这在研究场景下很有意思,因为普通推理框架很少把层间接口开放得这么整齐。
我个人实际操作下来的体会是:AirLLM 的最大价值不是在硬件极限上硬撑,而是把“大模型推理”这件事的边界打开了一条缝。它不会替代任何高性能框架,却能在你一无所有的时候,让想法先跑起来。如果你第一次尝试,我建议先拿 7B 模型练手,别直接挑战 70B,否则你会被漫长的等待和内存占用吓退。等把数据流、显存变化、KV Cache 都摸清楚了,再考虑更大的模型也不迟。最后分享一个小技巧:测试时把max_new_tokens设成 1,看模型能不能完整吐出一个 token,这个信号比什么监控都直接。