【Bug已解决】tool_choicerequiredPD disaggregation; internal server error for GLM-5 解决方案一、现象长什么样用 GLM-5 做工具调用function calling当请求里同时设置tool_choicerequired强制模型必须调用工具并且开启 PD 分离部署Prefill 与 Decode 拆成两个独立实例prefill 实例负责把 prompt 算到第一个 tokendecode 实例负责后续自回归生成时服务端返回 500 Internal Server Error。典型日志Internal Server Error: failed to forward tool_choicerequired request across PD boundary AssertionError: decode instance received no tool_call token from prefill或者更笼统tool_choicerequiredPD disaggregation; internal server error for GLM-5几个特征帮你判断是不是同一个坑报错是HTTP 500内部服务器错误而不是正常的工具调用响应。只要关掉 PD 分离prefill 和 decode 在同一实例同样的tool_choicerequired请求就正常——说明问题出在「PD 分离 强制工具调用」的组合。普通对话不带tool_choice或tool_choiceauto在 PD 分离下也正常只有required这种「强制首 token 必须是工具调用」的模式崩。错误里常出现tool_call、prefill、decode、forward、boundary这些关键字。二、背景要理解这个误报得先理清两件事tool_choicerequired到底要求什么以及PD 分离时两个实例之间传了什么。tool_choicerequired的语义它要求模型生成的第一个 token 必须是一个「工具调用开始」标记GLM-5 里通常是类似|tool_call|或对应特殊 token。换句话说prefill 阶段必须把 prompt 算到「下一个必须输出工具调用 token」的状态并且这个约束要延续到 decode 阶段让 decode 实例从「工具调用 token」继续自回归。PD 分离的工作方式prefill 实例处理完整 prompt算出 KV 缓存和「最后一个位置之后的下一个 token 的隐状态/或第一个生成 token」然后通过一个「传输协议」把 KV 缓存以及必要的元信息发给 decode 实例。decode 实例拿到 KV 缓存后从那里继续生成。冲突点tool_choicerequired这种约束有时不是单纯靠 KV 缓存就能传递的——它可能涉及首 token 的强制偏置bias / logit maskprefill 端为了让第一个 token 必为工具调用会在采样时加一个 logits 偏置或强制 mask。如果这个偏置只设在 prefill 实例、没随请求传给 decode 实例decode 实例就「不知道要强制工具调用」可能生成普通文本而非|tool_call|。KV 缓存外的元信息缺失PD 传输协议默认只传 KV 缓存 位置信息但required约束所需的「采样参数 / 强制标记」如果没被序列化进传输的 request metadatadecode 端就丢失了这个约束。特殊 token 在 decode 端的 vocab 对齐GLM-5 的工具调用特殊 token若 prefill 与 decode 实例的 tokenizer / 模型配置不一致比如两个实例加载了不同版本decode 端收到「工具调用 token id」却不认识触发断言失败。请求 schema 校验不全网关/路由层在收到tool_choicerequired PD 的复合请求时没有校验「这种组合是否支持」直接放行到了 decode 端才发现约束无法满足 → 500。核心矛盾required是一个「跨实例的生成约束」但 PD 分离默认只传递 KV 缓存不传递这个约束的元信息于是 decode 端丢失约束、生成出错、最终 500。三、根因根因一句话在 PD 分离部署下tool_choicerequired所需的「强制首 token 为工具调用」约束采样偏置 / 特殊 token 标记 / 采样参数没有被序列化进 prefill→decode 的传输元信息里decode 实例丢失该约束后要么生成了非工具调用 token 触发断言要么因不认识工具调用 token 而抛 500。具体成因约束未随 KV 传输PD 传输协议只搬 KV 缓存漏传tool_choicerequired对应的采样约束/偏置decode 端无约束可遵。首 token 偏置仅设在 prefill 端prefill 端为了required在 logits 上加了强偏置但 decode 端从第二个 token 起用自己的采样配置偏置没延续。特殊 token id 不对齐prefill 与 decode 实例 tokenizer 版本不同GLM-5 的|tool_call|token id 在两端的 id 不一致decode 收到 id 却映射到错误 token。required与 PD 组合未校验路由层没禁止/未适配「required PD」请求被直接转发到不支持该组合的 decode 实例。错误未优雅降级decode 端发现约束缺失时直接抛异常 → 网关转成 500而非返回一个清晰的「该组合暂不支持」或自动回退到非 PD。核心矛盾强制工具调用是一个需要两端协同的约束PD 传输链却把它当成「只在 prefill 端有效的一次性设置」导致约束在实例边界处断裂。四、最小可运行复现下面用纯 Python 模拟「prefill 设置 required 约束但 PD 传输只搬 KV、漏传约束decode 丢失约束」的情形# reproduce_pd_tool.py # 复现required 约束未随 PD 传输decode 端丢失 - 500 class Request: def __init__(self): self.kv_cache kv_data self.tool_choice None # 随请求传递的约束 self.sampling_bias None # prefill 端的强制偏置 def prefill(req: Request): if req.tool_choice required: req.sampling_bias force_tool_call_token # 只设在 prefill 端 return req def transfer(req: Request): # PD 传输: 只搬 kv_cache漏传 tool_choice / sampling_bias decoded Request() decoded.kv_cache req.kv_cache # decoded.tool_choice / decoded.sampling_bias 都为 None - 漏传! return decoded def decode(req: Request): if req.tool_choice required and req.sampling_bias is None: raise RuntimeError(decode 端丢失 required 约束无法强制工具调用 - 500) return ok if __name__ __main__: r Request() r.tool_choice required prefill(r) d transfer(r) try: decode(d) except RuntimeError as e: print(复现成功:, e)运行python reproduce_pd_tool.py会看到因「约束漏传」导致的失败正是 500 的来源。五、解决方案第一层最小直接修复最小修复在 PD 传输协议里把tool_choicerequired对应的约束强制标记 采样偏置一并序列化传输decode 端据此重建约束。如果暂时改不了传输协议先给路由层加一道「required PD 不支持则回退」的开关。# fix_layer1_pd_tool.py def transfer_full(req: dict) - dict: PD 传输时除 kv_cache 外强制带上 tool_choice 与采样约束。 return { kv_cache: req[kv_cache], tool_choice: req.get(tool_choice), sampling_bias: req.get(sampling_bias), forced_first_token: req.get(forced_first_token), # GLM-5 工具调用首 token } def decode_with_constraint(req: dict): if req.get(tool_choice) required: if not req.get(sampling_bias) and req.get(forced_first_token) is None: # 兜底: 即使漏传也强制首 token 为工具调用标记 req[forced_first_token] |tool_call| # 用 forced_first_token 约束 decode 首 token return ok if __name__ __main__: r {kv_cache: kv, tool_choice: required, sampling_bias: force} d transfer_full(r) print(decode_with_constraint(d)) # ok这一层要么把约束随 KV 一起传治本要么 decode 端用默认工具调用标记兜底治标让required不再丢约束。六、解决方案第二层结构性改进把「PD 请求约束的序列化 两端对齐」做成独立模块明确列出「哪些约束必须跨实例传递」并做 tokenizer 对齐校验# fix_layer2_pd_contract.py from dataclasses import dataclass, field # 必须在 PD 两端一致的请求约束 PD_CONSTRAINT_FIELDS (tool_choice, sampling_bias, forced_first_token, tool_ids) dataclass class PDRequestContract: kv_cache: object tool_choice: str | None None sampling_bias: str | None None forced_first_token: str | None None tool_ids: list field(default_factorylist) def to_wire(self) - dict: 序列化: 含 KV 与所有约束字段。 return { kv_cache: self.kv_cache, **{f: getattr(self, f) for f in PD_CONSTRAINT_FIELDS}, } classmethod def from_wire(cls, wire: dict) - PDRequestContract: missing [f for f in PD_CONSTRAINT_FIELDS if f not in wire] if missing and wire.get(tool_choice) required: raise ValueError( fPD 传输缺失约束字段 {missing}required 工具调用无法跨实例传递 ) return cls(kv_cachewire[kv_cache], **{f: wire.get(f) for f in PD_CONSTRAINT_FIELDS}) def validate_tokenizer_aligned(prefill_tok, decode_tok, forced_token: str): 确保工具调用特殊 token 在两实例 id 一致。 pid prefill_tok(forced_token) did decode_tok(forced_token) assert pid did, f工具调用 token id 不对齐: prefill{pid} decode{did} if __name__ __main__: c PDRequestContract(kv_cachekv, tool_choicerequired, forced_first_token|tool_call|) wire c.to_wire() restored PDRequestContract.from_wire(wire) print(约束跨实例保留:, restored.forced_first_token)这样任何「required PD」请求约束字段都会随 KV 一起序列化缺字段在 decode 端立即报错且 tokenizer 对齐被显式校验。七、解决方案第三层断言 / CI 守护把「PD 约束传输完整性 tokenizer 对齐」钉进断言和 CI# fix_layer3_guard.py # ---- pytest 用例进 CI ---- def test_required_constraint_travels_pd(): from fix_layer2_pd_contract import PDRequestContract c PDRequestContract(kv_cachekv, tool_choicerequired, forced_first_token|tool_call|) wire c.to_wire() r PDRequestContract.from_wire(wire) assert r.tool_choice required assert r.forced_first_token |tool_call| def test_missing_constraint_rejected(): from fix_layer2_pd_contract import PDRequestContract try: PDRequestContract.from_wire({kv_cache: kv, tool_choice: required}) assert False, 缺约束字段应被拒 except ValueError: pass def test_tokenizer_must_align(): from fix_layer2_pd_contract import validate_tokenizer_aligned try: validate_tokenizer_aligned(lambda t: 100, lambda t: 200, |tool_call|) assert False except AssertionError: pass再加路由层守卫避免把「required PD」发到不支持的实例def route_request(req: dict, pd_enabled: bool): if req.get(tool_choice) required and pd_enabled: # 确保约束能跨实例; 否则回退到非 PD 单实例 if not req.get(forced_first_token): return single_instance # 回退 return pd八、排查清单GLM-5 的tool_choicerequired PD 分离报 500按序查先关 PD 试同一请求关掉 PD 分离能正常说明问题在 PD 跨实例约束传递。查是否只有required崩auto/none正常而required崩说明是「强制首 token」约束没传过去。看 PD 传输协议确认 KV 传输时是否漏传tool_choice/ 采样偏置 / 强制首 token 标记。校验 tokenizer 对齐prefill 与 decode 两实例的 GLM-5 tokenizer 版本是否一致|tool_call|的 id 是否相同。加约束序列化把required相关约束字段写进 PD 传输的 request metadatadecode 端据此重建。decode 端兜底即使漏传也用默认工具调用标记强制首 token避免直接 500。路由层适配网关识别「required PD」组合要么保证约束可传要么回退单实例。优雅降级而非 500decode 发现约束缺失时返回清晰错误/回退而非抛异常让网关转 500。看 vLLM 版本PD 分离 工具调用的协同在新版本才完善升级常直接解决。最后才动采样核优先在传输协议/路由层修约束传递不要为了对齐去改采样内核。九、小结GLM-5 在tool_choicerequired PD 分离下报 500根子是「强制首 token 为工具调用」是一个跨实例生成约束但 PD 传输链默认只搬 KV 缓存、漏传该约束的元信息decode 端丢失约束后或生成非工具调用 token 触发断言、或不认识工具调用 token 而抛 500。修复三层第一层把约束字段随 KV 一起序列化传输decode 端缺失时兜底强制首 token第二层抽PDRequestContract明确「哪些约束必须跨实例传递」并校验 tokenizer 对齐第三层用 pytest 把「约束跨实例保留」「缺失即拒」「tokenizer 对齐」钉进 CI路由层对不支持的组合回退单实例。核心认识——PD 分离不是「只传 KV」那么简单任何影响生成结果的请求约束采样偏置、强制标记、tool_choice都必须被显式序列化进传输协议并在两端校验一致否则约束会在实例边界悄悄断裂。