1. 单卡跑大模型这件事,到底卡在哪
第一次看到 airllm 这个项目名的时候,我正蹲在一台只有单张 24G 显存的机器前发愁。手里有个 70B 级别的模型想跑推理,结果加载权重那一步就直接爆显存,连报错都来不及看完整。相信不少做本地推理的朋友都遇到过类似的窘境:模型参数动辄上百 GB,而手头的显卡显存就那么点,要么加卡,要么换更小的模型,要么干脆放弃。
airllm 这个项目解决的正是这个痛点。它的核心思路非常直接:不把整个模型一次性塞进显存,而是按层加载、按层计算、算完就释放。听起来简单,但真正落地要处理一堆细节,比如层与层之间的数据怎么传递、磁盘 IO 怎么优化、量化怎么配合、预取怎么调度。这个项目把这些脏活累活都封装好了,让一张消费级显卡甚至纯 CPU 环境都能跑起原本需要多卡才能承载的大模型。
这篇文章适合三类人看:一是手里只有单卡、想跑大模型但一直被显存卡住的开发者;二是对模型推理优化感兴趣、想了解分层加载这套机制的技术爱好者;三是想快速上手 airllm、不想啃源码的实践派。我会从设计思路讲到实操步骤,再把我踩过的坑和排查经验一并倒出来,尽量让你看完就能动手。
需要先说明一点,airllm 不是万能的。它用时间换空间,推理速度会比全量加载慢,这是物理规律决定的,谁也绕不过去。但如果你追求的是"能跑起来"而不是"跑得飞快",那它确实是个很实用的方案。
2. 分层加载的核心设计思路拆解
2.1 为什么是"按层"而不是"按块"
要理解 airllm 的设计,得先搞清楚大模型推理时显存到底被什么占满了。一个 Transformer 模型,显存占用主要分三块:模型权重、激活值(中间计算结果)、以及 KV Cache。其中权重是大头,70B 模型用 FP16 存,光权重就 140GB 左右,这还没算推理过程中的额外开销。
那为什么 airllm 选择按"层"切分,而不是按参数矩阵切分或者按注意力头切分?这里有个很实际的考量。Transformer 的结构是天然分层的,每一层的计算相对独立,层与层之间通过隐藏状态(hidden states)串起来。这意味着你可以在第 N 层算完后,把这一层的权重从显存里踢出去,换第 N+1 层的权重进来,中间只需要保留一个隐藏状态张量。这个张量的大小跟序列长度和隐藏维度有关,通常远小于一层权重的体积。
按参数矩阵切分就麻烦多了,因为矩阵乘法需要完整的权重参与,切一半算一半再拼起来,通信和调度开销反而更大。按注意力头切分也有类似问题,而且不同头之间的计算并不是完全独立的。所以按层切分是"切分粒度"和"调度开销"之间一个比较舒服的平衡点。
2.2 磁盘 IO 才是真正的瓶颈
分层加载听起来美好,但有个绕不开的问题:权重存在磁盘上,每次要用都得从磁盘读进内存再送进显存。如果磁盘读取速度跟不上计算速度,GPU 就会一直空转等数据,推理速度会慢到令人发指。
airllm 在这块做了几件事。第一是权重预取,在当前层计算的同时,后台线程已经在读下一层的权重了,这样计算和 IO 能重叠起来。第二是权重格式优化,它支持把原始权重转成更适合快速读取的格式,减少反序列化的开销。第三是量化支持,通过 4bit 或 8bit 量化把权重体积压下来,磁盘读取量直接减少一半到四分之三,这对 IO 瓶颈的缓解非常明显。
我实测过一个对比:同样跑一个 70B 模型,不做量化、纯 FP16 分层加载,在普通 SATA SSD 上每秒只能出几个 token;换成 4bit 量化后,速度能提升到原来的三倍左右。这个差距主要就来自磁盘读取量的减少。
2.3 内存和显存的分工
airllm 的运行时,数据会在三个地方流转:磁盘、内存(RAM)、显存(VRAM)。磁盘存全量权重,内存做中转缓冲,显存做实际计算。这个链条里,内存的大小也很关键。如果内存足够大,可以缓存更多层,减少磁盘读取次数;如果内存紧张,那就只能老老实实每层都从磁盘读。
这里有个经验值可以参考:跑 70B 模型 4bit 量化,大概需要 8GB 左右的显存用于单层计算和激活值,内存建议至少 32GB,磁盘建议用 NVMe SSD。如果是 13B 级别的模型,显存 6GB 左右就能跑,内存 16GB 够用。这些数字不是绝对的,跟序列长度、batch size 都有关系,但可以作为一个起步参考。
提示:如果你的机器内存特别小,airllm 也支持把内存缓冲关掉,直接从磁盘读到显存,但速度会进一步下降。这是用空间换时间的极端情况。
3. 核心细节解析与实操要点
3.1 环境准备与依赖安装
动手之前,先把环境理清楚。airllm 本身是个 Python 包,依赖 PyTorch、Transformers、Accelerate 这几个基础库。安装方式很直接:
pip install airllm但这里有个坑,airllm 对 PyTorch 版本和 CUDA 版本有要求。如果你用的是比较老的 PyTorch,可能会遇到算子不兼容的问题。我的建议是先用 conda 建一个干净的环境,然后按官方推荐的版本组合来装。比如:
conda create -n airllm python=3.10 conda activate airllm pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118 pip install airllm transformers accelerateCUDA 版本要根据你显卡驱动支持的版本来选,别盲目装最新的。装完之后用torch.cuda.is_available()验证一下 GPU 能不能正常识别。
3.2 模型权重的准备与量化
airllm 支持直接从 HuggingFace 拉模型,也支持加载本地的权重文件。如果你网络条件好,直接指定模型名就行;如果网络不稳定,建议先把权重下载到本地,再用本地路径加载,这样更可控。
量化这块是重点。airllm 支持 4bit 和 8bit 量化,量化后的权重体积会大幅缩小。以 70B 模型为例,FP16 大概 140GB,8bit 约 70GB,4bit 约 35GB。这个体积差异直接决定了磁盘读取时间和所需存储空间。
量化的配置方式通常是在加载模型时传参:
from airllm import AutoModel model = AutoModel.from_pretrained( "model_path_or_name", compression="4bit" )这里要注意,量化不是没有代价的。4bit 量化会带来一定的精度损失,对于需要高精度输出的任务(比如代码生成、数学推理),可能会感觉到质量下降。我的经验是,日常对话和文本摘要用 4bit 完全够用,但如果是严谨的推理任务,建议用 8bit 或者干脆不量化。
3.3 推理参数的关键配置
airllm 的推理接口跟 Transformers 很像,但有几个参数需要特别关注。第一个是max_new_tokens,控制生成的最大长度,设太大容易显存溢出,设太小又可能截断回答。第二个是batch_size,airllm 在单卡上通常只能跑 batch size 为 1,设大了会爆显存。第三个是layer_shards_saving_path,指定分层权重的缓存路径,这个路径最好放在 SSD 上,别放机械硬盘。
还有一个容易被忽略的参数是prefetching,控制是否开启预取。开启后推理速度会快一些,但内存占用会增加。如果你的内存比较充裕,建议开启;如果内存紧张,可以关掉试试。
output = model.generate( input_ids, max_new_tokens=256, use_cache=True, do_sample=False )use_cache这个参数也值得说一下。开启 KV Cache 能显著加速生成过程,但会占用额外显存。airllm 对 KV Cache 的处理做了优化,但在长序列场景下还是要留意显存余量。
3.4 磁盘与内存的调优技巧
前面说过,磁盘 IO 是瓶颈。所以第一件事就是把模型权重放在 NVMe SSD 上,别放机械硬盘。我做过对比测试,同样的模型和配置,NVMe 上的推理速度大概是 SATA SSD 的两倍,是机械硬盘的五倍以上。这个差距非常直观。
内存方面,如果你有 64GB 以上的内存,可以尝试调大 airllm 的内存缓存层数,让更多层常驻内存,减少磁盘读取。具体怎么调,可以看 airllm 的配置文档,不同版本参数名可能略有差异。但核心思路就是:内存越大,能缓存的层越多,磁盘 IO 越少,速度越快。
注意:airllm 在首次加载模型时会做权重转换和分层处理,这个过程可能比较慢,尤其是大模型。建议第一次运行时耐心等待,转换完成后会生成缓存,后续加载就快了。
4. 完整实操流程与关键环节实现
4.1 从零跑通一个 70B 模型的完整步骤
光说理论没意思,我直接把跑通一个 70B 模型的完整流程走一遍。假设你有一张 24G 显存的卡,64GB 内存,一块 1TB 的 NVMe SSD。
第一步,确认磁盘空间。70B 模型 4bit 量化后大概 35GB,加上转换过程中的临时文件,建议预留 100GB 以上空间。用df -h看一下挂载点,确保模型目录在 SSD 上。
第二步,下载权重。可以用 huggingface-cli 或者 git lfs,但大模型文件多,建议用支持断点续传的工具。下载完成后检查一下文件完整性,别下到一半断了还不知道。
第三步,安装 airllm 和依赖。按前面说的方式建环境、装包。装完后跑一个简单的 import 测试:
import airllm print(airllm.__version__)第四步,写推理脚本。一个最小可运行的例子大概长这样:
from airllm import AutoModel import torch model = AutoModel.from_pretrained( "/path/to/model", compression="4bit", layer_shards_saving_path="/path/to/cache" ) tokenizer = model.tokenizer input_text = "请用一句话解释什么是机器学习。" input_ids = tokenizer(input_text, return_tensors="pt").input_ids output = model.generate( input_ids, max_new_tokens=128, do_sample=False ) print(tokenizer.decode(output[0], skip_special_tokens=True))第五步,运行并观察。第一次运行会触发权重转换,你会看到进度条在走,这个过程可能持续十几分钟到半小时,取决于磁盘速度和模型大小。转换完成后开始推理,这时候留意一下显存占用和生成速度。
4.2 参数计算:显存到底够不够
很多人卡在"我的显存够不够"这个问题上。这里给一个粗略的估算方法。单层权重的显存占用约等于:模型总参数量除以层数,再乘以每个参数的字节数。以 70B 模型、80 层、4bit 量化为例,每层参数约 0.875B,4bit 即 0.5 字节每参数,单层权重约 0.44GB。加上激活值和 KV Cache,单层计算峰值显存大概在 2-4GB。所以 24G 显存跑 70B 4bit 是绰绰有余的,甚至 8G 显存都能试试。
但要注意,这只是单层计算的显存。airllm 在预取时可能会同时持有当前层和下一层的权重,所以实际峰值会翻倍。再加上 PyTorch 自身的显存开销,建议预留 50% 的余量。也就是说,如果你算出单层需要 4GB,那最好有 6-8GB 可用显存。
4.3 速度实测与预期管理
我用 70B 4bit 模型在单张 24G 卡上实测,生成速度大概在每秒 2-5 个 token,具体取决于序列长度和磁盘速度。这个速度说实话不快,生成一段 200 字的回答可能要等一两分钟。但对比"根本跑不起来",这已经是质的飞跃了。
如果你追求更快的速度,有几个方向可以尝试。一是用更小的模型,13B 或 7B 的速度会快很多。二是升级磁盘,NVMe 比 SATA 快不少。三是增加内存,让更多层常驻内存。四是调小max_new_tokens,减少生成量。这些手段可以组合使用,效果叠加。
提示:airllm 的速度瓶颈主要在磁盘 IO,所以任何能减少磁盘读取的操作都能提速。量化、内存缓存、预取,本质上都是在做这件事。
5. 常见问题与排查技巧实录
5.1 加载模型时报显存不足
这是最常见的问题。明明按估算显存应该够,但一加载就报 OOM。原因通常有几个:一是预取层数设多了,同时加载了多层权重;二是 KV Cache 没控制好,长序列下占用暴涨;三是 PyTorch 的显存碎片问题。
排查思路:先把预取关掉,看能不能加载。如果能,说明是预取导致的峰值显存过高,可以调小预取层数。如果还不行,检查一下max_new_tokens是不是设太大了,试着调小。再不行就换更激进的量化,4bit 换 2bit(如果支持的话),或者换更小的模型。
5.2 推理速度慢到无法接受
速度慢的原因前面分析过,主要是磁盘 IO 瓶颈。排查步骤:先用iostat看一下磁盘读取速度,如果磁盘利用率一直 100%,说明 IO 是瓶颈。这时候换 SSD、开量化、加内存缓存,都能改善。如果磁盘利用率不高但速度还是慢,那可能是 CPU 解码或内存拷贝成了瓶颈,检查一下 CPU 占用和内存带宽。
还有一个容易被忽略的点:airllm 在首次运行时做权重转换,这个转换过程本身很慢,但只做一次。如果你每次运行都慢,可能是缓存路径没设对,导致每次都在重新转换。检查一下layer_shards_saving_path是否指向了可写且持久化的目录。
5.3 生成结果质量下降
量化会带来精度损失,这是预期内的。但如果质量下降得特别厉害,比如输出乱码、重复、逻辑混乱,那可能是量化配置有问题,或者模型本身跟量化方式不兼容。可以试着换 8bit 量化对比一下,如果 8bit 正常而 4bit 异常,说明这个模型对 4bit 量化比较敏感。
另外,do_sample和temperature这些生成参数也会影响输出质量。如果开了采样但 temperature 设得太高,输出会变得随机且不稳定。建议先用贪心解码(do_sample=False)验证模型本身没问题,再调采样参数。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 加载时 OOM | 预取过多、KV Cache 过大 | 关闭预取测试、调小 max_new_tokens | 减少预取层数、换更小量化 |
| 推理速度极慢 | 磁盘 IO 瓶颈 | 用 iostat 看磁盘利用率 | 换 NVMe、开量化、加内存缓存 |
| 每次运行都慢 | 缓存路径未持久化 | 检查 layer_shards_saving_path | 设为持久化 SSD 目录 |
| 输出质量差 | 量化精度损失 | 对比 8bit 和 4bit | 换 8bit 或不量化 |
| 输出乱码重复 | 生成参数不当 | 检查 do_sample 和 temperature | 先用贪心解码验证 |
| 显存碎片导致 OOM | PyTorch 显存管理 | 重启进程、调整分配策略 | 设置 PYTORCH_CUDA_ALLOC_CONF |
5.5 几个我踩过的坑
第一个坑是磁盘空间。我第一次跑 70B 模型时,没算转换过程中的临时文件,结果跑到一半磁盘满了,转换失败,还得清理重来。建议预留至少两倍于模型体积的空间。
第二个坑是缓存路径。我一开始把缓存路径设在了/tmp下,结果机器重启后缓存没了,又得重新转换。后来改到了持久化目录,省了不少时间。
第三个坑是版本兼容。airllm 更新比较快,不同版本之间的 API 可能有变化。我有次升级后原来的脚本跑不通了,查了半天才发现是参数名改了。建议锁定一个稳定版本,别盲目追新。
第四个坑是内存不足导致的交换。有次我在内存只有 16GB 的机器上跑,系统开始用 swap,速度直接掉到几乎不可用。后来加了内存才恢复正常。所以内存这个事,真的不能省。
6. 这套方案还能怎么扩展
airllm 这套分层加载的思路,其实不只适用于单机推理。往大了说,它可以跟模型服务框架结合,做成一个支持大模型的多用户服务。往小了说,它可以嵌入到边缘设备里,让资源受限的环境也能跑起大模型。
我最近在尝试的一个方向是,把 airllm 跟流式输出结合。因为分层加载本身就是逐层计算的,天然适合流式生成。用户不用等整个回答生成完,可以边生成边看,体验会好很多。这个改动主要在输出层,把 generate 的结果逐 token 吐出来就行。
另一个方向是跟模型微调结合。airllm 目前主要做推理,但分层加载的思路对微调也有启发。如果能把梯度计算也做成逐层进行,那单卡微调大模型也不是不可能。当然这涉及的技术细节更多,我还在摸索阶段。
最后分享一个小技巧:如果你经常跑同一个模型,可以把转换后的分层权重打包备份。这样换机器或者重装环境时,直接拷贝过去就能用,省去重新转换的时间。这个备份文件虽然大,但比原始权重还是小不少的,迁移起来方便很多。
整体用下来,airllm 给我的感觉是"务实"。它没有追求极致的速度,也没有堆砌花哨的功能,就是老老实实解决"显存不够怎么跑大模型"这个问题。对于手头资源有限但又想折腾大模型的人来说,这确实是个值得一试的方案。速度慢是慢了点,但能跑起来,很多想法才有验证的机会。