1. 单卡跑大模型这件事,到底卡在哪
第一次看到 airllm 这个项目的时候,我正在一台只有单张消费级显卡的机器上折腾一个 70B 级别的模型。显存报错几乎是必然的,OOM 的红色提示刷了满屏,那种感觉就像你明明有一整柜的书,但书桌只有巴掌大,想同时摊开三本都做不到。airllm 想解决的就是这个矛盾:让显存不够的普通设备,也能把参数量远超显存容量的大模型跑起来。
它的核心思路并不神秘,一句话概括就是用时间换空间——把模型按层切开,需要哪一层就把哪一层加载进显存,算完立刻释放,再加载下一层。听起来简单,但真正落地要处理一堆细节:怎么切、什么时候加载、怎么避免反复读盘拖慢速度、量化怎么配合、不同模型架构怎么兼容。airllm 把这些脏活累活封装成了一个相对好用的接口,让你几行代码就能跑起来。
这篇文章适合几类人看:手里只有单卡、想本地跑大模型但被显存劝退的开发者;想理解分层推理(layer-wise inference)原理、准备自己造轮子的工程师;以及已经用过 airllm 但被速度、精度、兼容性问题卡住、想搞清楚背后机制的人。我会从设计思路讲到实操细节,再到踩坑排查,尽量把每个"为什么"都讲透,让你不只是会调用,而是真正明白它在干什么。
需要先说明一点:airllm 不是魔法。它不会让你的慢机器变快,它只是把"跑不起来"变成"跑得起来但慢"。理解这个前提,后面的所有取舍你都能想通。
2. 分层推理的设计思路与方案选型
2.1 为什么是"分层加载"而不是别的路子
要让大模型在有限显存里跑起来,业内常见有几条路:量化压缩、模型并行、CPU offload、分层加载。airllm 选的是最后一条,但其实是"分层加载 + 量化 + 磁盘缓存"的组合拳。我先说说为什么它不选其他几条。
量化压缩是最直接的,把 FP16 压到 INT8 甚至 INT4,显存占用直接砍半甚至砍到四分之一。但量化有天花板,一个 70B 模型即使压到 4bit,权重也要 35GB 左右,单张 24GB 的卡还是放不下。所以量化能缓解,但不能根治。
模型并行是把模型拆到多张卡上,每张卡负责一部分。这需要多卡,而且卡间通信开销不小,对普通玩家来说门槛太高——不是每个人都有两张以上的卡。
CPU offload 是把部分权重放内存、部分放显存,靠 PCIe 来回搬。这个思路和分层加载很像,区别在于 offload 通常是静态划分,而分层加载是动态的、按计算顺序来的,粒度更细,峰值显存更低。
airllm 的分层加载本质上是把"整个模型常驻显存"改成"当前层常驻显存"。一个 70B 模型假设 80 层,每层权重约 1.75GB(FP16),那么峰值显存只需要容纳一层加上激活值和 KV cache,单张 24GB 卡绰绰有余。代价是每算一层都要从磁盘或内存把权重读进来,这个 IO 就是速度瓶颈的来源。
提示:分层加载的峰值显存 ≈ 单层权重大小 + 激活值 + KV cache + 框架开销。层数越多、单层越小,峰值显存越低,但 IO 次数越多、越慢。这是一个可以调节的权衡。
2.2 磁盘缓存与压缩格式的取舍
如果每次推理都从原始模型文件读权重,速度会慢到无法接受。airllm 的做法是第一次运行时把模型按层拆分,转成特定格式存到磁盘,后续直接从这些分片文件加载。这一步叫"模型分片预处理"。
分片格式的选择很关键。原始 safetensors 是 FP16,读一层 1.75GB,从普通 SATA SSD 读大概要十几秒,一层十几秒、八十层就是十几分钟,完全没法用。所以 airllm 支持在分片时做量化,把每层压成 4bit 或 8bit,体积直接降到原来的四分之一到一半,IO 时间大幅缩短。
这里有个容易忽略的点:量化发生在分片阶段,而不是推理阶段。也就是说你第一次跑的时候会花很长时间做预处理,之后每次推理都直接用处理好的分片。很多人第一次跑以为卡死了,其实是在做分片,耐心等完就好。
| 方案 | 单层体积(70B/80层) | 读取耗时(SSD) | 精度损失 | 适用场景 |
|---|---|---|---|---|
| FP16 原始 | ~1.75GB | 10-15s | 无 | 显存充足,追求精度 |
| INT8 分片 | ~0.88GB | 5-8s | 极小 | 平衡速度与精度 |
| INT4 分片 | ~0.44GB | 2-4s | 可感知 | 显存紧张,追求速度 |
从表里能看出来,INT4 分片能把单层读取压到几秒,八十层下来几分钟能出结果,虽然还是慢,但至少可用了。我个人建议:如果显存够放 INT8 的单层,优先用 INT8,精度损失几乎可以忽略;如果实在紧张,再上 INT4。
2.3 内存作为二级缓存的角色
光靠磁盘还是慢,airllm 还会利用系统内存做缓存。如果你的机器有 64GB 甚至 128GB 内存,可以把一部分分片常驻内存,读取速度比 SSD 快一个数量级。这个策略叫"内存缓存层"。
实际配置时,你可以指定缓存多少层在内存里。缓存越多,内存占用越高,但速度越快。一个实用的经验值:留出 8-16GB 给系统和框架,剩下的内存尽量用来缓存分片。比如 128GB 内存的机器,缓存 60-80 层问题不大,速度能提升好几倍。
注意:内存缓存不是越多越好。如果缓存把内存占满,系统开始用 swap,反而会拖慢整体速度。一定要留足余量,尤其是同时跑其他程序的时候。
3. 核心机制拆解与关键参数实操
3.1 模型分片预处理到底做了什么
第一次调用 airllm 加载模型时,它会做几件事,我按顺序拆开讲。
第一步是读取原始模型权重。它支持 HuggingFace 格式的模型目录,会先扫描 config.json 拿到层数、隐藏维度、注意力头数这些结构信息。这一步决定了后面怎么切。
第二步是按 transformer 层边界切分。注意它不是随便按文件大小切,而是严格按模型结构切——embedding 一层、每个 transformer block 一层、最后的 norm 和 lm_head 一层。这样切的好处是每层可以独立加载、独立计算,层与层之间只传递隐藏状态(hidden states),数据量很小。
第三步是量化并保存分片。每个分片会被转成指定的精度格式,存成独立文件,同时生成一个索引文件记录每层的位置和元信息。索引文件很关键,推理时靠它快速定位要加载哪一层。
第四步是建立缓存映射。如果开启了内存缓存,会把部分分片预加载到内存,并维护一个 LRU(最近最少使用)淘汰策略,保证常用的层留在内存里。
整个过程是幂等的——如果分片已经存在,再次运行会直接跳过,不会重复处理。所以第一次慢,后面快。
3.2 关键参数逐个说清楚
airllm 的接口参数不多,但每个都影响很大,我挑几个最关键的讲。
layer_shards_saving_path:分片保存路径。默认在当前目录下建一个临时文件夹。建议显式指定到一个大容量 SSD 上,别放机械盘,否则 IO 会成为瓶颈。这个路径下的文件会一直存在,除非你手动删,所以磁盘空间要留够——一个 70B 模型 INT4 分片大概 35GB 左右。
compression:压缩方式。可选 '4bit'、'8bit' 或 None。前面表格分析过,8bit 是精度和速度的平衡点,4bit 是速度优先。实测下来,8bit 在大多数任务上输出质量和 FP16 几乎无差别,我一般默认用 8bit。
profiling_mode:性能分析模式。开启后会打印每层的加载和计算耗时,方便你定位瓶颈在哪。调试阶段建议开着,稳定运行后关掉减少开销。
max_seq_len:最大序列长度。这个直接影响 KV cache 大小,进而影响峰值显存。如果你只需要短文本推理,把它调小能省不少显存。但注意别调得比实际输入还小,否则会被截断。
device:计算设备。默认自动检测,但如果你有多个设备,可以显式指定。这里有个坑:airllm 的分层加载对设备切换比较敏感,指定后别中途改。
| 参数 | 默认值 | 推荐值 | 影响 |
|---|---|---|---|
| compression | None | '8bit' | 精度与速度平衡 |
| layer_shards_saving_path | 临时目录 | SSD 大容量路径 | IO 速度 |
| max_seq_len | 模型默认 | 按需调小 | 峰值显存 |
| profiling_mode | False | 调试时 True | 性能可见性 |
3.3 一次完整推理的数据流
理解数据流能帮你排查大部分问题。一次推理大致是这样走的:
输入文本先经过 tokenizer 变成 token id,然后进入 embedding 层。embedding 层的权重被加载进显存,算出初始隐藏状态,然后 embedding 层权重被释放。接着隐藏状态传给第一个 transformer block,这个 block 的权重被加载、计算、释放,输出传给下一个 block。如此循环直到最后一层,最后经过 lm_head 输出 logits,采样得到下一个 token。
如果是自回归生成,每生成一个 token 都要把上面整个流程走一遍。这就是为什么生成速度慢——不是算得慢,是每步都要重新加载所有权重。有些实现会做优化,比如把 KV cache 保留在显存里避免重算,但权重加载这块省不掉。
提示:正因为每生成一个 token 都要遍历所有层,所以 airllm 的瓶颈几乎永远在 IO 上,而不是算力上。想提速,优先优化存储(换 NVMe SSD、加内存缓存),而不是换更强的 GPU。
4. 从零跑通的完整实操流程
4.1 环境准备与依赖安装
先说环境。airllm 是 Python 包,依赖 PyTorch、transformers、accelerate 这些常规库。我建议用独立的虚拟环境,避免和系统里的其他包冲突。
python -m venv airllm_env source airllm_env/bin/activate pip install airllm安装完 airllm 会自动带上依赖,但 PyTorch 建议按你的 CUDA 版本单独装,别用默认的 CPU 版,否则跑起来慢到怀疑人生。装之前先确认显卡驱动和 CUDA 版本:
nvidia-smi看右上角的 CUDA Version,然后去 PyTorch 官网找对应版本的安装命令。这一步别偷懒,版本不匹配是新手最常见的坑。
磁盘空间也要提前规划。一个 70B 模型的 INT4 分片大概 35GB,加上原始模型文件(如果保留的话)又是 140GB 左右。建议准备至少 200GB 的 SSD 空间,机械盘就别想了。
4.2 加载模型与首次分片
环境好了就可以写代码了。最简版本大概长这样:
from airllm import AutoModel model = AutoModel.from_pretrained( "模型路径或名称", compression='8bit', layer_shards_saving_path='/path/to/ssd/shards' )第一次运行会触发分片预处理。这时候你会看到进度条慢慢爬,一个 70B 模型在普通 SSD 上大概要几十分钟到一两个小时,取决于磁盘速度和 CPU。别以为卡死了,去泡杯茶等着。
分片完成后,后续再运行就直接跳过这一步。你可以去保存路径下看看,会有一堆按层命名的分片文件和一个索引文件。
注意:分片过程中别中断,中断了可能留下不完整的分片,下次运行会报错。如果真中断了,把保存路径清空重新来一遍最稳妥。
4.3 推理调用与参数调节
分片好了就能推理了。airllm 的推理接口和 transformers 很像:
input_text = "你的问题" input_ids = model.tokenizer(input_text, return_tensors="pt").input_ids output = model.generate( input_ids, max_new_tokens=128, do_sample=True, temperature=0.7 ) print(model.tokenizer.decode(output[0], skip_special_tokens=True))几个参数值得调:max_new_tokens 控制生成长度,别设太大,每多一个 token 就多一轮全层加载;temperature 控制随机性,0.7 左右比较自然;do_sample 开启采样,关掉就是贪心解码,输出更确定但可能重复。
实测下来,70B 模型 INT8 分片、单张 24GB 卡、NVMe SSD 的配置,生成速度大概每秒 0.5-2 个 token。听起来很慢,但对于本地跑大模型来说,能跑起来本身就是胜利。
4.4 内存缓存的配置技巧
想提速,内存缓存是最有效的手段。airllm 支持指定缓存层数,具体怎么配要看你的内存大小。
假设你有 128GB 内存,系统和其他程序占 16GB,剩 112GB 可用。一个 70B 模型 INT8 分片每层约 0.88GB,理论上能缓存 120 多层,但实际要留余量,缓存 80-100 层比较稳。这样大部分层都在内存里,只有少数层需要从磁盘读,速度能提升好几倍。
配置方式一般是在加载时传参,具体参数名看版本,常见的是 cache 相关的选项。如果文档不清楚,直接看源码里的 from_pretrained 签名,参数都列在那。
提示:内存缓存和磁盘分片是配合使用的。缓存的是已经量化好的分片,不是原始权重。所以先完成分片,再开缓存,顺序别搞反。
5. 常见问题与排查技巧实录
5.1 显存还是不够怎么办
有人会问:不是说分层加载能省显存吗,怎么我还是 OOM?这种情况通常是峰值显存没算对。峰值显存不只是单层权重,还包括激活值、KV cache、框架开销。如果 max_seq_len 设得很大,KV cache 可能比单层权重还大。
排查思路:先把 max_seq_len 调到很小(比如 512),看还 OOM 不。如果不 OOM 了,说明是 KV cache 的问题,按需调小序列长度。如果还 OOM,检查是不是 compression 没生效,或者模型有特别大的层(比如某些 MoE 架构的专家层)。
还有一种情况是显卡被其他程序占用了。跑之前用 nvidia-smi 看看显存占用,把无关进程清掉。
5.2 速度慢到无法接受
速度慢基本是 IO 问题。排查顺序:先看磁盘类型,机械盘换 SSD,SATA SSD 换 NVMe;再看内存缓存开了没,没开就开;最后看分片精度,FP16 换 INT8 或 INT4。
如果都优化了还是慢,那可能是模型太大、硬件太弱,这是物理限制,没办法。这时候要么换更小的模型,要么接受这个速度。
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 首次运行卡住 | 正在分片预处理 | 耐心等待,别中断 |
| 推理 OOM | KV cache 过大 | 调小 max_seq_len |
| 生成极慢 | 磁盘 IO 瓶颈 | 换 NVMe,开内存缓存 |
| 输出乱码 | 量化精度过低 | 换 8bit 或 FP16 |
| 分片报错 | 上次中断残留 | 清空分片目录重来 |
5.3 输出质量下降怎么调
量化必然带来精度损失,但损失多少取决于量化方式和模型本身。如果发现输出明显变差,先确认用的是不是 4bit。4bit 在复杂推理任务上确实会掉点,换成 8bit 通常能恢复。
如果 8bit 还是不行,可能是模型对量化特别敏感。这时候可以试试混合精度——关键层用高精度,其他层用低精度。不过 airllm 默认不支持这么细的粒度,需要自己改代码。
还有一个容易被忽略的点:temperature 和 top_p 这些采样参数在量化模型上可能需要重新调。量化会让 logits 分布略有变化,原来的参数不一定最优。多试几组,找到适合当前配置的组合。
5.4 不同模型架构的兼容性
airllm 主要针对标准 transformer 架构,对 Llama、Qwen、Mistral 这些主流结构支持比较好。但遇到特殊架构,比如 MoE(混合专家)、Mamba(状态空间模型),可能就不兼容或者需要额外适配。
MoE 的问题是它的专家层不是每层都激活,分层加载的逻辑要改。Mamba 更麻烦,它没有标准的 transformer 层结构,整个分层思路都不适用。
所以选模型之前先确认架构。如果非要用特殊架构,做好自己改代码的准备,别指望开箱即用。
6. 一些实操心得与扩展方向
跑了一段时间 airllm,我最大的体会是:它解决的是"能不能跑",不是"跑得快不快"。如果你的目标是本地部署一个能实时响应的服务,airllm 不合适,它的延迟太高。但如果你的目标是离线跑个大模型做批量处理,或者单纯想在有限硬件上体验大模型能力,它非常合适。
几个实用心得。第一,分片目录一定要放在最快的盘上,这个投入回报比最高。第二,内存能加就加,大内存对分层推理的帮助比换显卡还明显。第三,第一次跑之前把参数想清楚,因为改参数可能要重新分片,很费时间。
扩展方向的话,可以研究一下把分片放到更快的存储介质上,或者做多进程预取——在算当前层的时候提前把下一层读进来,用计算时间掩盖 IO 时间。这个思路在数据库和操作系统里很常见,搬到分层推理上应该也有用。另外,如果有多台机器,可以考虑把不同层分布到不同机器上,做成一个分布式的分层推理集群,不过这复杂度就上去了。
最后分享一个小技巧:调试阶段把 profiling_mode 打开,看清楚每层到底花了多少时间在加载、多少在计算。很多时候你以为的算力瓶颈,其实是 IO 瓶颈,优化方向完全不一样。搞清楚这个,能省下不少瞎折腾的时间。