1. 单卡跑大模型这件事,到底卡在哪
大模型推理的显存瓶颈,几乎是每个想在自己机器上跑模型的人都会撞上的第一堵墙。拿一个 70B 参数的模型来说,光是权重本身,如果用 FP16 精度存储,就需要大约 140GB 的显存。这个数字是什么概念?一张消费级旗舰卡通常也就 24GB 显存,一张专业级的 A100 是 40GB 或 80GB,H100 是 80GB。也就是说,即便你手里有一张 80GB 的卡,想完整装下 70B 的 FP16 权重都还差得远,更别提推理过程中还要留出 KV Cache、激活值、中间张量这些额外开销。
airllm 这个项目,瞄准的就是这个痛点。它的核心主张非常直接:让一个 70B 的大模型,在单张 4GB 显存的显卡上跑起来。第一次听到这个说法,大部分人的反应都是"不可能",因为光是权重就装不下。但 airllm 的思路并不是把模型压缩到 4GB,而是换了一个角度——它不要求模型的所有层同时驻留在显存里,而是按层加载、按层计算、算完就释放。这样一来,任意时刻显存里只需要容纳一层或少数几层的权重,显存占用就被压到了一个极低的水平。
这个思路听起来简单,但真正落地要解决一堆工程问题:层怎么切、权重从哪来、加载和计算的流水线怎么重叠、精度怎么保持、速度会掉多少。airllm 给出的答案是一套基于分层推理的推理框架,它把模型按 Transformer 的层结构拆开,每一层单独从磁盘或内存加载到显存,前向计算完成后立刻释放,再加载下一层。整个过程对上层调用者基本透明,你拿到的还是一个可以 generate 的模型对象。
适合读这篇内容的人,大致是这几类:手里只有消费级显卡、想本地跑大模型做实验的开发者;想理解分层推理这套工程思路的技术爱好者;以及在做推理优化、想找一个低成本 baseline 做对比的工程师。如果你正好卡在"显存不够"这个坎上,airllm 提供的是一个不需要改模型结构、不需要重新训练、直接就能用的方案。下面我会把它背后的设计逻辑、关键实现细节、实操步骤和踩坑经验完整拆一遍。
2. 分层推理的整体设计与思路拆解
2.1 为什么是"分层"而不是"量化"
面对显存不够,业界最常见的两条路是量化和分层卸载。量化是把权重从 FP16 压到 INT8、INT4 甚至更低,用精度换空间;分层卸载则是把权重放在显存之外,用时间换空间。airllm 走的是后一条路,而且做得比较彻底——它连"部分层常驻显存"都不强求,而是几乎完全按需加载。
为什么选分层而不是量化?这里有个很实际的考量。量化虽然能把 70B 压到 35GB(INT8)或 18GB(INT4),但对消费级 4GB 卡来说还是太大,而且量化本身会带来精度损失,INT4 在某些任务上掉点明显。分层推理则不同,它的显存占用和模型总大小基本脱钩,只和单层大小相关。一个 70B 模型如果按 80 层算,每层大约 1.75GB(FP16),那么理论上 2GB 显存就能跑起来。这个下限比任何量化方案都低,而且精度是完整的 FP16,不掉点。
代价当然也有,而且是核心代价:速度。因为每一层都要从磁盘或内存搬到显存,计算完再搬走,这个搬运过程如果串行执行,整体推理速度会被 IO 拖垮。airllm 的关键工程价值,就在于它用预取和流水线的方式,把这个搬运开销尽量藏到计算背后,让 GPU 不至于一直空等。
2.2 分层粒度的选择逻辑
分层推理的第一个设计决策是:按什么粒度切。最粗的粒度是整个模型,那等于没切;最细的粒度是单个算子,那调度开销会爆炸。airllm 选择的是 Transformer 的层(layer)作为基本单位,也就是每个 attention block 加 FFN 作为一个整体来加载和释放。
这个粒度是有讲究的。一层的大小通常在几百 MB 到 2GB 之间,正好落在"显存装得下"和"加载开销可控"的平衡点上。如果粒度再细,比如把 attention 和 FFN 拆开,虽然单次加载更小,但加载次数翻倍,调度和同步的开销会明显上升。如果粒度再粗,比如两层一起加载,显存占用又会上去,失去了分层的意义。所以按层切,是这套方案里比较自然也比较合理的选择。
另外,层与层之间的依赖是严格串行的——第 N 层的输出是第 N+1 层的输入,没法并行。这个特性决定了流水线只能做"加载下一层"和"计算当前层"之间的重叠,而不能做层与层之间的并行计算。理解这一点,对后面理解它的性能瓶颈很关键。
2.3 权重存放位置与加载路径
权重放在哪,直接决定了加载速度。airllm 支持几种存放方式,最常见的是放在磁盘上(比如 SSD),运行时按层读进内存再送进显存。也有把权重放内存里的模式,适合内存足够大的机器。
这里有个容易被忽略的点:磁盘读取速度对整体性能影响极大。如果权重放在机械硬盘上,每层读取可能要几百毫秒,80 层就是几十秒,完全没法用。放在 NVMe SSD 上,每层读取可能降到几十毫秒,体验就好很多。如果内存足够,把权重全部预加载到内存,再从内存送显存,速度会更快,因为内存带宽远高于磁盘。所以 airllm 的实际体验,很大程度上取决于你的存储层级——这也是为什么同样的代码,在不同机器上跑出来的速度可能差好几倍。
2.4 与常规推理框架的定位差异
需要明确一点:airllm 不是要替代 vLLM、TensorRT-LLM 这类高吞吐推理框架。那些框架的目标是在充足显存下把吞吐和延迟做到极致,它们假设你有足够的显存装下模型。airllm 的目标场景恰恰相反——显存严重不足,但你又想跑起来。它牺牲的是速度,换来的是可行性。
所以拿 airllm 和 vLLM 比吞吐是不公平的,它们的优化目标根本不同。airllm 更像是一个"能跑就行"的兜底方案,适合实验、调试、小批量推理,不适合高并发生产服务。想清楚这个定位,你就不会对它每秒几个 token 的速度感到失望——它本来就不是为速度而生的。
3. 核心细节解析与实操要点
3.1 模型加载与层拆分的实现机制
airllm 在加载模型时,做的第一件事是把 HuggingFace 格式的权重按层重新组织。标准的大模型权重文件通常是按张量名存储的,比如model.layers.0.self_attn.q_proj.weight。airllm 会解析这些名字,把属于同一层的张量归到一起,形成"层 -> 张量集合"的映射。
这个重组过程通常在第一次运行时做一次,之后就可以直接按层读取。为什么要重组?因为原始权重文件可能是分片的(shard),一个层的数据可能跨多个文件,如果不预先整理,每次加载一层都要跨文件查找,效率很低。重组之后,每层的数据在磁盘上相对集中,读取更连续。
实操中要注意的是,重组会额外占用磁盘空间,大致等于原模型大小。如果你的磁盘紧张,这一步要提前规划。另外重组过程本身需要一定时间,70B 模型可能要几十分钟,取决于磁盘速度。
3.2 预取与流水线的重叠策略
这是 airllm 性能的核心。如果严格串行——加载第 0 层、计算第 0 层、释放第 0 层、加载第 1 层……那么 GPU 在加载期间完全空闲,利用率极低。airllm 的做法是在计算第 N 层的同时,异步预取第 N+1 层的权重。
实现上,它通常用独立的线程或流来做预取,把下一层的权重提前读进内存缓冲区,等当前层算完,下一层的数据已经就绪,直接送显存即可。这样加载时间就被部分隐藏到计算时间背后。理想情况下,如果加载时间小于计算时间,那么整体耗时接近纯计算耗时;如果加载时间大于计算时间,那么瓶颈就在 IO 上,GPU 仍然会等。
这就解释了一个现象:在小模型上,airllm 的速度可能还不错,因为单层计算量小但层数少;在大模型上,单层计算量大,反而更容易把加载藏住。但如果你用的是慢速磁盘,加载时间远超计算时间,那预取也救不了,速度会被 IO 死死卡住。
3.3 精度保持与数值一致性
airllm 的一个卖点是精度无损。因为它不做量化,权重还是 FP16(或 BF16),计算也是标准的前向传播,所以输出结果和原始模型理论上完全一致。这一点对某些对精度敏感的任务很重要,比如需要严格复现论文结果的实验。
但要注意,分层加载本身不改变数值,可如果你在加载过程中做了类型转换,比如把 FP16 转成 FP32 再转回来,就可能引入微小误差。airllm 默认保持原精度,不做多余转换。实操中如果你发现输出和预期有偏差,优先检查是不是精度设置被改动了,而不是怀疑分层机制本身。
3.4 显存占用的实际构成
虽然 airllm 把权重显存压得很低,但显存占用并不是只有权重。实际运行时,显存里还有几块开销:当前层的权重、KV Cache、激活值、以及框架本身的预留。KV Cache 的大小和序列长度、batch size 成正比,序列越长、batch 越大,KV Cache 越占地方。
所以"4GB 跑 70B"这个说法,通常是在短序列、batch size 为 1 的条件下成立的。如果你把序列拉到几千 token,或者 batch size 开到 8,KV Cache 可能就把显存撑爆了。实操中要控制序列长度和 batch size,别指望在小显存上还能跑大 batch。这一点很多初次使用的人会忽略,然后遇到 OOM 就以为是框架的问题。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
先说一下环境。airllm 是 Python 包,依赖 PyTorch 和 HuggingFace 的 transformers、accelerate 等库。安装本身不复杂,但版本匹配要注意。PyTorch 的版本要和你的 CUDA 驱动匹配,transformers 的版本要和模型格式匹配,否则加载模型时可能报错。
pip install airllm pip install torch transformers accelerate安装完之后,建议先确认 PyTorch 能正确识别 GPU:
import torch print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0)) print(torch.cuda.get_device_properties(0).total_memory / 1024**3, "GB")如果is_available()返回 False,那后面所有 GPU 相关的操作都无从谈起,先解决驱动和 CUDA 的问题。这一步看似基础,但实际排查中,相当一部分"跑不起来"的问题都出在这里。
4.2 模型加载与首次运行
airllm 的 API 设计得比较简洁,基本用法是这样:
from airllm import AutoModel model = AutoModel.from_pretrained("某模型路径")第一次调用from_pretrained时,它会做权重重组,把模型按层整理好。这个过程可能比较慢,取决于模型大小和磁盘速度。重组完成后,后续加载会快很多。所以第一次运行要有耐心,别以为卡死了。
加载完成后,推理的调用方式和普通 transformers 模型类似:
input_text = ["请用一句话解释什么是分层推理"] input_tokens = model.tokenizer(input_text, return_tensors="pt", return_attention_mask=False, truncation=True, max_length=128, padding=False) generation_output = model.generate( input_tokens['input_ids'].cuda(), max_new_tokens=64, use_cache=True, return_dict_in_generate=True ) output = model.tokenizer.decode(generation_output.sequences[0]) print(output)这里有几个参数值得说。max_length控制输入长度,max_new_tokens控制生成长度,这两个直接决定 KV Cache 大小,进而影响显存。use_cache=True是开启 KV Cache,能加速生成,但会占显存。如果你的显存实在紧张,可以试试关掉 cache,但速度会明显下降。
4.3 关键参数的选择与计算
显存占用可以粗略估算。假设模型有 L 层,每层权重 W 字节,那么单层权重显存约 W。KV Cache 的大小约等于2 * L * seq_len * hidden_dim * batch_size * dtype_size。以 70B 模型为例,hidden_dim 约 8192,L 约 80,如果 seq_len 是 128,batch_size 是 1,dtype 是 FP16(2 字节),那么 KV Cache 约2 * 80 * 128 * 8192 * 1 * 2字节,约 335MB。加上单层权重约 1.75GB,总共约 2GB 出头。这就是为什么 4GB 卡能跑起来——但如果你把 seq_len 拉到 1024,KV Cache 就涨到约 2.7GB,加上权重就超过 4GB 了。
所以参数选择的核心原则是:控制序列长度和 batch size,给 KV Cache 留足余量。实操中建议先用短序列、batch size 1 跑通,再逐步往上加,观察显存变化,找到你机器上的上限。
4.4 存储层级的优化配置
前面提到存储速度对性能影响极大。如果你有条件,把模型权重放在 NVMe SSD 上,速度会比机械硬盘快一个数量级。如果内存足够大(比如 128GB 以上),可以考虑把权重预加载到内存,再从内存送显存,这样加载延迟会低很多。
airllm 通常提供配置项来指定权重存放路径和加载方式。实操中,我建议把权重放在最快的存储上,哪怕为此腾出一块 SSD。这个投入带来的速度提升,比调其他参数都明显。另外,如果机器内存够,可以开启内存缓存模式,让重复推理时不用反复读盘。
5. 常见问题与排查技巧实录
5.1 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 加载模型时报 CUDA out of memory | 单层权重加 KV Cache 超过显存 | 减小 max_length、batch size,或换更小的模型 |
| 推理速度极慢,每秒不到 1 token | 磁盘 IO 是瓶颈 | 换 NVMe SSD,或预加载到内存 |
| 输出结果乱码或重复 | 精度转换或 tokenizer 配置问题 | 检查精度设置,确认 tokenizer 与模型匹配 |
| 首次运行卡住很久 | 权重重组耗时 | 耐心等待,或查看磁盘占用确认在写入 |
| 生成中途报错退出 | 序列过长导致 KV Cache 溢出 | 降低 max_new_tokens,或分段生成 |
| GPU 利用率长期接近 0 | 预取未生效或 IO 太慢 | 检查存储速度,确认预取线程正常 |
5.2 踩坑经验与避坑技巧
第一个坑是低估了首次重组的时间。很多人第一次跑,看到程序半天没输出,以为卡死了就 Ctrl+C,结果重组中断,下次还得重来。正确做法是看磁盘写入活动,如果磁盘在持续写入,说明重组在进行,耐心等。
第二个坑是忽略了 KV Cache 的显存占用。有人看到"4GB 跑 70B"就以为随便跑,结果序列一长就 OOM。记住那个估算公式,心里有数,就不会踩这个坑。
第三个坑是存储选错。把权重放机械硬盘上,速度慢到怀疑人生。这个坑的教训是:分层推理的性能,一半取决于你的存储。想跑得舒服,SSD 是刚需。
第四个坑是batch size 开太大。分层推理本来就慢,有人想用 batch 提高吞吐,结果显存爆了。在小显存场景下,batch size 老老实实设 1,别贪。
5.3 性能调优的实操心得
调优的核心思路是让 GPU 尽量不空等。具体做法:一是用最快的存储,减少加载时间;二是开启预取,让加载和计算重叠;三是控制序列长度,避免 KV Cache 挤占显存导致频繁换入换出。
我实测下来,同一台机器上,把权重从机械硬盘换到 NVMe SSD,速度能提升好几倍。如果内存够,预加载到内存再跑,又能再快一截。所以如果你的目标是"能跑且不太慢",存储升级的性价比最高。
另外,如果你的任务对速度要求不高,比如只是偶尔跑几个 prompt 做实验,那 airllm 的慢其实可以接受。它解决的是"能不能跑"的问题,不是"跑得多快"的问题。想清楚这一点,心态就稳了。
6. 这套方案适合谁,不适合谁
airllm 的价值边界其实很清晰。它适合显存严重不足、但又想本地跑大模型的场景,比如个人开发者用一张消费级卡做实验,或者教学演示里需要展示大模型推理过程。它的优势是门槛低、精度无损、不需要改模型,拿来就能用。
它不适合高并发、低延迟的生产服务。那种场景下,显存不足应该通过加卡、用专业推理框架、或者上量化方案来解决,而不是用分层推理硬扛。分层推理的速度瓶颈在 IO 上,这个瓶颈很难通过软件优化彻底消除,因为物理存储速度就在那。
还有一个容易被忽略的点:airllm 对模型格式有要求,主要支持 HuggingFace 格式的模型。如果你手里的模型是其他格式,可能需要先转换。转换过程本身也可能踩坑,比如张量名不匹配、配置缺失等。实操前先确认模型格式兼容,能省不少事。
我个人在实际操作中的体会是,airllm 更像是一把"应急钥匙"——当你被显存卡住、又没有别的办法时,它能帮你把门打开。但门打开之后,走得多快,还是取决于你的硬件底子。把存储和内存配好,它的体验会好很多;指望它在老机械硬盘上飞起来,那是不现实的。最后再分享一个小技巧:如果你的模型层数很多,可以观察每层的加载和计算耗时,找出最慢的那几层,针对性优化存储布局,有时候能挤出不少性能。