Prhub

#37307 fix(unified-memory): forward the KV-index translator through every wrapper backend

原始 PR 作者 caihuali95 合并时间 2026-09-01 10:55 文件变更 8 提交数 3 评论 0 代码增减 +163 / -1

执行摘要

修复 wrapper 后端未转发 KV 翻译器致 MLA 前缀缓存读错

PR body 明确指出:AttentionBackend.kv_index_translator 是默认为 None 的类属性,wrapper 不转发内部后端的副本时会被误判为“无需翻译”,ForwardBatchDeepseekMHAMixin.prepare_chunked_kv_indices 只有在 src is not None 时才做翻译,于是把虚拟 id 直接交给 kernel,gather 读错行且不报错。触发条件需要同时满足 --enable-unified-memory、MLA 后端(fa3/flashinfer/flashmla)和 radix cache,Kimi-Linear-48B 的 gsm8k 从 0.89–0.92 跌到 0.05–0.31(六个单元全部命中),而 Triton 和 chunked-prefill 单元保持正确。

值得精读。核心价值不在 1 行转发,而在两点设计:一是对“静默正确性损坏”的防御策略——启动断言让错误在部署时暴露而非推理时污染结果;二是对象图测试如何通过“只给正确来源携带 translator”来精确验证转发来源,并用 AST 推导保证新 wrapper 自动纳入检查。该模式可推广到其他带默认 None 类属性、需要透传内部状态的包装器场景。

讨论亮点

PR 没有公开 review 评论,但提交历史记录了两次重要的测试方案迭代(合入者 ch-wan 的提交说明):

c2e64584:"The scan was file-scoped: _iter_sources() yields whole files and all three regexes .search() them, so one forwarding class anywhere in a file covers every wrapper in it. hybrid_linear_attn_backend.py holds four AttentionBackend subclasses and one forward -- putting the bug back while leaving any other self.kv_index_translator = in that file keeps the test green."

即最初的源码正则扫描按文件粒度判断,只要一个文件里有任意一个类转发就会放过同文件的其他 wrapper,恰好放过了 bug 所在的 hybrid_linear_attn_backend.py。该版被替换为按类的 AST 推导 + 真实对象图断言。

e202516b:"Review follow-up on the two suggestions. The e2e cell only builds HybridLinearAttnBackend, via Kimi-Linear. The other six wrappers had nothing per-PR: assert_backends_carry_translator fires only when the wrapper is actually constructed..."

即 review 提出两点:e2e 单元只覆盖到 Kimi-Linear 实际构建的 HybridLinearAttnBackend,其余 6 个 wrapper 没有按 PR 的守护;最终补上全 wrapper 对象图测试,用“只让正确来源携带 translator、其余为 None”的方式让错误来源复制必然失败。

实现拆解

  1. 定位并修复 4 个 wrapper 的转发缺失hybrid_linear_attn_backend.pyfull_attn_backend 转发(与已转发的 token_to_kv_pool / req_to_token_pool 并列);hybrid_attn_backend.pymodel_runner 转发(它本来就从 runner 取 pool);dots_hybrid_backend.py 两处:DotsSWAMLAAttnBackendbackend 转发、DotsHybridAttnBackendswa_backend 转发;minimax_sparse_backend.pydense_backend 转发。每处一行,与已有 pool 转发保持同一来源,避免取错一侧。
  2. 新增启动守卫kv_index_translator.py 增加 assert_backends_carry_translator(backends),当 is_translating 为真时逐一断言 backend.kv_index_translator is self,否则直接 raise,把“静默漏翻译”变成“启动即失败”。model_runner.pyinit_attention_backendsbuild_attention_backends 之后立即对 attn_backenddecode_attn_backend 执行该断言。守卫仅覆盖主路径的两个 backend,decode_attn_backend_group 未覆盖。
  3. 对象图级单元测试test_kv_translate_ownership.py 新增 _derive_wrapper_names(),用 AST 推导“构造参数带另一个 AttentionBackend 类型注解的子类”集合;_build_wrappers() 为每个 wrapper 构造真实实例,只让必须提供 translator 的 inner 携带它、其余 inner 携带 NoneTestWrapperBackendsForwardTranslatorassertIs 校验转发来源正确,并用 test_every_wrapper_is_constructed_here 保证推导集合与实例集合一致,新 wrapper 出现当天即被覆盖。
  4. e2e 回归测试test_kimi_linear_models.py 新增 TestKimiLinearUnifiedMemory,以 Kimi-Linear-48B + --enable-unified-memory + 默认注意后端跑 gsm8k(阈值 0.88),est_time 从 600 调到 900,复用原有 2-GPU 任务。夜间 test_kimi_linear_unified_memory.py 保留原副本,因其 H100 runner 解析的默认后端不同。
文件 模块 状态 重要度
python/sglang/srt/mem_cache/kv_index_translator.py KV 翻译器 modified 6.58
test/registered/unit/layers/attention/test_kv_translate_ownership.py 翻译所有权 modified 7.27
python/sglang/srt/model_executor/model_runner.py 模型执行 modified 5.43
python/sglang/srt/layers/attention/hybrid_linear_attn_backend.py 混合注意力 modified 4.75
python/sglang/srt/layers/attention/hybrid_attn_backend.py 混合注意力 modified 4.75
python/sglang/srt/layers/attention/dots_hybrid_backend.py DOTS 混合 modified 4.89
python/sglang/srt/layers/attention/minimax_sparse_backend.py 稀疏注意力 modified 4.75
test/registered/models_e2e/test_kimi_linear_models.py Kimi 模型 modified 5.75

关键符号

assert_backends_carry_translator init_attention_backends _derive_wrapper_names _build_wrappers test_wrappers_forward_the_translator test_every_wrapper_is_constructed_here

关键源码片段

python/sglang/srt/mem_cache/kv_index_translator.py core-logic

新增 `assert_backends_carry_translator` 启动守卫,这是把静默漏翻译变成启动失败的机制核心;守卫断言 backend 携带的翻译器必须是 runner 的同一个实例。

# python/sglang/srt/mem_cache/kv_index_translator.py
# 新增的启动守卫:unified 池下所有可到达的 backend 必须携带本翻译器实例,
# 否则说明某个 wrapper 没有转发内部后端的 translator,直接让启动失败。
def assert_backends_carry_translator(self, backends) -> None:
    """启动守卫:under the unified pool,一次 forward 可达的每个 backend
    都必须携带 THIS translator。"""
    # 非翻译池(静态度量池)不需要翻译,直接放行,保证旧路径零开销。
    if not self.is_translating:
        return
    for backend in backends:
        if backend is None:
            continue
        # 用 is 而非 ==,确保是同一个翻译器对象,而不是某个等价副本。
        assert backend.kv_index_translator is self, (
            f"{type(backend).__name__} does not carry the runner's "
            "KVIndexTranslator. A backend (or wrapper) reachable under "
            "--enable-unified-memory must forward `kv_index_translator`, or "
            "read-index producers silently skip the virtual->kernel-facing "
            "translation."
        )
test/registered/unit/layers/attention/test_kv_translate_ownership.py test-coverage

承载最关键的防回归设计:AST 推导 wrapper 集合 + 真实对象图验证转发来源;按类而非按文件扫描,修复了源码扫描测试对 hybrid_linear_attn_backend.py 的漏检。

# test/registered/unit/layers/attention/test_kv_translate_ownership.py
# 对象图测试的核心:每个 wrapper 只让“必须提供 translator 的内部后端”携带副本,
# 其余内部后端携带 None。这样任何从错误来源复制 translator 的 wrapper 都会拿到 None,
# 断言必然失败——静态扫描看不到这种来源错误。
def _build_wrappers(translator):
    """每个 wrapper 构建一个真实实例,只有必须提供 translator 的 inner 携带它,
    其余 inner 携带 None,从而精确验证转发来源。"""
    carrier = _Inner(translator)
    # HybridAttnBackend 在 __init__ 中读取 spec bag,而 spec bag 在未启动的
    # server 里不可用,所以这里覆盖 server_args 指定投机解码模式为 decode。
    with get_context().override_server_args(speculative_attention_mode="decode"):
        hybrid = HybridAttnBackend(_Runner(translator), _Inner(), _Inner())
    return {
        "DotsSWAMLAAttnBackend": DotsSWAMLAAttnBackend(carrier),
        "DotsHybridAttnBackend": DotsHybridAttnBackend(_Inner(), carrier),
        # HybridAttnBackend 从 runner 取 translator,其余 wrapper 从 inner 取。
        "HybridAttnBackend": hybrid,
        "HybridLinearAttnBackend": HybridLinearAttnBackend(carrier, _Inner(), [0]),
        "ShortConvHybridAttnBackend": ShortConvHybridAttnBackend(carrier, _Inner(), [0]),
        "MiniMaxHybridAttnBackend": MiniMaxHybridAttnBackend(carrier, _Inner(), [0]),
        "TboAttnBackend": TboAttnBackend(carrier, [_Inner()]),
    }
​
​
class TestWrapperBackendsForwardTranslator(CustomTestCase):
    """回归测试:wrapper 若不转发内部后的 translator,读索引生产者会静默跳过
    虚拟地址到内核地址的翻译。"""
​
    def test_every_wrapper_is_constructed_here(self):
        # AST 推导出的 wrapper 集合必须与实例集合一致,新的 wrapper 出现当天即被检查。
        self.assertEqual(
            _derive_wrapper_names(),
            set(_build_wrappers(object())),
            "a wrapper backend has no instance in _build_wrappers; add one so "
            "its translator forwarding is checked",
        )
​
    def test_wrappers_forward_the_translator(self):
        translator = object()
        for name, wrapper in _build_wrappers(translator).items():
            with self.subTest(wrapper=name):
                # is 断言保证转发的是同一个对象,而非某个 None 或别的副本。
                self.assertIs(wrapper.kv_index_translator, translator)
python/sglang/srt/model_executor/model_runner.py data-contract

在 `init_attention_backends` 构建后端后立即执行启动守卫,是让静默错误在启动阶段暴露的接入点;仅 3 行增量。

# python/sglang/srt/model_executor/model_runner.py
# init_attention_backends 的收尾:build_attention_backends 之后立即校验翻译器契约。
# 时机放在 cuda graph capture 之前,保证 capture 用的 backend 链已满足翻译断言。
def init_attention_backends(self):
    # ... 前面是 resolve_attention_backend_strs 与 build_attention_backends ...
    backends = build_attention_backends(model_runner=self)
    self.attn_backend = backends.attn_backend
    self.decode_attn_backend = backends.decode_attn_backend
    self.decode_attn_backend_group = backends.decode_attn_backend_group
​
    # 启动守卫:unified 池下可达的每个后端都必须携带本 runner 的翻译器,
    # 否则 wrapper 漏转发会在推理时静默读错 KV。只覆盖主后端槽位,
    # decode_attn_backend_group 内部的后端不在本次校验范围。
    self.kv_index_translator.assert_backends_carry_translator(
        [self.attn_backend, self.decode_attn_backend]
    )

评论区精华

源码扫描测试的粒度缺陷与对象图测试替代方案 测试

提交 c2e64584 说明:最初的扫描按整个文件搜索,`hybrid_linear_attn_backend.py` 内有 4 个 AttentionBackend 子类但只有 1 个转发,只要文件内任意类带 `self.kv_index_translator =` 就通过,无法发现具体哪个 wrapper 漏转发。提交 e202516b 进一步说明 review 的两点建议:e2e 单元只覆盖 Kimi-Linear 构建的 HybridLinearAttnBackend,其余 6 个 wrapper 没有按 PR 的守护,需要对象级测试覆盖全部 wrapper。

结论:接受建议:改为按类(AST 推导构造函数参数带 AttentionBackend 类型注解的子类)推导 wrapper 集合,并用真实对象图断言 `assertIs(wrapper.kv_index_translator, translator)`;同时保留 e2e 单元作为真实链路回归。 · 已解决

风险与影响

  1. 启动守卫覆盖不完整init_attention_backends 只对 attn_backenddecode_attn_backend 执行断言,decode_attn_backend_group 内的其他后端未被检查,若未来有 wrapper 只进入 group 而未进主槽位,仍可能漏检。
  2. 断言误伤风险:守卫仅在 is_translating 为真时生效;若某个可达后端在 unified 池下本就不应携带翻译器(但语义上“需要翻译”),启动会直接失败。当前四个 wrapper 都从正确来源转发,风险可控,但对未来新 wrapper 是行为变更(从静默错误变为启动失败)。
  3. 时序依赖HybridAttnBackendmodel_runner.kv_index_translator 取值,需保证该属性在 backend 构建前已初始化;本 PR 在 init_attention_backends 内使用,时序正确,但若其他构造路径提前构建 backend 则可能拿到 None
  4. 测试对象图耦合_build_wrappers 直接 new 各 wrapper 的 __init__,对构造参数变更(如新增必填参数)敏感,需要在新增参数时同步更新测试,否则测试本身编译失败即可提示维护。

对用户:开启 --enable-unified-memory 且使用 MLA 后端(fa3/flashinfer/flashmla)与 radix cache 的场景(Kimi-Linear、DeepSeek 系混合注意力模型)从“静默输出错误结果”恢复为正确输出,且新增启动断言会在配置有误时快速失败。对系统:init_attention_backends 增加一次全量后端遍历断言,开销可忽略;失败模式从“推理错误”提前到“启动报错”。对团队:建立了一个可复用的“包装器后端契约校验”测试范式(AST 推导 + 对象图实例校验),未来新增 wrapper 会立即被测试覆盖;同时 e2e 增加了 300 秒的 Kimi-Linear 服务启动开销。

静默正确性风险 启动守卫覆盖不完整 多条件组合触发

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论