[Bug]: GatedDeltaNet metadata builder classifies a stateless first chunk as a decode (same root cause as #51483)

该报错出现在 vLLM 的 GatedDeltaNet(GDN)路径上,当一个调度批次里全部都是长度为 1 token 的行、且其中至少有一行是首次 chunk( num_computed_tokens == 0 )时,共享的 GDNAttentionMetadataBuilder 会把这条无状态的

快速结论:该报错出现在 vLLM 的 GatedDeltaNet(GDN)路径上,当一个调度批次里全部都是长度为 1 token 的行、且其中至少有一行是首次 chunk(num_computed_tokens == 0)时,共享的 GDNAttentionMetadataBuilder 会把这条无状态的首 chunk 误判成 decode,导致读取未清零的线性注意力状态槽。优先排查单 token 批次是否被错误分类为 decode、以及 has_initial_state 是否被置为 None

适用环境:Issue 中确认涉及 vLLM 的 vllm/v1/attention/backends/gdn_attn.py 共享 builder;受影响的模型包括 Qwen3-Next、Qwen3.5、OLMo-hybrid、InternS2-Mobius、Bailing-MoE-v3 以及独立的 Kimi-Linear(KimiLinearForCausalLM),其 MTP 与 drafter 变体复用同一个 base builder。评论中已验证的复现环境为 backport 镜像 lazymio/vllm-backport、PyTorch 2.13.0+cu130、NVIDIA CMP170HX/SM80、eager 模式、无投机解码;这不是官方 vLLM 0.12.0 包含相同模型代码的声明。

最快修复方案:暂无确认的一步修复方案。#51483 只修复了 KimiK3KDAMetadataBuilder 子类,base builder 及其他模型不受该改动影响。评论中验证过的关键实验是:把共享非投机路径的 split 改为 split_decodes_and_prefills(m, decode_threshold=1, treat_short_extends_as_decodes=False) 后,builder 报告 0 个 decode、1 个 prefill、has_initial_state=[False],两种 poison 都得到正确输出 2.828125。但该一行改动与尚未合并的 #51565 并不相同,可优先作为验证手段尝试,而不是正式替代方案。

注意事项:评论中已验证的一行改动会把“续接的短 prefill”也走 chunk 计算路径,与 #51565 更精细的 no_prior_state/padding/graph-staging 逻辑不同。GPU FULL-graph replay、已建立的 decode、续接 prefill、混合请求以及其他 GDN 模型消费者均未在该独立复现中验证;该受控 poison 实验本身也没有真正触发调度器页面重分配或证明私有数据被提取。Issue 作者表示已直接验证误分类与无条件 decode 读取,但尚未跑出端到端生成、证明回收 block 仍持有前一请求状态,因此“状态继承”应视为代码路径推论而非已观测失败。

问题场景

在使用 vLLM 运行基于 GatedDeltaNet 的线性注意力模型时触发,例如 Qwen3-Next、Qwen3.5、OLMo-hybrid、InternS2-Mobius、Bailing-MoE-v3,以及独立的 Kimi-Linear(KimiLinearForCausalLM,其 KimiGatedDeltaNetAttention 没有像 Kimi-K3 那样覆写后端)。这些模型及其 MTP、drafter 变体共用同一个 GDNAttentionMetadataBuilder

触发条件是某个调度步的批次全部由单 token 行组成,且其中至少包含一条首次 chunk(num_computed_tokens == 0)。在默认 flag 下这些模型解析为 mamba_cache_mode="align",此时一个与 decode 一同被接纳的普通单 token prompt 就是可达的触发路径;budget 截断的首 chunk 还额外受 _mamba_block_aligned_split 限制,只有当 align 关闭或 long_prefill_token_threshold / max_num_batched_tokens 小于 mamba block size 时才会降到 1 个 token。只要批次里存在任何更长的行,就不受影响。

报错原文

[Bug]: GatedDeltaNet metadata builder classifies a stateless first chunk as a decode (same root cause as #51483)

该 Issue 描述的并非异常堆栈,而是元数据构建阶段的逻辑错误:真实 GDNAttentionMetadataBuilder.buildseq_lens=[100, 50, 1]query_lens=[1, 1, 1]is_prefilling=[False, False, True] 下返回 3 个 decode、0 个 prefill、has_initial_state=None,与 #51483 修复前 Kimi 子类的错误分类一致。

原因分析

最可能的原因(Issue 中有明确代码路径依据):

  • gdn_attn.py:213 调用 split_decodes_and_prefills(m, decode_threshold=1) 时使用默认 treat_short_extends_as_decodes=True,因此所有 query_len <= 1 的行都被算作 decode,包括从未被 forward 过的行(num_computed_tokens == 0)。
  • has_initial_state 只在 num_prefills > 0 时计算(gdn_attn.py:391-392)。全单 token 批次全是 decode,num_prefills == 0,于是 has_initial_stateNone
  • decode 路径读取 recurrent state 槽时没有 has_initial_state 门控、也没有零填充。OLMo 层在 num_decodes > 0 分支调用 fused_recurrent_gated_delta_rule(initial_state=ssm_state, ssm_state_indices=non_spec_state_indices_tensor, ...)olmo_gdn_linear_attn.py);Qwen 层在相同分支调用 fused_sigmoid_gating_delta_rule_update / fused_recurrent_gated_delta_rule_packed_decodeinitial_state=ssm_state,同一分支的 conv 更新(causal_conv1d_update)也以同样方式读取槽;prefill 路径则按 has_initial_state 门控,会清零无状态槽。
  • Mamba 风格 state page 在重新分配时不会清零:mamba spec 不在 worker 侧的 zero-on-allocation 集合中(single_type_kv_cache_manager.py:86)。

综合起来,一条刚被接纳、其 block 之前归另一条请求所有的请求,会读到那条请求遗留的 recurrent 与 conv 状态,而不是零状态。这本身是正确性缺陷,也是一条受污染(NaN)状态块被新请求继承的路径。 Issue 作者已直接验证误分类与无条件 decode 读取,但尚未跑出端到端生成来证明回收 block 仍持有前一请求状态。

另外,评论中给出的 model-free poison 证据显示:真实 builder 看到 query length 1、computed context 0、is_prefilling=True 时选中了一个真实 state slot,而原 builder 报告 1 个 decode、0 个 prefill、has_initial_state=None。用合成投影和被 poison 为 2 或 6 的 cache slot 调用 Glm5NextLinearAttention._forward,输出分别为 7.875 和 18.0,而正确 fresh-state 输出应为 2.828125,说明仅改变逻辑上新 cache 的旧内容即可改变输出与结果 cache。

这与 #49918 / PR #47123 是不同缺陷。

环境排查

  • 确认 vLLM 版本与提交,检查是否包含 #51483 的改动;该改动只覆盖 KimiK3KDAMetadataBuilder 子类,不覆盖 base builder。
  • 确认运行模型是否属于受影响列表:Qwen3-Next、Qwen3.5、OLMo-hybrid、InternS2-Mobius、Bailing-MoE-v3、独立 Kimi-Linear 及其 MTP/drafter 变体。
  • 确认 mamba_cache_mode 取值,默认 flag 下这些模型为 "align"
  • 确认是否出现全单 token 批次、且含 num_computed_tokens == 0 的首次 chunk。
  • 评论中验证环境参考:PyTorch 2.13.0+cu130、NVIDIA CMP170HX/SM80、eager、无投机解码、backport 镜像 lazymio/vllm-backport。不要将其直接等同于官方 vLLM 0.12.0。

解决步骤

  1. 复现并确认分类错误:在目标模型上运行单 token 全 decode 批次,观察 GDNAttentionMetadataBuilder.build 是否返回 0 prefills、has_initial_state=None,同时 is_prefilling 中却含 True
  2. 检查共享非投机 split 的调用:gdn_attn.pysplit_decodes_and_prefills(m, decode_threshold=1) 是否使用了默认 treat_short_extends_as_decodes=True
  3. 作为验证手段(可优先尝试,非正式修复),把该调用改为 split_decodes_and_prefills(m, decode_threshold=1, treat_short_extends_as_decodes=False),观察 builder 是否变为 0 decodes、1 prefill、has_initial_state=[False],且 poison 输出回到正确值。
  4. 不要直接把这一行改动当作最终方案:它与尚未合并的 #51565 不同,会把续接短 prefill 也送进 chunk 计算路径。若需正式修复,应等待或跟进 #51565 的 no_prior_state/padding/graph-staging 逻辑。
  5. 若目标部署涉及 Kimi-K3 的 KDA 路径,确认 #51483 已合并;注意它不覆盖本 Issue 的 base builder 范围。

验证方法

在单 token、eager、无投机的受控场景下,用合成投影和被 poison 的 cache slot 调用对应层的 _forward,检查输出是否始终为 fresh-state 参考值(评论中为 2.828125),且完整 output/conv/recurrent cache 与独立 dyadic 参考一致,包括未选中槽保持不变。同时确认 builder 输出为 0 decodes、1 prefill、has_initial_state=[False]

评论中的复现脚本 SHA256 为 e3aaf38b824cc95ab3aa8bce88fcf9e7aca840b89e6472f05a5b58ac9f57ae0f,保存为 upstream-repro-gdn.py,在所述 backport 环境中运行 OMP_NUM_THREADS=2 MKL_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 MAX_JOBS=2 python3 upstream-repro-gdn.py。该脚本只覆盖 fresh 单 token eager 场景,不覆盖 GPU FULL-graph replay、已建立 decode、续接 prefill、混合请求或其他 GDN 模型消费者。

参考来源

vllm-project/vllm #51562

相关:vllm-project/vllm PR #51565(尚未合并的修复方向)

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 24906

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注