抗退化采样与思考档
模型在跑、日志全绿、接口全是 200,输出却是复读、多语种、几千字打转------根本原因是三条独立根因的叠加:MRV2 投机采样路径、服务端缺省落在病态采样点、思考档缺省为
Max。三条修完,agent 档退化从 15% 收到 0/200。
文章目录
- 抗退化采样与思考档
-
- 结论速览
- [一 现象](#一 现象)
- [二 三个根因](#二 三个根因)
-
- [根因一:MRV2 投机采样路径(仅病态点)](#根因一:MRV2 投机采样路径(仅病态点))
- 根因二:服务端缺省落在病态工作点
- [根因三:思考档不传 ⇒ `Max`](#根因三:思考档不传 ⇒
Max)
- [三 第一道防线:采样缺省 + 强夹紧](#三 第一道防线:采样缺省 + 强夹紧)
-
- [overlay 修两道门](#overlay 修两道门)
- 缺省三元组
- 强夹紧(第二道防线)
- 验收口径
- [四 第二道防线:runner 分情况](#四 第二道防线:runner 分情况)
- [五 第三道防线:思考档](#五 第三道防线:思考档)
-
- [`enable_thinking` 的陷阱](#
enable_thinking的陷阱) - 七档对照(`temperature=0`)
- [档位对吞吐:反直觉的 +7.8%](#档位对吞吐:反直觉的 +7.8%)
- [`enable_thinking` 的陷阱](#
- [六 长上下文放大退化](#六 长上下文放大退化)
- 复现命令
- 小结
结论速览
- 退化是三条根因的叠加 :① MRV2 投机采样在病态工作点放大退化(30% vs MRV1 7.5%,sane 采样下两者都干净);② 服务端采样缺省落在
1.0/1.0零核裁剪病态点(agent 档 15%);③ 不传reasoning_effort⇒ 模板按Max渲染(简单指令命中率 7/10)。 - 抗退化缺省是三个键 :
{"temperature":0.8,"top_p":0.9,"frequency_penalty":0.3}------主导变量是temperature,官方1.0/0.95仍有 7.5%。第二道防线是服务端强夹紧 (只降不升,min(t,0.8)/min(p,0.9)),堵死客户端显式传1.0/1.0的绕路。 frequency_penalty原本设不进去 :键要过白名单(penalty 不在内,静默丢弃)、协议字段默认0.0不是None,4 文件纯 Python overlay 修复,/v1/responses上游本来就对,不改。enable_thinking=false是陷阱 :模板层不认它,但解析器会关掉思维链切分,思考没减少,全部倒进content。reasoning_effort="none"更差:Max级思考量 + 不切分。- 缺省思考档定为
high:模板只认low/high,其余一律落回Max;Max吞吐虽高 7.8%(思考文本可预测、MTP 接受率高),但简单指令命中率只有 7/10。
一 现象
- 一次真实 agent 会话,单步生成 62,046 字符 纯重复的"思考"(正常步 46~4,662 字符),耗时 418 s,终止于
max-tokens。关键是服务端全程健康:故障窗口内所有请求 200 OK,最大 prompt 只占上下文的 17.6%。这是模型侧生成质量故障,骗过了所有配置层和 HTTP 层自检。
退化不止一种形态,各自逃过不同判据:
| 签名 | 形态 | 抓它的判据 |
|---|---|---|
| 单 token 复读吸子 | 打满 3000 token、token 唯一率 4.1%、最高频 token 占 91% | REP + LENGTH |
| 多语言 token 汤 | 希腊/西里尔/泰文混排、HTML 碎片 | LENGTH / REP(d4 抓不到,汤的 d4 高达 0.79) |
| 省略号死循环 | Given... ... ... Since... ......doc...... |
REP + 人工读尾部 |
- 原始
CHAR≥4000判据在 200 次实测里报 16 次退化,全部来自一条"长而连贯的正常思考"------真实退化率 0/200。用d4(不同 4-gram 占比)标定后分布清晰:复读0.008~0.197、长连贯思考0.394~0.518、正常文本0.526~0.821⇒ 判据改为CHAR≥4000且d4<0.30,长而丰富的记soft_long留痕不计退化。 - 教训:判据必须用三类样本标定,"长"这一维单独用必然误判。
二 三个根因
根因一:MRV2 投机采样路径(仅病态点)
- 同镜像、同模型、同参数(病态点
1.0/1.0)的唯一变量对照:
| Runner | 退化 |
|---|---|
| MRV2(v0.29 起缺省) | 30% ❌ |
| MRV1 | 7.5% |
| v0.28 MRV1(历史) | 5% |
缺陷只在 temp>0 专属的 Gumbel/拒绝采样子集(temp=0 干净),不是数值问题。上游 v0.30.0 之后没有修复合入 ⇒ 等升级解决不了。
根因二:服务端缺省落在病态工作点
- 客户端一个采样参数都不传,vLLM 库默认
1.0/1.0------在 154,880 词表上零核裁剪。agent 走的/v1/responses在字段缺省时读default_sampling_params⇒ 服务端设了就一定生效,客户端零改动。把真实失败会话原样重放(53K token + 27 tools,N=40/臂轮转):
| 服务端缺省 | agent 档退化 |
|---|---|
1.0/1.0(旧缺省) |
15% ❌ |
0.8/0.9(现缺省) |
0% ✅ |
1.0/0.95(官方口径) |
7.5% ❌ |
- 主变量是
temperature:只降top_p到 0.95 仍有 7.5%。
根因三:思考档不传 ⇒ Max
- 模板只认
low/high,其余一切值(含不传)都渲染成Max:
jinja
{%- set effective_reasoning_effort = reasoning_effort
if reasoning_effort is defined and reasoning_effort in ['low','high'] else 'max' -%}
- 判决实验(简单指令,N=10):不传 ⇒ 命中 7/10、token 中位 214;显式
high⇒ 10/10、43。**不传时每 10 条简单指令 3 条答不出,token 消耗是high的 5 倍。 - ** 修法用 vLLM v0.30 原生
--default-chat-template-kwargs(请求级优先、只补客户端没给的键),缺省{"reasoning_effort":"high"};修复后不传 ≡ 显式high。选high不选low:low更快但难题上的精度代价从未评估过。
三 第一道防线:采样缺省 + 强夹紧
overlay 修两道门
--override-generation-config 在 vLLM 里过不去:① get_diff_sampling_param() 的白名单没有 penalty ⇒ 静默丢弃;② chat/completion 协议的 frequency_penalty 默认 0.0 不是 None,"读服务端缺省"那支是死代码。patches/sampling_defaults/(4 个纯 Python 文件、零 GPU 依赖)修这两处,核心语义一句:"没给"才用服务端缺省;"显式给了"------哪怕 0.0------一律尊重客户端。 build_overlay.sh 写入原件 sha256 基线,prefill 每次启动比对,镜像被换即 die。
缺省三元组
bash
OVERRIDE_GENERATION_CONFIG='{"temperature":0.8,"top_p":0.9,"frequency_penalty":0.3}'
三个键必须都设------客户端一个都不传,缺一个就退回 1.0/1.0。演变都是实测:只设 penalty ⇒ 短 prompt 0/40 但 agent 档仍 15%(它治单 token 复读吸子,治不了 token 汤)⇒ 官方口径 7.5% ⇒ 定稿三元组 0/40。
强夹紧(第二道防线)
- 缺省只覆盖"客户端没传",显式传
1.0/1.0仍能绕过。夹紧打在唯一中枢SamplingParams.__post_init__,三条 OpenAI 路径都会经过:
python
if os.environ.get("VLLM_SAMPLING_CLAMP", "1") != "0":
_t = float(os.environ.get("VLLM_SAMPLING_CLAMP_TEMP", "0.8"))
_p = float(os.environ.get("VLLM_SAMPLING_CLAMP_TOPP", "0.9"))
if self.temperature > _t: self.temperature = _t
if self.top_p > _p: self.top_p = _p
只降不升 ⇒ 安全区内请求逐位不变、贪心不受影响;VLLM_SAMPLING_CLAMP=0 可关。已知代价:客户端显式值不再原样透传。
验收口径
0/40 不足以宣称修好(基线 6% 时零假设概率 8.4%)。按 rule of three,0/200 → ≤1.5%。最终收口:MRV1 + MTP + sane 参数、5 prompt 轮转 N=200 ⇒ 0 真退化 ;同口径病态点 3/100,说明 sane 参数确实有效。生产纪律:客户端不要传 frequency_penalty: 0。
四 第二道防线:runner 分情况
| 场景 | 钉哪个 | 原因 |
|---|---|---|
PP>1 + 投机 |
MRV2 | PP+spec 只有 MRV2 实现了 |
PP=1 + 投机 |
MRV1 | MRV2 投机采样在 temp>0 时分布错误 |
| 无投机 | 不设 | 缺陷走不到 |
启动后在日志核对实际生效的 runner;FORCE_V2_RUNNER 可强制覆盖。
五 第三道防线:思考档
enable_thinking 的陷阱
它被两套独立机制处理:模板层不认 (本模型模板没有这个变量,vLLM 直接过滤,prompt 逐 token 不变);解析层消费 ------Glm47MoeParser 读到它就把 THINK_START/THINK_END 从终结符表删掉、初始状态改为 CONTENT。所以 enable_thinking=false 的作用是让 serve 端不再切分思维链 ,不是"让模型不思考":实测 reasoning=0、content 1906 字且开头是英文 CoT。
reasoning=0有两种相反含义:low是真没思考(content 干净),none/enable_thinking=false是思考倒进了 content。判断只看reasoning长度必然误判,必须看content是否干净正文。 参考项目推荐enable_thinking: false"有效",真正起作用的是它同时塞的那条压制性 system 提示。
七档对照(temperature=0)
| 档位 | reasoning | content | completion_tokens | 判读 |
|---|---|---|---|---|
| 缺省(Max) | 763 字 | 127 字 | 382 | 干净 |
low |
0 | 104 | 81 | 干净,真没思考 |
high |
32 | 141 | 124 | 干净 |
none |
0 | 485 | 221 | ⛔ CoT 泄漏 |
enable_thinking=false |
0 | 681 | 344 | ⛔ CoT 泄漏 |
none 的陷阱拼起来最坑:模板层把它渲染成 Max(思考量最大),解析层又关掉切分------既没减少思考,又毁了响应结构 。客户端配置里绝不要提供 off: null 档。
档位对吞吐:反直觉的 +7.8%
reasoning_effort是质量/长度开关 。同会话两档对照:Max相对low聚合吞吐 +7.8% (conc8 +10.7%、conc16 +9.2%),接受长度 2.530 vs 2.495。机制:思考文本更可预测 ⇒ MTP 接受率更高。但"缺省保持 Max"的结论后来被推翻 :简单指令上Max命中率 7/10、token 中位 214,high是 10/10、43,吞吐基准跑的是长输出负载,agent 真实负载里有大量简单指令,换了负载形态最优档就变了,缺省定为high。
六 长上下文放大退化
- 窗口越大,退化越严重。 262144 不是服务端能力,是客户端对未声明模型的默认值。窗口填大等于解开安全带:压缩阈值 = 0.8 × 窗口,会话到 62K 就死了,压缩一次都没触发。唯一的硬刹车是
max_output_tokens=32768,没有它,那次乱码会烧 10 倍以上的时间和 KV;客户端把maxTokens降到 8192 是成本最低的止血。另外同一固定配置下退化有偶发性:temperature=0、同一 prompt 跑 5 次约 2 次退化成 3000-token 打转,命中检索与多轮生成稳定是两种能力。
复现命令
bash
# ── 服务端事实核对 ────────────────────────────────────────────
./deploy.sh start # 缺省即带采样三键 + reasoning_effort=high
./deploy.sh samplingverify # 缺任一键 exit 2
./deploy.sh samplingcheck # overlay 离线自检,无需 GPU
./deploy.sh cmd | grep -E "override-generation-config|default-chat-template-kwargs"
# ── 退化探针 ─────────────────────────────────────────────────
python3 scripts/degeneration_probe.py --label base --trials 20 --out /tmp/deg.json
./deploy.sh degen # agent 档硬门禁,有退化 exit 2
# ── MRV1/MRV2 对照(只在病态工作点看得到 4 倍差)──────────────
SPEC_METHOD=mtp FORCE_V2_RUNNER=1 TP_SIZE=8 PP_SIZE=1 ./deploy.sh start
python3 scripts/degeneration_probe.py --trials 40 --temperature 1.0 --top-p 1.0 --frequency-penalty 0 --out /tmp/mrv2.json
SPEC_METHOD=mtp FORCE_V2_RUNNER=0 TP_SIZE=8 PP_SIZE=1 ./deploy.sh start # 同上测 MRV1
# ── 思考档 ───────────────────────────────────────────────────
BASE_URL=http://127.0.0.1:8021 python3 scripts/thinking_mode_test.py
python3 scripts/measure_effort.py --efforts low,max
# ── 开关与回退(容器 env 必须走 DOCKER_ENV_EXTRA)─────────────
DOCKER_ENV_EXTRA="VLLM_SAMPLING_CLAMP=0" ./deploy.sh start # 关夹紧
OVERRIDE_GENERATION_CONFIG='' ./deploy.sh start # 关采样缺省(A/B 基线)
DEFAULT_CHAT_TEMPLATE_KWARGS='' ./deploy.sh start # 关思考档缺省(复现 Max)
小结
- 三条根因叠加出同一个症状:病态采样点(缺省
1.0/1.0)提供土壤,MRV2 在其上放大,Max思考档加剧浪费。修复按三层防线落地:服务端缺省三元组 + 强夹紧堵死绕路、runner 分情况钉、思考档缺省high。