【Bug已解决】Qwen 3.6 awq can't load, always OOM error 解决方案
一、现象长什么样
用 vLLM 加载 Qwen 3.6 的 AWQ(4 位激活感知量化)版本时,无论怎么调,进程总是在「加载权重」阶段直接被 OOM(显存不足)杀掉,日志里常常只有一句:
torch.OutOfMemoryError: CUDA out of memory. Tried to allocate ...或者更笼统:
Qwen 3.6 awq can't load, always OOM error几个特征,帮你判断是不是同一个坑:
- OOM 发生在权重加载 / 模型构建阶段,而不是推理时——说明是「装不进显存」,不是「跑起来显存涨」。
- 不管你把
--gpu-memory-utilization调多低、把--max-model-len调多小,依然 OOM——说明问题不在 KV 缓存预算,而在权重本身的内存峰值。 - 换用同尺寸的bf16 原版Qwen 3.6 反而能加载(或只差一点),但 AWQ「理论上更省显存」却反而 OOM,这很反直觉。
- 报错里常看到「Tried to allocate」的数额异常大,远超单卡剩余显存,说明加载过程中存在一次性的大块临时张量。
二、背景
AWQ 是「激活感知权重量化」,把权重压到 4 位。理论上 AWQ 模型权重比 bf16 小很多,应该更容易装进显存。但实践中,vLLM 加载 AWQ 存在几个隐性的显存峰值,正是这些峰值导致反复 OOM。
AWQ 权重的存储形式:AWQ 把权重存成qweight(4 位打包)+scales+qzeros+g_idx。加载时,框架需要:
- 先把
.safetensors里的量化张量读进 GPU/CPU; - 在「构建模型」时,某些实现会先把量化权重反量化为 fp16 临时副本,用来初始化
nn.Linear的weight属性,再在模型的load_weights里重新量化回去——这一瞬间的 fp16 副本,就是一次峰值; - 多卡 TP 下,每张卡还要持有「未切分前的完整权重切片」做 all-gather,再切分,又是一波峰值。
Qwen 3.6 的特殊点:它是大词表 + 大隐藏维的模型,甚至带 MoE(取决于具体子版本)。AWQ 对它的量化,如果group_size较小(如 128),scales/qzeros的辅助张量会显著膨胀;同时 AWQ 加载器在「先反量化再量化」的实现里,瞬时 fp16 峰值 ≈ 整个模型 bf16 大小——也就是说,AWQ 加载的峰值显存可能接近甚至超过 bf16 原版,完全抵消了量化的省显存优势**。
还有一层:vLLM 在「估算能否加载」时,有时只按「最终量化权重体积」估算,没把「加载期的反量化临时副本」算进去。于是它乐观地认为能装下,实际一加载就撞上峰值 OOM。这解释了为什么「调低 utilization 也没用」——因为利用率调低只会缩小 KV 缓存池,而 OOM 来自权重加载峰值,与 KV 缓存无关。
三、根因
根因一句话:vLLM 加载 Qwen 3.6 AWQ 时,存在一次性的「反量化临时 fp16 副本」+「TP 下的未切分权重切片」显存峰值,而这个峰值往往接近甚至超过 bf16 原版体积;加载器的显存预估却只按最终量化体积算,导致它乐观启动、实际撞峰值被 OOM。
具体成因:
- 反量化-再量化副本:加载器先
dequantize(qweight)出 fp16 全模型,喂给nn.Linear,再在load_weights里量化回去。这一瞬间的 fp16 副本就是峰值,AWQ 的省显存优势在此时荡然无存。 - TP 预聚合切片:多卡 TP 下,每张卡先持有完整权重的一份额外副本用于 all-gather/切分,峰值叠加。
- 辅助张量膨胀:小
group_size让scales/qzeros体积变大,尤其是大词表模型的lm_head量化后辅助张量很可观。 - 预估漏算峰值:
gpu_memory_utilization只控制 KV 缓存池,加载器没把「权重加载峰值」纳入能不能启动的判断,于是低 utilization 也救不了 OOM。 max_model_len间接影响:虽然 KV 不背 OOM 主责,但若max_model_len过大,KV 池 + 权重峰值同时吃显存,会加速撞墙。
核心矛盾:AWQ 的「持久显存占用」低,但「加载期瞬时峰值」高;而系统的启动判断只看持久占用,于是低估 → OOM。
四、最小可运行复现
下面用纯 Python 模拟「AWQ 加载峰值 ≈ bf16 全量」导致预估失准的情形:
# reproduce_awq_oom.py # 复现:AWQ 加载期反量化临时副本使峰值≈bf16,预估只看最终量化体积而 OOM MODEL_PARAMS = 36e9 # 360 亿参数 BYTES_FP16 = 2 BYTES_AWQ = 0.5 + 0.5 # qweight 0.5 + scales/zeros ~0.5, 约 1 字节/参数等效 def peak_and_final(use_dequant_roundtrip: bool): final = MODEL_PARAMS * BYTES_AWQ # 持久: AWQ 体积 peak = final if use_dequant_roundtrip: peak += MODEL_PARAMS * BYTES_FP16 # 反量化临时 fp16 副本 return final, peak def can_load(peak_bytes, gpu_bytes): return peak_bytes <= gpu_bytes if __name__ == "__main__": GPU = 40 * 1024**3 # 40GB 卡 final, peak = peak_and_final(use_dequant_roundtrip=True) print(f"AWQ 持久={final/1024**3:.1f}GB, 加载峰值={peak/1024**3:.1f}GB") # 加载器若只看 final 会误判可加载 print("按 final 预估可加载?", can_load(final, GPU)) # True (误判) print("按真实峰值可加载?", can_load(peak, GPU)) # False (实际 OOM)运行后能看到:按「最终量化体积」预估能装下,但实际峰值(含反量化副本)装不下 → OOM。复现了「调低 utilization 也没用」的现象。
五、解决方案(第一层:最小直接修复)
最小修复,按见效快慢排序:
招式 A——避开反量化回拷:如果加载器支持「直接把 qweight 挂到量化 Linear,不反量化临时副本」,务必开启(vLLM 的AWQLinearMethod本就支持原地加载,确认没被某个 wrapper 强制dequantize)。
招式 B——降低加载期峰值:单卡时把--tensor-parallel-size设为 1,避免 TP 预聚合副本;通过CUDA_VISIBLE_DEVICES限定用一张大显存卡。
招式 C——调小max_model_len:虽然 KV 不是主因,但减小它能腾出余量,让权重峰值 + 小 KV 池恰好塞下。
# fix_layer1_awq.py def suggest_awq_load(tp_size, max_model_len, gpu_gb): tips = [] if tp_size > 1: tips.append("单卡加载可避免 TP 预聚合峰值,建议 tp_size=1(或先用 1 卡加载再切分)") if max_model_len > 4096: tips.append(f"max_model_len={max_model_len} 偏大,先降到 2048/4096 释放余量") if gpu_gb < 48: tips.append(f"{gpu_gb}GB 卡对 36B AWQ 偏紧,优先考虑 4-bit + 单卡 + 小 context") return tips if __name__ == "__main__": for t in suggest_awq_load(tp_size=2, max_model_len=8192, gpu_gb=40): print("-", t)六、解决方案(第二层:结构性改进)
把「AWQ 加载显存预算」做成带峰值核算的规划器,预估时同时算持久占用与加载峰值,并据此给出能否启动的结论与回退策略:
# fix_layer2_planner.py from dataclasses import dataclass @dataclass class ModelProfile: params: float group_size: int is_moe: bool = False @property def awq_final_bytes(self): # qweight ~0.5B/param + scales/zeros 随 group_size 变化 scales_factor = 2.0 / self.group_size * 2 # 每个 group 的 scale/z 字节 return self.params * (0.5 + scales_factor) @property def load_peak_bytes(self): # 反量化临时 fp16 副本 + 持久 return self.awq_final_bytes + self.params * 2 def plan_awq_load(prof: ModelProfile, gpu_bytes: int, tp_size: int, max_model_len: int, kv_bytes: int) -> dict: peak = prof.load_peak_bytes / tp_size + kv_bytes # TP 分摊权重峰值 + KV final = prof.awq_final_bytes / tp_size + kv_bytes return { "load_peak_gb": peak / 1024**3, "final_gb": final / 1024**3, "fits": peak <= gpu_bytes, "recommend": ( "可加载" if peak <= gpu_bytes else "OOM 风险: 降低 max_model_len / 用单卡 / 或改用更激进量化(如 2-bit 或 GPTQ)" ), } if __name__ == "__main__": prof = ModelProfile(params=36e9, group_size=128, is_moe=True) res = plan_awq_load(prof, gpu_bytes=40 * 1024**3, tp_size=1, max_model_len=4096, kv_bytes=2 * 1024**3) print(res)这样:换模型/换卡/换量化参数时,规划器先算「真实峰值」判断能不能启动,而不是被「最终量化体积」误导。
七、解决方案(第三层:断言 / CI 守护)
把「AWQ 加载峰值预算」钉进断言和 CI,防止部署脚本误判:
# fix_layer3_guard.py # ---- pytest 用例,进 CI ---- def test_awq_peak_exceeds_final(): from fix_layer2_planner import ModelProfile p = ModelProfile(params=36e9, group_size=128) assert p.load_peak_bytes > p.awq_final_bytes, "AWQ 加载峰值应高于持久占用" def test_small_gpu_rejects_36b_awq(): from fix_layer2_planner import ModelProfile, plan_awq_load p = ModelProfile(params=36e9, group_size=128) r = plan_awq_load(p, gpu_bytes=24 * 1024**3, tp_size=1, max_model_len=4096, kv_bytes=2 * 1024**3) assert not r["fits"], "24GB 卡不应乐观认为能装下 36B AWQ 加载峰值" assert "OOM" in r["recommend"] def test_tp_spreads_peak(): from fix_layer2_planner import ModelProfile, plan_awq_load p = ModelProfile(params=36e9, group_size=128) single = plan_awq_load(p, 40 * 1024**3, 1, 4096, 2 * 1024**3)["load_peak_gb"] # 单卡峰值应高于多卡(多卡分摊),这里验证函数对 tp 的响应 assert single > 0再加启动断言:
def assert_awq_fits(prof: ModelProfile, gpu_bytes, tp_size, max_model_len, kv_bytes): r = plan_awq_load(prof, gpu_bytes, tp_size, max_model_len, kv_bytes) assert r["fits"], f"AWQ 加载将 OOM: {r}"八、排查清单
Qwen 3.6 AWQ 总是 OOM,按序查:
- 确认 OOM 在加载期还是推理期:加载期 OOM 才符合本文;推理期涨显存是另一类问题(看 max_num_seqs)。
- 看峰值是不是反量化副本:把加载日志里「Tried to allocate」数额和模型 bf16 体积比对,若接近则说明有反量化回拷,关掉它。
tp_size设为 1 先试:单卡加载避开 TP 预聚合峰值,能装下再考虑切分。- 调小
max_model_len:即使 KV 非主因,减小它能腾余量,先让权重峰值过去。 - 放大
group_size:128 改 64 会增大 scales 体积、改 256 会缩小;大 group 省辅助张量,但精度略降。 - 确认量化格式对得上:Qwen3.6 的 AWQ 是否真由当前 vLLM 版本支持;版本错配可能导致加载器走错路径(如当成 bf16 全量加载)。
- 监控峰值显存:用
nvidia-smi -l 1观察加载瞬间峰值,定位是否撞墙在某一层(通常是 lm_head / MoE 专家层)。 - 考虑 GPTQ / 更激进量化:AWQ 救不了就换 GPTQ,或对 lm_head 单独用更低位量化。
- 升级 vLLM:新版对 AWQ 原地加载支持更好,能消除反量化临时副本。
- 别只信 utilization:
gpu-memory-utilization只控 KV 池,救不了权重加载峰值;要从「峰值预算」层面解决。
九、小结
Qwen 3.6 AWQ 「总是 OOM」的反直觉现象,根子是AWQ 加载期存在「反量化临时 fp16 副本 + TP 预聚合切片」的显存峰值,往往接近 bf16 原版体积,而加载器只按「最终量化体积」乐观预估,导致低 utilization 也救不了、一加载就撞峰值被 OOM。修复三层:第一层避开反量化回拷、单卡加载、调小max_model_len;第二层做带峰值核算的plan_awq_load(),按真实峰值而非持久占用判断能否启动;第三层用 pytest 把「峰值 > 持久」「小卡拒载 36B AWQ」钉进 CI。核心认识——量化省的是「持久显存」,不是「加载峰值」;评估一个量化模型能不能装下,必须算加载期的瞬时峰值,而不是只看磁盘上的量化体积。