ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

airllm低显存跑大模型:分层推理与CPU/GPU协同实战指南

airllm低显存跑大模型:分层推理与CPU/GPU协同实战指南

1. airllm是什么,为什么我会盯上它

1.1 边缘推理的现实痛点

最近在调一批本地化大模型推理任务,想在自己的工作机上跑一个7B参数的对话模型,结果发现显存成了最大的瓶颈。单卡8GB的显存连7B模型的全精度权重都塞不下,更别提还要留出一部分空间给KV cache和激活值。折腾了一晚上,换过各种量化方案,甚至想过用CPU慢慢磨,但加载时间和推理延迟都让人崩溃。后来朋友提了句airllm,说这东西能把大模型的层拆开,让GPU只处理当前激活的层,剩下的层全丢给内存和CPU,我当时第一反应是:这不就是类似老式流水线吗?听起来有点离谱,但实测下来的确能解决一大批“显存不够但内存够”的场景。

很多人可能和我一样,手里并没有A100这种算力怪兽,只有一台配着家用级显卡的台式机,或者是装了独立显卡的笔记本。想跑Llama类模型,要么选择小得可怜的小参数模型,要么就得捏着鼻子用API。airllm这类的方案真正让我觉得有价值的地方,是它把显存从“硬约束”变成了“软约束”——你不一定需要在GPU上装下整个模型,只要内存够,就能硬吃掉大模型,GPU只负责计算正在跑的那几层。这个思路听起来简单,但落地的时候有不少门道,这篇文章把我踩过的坑、调过的参数、实际跑通的流程都整理出来,给有同样需求的朋友做个参考。

1.2 airllm的核心思路:把模型“分层摊开”

传统的大模型推理方式很直接:把模型的全部参数加载到GPU显存里,然后逐层计算。一旦模型超过了显存容量,要么换更大的卡,要么把模型缩小(量化或蒸馏),要么就彻底放弃本地跑。airllm的做法不一样,它把Transformer模型按层切分,一部分层固定在GPU显存中,剩下的层放在CPU内存里,推理时通过一种类似流水线分块的方式,让数据逐层流动,计算完当前层之后再把结果传递到下一层,同时用异步预取来掩盖CPU和GPU之间的传输延迟。

这样设计最直接的好处,是显存占用从“整个模型”缩减到了“少数几个层的权重+激活值”。举个例子,一个70B的模型如果全精度加载,大概需要140GB显存,这基本是数据中心级显卡才配得上的规格。但如果你用airllm,将模型的大部分层放在内存里,只把一小部分层动态地在GPU上计算,那么实际显存占用可以降到几个GB级别,内存占用则看你的机器能插多少内存条。这种思路与“虚拟内存”有点像,但更聪明的是它知道模型分层的规律,能主动安排数据流动。

当然,世上没有白吃的午餐。分层推理牺牲的一定是速度。以我实测的经验来看,同样的7B量化模型,如果完全塞进显存,生成速度可能稳稳超过20 tokens/s;但用airllm分层跑,速度可能掉到6到9 tokens/s之间,具体取决于你分多少层给GPU、内存带宽多大、CPU算力强不强。对于交互式聊天来说,这个速度勉强能忍,但对于需要高吞吐的场景,可能还是得考虑把模型缩小或干脆上云。

2. 环境准备与安装实战

2.1 硬件需求与选型

先用一张表把我的实测环境列出来,方便你对号入座:

硬件/软件我的配置说明
CPU某款8核16线程消费级处理器主要是承担系统调度和部分层的前向计算
内存32GB DDR4 3200决定你能装下多大的模型,内存越大越从容
GPU8GB显存的家用级显卡只需要负责一小部分层的计算,显存占用可控
操作系统Linux发行版Windows也可以跑,但Linux下兼容性更好
Python3.10建议3.8以上,避免依赖装不上

内存是关键。airllm虽然能缓解显存压力,但会把模型权重放到内存里,所以内存在推理时会占掉相当大的一部分。拿我常用的7B量化模型来说,模型文件本身就接近5GB,再加上缓存和运行开销,整套跑下来内存占用在12GB左右。如果你的内存只有16GB,建议选更小的模型,或者使用更激进的量化格式。80GB内存的机器理论上可以吃下70B级别的模型,但这只是理论,毕竟你还要考虑到操作系统和其他进程的占用。

对于GPU,不要以为有8GB显存就能为所欲为。实际上,airllm在推理时会把当前需要计算的层加载到显存中,同时还要保存激活值的缓存,因此显存占用并不是固定不变的。一般来说,你分配给GPU的层越多,显存占用就越高,推理速度也越快。你需要根据自己的显存大小去调整一个合适的层数。我的建议是一开始保守一点,先把大部分层放CPU,跑通流程后再逐渐增加GPU层数,找到一个既不会爆显存、速度也能接受的点。

2.2 安装步骤与依赖坑

airllm的安装过程本身不复杂,但依赖环境比较容易出问题。我先说成功的路径,再讲我踩过的坑。

# 创建独立的虚拟环境,避免污染全局环境 python -m venv airllm_env source airllm_env/bin/activate # 安装基础依赖 pip install --upgrade pip pip install torch --index-url 根据你的CUDA版本选择对应的源 pip install airllm

这里有几个容易让人头大的地方:

第一,PyTorch和CUDA版本的匹配问题。如果你机器上的驱动是较新的版本,装PyTorch时最好去官网挑对应CUDA的wheel包,不要直接pip install torch,否则可能装成CPU版本的,导致后续airllm检测不到GPU,推理时全部落到CPU上,那速度会慢到怀疑人生。验证是否装对了,可以用这一句:

import torch print(torch.cuda.is_available())

输出True才算正常。

第二,airllm对某些旧版本PyTorch的兼容性不太好。我第一次在Python 3.8 + 老版本PyTorch上安装,结果编译扩展的时候直接报错,后来换到Python 3.10 + 较新的Torch就一切顺畅了。如果你也遇到"Failed to build airllm"这种报错,多半是编译器或者Python版本不匹配,试着升级一下Python版本,或者把pip版本更新到最新,再重新安装。

第三,如果你要跑量化模型(比如GPTQ或者AWQ格式),airllm还需要额外安装对应的库。具体依赖项我放到后面的量化章节去说。安装完以后,可以用一行代码测试导入是否正常:

from airllm import AirLLMLlama2 print("import ok")

如果没报错,环境就算准备好了。

3. 从加载模型到跑通推理的完整过程

3.1 Transformers接口兼容性

airllm在API设计上参考了Transformers库的习惯,有AirLLMLlama2、AirLLMLlama3等类,以及一个通用的AirLLM,它们都保持了类似from_pretrained的加载方式。对用惯了Transformers的人来说,上手成本很低。

我第一次跑通的时候,代码短得有点意外。核心就是三行:

from airllm import AirLLMLlama2 model = AirLLMLlama2.from_pretrained("你的本地模型路径") result = model.generate(input_ids, max_new_tokens=50)

没有显存分配的逻辑,没有手动切层的代码,一切都藏在内部。这种设计有个好处:你可以把airllm当成一个普通的模型对象,替换你原来基于Transformers的推理脚本,只需要改动很少的接口调用。但坏处是,如果你不了解它的内部行为,遇到性能问题时就会两眼一抹黑。所以我才特意去读了读它的源码和文档,把关键参数挖了出来。

另外,airllm支持的模型格式主要是Hugging Face格式的权重。如果你手里是其他格式的权重,需要先转换,或者直接去模型仓库里下载已经转换好的版本。我在实操中,直接读取本地目录,airllm会自动识别模型结构和量化配置,这一步还算省心。

3.2 推理参数配置详解

除了最基础的from_pretrained和generate,实际操作中还有很多关键参数值得关注。我把自己常用的配置整理成一个表格,方便你复制参考:

参数名示例值作用
max_new_tokens128限制生成长度,避免无限生成
temperature0.7控制随机性,太高容易胡言乱语
top_p0.9用核采样削减低概率词
repetition_penalty1.05避免重复文本
do_sampleTrue开启采样,否则就是贪心解码
output_hidden_statesFalse不需要隐藏层输出时保持关闭,节省内存

还有一个容易被忽略的参数是max_length或者max_position_embeddings,它决定了模型能处理的序列总长度。由于airllm会把输入序列的KV cache都留在内存里,当你输入很长文本时,内存占用会上升得很快。所以如果你只是做一些短问答,建议把输入长度尽量截断,而不是无脑填满。

关于generate背后的实现,airllm并不是每次生成一个token都重新从第一层开始算。它会维护一个已计算的层状态缓存,每次生成时只对新增token相关的层进行前向计算。这种机制有点类似KV cache,但它在层之间做了更细粒度的调度。这个细节你不需要手动管,但理解它有助于你解释为什么速度会比全显存模型慢:因为每生成一个token,GPU和CPU之间都要进行若干次数据传输,如果内存带宽不足,这部分时间就成了瓶颈。

3.3 实测一个生成任务

我拿一个本地部署好的对话模型,用airllm跑了一条真实的prompt,完整过程大概是这样的:

import torch from airllm import AirLLMLlama2 model = AirLLMLlama2.from_pretrained( "models/my-llama2-7b-chat", device="cuda", # 主设备指定为GPU gpu_layer_num=4, # 只放4层在GPU,其余全在CPU compression_mode="quantization", ) model = model.eval() messages = "请用一句话介绍什么是边缘计算。" inputs = tokenizer(messages, return_tensors="pt").to("cuda") with torch.no_grad(): output = model.generate( inputs.input_ids, max_new_tokens=64, temperature=0.7, top_p=0.9, ) print(tokenizer.decode(output[0], skip_special_tokens=True))

第一次跑的时候,模型加载花了差不多40秒,因为要从磁盘把权重读进内存,然后再把一小部分传到GPU。生成阶段,前几个token速度还可以,后面逐渐平稳,整体算下来每秒约7个token。这个数字对于阅读代码注释、生成短文本来说还算能用,但如果要我拿它去写一篇两千字的文章,等得就有些焦躁了。

这里有一个值得注意的点:gpu_layer_num这个参数不一定要手动指定。如果不传,airllm会根据显存大小自动估算一个数值,但自动估算的结果往往比较保守,实际速度不一定是最优的。我建议你从默认值开始,然后每次增加1到2层,反复跑几个短prompt,观察显存占用和生成速度,找到临界点。

4. 性能调优:速度、内存与精度的平衡术

4.1 压缩开关怎么开

airllm最吸引人的一点,是它在内存不足时还能开启量化压缩。我这里说的“压缩”不是普通的INT8整型量化,而是airllm提供的更高压缩比模式。它可以把每个权重用更少的bit存储,但推理时会先解压到原始精度再计算,从而避免精度损失过大。听起来很美好,但代价是推理速度变慢,因为解压过程本身消耗CPU时间。

我实测下来的感受是:如果你内存确实不够,或者想跑一个超大的模型,开启压缩模式是值得的;如果你的内存还比较宽裕,建议不要开,因为速度差距可能有20%到30%。压缩模式一般通过compression_mode参数开启,有quantization和faster等不同档位。faster档位会优先保证速度,但压缩率不如quantization档位。对于老CPU来说,压缩模式下的解压开销可能不可接受,这时候宁可把层多分一点给GPU,也别开压缩。

下面是两种模式的对比,基于我自己跑同一个模型的结果:

模式内存占用(模型权重部分)生成速度输出质量
关闭压缩5.2GB9.1 tokens/s无明显损失
压缩(quantization)3.4GB6.3 tokens/s偶见个别词重复
压缩(faster)3.8GB7.8 tokens/s与关闭压缩相近

如果你对输出质量要求很高,比如要做一些自动化生成内容的评估,建议关闭压缩模式。如果你只是自己玩玩对话,开faster档位完全能接受。

4.2 如何调整层分配比例

层分配是本方案的精髓所在。简单来说,gpu_layer_num指定了多少层Transformer层放在GPU上,其余的放在CPU内存里。直观地看,GPU层数越多,计算速度就越快,因为大部分计算都在GPU上完成,减少了PCIe传输。但GPU层数越多,显存占用也越高,一旦超过显存容量就会直接报错,甚至可能导致显卡驱动的崩溃。

我从4层开始尝试,每次增加2层,记录下显存和速度数据:

GPU层数显存占用平均生成速度是否稳定
43.1GB7.2 tokens/s稳定
64.3GB8.5 tokens/s稳定
85.5GB9.9 tokens/s稳定
106.8GB10.7 tokens/s接近临界
128.2GB11.4 tokens/s显存不足,运行失败

从表格可以看出,显存占用随GPU层数线性增长,速度也是逐步提升,但并不是线性的。层数少时,传输延迟是主要瓶颈,增加一层能带来明显收益;层数多了以后,GPU计算本身开始成为瓶颈,收益变小。我最后定格在8层,因为留了2GB显存余量给系统和其他进程,稳定性最好。

在调这个参数时,一个小技巧是把nvidia-smi开在旁边实时监控显存。如果显存占用已经超过总显存的80%,就不要再往上加了,因为有些系统在显存接近满载时会触发保守的分配策略,导致算子运行变慢,反而得不偿失。

4.3 CPU/GPU协同的工作流细节

airllm并不是简单地“GPU算两层,CPU算两层”,而是采用了一种类似流水线并行的方法。我把它在推理时的数据流概括为三步:

  1. 将当前token序列的输入向量送入第一层,此时该层权重必须在GPU显存中。
  2. 计算完一层后,将输出激活值通过内存传递到下一层。如果下一层在CPU上,就需要用CPU执行该层的矩阵乘法。
  3. CPU层计算完之后,再把结果送回GPU继续后续层的计算。

这个流程中最费时间的是步骤2和3中的内存拷贝。如果你的主板只支持PCIe 3.0,数据传输带宽会比较低,这会显著拖慢速度。我后来把某个PCIe 3.0平台换成了PCIe 4.0,同配置下生成速度提升了接近15%。所以如果你有条件,优先把设备插在靠近CPU的直连PCIe插槽上,别用那些共享带宽的扩展槽。

另外,在Linux下可以开启transparent huge pages,减少内存页映射开销。虽然提升幅度不算大,但积少成多。Windows环境的话,这个优化可能比较难,就不额外折腾了。

5. 踩坑记录与排查思路

5.1 显存爆掉怎么办

airllm虽然号称低显存,但并不是零显存。我在调参过程中,就因为盲目调大gpu_layer_num把显存干爆过。报错信息通常是"CUDA out of memory",然后整个进程崩溃。遇到这个情况,不要慌,按照顺序处理:

第一步,降低gpu_layer_num,比如直接从8降到4再试,先确认能跑通。第二步,检查系统里是否有其他程序占用显存,比如浏览器硬件加速、绘图软件等。把它们关掉能腾出不少空间。第三步,如果显存依然不够,就把compression_mode打开,让权重更紧凑一些。

如果你是做服务端部署,还可以考虑用多进程方式,每个进程只加载模型的一部分,但这已经超出airllm的默认能力范畴了。我个人的建议是:不要把显存压到极限,留出10%到15%余量,否则推理过程中一旦遇到长序列,KV cache激增时还是会崩。

5.2 输出质量变差的原因

另一个常见问题是,启用压缩模式后,模型的回答开始出现重复词语、逻辑不畅、甚至中文拼音混杂的乱码。原因可能是解压误差在层间累积。由于每一层都经过解压-计算-再压缩的过程,误差会逐层放大,尤其是深层的Transformer层对误差更敏感。

解决思路有三个:

  1. 尽量使用faster档位而不是quantization档位,因为faster档位的压缩损耗更小。
  2. 适度降低temperature,让模型更倾向于高概率词,减少乱码类输出。
  3. 如果模型特别大、压缩特别狠,不妨换一个CPU,或者将更多层放到GPU上,降低CPU侧的计算负担。

另外,要检查一下你的tokenizer和模型是否匹配。我自己遇到过的情况是,模型是Llama2的中文微调版,但用错了原版tokenizer词典,导致解码出来的文字时好时坏。这个问题跟airllm本身无关,但排查起来特别迷惑。

5.3 环境问题速查表

整理一份我遇到过的问题和对应解决方法:

问题现象可能原因解决办法
导入airllm时提示CUDA不可用PyTorch是CPU版本重新安装匹配GPU的PyTorch
模型加载后推理极慢GPU层数太少或数据走PCIe传输调大gpu_layer_num,确认插槽带宽
显存不足崩溃GPU层数太多降低gpu_layer_num或开启压缩
压缩模式乱码量化误差过大换faster档位,或关闭压缩
输入长文本后内存暴涨KV cache全部放内存截断输入,或改用时延更低的压缩方案
多线程/多进程冲突多个实例同时占显存加锁,或者每个进程显式指定不同GPU

还有一种贼隐蔽的坑:如果你在Linux下用root用户跑,有时候某些库会去读取/root/.cache/huggingface里的缓存权重,如果权限不对,模型可能加载到一半就报错。然后你换普通用户跑,又发现目录下没有权重,需要重新下载。这种问题最好统一用环境变量指定缓存目录,比如export HUGGINGFACE_HUB_CACHE=/path/to/cache,省得来回折腾。

6. 更多扩展:airllm的进阶玩法

6.1 与其他推理框架的思路差异

在接触airllm之前,我也试过一些其他推理加速方案,最大的感受是它们的优化逻辑不太一样。主流的推理框架往往聚焦于“如何在有限的显存里最大化吞吐量”,所以会做算子融合、KV cache管理、动态批处理这些优化,它们的目标是服务端高并发。airllm则是反过来的思路:不追求高吞吐,而是追求“显存不够也能跑”,它把重量从显存转移到了内存和CPU,用延迟换容量。

这也就决定了airllm的应用场景。如果你要做在线API服务,需要同时服务很多请求,那airllm不是最好的选择,因为它在并发场景下CPU会成为瓶颈,吞吐上不去。但如果你只想在自己的电脑上离线跑一个任务,或者在不能上云的内网环境里做一个轻量级推理Demo,airllm这类的方案反而成了最优解。我甚至在跑某些图像相关的模型时也用类似思路做了迁移,效果还不坏。

6.2 适合你的场景判断

到底要不要用airllm,拿一张图来总结可能不够直观,我直接说几个典型的适配判据:

  • 你有至少16GB内存,且模型权重超出显存很多倍。
  • 你不追求每秒几十个token的速度,能和ChatGPT时代龟速网络抗衡就行。
  • 你不想向外部API发送敏感数据,坚持本地推理。
  • 你想在一台普通的游戏电脑上体验大模型,而不是为了跑模型去专门买昂贵的专业卡。

满足以上任意三条,airllm基本都能帮上忙。一个典型的例子是运行包含私有数据的对话模型,数据不能出内网,但服务器只有8GB显存。以前只能用小模型凑合,现在借助分层推理,可以上更大的模型,效果自然好不少。

当然,这也不是万能钥匙。如果你的模型本身极小,比如1B以下,用传统推理方式完全够用,没必要引入额外复杂度。如果你的机器内存也很小,只有8GB,那同样不适合,因为分层后权重全压在内存里,内存不足比显存不足更麻烦。这时候老老实实租个云端实例,或者选用量化后的小模型,才是最务实的路。

我在实际使用中发现,airllm最大的价值不在于性能,而在于它把“本地大模型”这件事的门槛降下来了。以前我劝人本地跑大模型,对方总是一脸苦笑说“显卡太挫”,现在我可以回答“没关系,内存大点就行”。这种思路上的转变,比单纯堆硬件有意思得多。如果你手头恰好有一台配置不太差的旧电脑,又对本地大模型充满好奇,完全可以按这篇文章的路径试一遍。先跑通一个小模型,再慢慢调参去找性能平衡点,你会对整个推理过程多一层实实在在的理解,而不是只当一个调API的人。最后再分享一个小技巧:每次调参时都把显存占用和速度记录到一个文本文件里,看起来麻烦,但当你调了几十次之后回头看,你会庆幸这个习惯帮你省了大量重复实验的时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进