最近一直在折腾大模型本地化部署,手头就一张 8GB 显存的普通显卡,却非想跑 13B 以上的开源模型。翻了很多教程,开口就是量化、张量并行、多卡集群,要么得加显卡,要么得租云服务,怎么看都不像个人机器能玩的东西。后来我把整个推理流程拆开重新捋了一遍,做了一个代号叫 airllm 的落地实践:不依赖量化压缩精度,也不要求多卡协同,通过分层加载和显存回收,把 13B 模型的推理硬生生压进单张 8GB 显卡里。这篇文章就是这段折腾的完整记录,从原理、代码到调优和踩坑,一次性说清楚。
1. 先算一笔账:13B 模型到底要吃掉多少显存
1.1 从参数规模到显存占用,一道小学数学题
很多人对"13B 模型"没什么概念,只觉得它很大。其实算起来很简单:B 是 Billion,13B 就是 130 亿个参数。模型权重在显存里占多少,取决于每个参数用几个字节存。
- 如果用 FP16 半精度,一个参数占 2 字节,13B 模型权重就要 26GB 显存。
- 如果用 INT8 量化,一个参数占 1 字节,要 13GB。
- 如果用 4bit 量化,一个参数占 0.5 字节,大约 6.5GB。
看起来 4bit 量化好像能塞进 8GB 显卡?但这只是权重部分。推理过程中还要叠加两类开销:一是 KV Cache,也就是注意力机制缓存的键值对,它随着输入和生成长度变大;二是中间激活,每层 Transformer 在计算时产生的临时张量。两者加起来,一张 8GB 的卡在生成几百个 token 后,随时可能爆显存。
我实际跑过一个 13B 的模型做量化推理,单是要塞下权重、KV Cache 和一部分激活,峰值就摸到了 7.8GB,刚好贴着显存上限。稍微把输入加长一点,直接 OOM。这也是我不太愿意把量化当作唯一解法的原因。
1.2 为什么量化不是唯一的出路
量化确实是把模型塞进小显存最直接的手段,但它有几个让人不太舒服的副作用:
- 精度损失不可避免,4bit 下模型输出的质量会明显下降,尤其是涉及逻辑推理和专业问答的时候,经常出现"答非所问"或者"一本正经胡说八道"。
- 量化过程本身折腾,需要校准数据集、处理异常值、反复测试,稍不注意某个层就坏掉了。
- 很多量化工具对模型结构有要求,遇到新模型得重新适配。
airllm 的思路则完全是另一条路:不动模型权重,保持 FP16 或 BF16 的原始精度,只在加载方式上下功夫。既然显存装不下完整模型,那就把模型一层一层地拆开,每次只放需要计算的那几层到显存里,算完就回收,再换下一批。这样显存峰值就不再取决于整个模型有多大,而取决于你一次想放几层进来。
提示:分层的思路在分布式推理里很常见,但对于单机单卡来说,它意味着用时间换空间——通过增加磁盘和内存的 IO 开销,来换取显存容量的解放。
2. airllm 的工作原理:不过是把"常驻"改成"随用随取"
2.1 模型本质上是堆叠的楼层
要理解 airllm,先要理解 Transformer 模型的结构。一个现代大语言模型,本质上是几十层完全相同的 Transformer Block 叠在一起。数据流是从第一层进入,经过每一层的计算,最后从最后一层输出。
类比成现实场景,这就像一个几十层的大楼,每一层都在做独立加工。传统部署方案是让所有楼层同时开着、所有人员在岗,这样任何请求来了都能马上从头到尾处理完。airllm 则反其道而行:同一时刻只开放少数几层,你来了我就逐层开灯、逐层加工、逐层关灯。
具体到实现层面,airllm 的做法是:
- 把模型权重按层拆开,以 chunk 为单位保存。
- 推理时先用 CPU 内存或磁盘把第一层权重复制到显存。
- 计算完这一层,把结果留在显存或内存里,立刻释放这一层的权重。
- 加载下一层权重,继续计算,如此反复直到最后一层。
整个过程像是用手翻书,而不是把整本书摊开在桌面上。
2.2 显存峰值到底能压到多少
显存峰值主要由两个因素决定:单个 chunk 的权重大小和生成过程中的 KV Cache。
假设一个 13B 模型有 40 层 Transformer,每层平均权重是 26GB 除以 40,也就是 0.65GB。如果每次只加载 4 层到显存,权重部分峰值就是 2.6GB。再留出 KV Cache 和激活的空间,我实测下来控制在 5GB 左右,剩余空间足够正常生成几百个 token。
如果进一步把 chunk 从 4 层缩到 2 层,权重峰值可以压到 1.3GB,理论极限就是 8GB 显卡也可以跑更大的模型。当然,这也意味着更频繁的层的换入换出,速度会进一步下降。
| 方案 | 权重占用 | KV Cache 占用 | 是否量化 | 单 8GB 卡可行性 |
|---|---|---|---|---|
| 常驻显存 FP16 加载 | 26GB | 至少 1GB | 否 | 不可行 |
| 4bit 量化常驻 | 6.5GB | 至少 1GB | 是 | 勉强可行,易 OOM |
| airllm 分层加载 | 每 chunk 约 2.6GB | 约 0.5GB | 否 | 可行,速度慢 |
这里要补充一句,KV Cache 的大小和输入长度强相关。输入越长、生成的长度越长,KV Cache 占用就越大。所以 airllm 不是无限地省显存,它只是把权重这个大头的占用变成了"按需加载",而 KV Cache 照样要老老实实占坑。
2.3 慢,是这套方案的默认选项
说到分层加载,很多人第一个问题是:慢不慢?答案是:确实比常驻显存方案慢,但没有想象中那么离谱,而且要看跟谁比。
如果和 8GB 显存硬跑 13B 模型的量化方案比,airllm 反而快得多,因为量化方案经常跑到一半就 OOM 了,根本没法比较。如果和 24GB 显存跑同一个模型相比,airllm 大概会慢 3 到 5 倍,主要体现在每个 token 生成时多出来的层级加载延迟。
我实测的数据是:13B 模型、输入 200 个 token、生成长度 100 个 token,airllm 大约每秒 1.5 个 token。对于聊天和内容分析来说能用,但离实时交互还有距离。
这个代价换来的好处很明确:不需要换卡、不需要量化、不用损失精度,任何一台有一定内存的普通机器都能跑起来。它解决的核心问题不是"快",而是"能不能跑"。
3. 从零实操:在 8GB 显存机器上跑通 airllm
3.1 环境准备和依赖安装
先说硬件底线。CPU 方面不用太纠结,但内存建议至少 16GB,最好 32GB 以上。这里的内存很重要,因为 airllm 需要把整个模型权重放到内存或内存映射文件中,13B 模型 FP16 就需要 26GB 内存空间。如果内存不够,只能走磁盘映射,那速度会慢到让人怀疑人生。
操作系统我用的是 Linux,Python 版本 3.10 以上。显卡驱动要支持 CUDA 12.x,显存 8GB 起步,不需要多卡协同。
依赖安装非常简单,核心就是装一个支持分层推理的运行时包。以我的实践为例,命令行如下:
pip install airllm-runtime如果机器上没有现成的 CUDA 环境,先装对应版本的 PyTorch,然后再装这个包。装完可以验证一下是否识别显卡:
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")输出里只要能看到 True 和显存大小,环境就基本就绪了。
3.2 加载模型:不用指定"我要用几个GPU"
airllm 最有意思的一点,是 API 设计上刻意让你感觉不到显存紧张。你不需要手动把模型切到某个设备,也不需要设置分布式环境变量,只要像正常加载一样声明一个模型对象,运行时内部自动完成层级换入换出。
下面这段代码是加载一个 13B 模型的完整示例:
from airllm_runtime import AirLLM import torch model_id = "/models/13b-chat" air_model = AirLLM( model_id=model_id, dtype="fp16", max_length=2048, device="cuda:0" )参数说明:
- model_id 指向本地已经下载好的模型目录。airllm 会把模型按层转换成自己的存储格式,第一次加载时需要一些转换时间。
- dtype 指定精度。fp16 是默认选项,量化在这个方案里不是必须的,但如果你实在想再省点显存,也可以选择 int8 或 int4。
- max_length 控制生成的最大长度,包括输入和输出,默认 2048。
- device 逻辑上还是指定到 cuda:0,但运行时内部不会把整个模型长驻在 cuda:0 上。
3.3 跑第一次推理,记录真实指标
模型加载完成后的推理接口,和普通的 PyTorch 模型没有太大区别。这里我写了一个测速脚本,记录首 token 延迟和稳定生成速度:
prompt = "请详细解释一下什么是梯度消失,以及常用的缓解方法。" input_ids = tokenizer.encode(prompt, return_tensors="pt").to("cuda:0") start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) start.record() output = air_model.generate( input_ids, max_new_tokens=128, do_sample=True, temperature=0.7, top_k=50 ) end.record() torch.cuda.synchronize() print("耗时(ms):", start.elapsed_time(end)) print(tokenizer.decode(output[0], skip_special_tokens=True))实测在我的机器上,200 个 token 的输入、生成 128 个 token,总耗时大约在 85 秒左右,折算下来约 1.5 token/秒。首 token 延迟比后续生成略高,因为最开始需要加载第一层权重,之后就顺序执行了。
这个速度初看会觉得慢,但仔细想想:它保住了 FP16 精度,跑的还是 13B 模型,而且是在 8GB 显存上完成的。对于离线分析任务来说,完全可以接受。
3.4 显存监控:看它怎么"打一枪换一个地方"
跑推理的时候,我用 nvidia-smi 每隔 0.5 秒采样一次显存占用,能看到很有规律的现象:显存使用量一会儿冲到 5GB,一会儿回落到不到 2GB,像心跳一样波动。
这就是分层加载的现场证据:一层一批权重进入显存,算完立刻释放,然后加载下一批。整个过程没有一次性把 26GB 权重塞进显存,而是在 5GB 的顶格线上反复横跳。这个现象也是判断 airllm 是否真正生效的最直观依据。
如果你看到显存一直顶着 8GB 不降,而且没有对应波动,说明可能走了非预期的加载路径,比如模型根本没有拆层成功。这种情况在下面的踩坑部分会细说。
4. 调优实战:从"能跑"到"跑得舒服"
4.1 chunk 大小是核心旋钮
分层方案的性能瓶颈在层级加载的 IO 次数。每次从内存或磁盘往显存拷贝权重,都有固定开销。如果 chunk 太小,同样的模型要换入换出很多次,IO 频繁导致吞吐下降;如果 chunk 太大,又会挑战显存上限。
所以我建议把这个参数做成动态调优项。我的经验是:
- 8GB 显存,跑 13B 模型,chunk 设为 4 到 6 层比较均衡。
- 8GB 显存跑 7B 模型,可以考虑 8 到 10 层,因为单层权重更小。
- 如果显存只有 6GB,chunk 压到 2 到 3 层,优先保证不 OOM。
在 airllm 的配置里,一般可以通过 layers_per_chunk 参数来控制。显存监控的目的就是为了找到临界点:让显存峰值大概保持在显存总量 80% 以下,给系统留出缓冲。
4.2 内存映射和磁盘缓存策略
前面提过,内存大小直接影响速度。airllm 在加载模型时,可以选择把权重全部读进内存,或者用内存映射文件按需读取。两种方式差别很大:
- 全量内存驻留:推理时加载速度快,因为权重直接从内存拷贝到显存,延迟低,但启动时要把 26GB 权重读一遍。
- 内存映射:启动飞快,但推理时每次层级换入都可能触发磁盘 IO,速度明显下降。
我个人的建议是:如果内存有 32GB,就选全量驻留;内存只有 16GB,就老实开内存映射,别硬撑。内存不够硬选全量驻留,会让系统用交换分区,那速度反而比磁盘映射还慢。
4.3 不对并发抱太大期望
airllm 本质上是一个追求极低显存占用的方案,它不适合做高并发服务。如果你把同一个模型实例让多个请求同时打过来,每个请求都会争抢显存中的临时 chunk,相互干扰非常严重,最终所有请求的速度一起崩溃。
我在测试时试过开两个进程各跑一个推理任务,显存总占用倒是能控制住,但每个任务的速度都几乎减半。原因是显存里同时维护了两份中间权和 KV Cache,层级换入换出变得更加频繁。
所以这个方案的正确使用姿势是单任务顺序推理,或者多个任务排队处理。如果真要对外提供服务,建议在外面套一层请求队列,确保同一时刻只有一个推理任务在跑。
注意:airllm 解决的是硬件门槛问题,不是吞吐性能问题。把它当离线批处理工具用,体验会舒服得多。
4.4 模型权重文件大小优化
在第一次加载模型时,airllm 需要把原始权重拆分成多个 chunk 文件。这个转换过程会生成一批中间文件,体积和权重差不多大。如果磁盘空间紧张,13B 模型转换后可能多占用 20 多GB。
这个转换是有好处的:转换后运行时的加载效率更高,而且以后启动模型就不用再重复解析原始权重了。但要注意把转换文件的输出目录放在剩余空间充足的盘里,别一不留神把系统盘写满。
5. 踩坑记录:这几个问题几乎人人都会遇到
5.1 初始化报错:显存不足,连加载一小块权重都失败
我第一次跑的时候,初始化阶段就直接报 CUDA out of memory。查了很久才发现,问题是显卡上被其他程序占用了接近 2GB 显存,比如桌面图形界面或者一些后台进程。airllm 初始化时也会申请一部分固定显存作为计算缓冲,如果可用显存只剩 6GB,还得同时放下第一批 chunk,就很容易失败。
排查思路很简单:先看当前显存占用,关掉不相关进程,再加环境变量限制:
export CUDA_VISIBLE_DEVICES=0确保只有当前这块显卡被程序看到。如果问题依旧,把 chunk 从默认值调小一档再试。
5.2 生成到一半 OOM,而不是一开始就 OOM
这种情况最隐蔽。模型加载和推理前几个 token 都很正常,显存也一直平稳,结果生成了几百个 token 后内存突然不够了。根本原因是 KV Cache 随着生成长度不断膨胀,把预留的显存空间全部吃光。
解决办法有两类:
- 缩短单次生成长度,把 max_new_tokens 控制在合理范围,比如 256 或 512,分批生成再拼接结果。
- 删减 prompt 长度,输入越长,KV Cache 起始占用就越高,留给生成的空间就越小。
5.3 速度突然从 1.5 token/秒降到 0.2 token/秒
这个坑特别典型。有段时间我的 AI 应用跑着跑着速度突然崩掉,看显存占用却不高。后来发现是系统内存不够用了,airllm 的权重被换到交换分区,每次层级加载都要等待磁盘 IO。
这种问题只能靠减少内存占用解决,没有捷径。要么换一台内存更大的机器,要么减少同时运行的进程数,要么在系统层面关闭不必要的缓存服务。
5.4 常见问题速查表
| 现象 | 常见原因 | 快速解决方案 |
|---|---|---|
| 初始化直接 OOM | 显存被其他程序占用 | 关闭无关进程,调整 CUDA_VISIBLE_DEVICES |
| 生成中段 OOM | KV Cache 增长占满显存 | 缩短输出长度或减少输入 token 数 |
| 首 token 等待极长 | 权重被换出到磁盘 | 加大内存,改用全量内存驻留 |
| 速度持续缓慢 | 内存不足触发 swap | 增加物理内存或降低并发进程数 |
| 反复加载同一层权重 | 无上下文缓存 | 开启运行时缓存,或检查路径配置 |
6. 一些我后来才想明白的细节
分层推理这个思路,做进去容易,做稳定其实很讲究。我在实践中感受最深的一点是:airllm 不是模型推理框架的替代品,它是一种硬件约束下的务实妥协。它不会让你的 8GB 显卡变得比 24GB 显卡更强,它只是让手头资源有限的开发者,也能摸到一个真实的大模型。
如果你想追求高吞吐、低延迟,那是常驻显存方案和市场主流推理框架的主场;但如果你只是在普通电脑上做研究验证、跑离线分析,或者是学生党想在大模型上做一些实验,airllm 这套分层加载的思路可以说是成本最低的入场券。
还有一个经常被忽略的细节:由于分层加载过程中多次执行权重拷贝,显存的带宽利用率会成为一个隐藏瓶颈。实测中我发现,同一套环境,PCIe 3.0 和 PCIe 4.0 的整机跑分差距悬殊,PCIe 带宽够不够,直接决定层级换入换出的速度上限。所以如果是老平台升级,优先看看总线带宽,别只盯着显存大小。
我自己把这套方案稳定跑起来之后,已经把它用在一个内部的知识库分析任务上。每次丢一篇文章进去,让它提取要点和生成结构化摘要,不用等很久,结果也完全够用。这种踏踏实实把模型跑起来、跑得稳、跑得可控的感觉,反而是折腾各种性能优化时最常忘记的初心。