DAC'26 | SlideFormer 让单张 4090 全参微调 123B 模型

DAC’26 | SlideFormer 让单张 4090 全参微调 123B 模型

原文:An Efficient Heterogeneous Co-Design for Fine-Tuning on a Single GPU


1. 前言

你有没有算过一笔账:全参微调一个 8B 模型,到底需要多少显存?

答案可能比你想的夸张——混合精度训练下超过 128 GB,一张 24GB 的 RTX 4090 连零头都装不下。这也是为什么大家一提微调就默认 LoRA:不是 LoRA 有多好,而是全参微调根本跑不起来。但 LoRA 在很多 domain-critical 任务上打不过全参微调,这一点已经有不少工作(比如 “LoRA vs Full Fine-tuning: An Illusion of Equivalence”)实锤了。

今天想和大家聊聊 HKUST(GZ) 发表在 DAC’26 的这篇 SlideFormer:在单张 RTX 4090 上全参微调 123B+ 的模型,吞吐比 ZeRO-Offload / ZeRO-Infinity / ColossalAI 高 1.40~6.27 倍,CPU/GPU 内存还各省一半左右。代码已开源:github.com/RegiaYoung/SlideFormer

这篇论文最打动我的地方是它的出发点:GPU 显存涨得慢,但 CPU 内存涨得飞快。如下图,消费级 GPU 从 2020 年的 RTX 3090(24GB)到 2025 年的 RTX 5090 也就 32GB;而同期消费级平台的 DDR5 内存已经干到 256GB 了。这个”剪刀差”意味着:单卡微调本质上不是一个 GPU 问题,而是一个异构系统设计问题——GPU、CPU、RAM、NVMe 要当成一个整体来调度。

CPU 与 GPU 内存容量差距逐年拉大

2. 先算账:显存都去哪了

对一个 N 参数的模型做混合精度全参微调,内存开销大致是:

Mem = 2N (BF16 参数) + 2N (BF16 梯度) + 4N (FP32 master copy) + 8N (Adam momentum + variance) + 激活值

前面静态部分加起来就是 16N 字节——8B 模型就是 128GB,这还没算激活值(正比于 层数×hidden×seq_len×batch)。其中 12N 是优化器相关的(master copy + Adam 状态),是最大的一块,而且只在参数更新那一刻才用得上——这就是 offloading 的机会所在。

现有方案的问题在哪?

  • ZeRO-Offload / ZeRO-Infinity:为多卡分布式设计,单卡跑的时候那些多卡通信机制还挂在那里空转,额外内存和延迟白白浪费。更致命的是它们在 update 阶段是同步的:CPU 更新参数时 GPU 干等着。
  • ColossalAI(Gemini):chunk 粒度的动态内存管理,对单卡效率不友好,同样是同步 update。
  • LoHan(近期最接近的工作):用多进程做异步更新,IPC 开销不小;on-demand 内存分配容易碎片化;而且只支持 GPT-2,评估时还把 loss 换成 MSE 来回避 logits 显存问题——这在真实任务里是不可用的。

3. SlideFormer 的解法:三根支柱

SlideFormer 的整体架构如下图:GPU 上只保留一个滑动窗口大小的活跃层,CPU 常驻 FP32 master 参数和优化器状态,NVMe 作为可选的第三层,激活值可以通过 GPUDirect 直接在 GPU 和 NVMe 之间搬运,完全绕过 CPU。

SlideFormer 系统总览

3.1 Layer-Sliding:把 GPU 当滑动窗口

核心思想一句话:GPU 不需要装下整个模型,只需要装下”正在算的那一层”

反直觉的地方在于粒度选择。直觉上窗口开大点(一次放多层)应该更好?论文的答案是恰恰相反:层是串行计算的,多放几层在 GPU 上纯属占着显存不干活,这些显存拿去加大 batch size 收益大得多。而层(Layer)是 LLM 里最小的重复结构单元,用它做粒度天然规整——不像 ZeRO-Offload 或 LoHan 用 param-group,各种大小不一的组件需要复杂的手动配置。

真正的性能来源是backward 和参数更新的重叠。看下图这个对比:ZeRO-Offload 是”GPU 算完 backward → 梯度全部搬到 CPU → CPU 统一 update”,三个阶段串行,GPU 在 update 阶段完全 IDLE;SlideFormer 则是层粒度流水线——第 i 层 backward 一算完,梯度立刻异步 d2h,CPU 马上开始更新第 i 层参数,与此同时 GPU 继续算第 i-1 层的 backward,还顺手预取第 i-2 层的参数。仅这一项就带来约 40% 的性能提升。

ZeRO-Offload 与 SlideFormer 的 backward 时间线对比

更细的重叠时序如下图:GPU compute、d2h stream、h2d stream、CPU compute 四条轨道同时在跑,梯度下传、激活值/参数上传、CPU 更新全部藏在 backward 计算的影子里。

GPU-CPU 流水线中的计算-通信重叠

什么时候能完全藏住? 论文给了一个很实用的量化指标——hiding factor:

η = T_bwd / (T_d2h + T_update)

η ≥ 1 表示 offloading 零开销。看实测数据(Qwen2.5-14B):

backward 阶段各环节耗时与 GPU 利用率

规律很清晰:batch size 是决定性变量。4090 上 batch=16 时 η 只有 0.66(T_bwd=170ms 藏不住 22+175ms 的传输和更新),GPU 利用率 93.1%;batch 拉到 64,T_bwd 涨到 660ms 而 T_update 几乎不变(195ms),η=3.00,GPU 利用率 98.4%。这也解释了为什么”支持更大 batch”本身就是个性能 feature——batch 不够大,流水线就填不满。

还有个有意思的观察:达到完全重叠所需的临界 batch size 几乎不随模型大小变化(如下图,从 3B 到 72B 都稳定在 4090≈15、A100≈21 附近)。原因是 T_bwd、T_d2h、T_update 都正比于层的大小,比值自然就和层大小无关了。

不同模型大小下的临界 batch size

顺带一提工程实现:SlideFormer 用的是线程 + CUDA Stream 的轻量组合(一个线程管 h2d/d2h 传输,一个线程跑 Layer-Adam 更新),而不是 LoHan 的多进程方案——省掉了 IPC 开销。这个选择看似平平无奇,但对流水线这种延迟敏感的场景,进程间通信的开销真的会咬人。

3.2 内存管理:预分配 + 共享 buffer

  • GPU 侧:一个预分配的 cache unit 队列,每个 unit 刚好装下一层的参数和梯度,层滑进来算完就把 unit 让给下一层。固定 footprint、零碎片、零重分配——对比 LoHan/StrongHold 的 on-demand 分配,这是典型的”空间确定性换运行时开销”。
  • CPU 侧:FP32 master 参数存成 flattened pinned tensor 方便 h2d;梯度用一个全层共享的 pinned buffer 接收——梯度的 CPU 占用直接从 2N 降到 2N/num_layers;FP32→BF16 的类型转换也在 CPU 侧用共享 buffer 完成,GPU 上不再需要为转换付出额外显存和传输。这套布局让 CPU 峰值内存降了 25% 以上。
  • 激活值:Sliding checkpointing——每层 forward 完激活值异步下放到 CPU/NVMe,backward 前再预取回来重算,显存里永远只有一个小窗口的激活值。

3.3 I/O 与 kernel:两个容易被忽视的坑

GPUDirect Storage(GDS):模型超过 CPU 内存怎么办?上 NVMe。SlideFormer 是第一个把 GDS 用于微调 offload 的工作——激活值直接在 GPU 和 NVMe 之间走 DMA,绕过 CPU bounce buffer,把 CPU 和 PCIe 带宽留给流水线里更紧要的参数传输和更新。

有个设计决策值得细品:参数不下放 NVMe。参数的 h2d 传输在重叠的关键路径上,换到 NVMe 速度立刻从 PCIe 掉到 SSD 档位,整条流水线都得跟着降速;而且强行加第三层会让数据路径变复杂。每种数据只在两层之间移动——这种克制反而是好的系统设计。

Fused LinearCrossEntropy(LCE)kernel:这是我觉得最”实诚”的一个优化点。现在的模型词表越来越大(Llama-3.1 词表 128K),输出层那个 logits tensor(B×S×V)的显存可以超过前面所有激活值的总和——这个瓶颈很多系统工作直接装看不见(LoHan 干脆换 MSE loss 绕过去)。SlideFormer 把 projection 和 loss 融合成一个 Triton kernel,分 chunk 算梯度,从头到尾不物化完整 logits。效果如下图:batch=64 时 torch 标准做法要近 50GB,LCE 不到 6GB,输出层显存降 88.9%,速度还基本不变。

torch 标准方法与 LCE 的显存和耗时对比

除了 LCE,RoPE、RMSNorm、SwiGLU 也都换成了优化的 Triton kernel,加上 FlashAttention,intra-layer 的计算效率也补齐了。

4. 实验:数字说话

实验平台两套:高端 PC(RTX 4090 24GB 或 AMD RX 7900XT 20GB + Ryzen 9 9950X + 256GB DDR5)和服务器(A100 80GB + 1024GB DDR4)。对比对象是 ZeRO-Offload、ZeRO-Infinity、ColossalAI、LoHan。

吞吐 vs batch size(Llama-3.1-8B on 4090):如下图,SlideFormer 全 batch 领先,相对三个 baseline 分别是 1.39×、2.82×、6.34×。注意 batch=64 时其他框架全部画 ×(跑不动了),只有 SlideFormer 还在继续涨——回到前面说的,大 batch 才能填满流水线,而能开大 batch 本身就是内存管理带来的红利。下半张图还有个扎眼的细节:ZeRO-Offload 的 CPU 内存占用(黄色)比 SlideFormer 高出一大截,这就是”分布式架构在单卡空转”的代价。

Llama-3.1-8B 各框架吞吐与 CPU 内存对比

吞吐 vs 模型大小(Qwen2.5 3B~72B on 4090):ZeRO 系列 14B 就跑不动了,ColossalAI 也撑不到 72B,SlideFormer 一路跑到 72B+,而且吞吐始终贴着”不做 offload 的理论峰值”(红虚线)的 90~95%。也就是说 offloading 的开销几乎被完全藏掉了。

Qwen2.5 各尺寸模型的吞吐与内存对比

GPU 显存:同样是 Llama-3.1-8B,SlideFormer 的显存占用比 ZeRO-Offload 低 50% 以上,而且随 batch 增长的斜率也最缓——预分配 cache 队列 + LCE kernel 的合力。

各框架 GPU 显存随 batch size 的变化

跨平台:AMD RX 7900XT 和 A100 上同样保持 95%+ 峰值性能(如下图)。能同时把 N 卡和 A 卡都伺候好的 offloading 系统真不多,这说明设计本身没有绑死在 CUDA 特有机制上。

AMD 和 NVIDIA 平台上的微调吞吐

NVMe offloading 的取舍:如下图,开满所有 offload 选项能把 CPU 内存再降 60~80%,代价是吞吐掉 30~50%;I/O 带宽是瓶颈,所以性能随 SSD 数量近线性提升。论文还给了个实用结论:小模型优先 offload 优化器状态,激活值别动(激活值 offload 会同时拖累 forward 和 backward);只有最大的模型或 CPU 内存实在紧张时才 offload 激活值

不同 NVMe 数量与 offload 策略的性能对比

能跑多大的模型:这张图是全文的”结论图”。横轴 CPU 内存,纵轴最大可训练模型。ZeRO-Offload 上限 8B,ColossalAI 上限 32B,而 SlideFormer 开满 NVMe offload 后在 256GB 内存的 PC 上能微调 90B,服务器上超过 123B(Mistral-Large 级别)。瓶颈成功从 GPU 显存转移到了 CPU 内存——而后者便宜得多。

各框架最大可训练模型大小对比

和 LoHan 正面刚:GPT2-13B(LoHan 只支持 GPT-2,只能迁就它)上用标准 loss 对比,SlideFormer 吞吐更高、GPU 显存不到对方一半、CPU 内存省 30%。

SlideFormer 与 LoHan 在 GPT2-13B 上的对比

5. 我的一些 take

  1. 这篇论文的”新”不在单点技术,而在 co-design 的完整度。异步 offload、层粒度流水线、共享 buffer、fused kernel,每一样单拎出来都有前人的影子(Stronghold、LoHan、Liger Kernel),但把它们在单卡场景下捏成一个自洽的系统,并且每个设计决策都有量化依据(η 因子、临界 batch size、参数为什么不下 NVMe),这才是系统论文该有的样子。

  2. η = T_bwd / (T_d2h + T_update) 这个指标很值得偷学。做任何 overlap 类优化,先把各环节耗时 profile 出来算个比值,就知道还有多少油水、瓶颈在哪一侧。比”我们做了 overlap,快了 X 倍”这种黑盒叙事有营养得多。

  3. LCE 那个点提醒我们:显存瓶颈会迁移。前几年大家都在卷激活值 checkpoint、参数 offload,词表膨胀之后 logits 反而成了大头。做系统优化不能只盯着经典瓶颈,要定期重新 profile——瓶颈永远在你不看的地方长大。

  4. 对个人研究者来说这是实打实的福音:一台 128GB 内存的 PC + 一张 4080 就能全参微调 Llama-3.1-8B,吞吐几乎无损。全参微调的门槛第一次降到了”高配游戏 PC”的水位。当然也要实事求是:123B 那个数字是开满 NVMe offload 跑出来的,吞吐会打折扣,别指望拿它日常炼丹;但”能跑”和”不能跑”是质的区别。

欢迎评论区交流,如果有跑过 ZeRO-Offload / LoHan 踩坑经历的朋友也欢迎分享。


扯一句题外话。SlideFormer 解决的是”全参微调跑不起来”的问题,而我自己更多在琢磨另一条路:能不能通过剪枝、量化这类压缩手段,让模型本身变小,从源头缓解内存压力?两条路角度不同但目标一致。我们把这个方向的积累整理成了《动手学 AutoML:从 NAS 到大语言模型优化实战》,里面有 LLM 压缩(剪枝/量化/模型融合)的原理章节和后训练剪枝的完整实战,对”穷人玩大模型”这个课题感兴趣的朋友可以翻翻。

动手学AutoML书籍封面

Flag Counter