1. 单卡跑大模型这件事,到底卡在哪
大模型推理的显存瓶颈,几乎是每个想在自己机器上跑模型的人都会撞上的第一堵墙。拿一个 70B 参数的模型来说,光是权重本身,FP16 精度下就需要大约 140GB 显存,即便量化到 4bit,也要 35GB 上下。而一张消费级显卡的显存通常是 8GB、12GB、24GB 这个量级,中间差着一大截。很多人第一反应是"那我多买几张卡",但多卡方案对普通开发者来说,成本、主板、电源、散热全是问题,更别提很多人的机器压根插不下第二张卡。
airllm 这个项目要解决的就是这个矛盾:让大模型能在单张显存有限的卡上跑起来,甚至能在纯 CPU 环境下推理。它的核心思路不是去压缩模型精度,而是做分层加载——把模型按层切开,推理时只把当前需要的那一层搬进显存,算完就换下一层。这样一来,显存占用从"整个模型"降到"单层模型",量级上直接砍掉几十倍。
这个方案适合谁?如果你手头只有一张 8GB 或 12GB 的卡,想跑 7B、13B 甚至 70B 的模型做实验、做推理验证、做本地部署测试,airllm 就是为你准备的。如果你完全没有独立显卡,只有一台内存够大的普通机器,它也能让你用 CPU 把模型跑起来,只是速度会慢一些。它不适合追求高并发、低延迟的生产级服务场景,那是 vLLM、TensorRT-LLM 这类方案的战场,airllm 的定位是单机、单卡、低显存、能跑起来。
我最初接触这个项目,是因为手上只有一张 12GB 的卡,想验证一个 70B 模型的实际输出效果,又不想花钱租多卡机器。试过量化、试过 CPU offload,效果都不理想,直到用了 airllm 的分层加载思路,才真正把这件事跑通。下面我把整个方案的设计逻辑、核心细节、实操过程和踩过的坑,完整地拆一遍。
2. 分层加载的核心设计思路拆解
2.1 为什么是"分层"而不是"量化"
大模型推理省显存,主流有两条路:一条是量化,把 FP16 压到 INT8、INT4,权重体积直接减半甚至减到四分之一;另一条是分层加载,权重精度不变,但同一时刻只有一小部分在显存里。
量化的问题在于,它是有损的。4bit 量化虽然能把 70B 模型压到 35GB 左右,但对一张 12GB 的卡来说还是放不下,而且量化本身会带来精度损失,某些对数值敏感的任务上输出质量会明显下降。更麻烦的是,量化后的模型往往还需要额外的反量化计算,推理速度不一定快。
分层加载走的是另一条路:精度完全不动,显存占用靠"时间换空间"。它的逻辑很朴素——模型是一层一层串行计算的,第 1 层的输出是第 2 层的输入,第 2 层算完第 1 层的中间结果就可以扔掉了。既然同一时刻只需要一层的权重,那为什么要把整个模型都塞进显存?
这个思路的关键在于,它把"显存不够"这个硬约束,转化成了"磁盘 IO 和内存带宽"这个软约束。代价是推理速度会变慢,因为每算一层都要从磁盘或内存把权重搬进来。但对于"能跑起来"这个目标来说,这个代价是值得的。
2.2 分层加载的三种存储层级
airllm 的设计里,权重会在三个层级之间流转:磁盘、内存、显存。理解这三层的关系,是理解整个方案的基础。
磁盘层存放完整的模型权重文件,通常是 safetensors 或 pytorch bin 格式。这一层容量最大,但读取速度最慢。内存层是磁盘和显存之间的缓冲,模型加载时会把权重先读进内存,再按需送进显存。显存层只保留当前正在计算的那一层,算完立刻释放。
理想情况下,如果内存足够大,可以把整个模型都放进内存,这样每次换层只需要从内存搬到显存,速度会快很多。如果内存也不够,那就只能从磁盘直接读,速度会明显下降。所以 airllm 的实际性能,很大程度上取决于你的内存有多大。
这里有个容易被忽略的点:分层加载的粒度。如果按单个权重矩阵切分,层数太多,IO 次数就多,开销大;如果按整个 transformer block 切分,粒度又太粗,单层显存占用可能还是超标。airllm 的做法是按 transformer 层来切,每一层包含自注意力、前馈网络等子模块,这个粒度在显存占用和 IO 次数之间取得了比较好的平衡。
2.3 与 CPU offload 的本质区别
很多人会把 airllm 和 PyTorch 自带的 CPU offload 混为一谈,其实两者有本质区别。
CPU offload 的思路是:模型权重常驻内存,计算时把需要的部分搬到 GPU,算完再搬回去。它假设内存足够放下整个模型,只是显存不够。而 airllm 面对的场景更极端——内存可能也不够放下整个模型,所以它必须支持从磁盘直接分层读取。
另一个区别是控制粒度。CPU offload 通常是框架自动调度的,开发者能干预的空间有限。airllm 则是显式地控制每一层的加载和释放,开发者可以清楚地知道当前哪一层在显存里、下一层从哪里读。这种显式控制带来的好处是,你可以针对自己的硬件做精细调优,比如调整预读层数、控制内存缓存大小。
2.4 方案选型的取舍逻辑
任何方案都有取舍,airllm 也不例外。它的优势是显存占用极低、精度无损、硬件门槛低;代价是推理速度慢、不适合高并发、对磁盘和内存有一定要求。
我个人的判断是:如果你的目标是"验证模型效果"而不是"提供服务",airllm 是非常合适的选择。比如你想测试一个 70B 模型在某个任务上的表现,或者想对比不同模型对同一批输入的输出差异,用 airllm 在单卡上跑完全够用。但如果你要做的是线上服务,需要同时处理几十个请求,那 airllm 的串行分层加载会成为瓶颈,这时候应该考虑多卡方案或者专业的推理框架。
3. 核心细节解析与实操要点
3.1 模型格式与权重切分
airllm 支持从 Hugging Face 格式的模型目录加载,权重文件通常是 safetensors 格式。safetensors 相比传统的 pytorch bin 格式,加载速度更快,而且支持内存映射,这对分层加载很关键——它可以让程序只读取需要的部分,而不是把整个文件读进内存。
权重切分的逻辑是:先读取模型的配置文件,拿到层数、隐藏维度、注意力头数等参数,然后按 transformer 层的边界把权重分组。每一层的权重包括自注意力的 Q、K、V、O 矩阵,前馈网络的两个线性层,以及层归一化的参数。这些参数被打包成一个"层单元",推理时整层加载、整层释放。
这里有个细节值得注意:embedding 层和 lm_head 层的处理。这两层通常参数量很大,尤其是词表大的模型,embedding 层可能占整个模型参数的相当比例。airllm 对这两层的处理方式和中间的 transformer 层略有不同,因为它们只在推理的开头和结尾各用一次,不需要反复加载。实际实现中,这两层往往会被单独处理,避免占用宝贵的显存。
3.2 显存与内存的调度策略
调度策略的核心问题是:什么时候加载下一层,什么时候释放当前层。
最朴素的策略是串行处理:加载第 N 层,计算,释放第 N 层,加载第 N+1 层。这种策略显存占用最低,但 IO 等待时间完全暴露在关键路径上,速度最慢。
稍微优化一点的策略是预读:在计算第 N 层的同时,后台异步加载第 N+1 层。这样当第 N 层算完时,第 N+1 层已经准备好了,IO 和计算可以重叠。代价是需要额外的显存或内存来存放预读的层。airllm 支持配置预读层数,你可以根据自己显存的大小来调整。
还有一个策略是内存缓存:把最近用过的层留在内存里,下次需要时直接从内存加载,避免重复读磁盘。对于多轮对话场景,如果两次推理用到的层有重叠,缓存就能派上用场。但缓存会占用内存,需要权衡。
我实测下来的经验是:预读 1 到 2 层是比较稳妥的配置。预读太多会挤占显存,反而导致当前层放不下;预读太少则 IO 等待明显。具体数值要根据你的显存大小和模型层大小来算。
3.3 量化与分层的组合使用
airllm 本身不做量化,但它可以和量化后的模型配合使用。如果你把模型量化到 4bit,每层的体积会缩小到原来的四分之一左右,这样单层显存占用更低,预读层数可以更多,整体速度会提升。
不过这里有个坑:量化模型的加载和反量化会带来额外开销。如果量化是在加载时动态进行的,那每次换层都要做一次反量化计算,这个开销可能抵消掉量化带来的显存收益。所以更推荐使用已经量化好的模型文件,直接加载量化权重,避免运行时反量化。
另一个坑是量化格式的兼容性。不同量化工具产出的模型格式不一样,airllm 对某些格式的支持可能不完整。我建议优先选择社区验证过的量化版本,或者自己用量化工具重新导出,确保格式兼容。
3.4 关键参数与配置项
airllm 的配置项不多,但每一个都影响实际表现。下面这张表是我整理的核心参数说明:
| 参数名 | 作用 | 推荐值 | 注意事项 |
|---|---|---|---|
| layer_shards_saving_path | 分层权重缓存目录 | 独立 SSD 路径 | 不要放在系统盘,避免 IO 争抢 |
| prefetching | 是否开启预读 | True | 显存紧张时可关闭 |
| prefetch_size | 预读层数 | 1-2 | 根据显存和层大小调整 |
| compression | 权重压缩方式 | 4bit 或 None | 压缩会损失精度,按需选择 |
| profiling_mode | 性能分析模式 | False | 调试时开启,正式运行关闭 |
这些参数里,layer_shards_saving_path 是最容易被忽视但影响最大的。分层加载会把切分后的权重写到这个目录,如果放在机械硬盘上,IO 速度会成为严重瓶颈。我建议放在 NVMe SSD 上,读取速度能差出好几倍。
4. 完整实操过程与核心环节实现
4.1 环境准备与依赖安装
先说环境。airllm 对 Python 版本的要求是 3.8 以上,推荐 3.10 或 3.11。PyTorch 版本建议 2.0 以上,因为新版本对内存管理和异步操作的支持更好。
安装过程本身不复杂,但有几个依赖需要注意。transformers 库的版本要和模型兼容,accelerate 库用于设备管理,safetensors 用于权重加载。如果你要用 4bit 量化,还需要 bitsandbytes。
pip install airllm pip install torch transformers accelerate safetensors pip install bitsandbytes # 可选,用于量化安装完之后,建议先跑一个小的模型验证环境是否正常,比如 7B 级别的模型。不要一上来就上 70B,那样如果环境有问题,排查起来会很痛苦。
提示:如果你的 CUDA 版本和 PyTorch 不匹配,会出现各种奇怪的报错。先用
python -c "import torch; print(torch.cuda.is_available())"确认 GPU 可用,再继续下一步。
4.2 模型加载与分层切分
加载模型是第一步,也是最耗时的一步。airllm 在首次加载时会把模型按层切分,并保存到指定的缓存目录。这个过程可能需要几分钟到十几分钟,取决于模型大小和磁盘速度。
from airllm import AutoModel model = AutoModel.from_pretrained( "模型路径或名称", layer_shards_saving_path="/path/to/ssd/cache", prefetching=True, prefetch_size=1, )这段代码背后做的事情是:读取模型配置,确定层数和每层参数,按层切分权重文件,保存到缓存目录。切分完成后,后续加载会直接使用缓存,不再重复切分。
这里有个实操细节:切分后的缓存目录会占用和原模型差不多的磁盘空间。如果你要跑多个模型,磁盘空间要提前规划好。我一般会给每个模型单独建一个缓存目录,方便管理和清理。
4.3 推理调用与参数配置
推理调用和普通的 transformers 用法类似,但有几个参数需要特别注意。
input_text = "你的输入文本" input_tokens = model.tokenizer( input_text, return_tensors="pt", return_attention_mask=False, truncation=True, max_length=128, ).input_ids generation_output = model.generate( input_tokens, max_new_tokens=128, use_cache=True, return_dict_in_generate=True, ) output = model.tokenizer.decode( generation_output.sequences[0], skip_special_tokens=True, )max_new_tokens控制生成的最大长度,这个值越大,推理时间越长,因为每生成一个 token 都要把所有层过一遍。use_cache开启 KV 缓存,能显著减少重复计算,但会占用额外显存。如果显存紧张,可以关掉,代价是速度变慢。
我实测下来,生成 128 个 token 在 12GB 卡上跑 70B 模型,大概需要几分钟。这个速度做交互式对话肯定不行,但做批量推理、结果验证是够用的。
4.4 性能调优的实操记录
性能调优这块,我踩过不少坑,这里把有效的几个手段列出来。
第一个手段是把缓存目录放到 NVMe SSD 上。我一开始放在机械硬盘上,换层时 IO 等待非常明显,后来换到 SSD,整体速度提升了将近一倍。这个改动成本最低,收益最直接。
第二个手段是调整预读层数。默认配置下预读是关闭的,开启后设置 prefetch_size=1,速度有明显提升。但设置到 2 或 3 时,显存开始吃紧,反而出现了 OOM。所以预读层数不是越大越好,要找到自己硬件的平衡点。
第三个手段是用 4bit 量化模型。量化后每层体积缩小,预读层数可以增加,整体速度提升明显。但要注意量化带来的精度损失,如果你的任务对输出质量要求高,要谨慎使用。
第四个手段是控制输入长度。输入越长,注意力计算量越大,每层的计算时间越长。如果只是做简单验证,把输入截断到 128 或 256 个 token,能省不少时间。
4.5 纯 CPU 环境的运行方案
如果你完全没有显卡,airllm 也能在纯 CPU 环境下运行。这时候显存的概念变成了内存,分层加载的逻辑不变,只是"显存"换成了"内存"。
CPU 环境下,内存大小是关键。70B 模型 FP16 需要 140GB 内存,这个门槛很高。如果内存不够,就只能用量化模型,4bit 量化后大概 35GB,普通工作站能放下。
CPU 推理的速度比 GPU 慢很多,我实测下来大概是 GPU 的十分之一到二十分之一。所以 CPU 方案适合的场景是:没有显卡、但需要验证模型效果、对速度不敏感。如果你要做的是批量处理大量数据,CPU 方案的时间成本会很高。
5. 常见问题与排查技巧实录
5.1 显存溢出(OOM)的排查思路
OOM 是分层加载最常见的报错。排查思路是:先确认单层显存占用,再看预读层数是否过多,最后检查是否有其他进程占用显存。
单层显存占用的估算方法是:模型总参数量除以层数,再乘以每个参数的字节数。比如 70B 模型有 80 层,FP16 精度下每层大约 1.75GB。如果你的卡只有 8GB,预读 1 层加上当前层就是 3.5GB,再加上 KV 缓存和中间激活值,可能就接近上限了。
排查时可以用nvidia-smi实时监控显存占用,观察是在加载哪一层时爆掉的。如果是加载时爆,说明单层太大,需要量化;如果是计算时爆,说明 KV 缓存或激活值占用过多,需要减小输入长度或关闭 use_cache。
5.2 加载速度慢的优化方向
加载慢通常有三个原因:磁盘 IO 慢、内存不足导致频繁换页、预读配置不合理。
磁盘 IO 慢的解决办法是换 SSD,这个前面说过了。内存不足的判断方法是看系统监控,如果内存使用率长期在 90% 以上,说明内存是瓶颈,需要减少缓存或换更大内存的机器。预读配置不合理则表现为 IO 和计算没有重叠,可以尝试调整 prefetch_size。
还有一个容易被忽视的点是文件系统的影响。某些文件系统对小文件读写性能差,而分层加载会产生大量小文件。如果条件允许,用 ext4 或 xfs 这类对小块 IO 友好的文件系统。
5.3 输出质量异常的定位方法
输出质量异常通常不是 airllm 本身的问题,而是模型或参数的问题。定位方法是:先用小模型验证流程,再换大模型;先用短输入验证,再换长输入;先关闭量化验证,再开启量化。
如果小模型正常、大模型异常,可能是大模型的层切分有问题,检查缓存目录是否完整。如果短输入正常、长输入异常,可能是位置编码或注意力掩码的问题。如果关闭量化正常、开启量化异常,那就是量化精度损失导致的,需要换量化方案或提高量化精度。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| OOM 报错 | 单层太大或预读过多 | nvidia-smi 监控 | 量化模型或减少预读 |
| 加载极慢 | 磁盘 IO 瓶颈 | 检查磁盘类型 | 换 SSD 或减少缓存 |
| 输出乱码 | 权重切分错误 | 检查缓存完整性 | 删除缓存重新切分 |
| 推理中断 | 内存不足 | 监控内存使用 | 减少缓存或加内存 |
| 速度骤降 | 预读失效 | 检查配置 | 调整 prefetch_size |
5.5 独家避坑经验
第一个坑是缓存目录的权限问题。如果缓存目录没有写权限,切分过程会失败,但报错信息可能不明确。建议提前确认目录权限,或者用绝对路径。
第二个坑是模型版本和 transformers 版本的兼容性。某些新模型需要特定版本的 transformers,版本不匹配会导致加载失败。建议先查模型的文档,确认依赖版本。
第三个坑是多进程同时访问缓存目录。如果你同时跑多个推理任务,它们会争抢缓存目录的读写,导致性能下降甚至报错。建议每个任务用独立的缓存目录,或者串行执行。
第四个坑是显存碎片化。长时间运行后,显存可能出现碎片,导致明明有足够空间却分配失败。解决办法是定期重启进程,或者用torch.cuda.empty_cache()手动清理。
6. 这套方案还能怎么扩展
airllm 的分层加载思路,其实可以延伸到很多场景。比如你可以把它和模型并行结合,在多张低显存卡上分布不同的层,每张卡只负责一部分层的计算。这样既利用了多卡,又不需要每张卡都放下整个模型。
另一个扩展方向是和推理服务框架结合。airllm 本身不适合高并发,但你可以用它做模型预热和权重切分,然后把切分后的层交给更高效的推理引擎来调度。这样既保留了分层加载的灵活性,又提升了并发能力。
还有一个思路是针对特定任务做层裁剪。某些任务可能不需要模型的所有层,比如简单的分类任务可能只用前几层就够了。你可以用 airllm 的分层能力,只加载需要的层,进一步降低资源占用。
我个人在实际操作中的体会是,airllm 最大的价值不是它本身有多快,而是它把"能不能跑"这个问题变成了"跑多快"的问题。在它出现之前,单卡跑大模型基本是无解的;有了它之后,至少有了一个可行的路径。至于速度,随着硬件进步和方案优化,会慢慢好起来的。