Qwen3 MoE 模型 vLLM 死循环问题排查与修复经验

日期: 2026-08-24 | 模型: Qwen3 系列 MoE (35B-A3B-FP8) | vLLM: 0.27.1


1. 前提条件

1.1 硬件环境

项目
GPU 4 × NVIDIA GeForce RTX 4090
显存 24 GB / 卡 (共 ~96 GB)
GPU 互联 PCIe (无 NVLink)
操作系统 Ubuntu 22.04 LTS

1.2 软件版本

组件 版本
vLLM 0.27.1
Python 3.13
推理后端 FlashInfer
KV Cache FP8 量化

1.3 模型信息

项目
模型系列 Qwen3 MoE (混合注意力架构: GDN)
总参数 / 激活参数 ~35B / ~3B
量化方式 FP8 (e4m3, block 128×128)
上下文长度 256K
EOS token IDs [248046, 248044]
原始 generation_config temperature=1.0, top_k=20, top_p=0.95

1.4 vLLM 启动关键参数

vllm serve /path/to/model \
  --tensor-parallel-size 4 \
  --max-model-len 262144 \
  --gpu-memory-utilization 0.92 \
  --max-num-seqs 32 \
  --enable-chunked-prefill \
  --reasoning-parser qwen3 \
  --enable-auto-tool-choice \
  --tool-call-parser qwen3_xml \
  --enable-expert-parallel \
  --async-scheduling \
  --attention-backend flashinfer \
  --kv-cache-dtype fp8 \
  --override-generation-config '{"temperature":0.6,"top_k":20,"top_p":0.95,"min_p":0.0,"repetition_penalty":1.0,"presence_penalty":1.5}'

1.5 业务调用方式

  • 通过 OpenAI 兼容 API (/v1/chat/completions) 调用
  • 支持 streaming 和 non-streaming
  • 使用 thinking 模式 (enable_thinking=true)
  • 使用工具调用 (tool_choice=auto)
  • 业务方未显式传 presence_penalty / frequency_penalty / stop_token_ids, 依赖服务端默认值

2. 问题现象

2.1 线上表现

  • 某些请求 (尤其是"重复 X 次"类 prompt) 触发模型无限生成, 持续 ~155 tokens/s 不停止
  • GPU 利用率 97%, KV cache 使用率从 0.2% 持续增长直至耗尽
  • 请求一直处于 Running: 1 状态, 超过 5 分钟未结束
  • vLLM 日志无 error, 仅显示持续的高吞吐量生成
  • 只有重启 vLLM 服务才能终止该请求

2.2 触发条件

  • 高重复性 prompt: 如"请重复以下内容 50 次:XXX"
  • 低 temperature (0.6): 分布尖锐化, 容易锁定重复模式
  • 高 repetition_penalty (1.2): 反向将概率分布推向更窄的 token
  • 未设置 presence_penalty: 缺少对已出现 token 的存在性惩罚

2.3 复现方法

# 高概率触发死循环的请求 (原始配置下)
curl -s http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "your-model-name",
    "messages": [{"role": "user", "content": "请重复以下内容50次:人工智能"}],
    "max_tokens": 4096
  }'
# 观察: finish_reason=length, 输出 13000+ 字符, 全部是重复内容

3. 根因分析

3.1 Qwen3 MoE 死循环机制

Qwen3 系列 MoE 模型在特定采样参数组合下, 概率分布会塌缩到极少数 token 上, 形成 N-gram 重复循环, 且无法生成 EOS token 终止:

高 repetition_penalty (1.2)
  → 高频 token 被过度惩罚
  → 概率分布推向低频但已出现的 token
  → 这些 token 再次出现时又被惩罚
  → 最终锁定在少数未惩罚 token 的循环中
  → 无 EOS token 生成 → 无限循环

3.2 关键参数影响

参数 问题值 影响
repetition_penalty 1.2 最关键 — 按频率惩罚, 出现越多惩罚越大, 但反向将分布推向更窄 token, 加剧循环
presence_penalty 未设置 (0.0) 缺少按存在性惩罚的机制, 已出现 token 无额外抑制
min_p 0.0 不裁剪低概率 token, 允许模型选择极低概率的重复 token
temperature 0.6 偏低温度使分布尖锐化, 容易锁定重复模式

3.3 vLLM bug: presence_penalty 被静默忽略

vLLM 0.27.1 的 --override-generation-config 不支持 presence_penaltyfrequency_penalty:

  • 这两个参数不在 get_diff_sampling_param()available_params 白名单中
  • 即使写入 JSON 配置也不会生效, 且无任何警告
  • 客户端不传时, 始终回退到 0.0 而非服务端配置值

Bug 详情: vLLM #50767
官方修复 PR: vLLM PR #35988 (截至 2026-08 尚未合并)

3.4 参考来源

来源 关键信息
Qwen 官方 Best Practices 推荐 presence_penalty=1.5 防循环, repetition_penalty=1.0
QwenLM/Qwen3.5 #88 社区报告 LiveCodeBench 重复循环问题
HuggingFace Qwen3.5-35B-A3B 讨论 MoE 模型 looping prevention 讨论
vLLM #50767 presence_penalty 被 override-generation-config 静默忽略
vLLM PR #35988 官方修复 PR (添加 presence/frequency penalty 到 available_params)
vLLM PR #35451 repetition_detection 引擎层兜底机制
vLLM PR #32443 EOS token 未生效修复

4. 修复方案

4.1 方案迭代

方案 1: 调整采样参数 (min_p=0.2) — ❌ 废弃
参数 原值 方案 1 思路
min_p 0.0 0.2 裁剪低概率 token
temperature 0.6 0.8 增加分布多样性
top_k 20 40 扩大候选范围
repetition_penalty 1.2 1.05 降低惩罚

测试结果:

  • 48 项完整测试: 47 通过, 1 死循环
  • F5 (重复 50 次) 单独测 20 次: 20% 死循环率 (2/10)
  • 问题: 偏离 Qwen 官方推荐参数, 高温 + 高 min_p 影响输出质量
方案 2: Qwen 官方参数 + presence_penalty=1.5 + vLLM patch — ✅ 最终方案

核心思路: 保持 Qwen 官方推荐采样参数不变 (保质量), 通过 presence_penalty=1.5 防循环 (Qwen 官方推荐), 手动 patch vLLM bug 让该参数能通过 --override-generation-config 全局生效。

4.2 vLLM Patch (手动修复 #50767)

适用版本: vLLM 0.27.1 | 文件路径根据实际安装位置调整

文件 1: vllm/config/model.py

get_diff_sampling_param() 方法中, available_params 列表添加 presence_penaltyfrequency_penalty:

# 修改前
available_params = [
    "repetition_penalty",
    "temperature",
    "top_k",
    "top_p",
    "min_p",
    "max_new_tokens",
]

# 修改后
available_params = [
    "repetition_penalty",
    "temperature",
    "top_k",
    "top_p",
    "min_p",
    "max_new_tokens",
    "presence_penalty",      # ← 新增
    "frequency_penalty",     # ← 新增
]
文件 2: vllm/entrypoints/openai/chat_completion/protocol.py

修改 1: ChatCompletionRequest 字段默认值从 0.0 改为 None, 让 fallback 逻辑能区分 “客户端未传” 和 “客户端显式传 0”:

# 修改前
frequency_penalty: float | None = 0.0
presence_penalty: float | None = 0.0

# 修改后
frequency_penalty: float | None = None
presence_penalty: float | None = None

修改 2: _DEFAULT_SAMPLING_PARAMS 添加两个参数:

# 修改前
_DEFAULT_SAMPLING_PARAMS: dict = {
    "repetition_penalty": 1.0,
    "temperature": 1.0,
    "top_p": 1.0,
    "top_k": 0,
    "min_p": 0.0,
}

# 修改后
_DEFAULT_SAMPLING_PARAMS: dict = {
    "repetition_penalty": 1.0,
    "temperature": 1.0,
    "top_p": 1.0,
    "top_k": 0,
    "min_p": 0.0,
    "presence_penalty": 0.0,     # ← 新增
    "frequency_penalty": 0.0,    # ← 新增
}

修改 3: to_sampling_params() 方法中, 在 min_p 的 fallback 逻辑之后添加:

if (presence_penalty := self.presence_penalty) is None:
    presence_penalty = default_sampling_params.get(
        "presence_penalty",
        self._DEFAULT_SAMPLING_PARAMS["presence_penalty"],
    )
if (frequency_penalty := self.frequency_penalty) is None:
    frequency_penalty = default_sampling_params.get(
        "frequency_penalty",
        self._DEFAULT_SAMPLING_PARAMS["frequency_penalty"],
    )

修改 4: SamplingParams.from_optional() 调用处, 使用解析后的变量而非 self 原始值:

# 修改前
presence_penalty=self.presence_penalty,
frequency_penalty=self.frequency_penalty,

# 修改后
presence_penalty=presence_penalty,
frequency_penalty=frequency_penalty,

注意: 此 patch 在 vLLM 升级 (pip install --upgrade vllm) 后会被覆盖, 需重新应用。未来 vLLM 合并 PR #35988 或 #51175 后将不再需要。

4.3 最终采样参数

参数 原始值 最终值 来源
temperature 0.6 0.6 Qwen 官方推荐 (thinking 模式)
top_k 20 20 Qwen 官方推荐
top_p 0.95 0.95 Qwen 官方推荐
min_p 0.0 0.0 Qwen 官方推荐 (配合 presence_penalty)
repetition_penalty 1.2 1.0 中性值, 不干扰概率分布
presence_penalty 1.5 Qwen 官方推荐防循环
{"temperature":0.6,"top_k":20,"top_p":0.95,"min_p":0.0,"repetition_penalty":1.0,"presence_penalty":1.5}

4.4 额外防护 (per-request 层, 可选)

vLLM 0.22.1+ 内置 RepetitionDetectionParams, 可在 API 请求中传入作为引擎层兜底:

{
  "repetition_detection": {
    "max_pattern_size": 20,
    "min_pattern_size": 4,
    "min_count": 3
  }
}

含义: 检测 4~20 token 的 N-gram 模式, 重复 ≥3 次则自动终止生成。

注意: 此参数为 per-request, 无法通过 vLLM 启动参数全局设置, 需在业务调用方统一传入。


5. 验证结果

5.1 测试方法

  • 基础测试: 12 类 48 个测试用例, 覆盖短/长文本、思考模式、工具调用、多轮对话、边界条件、特殊字符、温度参数、结构化输出、Agent 场景、递归陷阱、系统提示词、并发测试
  • 专项测试: F5 (重复输入 50 次) 连续 20 次, 专门针对死循环触发场景

5.2 测试结果

48 项完整测试
测试类别 用例数 结果
A-基础功能 5 ✅ 全通过
B-思考模式 5 ✅ 全通过
C-长文本 4 ✅ 全通过
D-多轮对话 3 ✅ 全通过
E-工具调用 3 ✅ 全通过
F-边界条件 5 ✅ 全通过 (含 F5-重复50次)
G-特殊字符 4 ✅ 全通过
H-温度参数 4 ✅ 全通过
I-结构化输出 3 ✅ 全通过
J-Agent场景 3 ✅ 全通过
K-递归陷阱 3 ✅ 全通过
L-系统提示词 3 ✅ 全通过
M-并发测试 3 ✅ 全通过
总计 48 48/48 = 100%, 0 死循环
F5 专项死循环测试 (连续 20 次)
次数 结果
1-10 ✅ 10/10 正常 (finish_reason=stop)
11-20 ✅ 10/10 正常 (finish_reason=stop)
总计 20/20 = 100%, 0 死循环

5.3 方案对比

对比项 原始配置 方案 1 (min_p=0.2) 最终方案
temperature 0.6 0.8 0.6 (Qwen 官方)
top_k 20 40 20 (Qwen 官方)
min_p 0.0 0.2 0.0 (Qwen 官方)
repetition_penalty 1.2 1.05 1.0 (中性)
presence_penalty 1.5 (Qwen 官方推荐)
F5 重复50次 (20次) 死循环 20% 死循环 0% 死循环
48项完整测试 - 47/48 48/48
质量保持 - ❌ 有损 ✅ 无损
需要 vLLM patch

6. 经验总结

6.1 核心教训

  1. 优先使用模型官方推荐参数: Qwen 官方文档明确推荐 presence_penalty=1.5 防循环, 不要自行调参偏离官方值。官方参数经过充分测试, 在质量和稳定性之间取得了最佳平衡。

  2. repetition_penaltypresence_penalty:

    • repetition_penalty: 按频率惩罚, token 出现越多惩罚越大。过高 (如 1.2) 反而加剧循环 — 高频 token 被过度惩罚后, 概率分布推向更窄的低频 token, 形成新的循环。
    • presence_penalty: 按存在性惩罚, token 只要出现过就施加固定惩罚。对防止重复循环更有效, 且不会反向扭曲概率分布。
  3. vLLM --override-generation-config 有 bug: 0.27.1 版本静默忽略 presence_penalty / frequency_penalty。这两个参数不在 available_params 白名单中, 写入 JSON 不会生效且无警告。必须手动 patch 或等待官方合并 PR。

  4. min_p 调整不是万能药: min_p=0.2 虽然能降低死循环概率, 但在极端重复输入场景下仍有 20% 死循环率, 且偏离官方参数影响输出质量。应优先使用 presence_penalty 而非 min_p 来防循环。

  5. 多层防护策略: 单一手段无法 100% 防止死循环, 建议三层兜底:

    • 服务端全局: presence_penalty=1.5 (通过 --override-generation-config + vLLM patch)
    • 业务方 per-request: repetition_detection (N-gram 检测, 自动终止)
    • 硬限制: 合理的 max_tokens 设置 (防止无限生成耗尽资源)
  6. vLLM patch 维护: 手动 patch 的代码在 pip install --upgrade vllm 后会被覆盖。建议:

    • 记录 patch 内容和位置 (本文档)
    • 升级后检查 patch 是否需要重新应用
    • 关注官方 PR #35988 / #51175 合并状态

6.2 排查方法论

  1. 确认现象: 通过 vLLM metrics / logs 确认是死循环 (持续高吞吐量生成, KV cache 持续增长, 请求不结束)
  2. 复现问题: 构造最小复现用例 (如"重复 X 次"类 prompt)
  3. 查阅官方文档: 模型官方 Best Practices 通常有推荐参数和已知问题说明
  4. 查阅社区 issue: HuggingFace discussions / GitHub issues 搜索 “loop” “repetition” “infinite”
  5. 验证参数生效: 检查 vLLM 日志中 override_generation_config 是否包含目标参数, 确认未被静默忽略
  6. 迭代测试: 从简单到复杂, 先跑专项测试 (复现用例), 再跑完整测试套件
  7. 对比方案: 记录每个方案的测试结果, 量化对比 (通过率、死循环率、质量评估)

6.3 适用范围

  • 模型: Qwen3 系列 MoE 模型 (包括 Qwen3-235B-A22B, Qwen3-30B-A3B, Qwen3.5-35B-A3B 等)
  • vLLM 版本: 0.22.x ~ 0.27.x (presence_penalty bug 在 0.27.1 仍存在)
  • 不适用: 非 MoE 架构的 Qwen 模型 (如 Qwen2.5 系列) 可能不需要此修复

7. 快速修复 Checklist

如果你遇到类似问题, 按以下步骤操作:

  • 1. 确认死循环现象 (vLLM logs 显示持续生成不停止)
  • 2. 构造复现用例 (如 "请重复以下内容50次:XXX")
  • 3. 检查当前 --override-generation-config 参数
  • 4. 确认 repetition_penalty 是否为 1.2 (需改为 1.0)
  • 5. 确认是否设置了 presence_penalty (需设为 1.5)
  • 6. 检查 vLLM 版本是否 < 0.28 (需要 patch)
  • 7. Patch vllm/config/model.py (添加 presence/frequency_penalty 到 available_params)
  • 8. Patch vllm/entrypoints/openai/chat_completion/protocol.py (4 处修改)
  • 9. 更新 --override-generation-config 为最终参数
  • 10. systemctl daemon-reload && systemctl restart vllm
  • 11. 验证日志中 presence_penalty: 1.5 已生效
  • 12. 运行死循环专项测试 (重复 50 次 prompt × 20 次)
  • 13. 运行完整测试套件确认质量无损
  • 14. 通知业务方可选添加 repetition_detection 作为 per-request 兜底
  • 15. 记录 patch 内容, 标记 vLLM 升级时需重新应用

更多推荐