【Bug已解决】[Bug]: vllm-0.22.0 fail to run “Qwen/Qwen3.5-9B“ in offline `LLM` mode 解决方案 【Bug已解决】[Bug] vllm-0.22.0 fail to run Qwen/Qwen3.5-9B in offlineLLMmode 解决方案一、现象长什么样用 vLLM 0.22.0 的离线LLM模式直接from vllm import LLM不走vllm serve服务器加载Qwen/Qwen3.5-9B在构造LLM对象时就炸ValueError: Cannot find a suitable model class for Qwen3.5ForCausalLM (offline mode)或者更晚一点在第一次llm.generate时RuntimeError: shape mismatch: attention head_dim128 but q_proj out_features5760 mismatch还有一类是配置相关KeyError: head_dim几个特征同一个模型用vllm serve Qwen/Qwen3.5-9B在线服务器模式能正常起。只有离线LLM模式炸报错在LLM构造或首次 generate。换更老的 vLLM如 0.20.x或更新的nightly有的能过唯独 0.22.0 离线模式踩雷。报错常指向「模型类找不到」或「某个 config 字段缺失 / 形状对不上」。本质Qwen3.5-9B 用了一个较新的 HF config 字段比如head_dim、partial_rotary_factor或新的rope_type而 vLLM 0.22.0 的离线LLM初始化路径和在线服务器路径对 config 的「归一化处理」不一致——离线路径少做了一道工序导致该字段没被正确注入模型进而构造/前向失败。二、背景vLLM 加载一个 HF 模型核心是把config.json读进来映射成内部的ModelConfig再根据architectures找到对应的VllmModel类最后按 config 实例化权重。这条链路有两条入口在线路径vllm serve/EngineArgs走完整的EngineArgs→ModelConfig解析其中包含一个hf_config的后处理/归一化步骤把各种 HF 版本的差异字段统一成 vLLM 内部格式。离线路径LLM类为了「轻量」它的初始化更短部分版本里漏调了那步归一化或者归一化的触发条件不同于是hf_config里 Qwen3.5 需要的新字段没被转换成 vLLM 内部字段。Qwen3.5-9B 恰恰是个「新模型」它在config.json里用head_dimGQA 下每个头的维度可能和hidden_size // num_heads不等或partial_rotary_factor或rope_scaling.rope_typeqk_rope等新字段。在线路径会把它们归一化好离线路径没做于是要么ModelConfig找不到匹配的模型类因为归一化后才注册了Qwen3.5ForCausalLM→Cannot find a suitable model class要么字段缺失导致前向形状对不上head_dim用了老默认值 128但模型实际是 256→ shape mismatch。一句话离线LLM路径比在线路径少做了一次 config 归一化Qwen3.5 的新字段因此没被正确处理。三、根因根因是离线LLM的ModelConfig构建没有复用在线路径的 HF config 归一化逻辑导致 Qwen3.5 新字段缺失/错值三层第一层主因离线路径跳过 config 后处理。LLM.__init__里构造ModelConfig时只调了hf_config AutoConfig.from_pretrained(...)没有调在线路径里那句model_config normalize_hf_config(hf_config, model_name)。于是head_dim/partial_rotary_factor等新字段停留在「HF 原始形态」vLLM 内部读不到。第二层模型类注册依赖归一化结果。vLLM 的模型类注册表是按「归一化后的 architectures 名」匹配的。Qwen3.5 的architectures在归一化前可能写成Qwen3_5ForCausalLM带下划线变体归一化后才统一成注册表里那个Qwen3.5ForCausalLM。离线路径没归一化注册表查无此类 →Cannot find a suitable model class。第三层缺失字段用了危险的默认值。就算模型类勉强匹配上代码里读head_dim时用了getattr(hf_config, head_dim, hidden_size // num_heads)这种「老默认值」。Qwen3.5 的真实head_dim不等于这个默认值于是q_proj输出维度对不上 → shape mismatch。这个默认值在「老模型」上没问题在新模型上就是炸弹。一句话离线LLM漏做 config 归一化 → 模型类查不到 新字段用错默认值 → 构造/前向失败。四、最小可运行复现下面用纯 Python 模拟「在线/离线两条路径对 config 的归一化不一致导致模型类查不到 / 字段缺失」的控制流不需要 GPUfrom dataclasses import dataclass, field # 注册表只认「归一化后的名字」 REGISTRY {Qwen3.5ForCausalLM: vllm_impl} def online_normalize(raw_cfg: dict) - dict: 在线路径把 HF 原始 config 归一化。 cfg dict(raw_cfg) # 把带下划线变体统一成点号 if cfg.get(architectures) [Qwen3_5ForCausalLM]: cfg[architectures] [Qwen3.5ForCausalLM] # 注入归一化后的 head_dim if head_dim not in cfg: cfg[head_dim] cfg.get(_real_head_dim, 256) return cfg def offline_init_buggy(raw_cfg: dict) - dict: 离线路径有 bug直接用 raw config不归一化。 return dict(raw_cfg) def resolve_model_class(cfg: dict): arch cfg[architectures][0] return REGISTRY.get(arch) def main(): raw { architectures: [Qwen3_5ForCausalLM], _real_head_dim: 256, hidden_size: 4096, num_heads: 32, } # 在线路径 on online_normalize(raw) print(在线: 模型类 , resolve_model_class(on), head_dim , on[head_dim]) # 离线路径 off offline_init_buggy(raw) print(离线: 模型类 , resolve_model_class(off), head_dim , off.get(head_dim)) # 离线会查不到类None且 head_dim 缺失 if __name__ __main__: main()跑出来在线路径能解析出模型类并拿到正确head_dim256离线路径解析为None且head_dim缺失——和线上「离线模式找不到模型类/字段缺失」完全一致。五、解决方案第一层最小直接修复最省事的救火优先用在线服务器模式避开离线LLM的归一化缺失# 不要这样离线可能炸 from vllm import LLM llm LLM(modelQwen/Qwen3.5-9B) # 改用服务器模式在线路径会做完整归一化 # vllm serve Qwen/Qwen3.5-9B --port 8000 # 然后用自己的脚本通过 OpenAI 客户端调用如果必须用离线LLM临时做法是手动补全缺失字段再传进去from vllm import LLM from transformers import AutoConfig hf AutoConfig.from_pretrained(Qwen/Qwen3.5-9B) # 手动注入离线路径漏掉的字段 hf.head_dim getattr(hf, head_dim, 256) # 用模型真实值 llm LLM(modelQwen/Qwen3.5-9B, hf_confighf)或者干脆换一个能正确归一化的版本如 nightly 或 0.20.x绕开 0.22.0 这个特定回归。六、解决方案第二层结构性改进第一层是「避开/手动补」第二层是「让离线LLM复用在线路径的归一化」从设计上消灭双路径不一致from dataclasses import dataclass from typing import Dict, Any dataclass class ModelConfig: model_name: str hf_config: Dict[str, Any] classmethod def from_model_name(cls, model_name: str, offline: bool False): hf AutoConfig.from_pretrained(model_name) raw hf.to_dict() # 关键离线和在线都走同一个归一化函数杜绝双路径分歧 normalized normalize_hf_config(raw, model_name) return cls(model_namemodel_name, hf_confignormalized) def normalize_hf_config(raw: Dict[str, Any], model_name: str) - Dict[str, Any]: 唯一事实来源把 HF 原始 config 统一成 vLLM 内部格式。 cfg dict(raw) # 1) architectures 名归一化 arch cfg.get(architectures) if arch: cfg[architectures] [a.replace(_, .) for a in arch] # 2) 新字段兜底head_dim 缺失时用模型声明的真实值 if head_dim not in cfg: cfg[head_dim] cfg.get(_real_head_dim, cfg[hidden_size] // cfg[num_attention_heads]) # 3) 其它 Qwen3.5 需要的字段同理归一化 if partial_rotary_factor in cfg: cfg[partial_rotary_factor] float(cfg[partial_rotary_factor]) return cfg def resolve_model_class(cfg: Dict[str, Any]): arch cfg[architectures][0] return REGISTRY.get(arch)这样无论离线还是在线都调用normalize_hf_config模型类匹配和字段注入永远一致。七、解决方案第三层断言 / CI 守护把「离线/在线归一化一致」「新字段被注入」「模型类可解析」固化成测试import pytest RAW { architectures: [Qwen3_5ForCausalLM], _real_head_dim: 256, hidden_size: 4096, num_attention_heads: 32, } def test_normalize_fixes_architecture_name(): cfg normalize_hf_config(RAW, Qwen/Qwen3.5-9B) assert cfg[architectures] [Qwen3.5ForCausalLM] def test_normalize_injects_head_dim(): cfg normalize_hf_config(RAW, Qwen/Qwen3.5-9B) assert cfg[head_dim] 256 def test_offline_resolves_model_class(): cfg normalize_hf_config(RAW, Qwen/Qwen3.5-9B) assert resolve_model_class(cfg) vllm_impl def test_offline_online_consistent(): # 离线路径也必须用 normalize_hf_config off ModelConfig.from_model_name(Qwen/Qwen3.5-9B, offlineTrue) on ModelConfig.from_model_name(Qwen/Qwen3.5-9B, offlineFalse) assert off.hf_config on.hf_config # 两条路径结果一致 def test_missing_field_no_dangerous_default(): # 即便归一化也没给 head_dim也应显式报错而非用错默认值 bad {architectures: [Qwen3.5ForCausalLM], hidden_size: 4096, num_attention_heads: 32} cfg normalize_hf_config(bad, x) assert cfg[head_dim] 4096 // 32 # 显式、可预期再加一个端到端回归离线LLM加载 Qwen3.5-9B 能构造并 generatedef test_offline_llm_qwen3_5_loads(): llm LLM(modelQwen/Qwen3.5-9B) # 不应抛 Cannot find model class out llm.generate(Hello) assert out is not None八、排查清单看报错是Cannot find a suitable model class还是 shape mismatch /KeyError: head_dim→ 都是归一化缺失信号。用在线vllm serve试同一个模型能起则说明离线路径独有此问题。临时救火改用服务器模式或手动AutoConfig注入缺失字段传给LLM(hf_config...)。检查 vLLM 版本0.22.0 离线模式已知回归换 nightly/0.20.x 验证。长期修复让离线LLM复用在线路径的normalize_hf_config双路径归一化一致。升级 vLLM 到合了离线归一化修复的版本并跑上面的「离线加载 Qwen3.5」回归。若报错是head_dim/形状相关优先核对hf_config.head_dim真实值是否被默认值覆盖。九、小结vLLM 0.22.0 离线LLM模式加载 Qwen3.5-9B 失败不是模型本身的问题而是离线路径比在线路径少做了一次 HF config 归一化导致 Qwen3.5 的新字段head_dim等没被注入、模型类也查不到。最小修复是改用服务器模式或手动注入字段结构性修复是让离线/在线共用同一份normalize_hf_config杜绝双路径分歧最后用 pytest 把「归一化一致」「新字段注入」「离线可加载」锁死。抓住「多入口必须共用同一份配置归一化」这条所有「离线能跑在线不能 / 反之」的坑都能照此化解。