Prhub

#33561 [Model] Support Ling-3.0-flash (BailingMoeV3)

原始 PR 作者 JustinTong0323 合并时间 2026-08-27 08:27 文件变更 76 提交数 85 评论 11 代码增减 +5188 / -319

执行摘要

新增 Ling-3.0-flash 混合 MoE 模型支持,含 DSPARK 投机解码

PR body 明确目标是 "Day-0 support for inclusionAI/Ling-3.0-flash":在新模型发布当天让 sglang 能直接加载推理。Ling-3.0-flash 是混合架构(KDA 线性注意力 + MLA,512-expert MoE),且官方 checkpoint 同时发布 BF16/FP8/INT4/MXFP4 四种格式,因此 Day-0 意味着模型、投机解码、量化三个维度都要就绪;为此共享的 KDA 接口必须支持 safe-gate,DSPARK 需要为 CUDA Graph 折叠贪心草稿步。

值得精读,尤其建议关注三点设计:metadata_glue_graph 以"capture 设备算子 + 快照恢复宿主对象"消除 bs=1 spec decode 的 host 开销,及其对 host-fed plan 的静默失败防线;KDA safe-gate 如何在共享接口上以"不支持则回退 Triton 内核"的方式向后兼容;routed scaling 双应用这类跨 helper 交互 bug 的排查方法。对只想使用模型的人,直接参考 PR body 的 benchmark 表与部署 recipe(#34363)。

讨论亮点

Fridge003 在 review 中给出 DISMISSED 结论:"This PR is really messy. We need some cleaning before merge",这是最大争议点。yuan-luo 回应解释了 mess 的结构性根源:Ling-3.0-flash 是混合 KDA/MLA MoE,KDA safe-gate 必须穿过公共接口,DSPARK 图折叠影响框架层,四种量化 checkpoint 共享 MoE/quant 改动,核心部分无法独立验证。JustinTong0323 补充量化论证:约 60% 是自包含新模型代码,共享改动约 75% 是 gated 新增或语义保持重构,并列出 6 处行为变更与证据表;同时定位了 DSV4-FP4 accept-length 回归的根因(MXFP4 路径 routed scaling 双应用),在 ee800a63 修复。

实现拆解

  1. 核心模型与配置:新增 python/sglang/srt/models/bailing_moe_v3.py(约 1982 行),实现 BailingMoeV3ForCausalLMDsV3MLABailingMoELinearDecoderLayerDsV3MLA 继承 DeepseekV2AttentionMLA,通过 _forward_gated / _apply_gated 将 KDA 层的 safe-gate 下界并入注意力输出,gate 以追加 inner_state 元组的形式传给各 forward_core 实现;KimiDeltaAttention 被参数化供 V3 复用。该文件是 Theta fork 约 37 个内部 commit 的累积移植。model_config.py 同步接入 bailing_hybrid,并以 use_kda 区分 V2.5(lightning)与 V3(KDA)。
  2. KDA 注意力后端与 safe-gate 传播python/sglang/srt/layers/attention/linear/kda_backend.pydecode / extend 收到 lower_bound 且当前内核不支持 safe-gate 时回退到 Triton 内核,保证旧内核向后兼容;新增 _can_run_fused_chain_verify 门控,为 MTP chain-verify 选择 fused_kda_conv_gating_verify 融合内核(替代 transpose-copy + conv1d + 递推序列),并修复其 conv_state 必须是 stride 1 连续布局的调用约束。
  3. 投机解码bailing_moe_nextn.py_is_bailing_moe_v3_config 让 NEXTN 草稿层在 V3 时选用 BailingMoeV3DecoderLayer 并透传 num_fused_shared_expertsdspark.py 注册 LingDSparkModel,为 VanillaMarkov 新增 sample_block_greedy_fused(单内核完成 bias-dot + add + argmax),run_markov_block 增加 collect_corrected 开关;同时修复 TARGET_VERIFY 后 KDA/Mamba 状态提交,并在 pool_configurator.py 中修正 DSA 草稿 KV 池按真实 per-token 成本计算以避免 OOM。新增 metadata_glue_graph.py 将每个 replay 的 attention metadata prep 捕获为按 key 复用的 CUDA Graph。
  4. 量化与精度修复:覆盖 BF16/FP8/compressed-tensors INT4/native MXFP4,分别路由到 Marlin、Triton WNA16、FlashInfer CUTLASS 等后端;修复 native MXFP4 路径 routed scaling 被应用两次(折入 topk_weightsmaybe_fuse_routed_scale_and_shared_add 又应用一次)导致的 DSV4-FP4 accept-length 回归;修复 Blackwell INT4 图捕获、MXFP4 CUTLASS 忽略 Bailing expert clamp(gemm1_clamp_limit)等问题。
  5. 解析器、测试与 CI:新增 python/sglang/srt/function_call/ling3_detector.py 并在 reasoning_parser.py 注册,支持 ling3 推理与流式工具调用解析。测试覆盖 fused KDA verify 与 unfused 参考的一致性、full GSM8K DSpark 端到端(4x B200 TP4)、shared-experts fusion-gate 布局约束(INT4 混合布局禁融合)、compressed-tensors WNA16 Blackwell 自动选择、parser 流式工具调用等,并注册进 base-b / extra / ROCm CI。
文件 模块 状态 重要度
python/sglang/srt/models/bailing_moe_v3.py 模型实现 added 9.36
python/sglang/srt/model_executor/runner/metadata_glue_graph.py 图捕获 added 8.81
python/sglang/srt/models/bailing_moe_nextn.py 模型实现 modified 8.21
python/sglang/srt/models/dspark.py 投机解码 modified 8.11
python/sglang/srt/layers/attention/linear/kda_backend.py 注意力后端 modified 7.93
python/sglang/srt/layers/attention/flashinfer_mla_backend.py 注意力后端 modified 7.45
python/sglang/srt/parser/reasoning_parser.py 解析器 modified 7.6
test/registered/kernels/test_fused_kda_conv_recurrent_verify.py 内核测试 added 7.67

关键符号

BailingMoeV3ForCausalLM.forward DsV3MLA._forward_gated DsV3MLA._apply_gated BailingMoeV3ForCausalLM.determine_num_fused_shared_experts BailingMoeV3ForCausalLM.resolve_nextn_layer_id BailingMoeV3ForCausalLM.rewrite_nextn_weight_name BailingMoEModelNextN.__init__ VanillaMarkov.sample_block_greedy_fused MetadataGlueGraph.run KDAAttnBackend._can_run_fused_chain_verify Ling3Detector.detect_and_parse

关键源码片段

python/sglang/srt/model_executor/runner/metadata_glue_graph.py core-logic

新增通用机制:把 spec decode 每 replay 的 attention metadata prep 捕获为按 key 复用的 CUDA Graph,消除 host 侧 op soup 开销,是本 PR 对框架层最有通用价值的优化。

# MetadataGlueGraph:把 spec decode 的 metadata prep 捕获成 CUDA Graph
# decode_cuda_graph_runner.load_batch 在每个 replay 都会跑一遍
# init_forward_metadata_out_graph,bs=1 spec decode 下这是几十个小算子,
# host 调度开销主导了阶段间缝隙;按 replay key 捕获一次即可把每步开销
# 压到一次图发射。关键约束:Python 侧分支在 key 内必须恒定。class MetadataGlueGraph:
    NUM_WARMUP = 2
​
    def __init__(self, device):
        self.device = device
        self.disabled = False
        self._states: Dict[Any, dict] = {}
        self._capture_stream = None
​
    def reset(self):
        # 丢弃已捕获图:runner 重捕时静态缓冲区与后端状态可能已重建
        self._states.clear()
​
    @staticmethod
    def _leaves(attn_backend) -> List[Any]:
        # 需覆盖主后端及其挂载的后端列表(例如 MLA + KDA 混合验证)
        backends = [attn_backend]
        if attn_backend.attn_backend_list is not None:
            backends.extend(attn_backend.attn_backend_list)
        return backends
​
    def run(self, attn_backend, fb_view, key) -> None:
        st = self._states.get(key)
        if st is None:
            st = {"warmups": 0, "graph": None, "meta": None}
            self._states[key] = st
​
        if st["graph"] is not None:
            # 快照的 forward_metadata 对象每次 replay 前重新挂回后端,
            # 图只重放刷新这些对象所指向 tensor 的设备算子
            for backend, metadata in st["meta"]:
                backend.forward_metadata = metadata
            st["graph"].replay()
            return
​
        if st["warmups"] < self.NUM_WARMUP:
            st["warmups"] += 1
            attn_backend.init_forward_metadata_out_graph(fb_view)
            return
​
        if self._capture_stream is None:
            self._capture_stream = torch.cuda.Stream()
        graph = torch.cuda.CUDAGraph()
        try:
            # 捕获只记录不执行,之后需要 replay 一次完成本次 prep
            with torch.cuda.graph(graph, stream=self._capture_stream):
                attn_backend.init_forward_metadata_out_graph(fb_view)
        except Exception:
            logger.warning(
                "Metadata glue-graph capture failed for key %s; falling back "
                "to eager metadata prep permanently.",
                key,
                exc_info=True,
            )
            self.disabled = True
            attn_backend.init_forward_metadata_out_graph(fb_view)
            return
​
        # 捕获 Python 副作用(各后端 forward_metadata 对象)以便 replay 前恢复
        st["meta"] = [(b, b.forward_metadata) for b in self._leaves(attn_backend)]
        st["graph"] = graph
        graph.replay()
python/sglang/srt/models/bailing_moe_nextn.py data-contract

NEXTN(MTP)草稿模型的关键分支:用 use_kda 区分 Ling-V3 与 V2.5,并为 V3 选择对应的 KDA + gated-MLA 解码层与融合共享专家透传。

# bailing_moe_nextn.py:_is_bailing_moe_v3_config 用 use_kda 区分两代
# 混合架构 checkpoint;NEXTN 草稿层据此选择 V2.5 的 lightning 解码层还是
# V3 的 KDA + gated-MLA 解码层,并把融合共享专家数透传给草稿层。def _is_bailing_moe_v3_config(config: PretrainedConfig) -> bool:
    # use_kda 由 BailingHybridConfig 根据是否存在 short conv 设置,
    # 恰好是两代模型的分界点
    return config.model_type == "bailing_hybrid" and config.use_kda
​
​
class BailingMoEModelNextN(nn.Module):
    def __init__(
        self,
        config: PretrainedConfig,
        quant_config: Optional[QuantizationConfig] = None,
        prefix: str = "",
        num_fused_shared_experts: int = 0,
    ) -> None:
        ...
        self.is_hybrid = (
            hasattr(config, "model_type") and config.model_type == "bailing_hybrid"
        )
        if self.is_hybrid:
            config.attention_type = 1
            decoder_layer_cls = BailingMoELinearDecoderLayer # V2.5 默认路径
            decoder_kwargs = {
                "quant_config": quant_config,
                "layer_id": 0,
                "is_nextn": True,
                "prefix": add_prefix(f"layers.{config.num_hidden_layers}", prefix),
            }
            if _is_bailing_moe_v3_config(config):
                # Ling-V3 换用 V3 的 KDA + gated-MLA 解码层,
                # 并透传融合共享专家数,保持与主模型 fusion-gate 一致
                decoder_layer_cls = BailingMoeV3DecoderLayer
                decoder_kwargs["num_fused_shared_experts"] = num_fused_shared_experts
            self.decoder = decoder_layer_cls(config, **decoder_kwargs)
        else:
            self.decoder = BailingMoEBlock(
                config,
                0,
                quant_config=quant_config,
                prefix=add_prefix("decoder", prefix),
            )
python/sglang/srt/models/dspark.py core-logic

DSpark 投机解码支持的核心:注册 LingDSparkModel、新增 sample_block_greedy_fused 融合贪心采样,并让 run_markov_block 支持跳过 corrected_logits 收集以适配图折叠。

# dspark.py:VanillaMarkov.sample_block_greedy_fused 用单个内核
# (MarkovGreedyStep)完成 bias-dot + add + argmax,替代 GEMV + add +
# 两遍 argmax,且不物化全词表 bias / step logits,是 DSPARK 支持
# CUDA Graph 图折叠的关键。def sample_block_greedy_fused(
    self,
    base_logits: torch.Tensor,
    *,
    first_prev_tokens: torch.Tensor,
) -> Optional[torch.Tensor]:
    if not base_logits.is_cuda:
        # 非 CUDA 环境回退到普通 sample_block
        return None
    batch_size, proposal_len = base_logits.shape[:2]
    if proposal_len == 0:
        return torch.empty(
            batch_size, 0, dtype=torch.long, device=base_logits.device
        )
    sampled_tokens = []
    prev_tokens = first_prev_tokens.long()
    for step_idx in range(proposal_len):
        prev_embeds = self.get_prev_embeddings(prev_tokens)
        # 融合内核直接根据 base_logits、prev embedding 与 w2 权重产出贪心 token
        prev_tokens = MarkovGreedyStep.execute(
            base_logits=base_logits[:, step_idx, :],
            prev_embeds=prev_embeds,
            w2_weight=self.markov_w2.weight,
        )
        sampled_tokens.append(prev_tokens)
    return torch.stack(sampled_tokens, dim=1)

评论区精华

PR 是否应拆分再合并 设计

Fridge003 在 review 中给出 DISMISSED 结论:"This PR is really messy. We need some cleaning before merge"。yuan-luo 回应解释 mess 的结构性根源:Ling-3.0-flash 是混合 KDA/MLA MoE,safe-gate 必须穿过公共接口,DSPARK 图折叠影响框架层,四种量化共享 MoE/quant 改动。JustinTong0323 补充量化论证:约 60% 是自包含新模型代码,共享改动约 75% 是 gated 新增或语义保持重构,并列出 6 处行为变更与证据表。

结论:维持单 PR 合并,但作者承认代码库 messy;后续通过 #36584 等修复持续打磨。 · 已解决

DSV4-FP4 spec accept-length 回归根因 正确性

JustinTong0323 定位:native MXFP4 Marlin / FlashInfer-Cutlass MoE 路径上 routed scaling 被应用两次——fc4fb07 折入 topk_weights 后,maybe_fuse_routed_scale_and_shared_add 又应用一次,等效 1.5x 两遍,DSV4 目标与 NEXTN 草稿都命中,accept length 2.7-2.78 低于 2.8 门槛。

结论:ee800a63 修复:helper 在已折叠折入 topk 时不再重复应用,回归验证通过。 · 已解决

MXFP4 CUTLASS 路径丢弃 expert clamp 正确性

2f85329efe 修复:FlashInfer CUTLASS MXFP4 runner 硬编码 Swiglu,忽略 Bailing 的 post-SiLU clamp(layers 35-41,expert_swiglu_limit_list=4),现路由到 clamp tensor 并选 SwigluStep,移植自 inclusionAI fork;对无 clamp 模型 inert。

结论:已合入 PR 头部,23 行 / 2 文件修复。 · 已解决

metadata glue graph 的静默失败契约 设计

metadata_glue_graph.py 文档明确:capture 只记录设备算子,host 侧写 plan 的 DFlash family fast verify 若被 glue,capture 成功但 accept length 塌缩且无报错,因此 decode_cuda_graph_runner 对该类配置强制禁用。

结论:以保守禁用换取安全,设计被合入方接受。 · 已解决

Ling3 parser 默认值与 thinking-on 语义对齐 question

commit 5f9b16fa 调整 Ling3 reasoning 默认值,使其与模板的 thinking-on 语义一致,避免解析器默认行为与模型训练模板不匹配。

结论:已修复并合入。 · 已解决

风险与影响

1) 共享路径回归:6 处改变现有模型行为的改动集中在 MoE 量化与融合路径,DeepSeek 系模型在回归半径内,DSV4-FP4 已在 PR 内实际触发 accept-length 回归。
2) metadata_glue_graph 静默失败:只捕获设备算子,host 侧写 plan 的 DFlash family 若被 glue 会 accept length 塌缩且无报错,只能靠调用方强制禁用。
3) 长分支合并:85 个 commit 多次 merge main,冲突集中在 fused_moe_triton_kernels、kda_triton、flashinfer_mla_backend、compressed_tensors;后期仍在修 bring-up 问题,多后端 x 多量化组合验证深度有限。
4) 部署约束:DSPARK 路径依赖 decode CUDA Graph,文档明确所有验证未使用 --disable-cuda-graph。

用户获得 Ling-3.0-flash Day-0 推理,H200/B200 上 BF16/FP8/INT4/MXFP4 全格式可用,DSPARK 在 4x B200 TP4 full GSM8K 96.66%、0.00% error rate。系统层面,KDA safe-gate、fused chain-verify、metadata glue graph、融合贪心 Markov、router-gate matvec 单发射等被设计为通用机制,惠及后续混合线性注意力模型与宽专家 MoE。团队层面,单 PR 与拆分之争暴露了大型模型移植的组织问题,合入后仍需 #36584 等后继修复持续打磨,维护成本偏高。

共享路径行为变更 量化双重缩放回归 glue graph 静默失败隐患 长分支合并冲突 DSPARK 依赖 CUDA Graph

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论