Prhub

#51655 Add Muse Glimmer model support

原始 PR 作者 xianbaoqian 合并时间 2026-08-14 12:28 文件变更 21 提交数 9 评论 40 代码增减 +4333 / -16

执行摘要

新增 Muse Glimmer VLM 支持:ATEM 工具解析、DFlash 投机解码

PR body 明确说明 Muse Glimmer 的协议特殊性:该模型不输出 JSON 工具调用、不包裹推理内容在 <think> 标签中,每一轮都是 channel-scoped 消息序列,因此必须配套 --tool-call-parser muse_glimmer--reasoning-parser muse_glimmer 一起使用,并强制 skip_special_tokens=False,否则标记被剥离后两个通道会全部塌缩进 content。Draft head 复用现有 qwen3_dflash 实现(同架构、同张量),在目标模型 residual stream 的 [1, 13, 25, 37, 49] 层读取并每 forward 预测 16 槽 block。社区在 Issue 评论中表达了对该模型的高需求(Google 搜索词条、recipe 页面、官方 docker tag 均已被引用),同时暴露了 DFlash 在发布镜像中不可用的问题。

值得精读。三个设计点有很强的可迁移性:(1) 对非 JSON 协议模型,选择“分段 + 选择”替代“正则减法”解析工具调用,并在流式场景用 holdback 不动点保证输出只增不减;(2) config 双布局归一化(flat vs nested)防止 checkpoint 静默错配;(3) DFlash 的 RoPE 布局从目标模型推断而非依赖 draft 检查点。同时建议跟踪社区未决问题:draft buffer 越界修复、is_reasoning_end_streaming 增量解码、无工具结构化输出缺口,这三项都有对应后续 PR 或框架修复的空间。

讨论亮点

评审与社区讨论的焦点集中在三处:

  1. 测试整合(已解决):DarkLight1337 要求合并独立测试脚本为 test 函数,作者随后在 commit fe0b872 将 9 个 case 转为 pytest 函数并合并纯单元测试文件。

  2. ATEM 解析器健壮性(部分未决):honziik 在生产中遇到两个边界问题——_decode_valuetype: string 的参数也做 JSON 解码导致 SchemaError;模型偶发吞掉 <atem:parameter> 开标签导致参数丢失。作者在 fork 分支提供补丁,未合入本 PR。

  3. DFlash 的系列问题(社区深度追踪):himorishige 定位注册键不匹配根因——EAGLEConfig 的 dflash 分支会给架构名加 DFlash 前缀,而 MuseGlimmerAssistantModel 是首个不以 DFlash 开头的 draft 检查点;作者确认修复已包含。maxw1489 与 myfykris 进一步追踪到 kernel warmup 崩溃的根因是 draft 输入缓冲区按 max_num_batched_tokens(2048)定尺寸,而 DFlash 一次 forward 宽度为 max_num_seqs x (1 + num_speculative_tokens) = 256 x 16 = 4096,越界写后 positions 变成垃圾值;该问题属框架级,社区用 --max-num-batched-tokens 4096 绕过,本 PR 未修复。dharaghodasara 报告 is_reasoning_end_streaming 忽略 delta_ids 导致长上下文结构化输出时每步 O(context) 解码(115.8s → 7.0s),以及无工具调用时 structured outputs grammar 可能永不生效。stepnivlk 请求将 DFlash 的 target_inner 兼容逻辑同步到 dspark 路径。

实现拆解

  1. 模型与配置落地vllm/transformers_utils/configs/muse_glimmer.py 新增 MuseGlimmerTextConfig / MuseGlimmerVisionConfig / MuseGlimmerConfig / MuseGlimmerAssistantConfig(文本栈为 Gemma2 派生 + QK-norm + iRoPE + 输出门 + logit softcapping),并在 vllm/transformers_utils/config.pyconfigs/__init__.py 注册。MuseGlimmerConfig 内置 flat(legacy converter)与 nested(canonical)两种 config 布局的归一化(_looks_flat / _normalize_flat),防止旧版扁平 config 静默落到全默认值导致错误形状加载。vllm/model_executor/models/muse_glimmer.py 原生移植文本解码器与视觉编码器(稀疏块注意力、2-D RoPE、pixel-shuffle 下采样),并在 registry.py 注册 MuseGlimmerForCausalLM

  2. 多模态处理器vllm/transformers_utils/processors/muse_glimmer.py 新增 MuseGlimmerProcessor 及 Image/Video 子处理器。chat template 每个媒体项发出一个哨兵 token(<|image|> / <|video|>),__call___grid_size 计算出的 patch 网格展开成 <|patch|> 序列;视频走 torchcodec 训练一致解码路径,按 patch_temporal 分组并输出真实 PTS 时间戳。

  3. 双解析器(核心设计)vllm/reasoning/muse_glimmer_reasoning_parser.pyvllm/tool_parsers/muse_glimmer_tool_parser.py 均围绕 channel-scoped 消息工作,adjust_request 强制 skip_special_tokens=False;工具解析器声明 supports_required_and_named=False 以绕开 JSON guided decoding 路径。两者在流式场景均采用“先分段再选通道”策略,并用 holdback 机制(_trim_open_body / _safe_open_body)保证已发出内容前缀只增不减。

  4. DFlash 投机解码适配qwen3_dflash.py 新增 dflash_target_rope_is_neox_style(从目标模型第一个注意力层读 RoPE 布局,避免 Q/K 旋转不匹配导致的静默接受率崩塌),扩展权重映射(encoder.output_norm_enc.hidden_norm.encoder.fc.fc.);vllm/v1/spec_decode/dflash.pyload_model 在构造 draft 前注入该布局到 draft 配置。vllm/config/speculative.py 改为显式声明 Muse Glimmer draft head 的 causality,而非放宽所有 DFlash/DSpark drafter 的默认判定(后者破坏 test_dflash_causality.py)。

  5. 测试与配套:新增 7 个测试文件,覆盖 ATEM 工具解析(含通道作用域、截断隔离、逐字符流式)、parse_delta 级回归(需要真实 checkpoint,用 MUSE_GLIMMER_CKPT 环境变量控制,缺失时跳过)、config 双布局归一化与 native/modular attention 数值收敛;tests/models/registry.py 补注册测试。测试文件在 review 后从独立脚本整合为 pytest 函数。

文件 模块 状态 重要度
vllm/model_executor/models/muse_glimmer.py 模型实现 added 9.17
vllm/transformers_utils/configs/muse_glimmer.py 模型配置 added 8.89
vllm/tool_parsers/muse_glimmer_tool_parser.py 工具解析 added 8.89
vllm/reasoning/muse_glimmer_reasoning_parser.py 推理解析 added 8.89
vllm/transformers_utils/processors/muse_glimmer.py 媒体处理 added 8.98
vllm/model_executor/models/qwen3_dflash.py 投机解码 modified 7.25
vllm/v1/spec_decode/dflash.py 投机解码 modified 6.44
tests/tool_use/test_muse_glimmer.py 单元测试 added 7.48
tests/tool_use/test_muse_glimmer_parse_delta.py 单元测试 added 7.38
tests/transformers_utils/test_muse_glimmer_config.py 单元测试 added 7.35

关键符号

dflash_target_rope_is_neox_style MuseGlimmerConfig._normalize_flat MuseGlimmerToolParser.adjust_request MuseGlimmerToolParser._iter_messages MuseGlimmerToolParser._safe_open_body MuseGlimmerReasoningParser._classify_bodies MuseGlimmerReasoningParser._trim_open_body MuseGlimmerReasoningParser.is_reasoning_end MuseGlimmerReasoningParser.extract_reasoning_streaming MuseGlimmerProcessingInfo.get_mm_max_tokens_per_item MuseGlimmerImageProcessor.preprocess_image MuseGlimmerVideoProcessor.decode_video load_model

关键源码片段

vllm/model_executor/models/muse_glimmer.py data-contract

模型主体(1649 行新增):原生移植 MuseGlimmer 文本解码器(Gemma2 派生 + QK-norm + iRoPE + 输出门 + logit softcapping)与视觉编码器,定义多模态输入 TensorSchema 契约与 ProcessingInfo,是整 PR 的核心。

# vllm/model_executor/models/muse_glimmer.py
# MuseGlimmer 检查点存在两种形态:多模态 `MuseGlimmerConfig` 把文本配置
# 嵌套在 `text_config` 下,纯文本形态直接暴露 `MuseGlimmerTextConfig`。
# 这两个小工具函数统一了访问路径,后续的 attention 装配、权重加载
# 都通过它们取配置,避免到处写 `getattr` 判断。
def _text_config(config):
    return getattr(config, "text_config", config)def _vision_config(config):
    return getattr(config, "vision_config", config)# 纯文本版检查点没有 vision_config,也没有显式 has_vision 标志,
# 此时多模态 registry 的 mm_limits 必须返回空,否则会误判为支持图像。
def _muse_glimmer_has_vision(config) -> bool:
    has_vision = getattr(config, "has_vision", None)
    if has_vision is not None:
        return bool(has_vision)
    return hasattr(config, "vision_config")
​
​
# 多模态像素输入契约:图像与视频都允许变分辨率、变长度,用 TensorSchema
# 描述维度以支持动态 shape 推断。视频比图像多一个 temporal group 维度,
# 因为模型按 patch_temporal 帧做通道拼接后送入编码器。
class MuseGlimmerImagePixelInputs(TensorSchema):
    type: Literal["image_pixels"]
    pixel_values: Annotated[
        torch.Tensor | list[torch.Tensor],
        TensorShape("bn", 3, "h", "w", dynamic_dims={"h", "w"}),
    ]
    feature_sizes: Annotated[torch.Tensor, TensorShape("bn")]class MuseGlimmerVideoPixelInputs(TensorSchema):
    type: Literal["video_pixels"]
    pixel_values: Annotated[
        torch.Tensor | list[torch.Tensor],
        TensorShape("bn", "ng", "c", "h", "w",
                    dynamic_dims={"ng", "h", "w"}),
    ]
    feature_sizes: Annotated[torch.Tensor, TensorShape("bn")]
vllm/transformers_utils/configs/muse_glimmer.py dependency-wiring

HF 配置原生副本(389 行),核心价值在 flat/nested 双布局归一化:旧版扁平 converter 的 config 若未被识别会静默落到全默认值,造成错误形状加载且无报错,`_normalize_flat` 配合两个重命名表从根上消除该类静默正确性 bug。

# vllm/transformers_utils/configs/muse_glimmer.py
# 野外存在两种 MuseGlimmer config 布局:
# * CANONICAL(新 HF converter):嵌套 text_config / vision_config,
# 字段名规范(hidden_activation / final_logit_softcapping)。
# * FLAT(旧 converter):全部字段平铺在顶层,且命名不同
# (hidden_act / output_soft_cap_temp / vision_latent_dim ...)。
# 若不归一化,扁平 config 会静默反序列化成“全默认”文本配置——checkpoint
# 里所有值都被忽略,架构与默认值不同时直接加载出错误形状,且没有任何报错。
class MuseGlimmerConfig(PretrainedConfig):
    model_type = "muse_glimmer"
    sub_configs = {
        "text_config": MuseGlimmerTextConfig,
        "vision_config": MuseGlimmerVisionConfig,
    }
​
    # 扁平字段名 -> 规范字段名
    _FLAT_TEXT_RENAMES = {
        "hidden_act": "hidden_activation",
        "output_soft_cap_temp": "final_logit_softcapping",
    }
    _FLAT_VISION_RENAMES = {
        "vision_latent_dim": "hidden_size",
        "vision_heads": "num_attention_heads",
        "vision_layers": "num_hidden_layers",
        "vision_output_dim": "output_dim",
        "vision_patch_size": "patch_size",
        "vision_patch_temporal": "patch_temporal",
        "vision_adapter_dim": "adapter_dim",
        "vision_pos_emb_grid_h": "pos_emb_height",
        "vision_pos_emb_grid_w": "pos_emb_width",
        "vision_downsample_factor": "merge_kernel_size",
    }
​
    @classmethod
    def _looks_flat(cls, kwargs: dict) -> bool:
        # 没有显式 text_config 却在顶层出现文本级字段(hidden_size 等),
        # 就判定为扁平布局,触发归一化。
        if kwargs.get("text_config") is not None:
            return False
        return any(k in kwargs for k in ("hidden_size", "num_hidden_layers"))
​
    @classmethod
    def _normalize_flat(cls, kwargs: dict) -> dict:
        kwargs = dict(kwargs)
        text: dict = {}
        # 顶层文本字段先抽走并改名
        for key in list(kwargs.keys()):
            if key in cls._FLAT_TEXT_KEYS:
                canon = cls._FLAT_TEXT_RENAMES.get(key, key)
                text.setdefault(canon, kwargs.pop(key))
        vision: dict = {}
        # vision 字段同理,并处理 vision_mlp_ratio / sparse_factor 派生字段
        for key in list(kwargs.keys()):
            if key in cls._FLAT_VISION_RENAMES:
                vision.setdefault(cls._FLAT_VISION_RENAMES[key], kwargs.pop(key))
        if text:
            kwargs["text_config"] = text
        if vision:
            kwargs["vision_config"] = vision
        return kwargs
vllm/tool_parsers/muse_glimmer_tool_parser.py dependency-wiring

ATEM 工具调用解析器(501 行)。核心设计是“分段 + 选择”替代 HF 参考实现的“正则减法”,后者在截断回合会把真实工具调用与未闭合推理段一起删掉;同时处理流式标记跨 chunk、通道作用域、裸工具名归一化。

# vllm/tool_parsers/muse_glimmer_tool_parser.py
# MuseGlimmer 不输出 JSON 工具调用,而是 ATEM 风格 XML,且每条消息带
# 通道头:to=self(推理)、to=user(回答)、to=<tool>(工具调用)。
# 通道作用域是硬约束:推理回显的 <atem:invoke> 绝不能解析成真实调用。def _iter_messages(text: str) -> Iterator[tuple[str | None, str, bool]]:
    """把整段输出切成助手消息,产出 (接收方, 正文, 是否闭合)。    HF 参考实现用正则“减法”剔除推理/回答段,只在完整回合上安全;
    本解析器改为先分段、再按通道选择工具正文。截断或损坏的回合里,
    减法可能把未闭合推理段之后的真实工具调用一起删掉,选择不会。
    """
    pos = 0
    while pos < len(text):
        header = _MSG_HEADER_RE.search(text, pos)
        if header is None:
            return
        body_start = header.end()
        end = _MSG_END_RE.search(text, body_start)
        nxt = _MSG_HEADER_RE.search(text, body_start)
        body_end = end.start() if end is not None else len(text)
        closed = end is not None
        # 下一条消息头同样终止当前正文:推理段的 <|eom|> 丢失(截断或
        # chunk 在推理→工具切换处丢失)时,不能让它吞掉后面的工具调用
        if nxt is not None and nxt.start() < body_end:
            body_end = nxt.start()
            closed = False
            next_pos = nxt.start()
        else:
            next_pos = end.end() if end is not None else len(text)
        body = text[body_start:body_end]
        # 正文里不可能合法出现 <|start|>;一旦出现,说明下一条消息头
        # 只生成了一半(<|message|> 未到),正则还认不出它。先切断,
        # 否则流式正文会长大再缩回,客户端会看到已发出的内容被回退。
        start_tok = body.find("<|start|>")
        if start_tok != -1:
            body = body[:start_tok]
            closed = False
        yield header.group("rcpt"), body, closed
        pos = next_pos

评论区精华

DFlash 注册键不匹配:DFlashMuseGlimmerAssistantModel 未注册 正确性

多位用户(DanTup、tjtanaa、stepnivlk、BlivionIaG、himorishige)报告发布镜像中 `--speculative-config '{"method": "dflash"}'` 报 `Model architectures ['DFlashMuseGlimmerAssistantModel'] are not supported`。himorishige 精确定位根因:EAGLEConfig 的 dflash 分支会给架构名加 `DFlash` 前缀,而 `MuseGlimmerAssistantModel` 是首个不以 `DFlash` 开头的 draft 检查点,导致查找键从裸名变成带前缀的未注册键。

结论:作者确认修复已包含在 PR 中('the fix has been included now'),registry 注册补充了 resolved 名称;社区在后续镜像中确认 drafter 可加载。 · 已解决

DFlash draft 缓冲区越界导致 kernel warmup 崩溃 正确性

maxw1489 报告 SM120(RTX PRO 6000)上三种 attention 后端(FA2、FlashInfer、Triton)全部在 warmup 崩溃。myfykris 给出根因:DFlash 一次 forward 宽度为 max_num_seqs x (1 + num_speculative_tokens) = 256 x 16 = 4096,但 draft 输入缓冲区按 max_num_batched_tokens(自动封顶 2048)分配,positions 越界后变成垃圾值(8.6e14),在注意力层触发 assert。num_speculative_tokens=1 时不触发,换 attention 后端无效。workaround:`--max-num-batched-tokens 4096`。

结论:属 spec decode 框架级 buffer sizing 问题,本 PR 未修复;社区以调高 token budget 绕过。合并后仍可能影响默认 recipe 用户。 · 待处理

is_reasoning_end_streaming 长上下文 O(context) 热路径开销 性能

dharaghodasara 指出 `is_reasoning_end_streaming()` 忽略 `delta_ids`、直接对全量 `input_ids` 解码,每个 decode step 都会在调度器热路径上做 O(context) 工作;长上下文结构化输出测试中从 115.8s 降至 7.0s。同类问题 KimiK3ReasoningParser 已用增量 / 窗口化检查规避。

结论:未在 PR 内修改;建议后续将 streaming 检查改为有界尾部解码并回退到全量解码。 · 待处理

结构化输出且无工具调用时 guided decoding 可能永不生效 正确性

dharaghodasara 分析:无工具调用的 structured outputs 请求中,`is_reasoning_end()` 只在模型进入工具通道时返回 True;若模型直接 to=user 回答,推理相位永不结束,vLLM 可能不应用请求的 JSON schema grammar,模型输出不受约束。

结论:作者未回应;是否把结构化输出门控与工具解析器交接语义分离,留待后续讨论。 · 待处理

ATEM 解析器边界:string 参数被 JSON 解码与吞标签 正确性

honziik 报告两个线上问题:(1) `_decode_value` 对 `type: string` 的参数也跑 JSON 解码,客户端写 JSONL 文件时得到 SchemaError;(2) 模型偶发吞掉 `<atem:parameter>` 开标签,参数被粘到 invoke 名上,args 为空。补丁在 xianbaoqian/vllm#7,未合入本 PR。

结论:未解决;解析器对损坏输出的鲁棒性仍需加固。 · 待处理

测试整合为 pytest 函数 测试

DarkLight1337 在 review 中要求合并独立测试脚本('Can we consolidate these test files? Each test should be one test_ function instead of its own file...'),指出部分文件只有模块级 assert,pytest 收集零用例。

结论:已解决:commit fe0b872 将 9 个 case 转为 test 函数并合并为 tests/tool_use/test_muse_glimmer.py。 · 已解决

DFlash 的 target_inner 兼容逻辑应同步到 DSpark 路径 设计

stepnivlk 请求把 `target_inner = getattr(target_language_model, "model", target_language_model)` 的兼容写法同步到 `vllm/v1/worker/gpu/spec_decode/dspark/utils.py`,称 DSpark 路径需要它来支持 Muse Glimmer 式无 .model 子模块的目标。

结论:未在 PR 内处理;留待 DSpark 后续支持跟进。 · 待处理

测试文件硬编码开发者家目录路径 安全

depthfirst-app[bot] 标记 LOW 风险:测试默认路径暴露真实用户名与内部项目结构(/home/betodepaola/projects/onyx-final-nvidia-repro/...),建议改用通用占位路径。

结论:已解决:最终版本默认值改为空字符串,仅通过 MUSE_GLIMMER_CKPT 环境变量指定。 · 已解决

风险与影响

  1. DFlash draft 缓冲区越界(高):社区在 MI355X、GB10、SM120(RTX PRO 6000)上均复现 draft 阶段崩溃,根因是 spec decode 框架按 max_num_batched_tokens 而非 DFlash 实际 token 宽度 max_num_seqs * (1 + num_speculative_tokens) 分配缓冲区。涉及 vllm/v1/spec_decode 与调度器配置,本 PR 未做框架级修复,默认 recipe(256 并发 x 16 槽)必然触发。
  2. 解析器复杂度(中高)muse_glimmer_reasoning_parser.py_STRIP_OPEN_REASONING_RE / _OPEN_REASONING_RE 等正则以互斥条件手工建模流式边界,任何未覆盖的 tokenizer 变体都可能造成通道塌缩;honziik 报告的 string 参数被 JSON 解码与吞标签问题均未被本 PR 修复。
  3. 长上下文热路径开销(中)is_reasoning_end_streaming 每次 delta 都解码全序列,在 128K 上下文下进入调度器热路径,存在 GPU 饥饿风险(参照 KimiK3 同款问题)。
  4. 结构化输出缺口(中):无工具调用的 structured outputs 请求可能因 is_reasoning_end 永不置真而跳过 guided decoding。
  5. 请求级全局副作用(低中)adjust_request 无条件 skip_special_tokens=False,影响该模型所有输出流的 framing 保留。
  6. 流程/供应链风险(流程性):mdierolf 质疑 fork 代码提前发布官方 docker tag 的供应链可追溯性;作者说明 Day 0 镜像基于本 PR 构建。

用户侧:Muse Glimmer 30B 成为开箱可用模型(recipes.vllm.ai 已有推荐页),官方 docker tag vllm-openai:muse-glimmer 已被社区广泛使用;但 DFlash 投机解码在默认配置下不可用,需 workaround 或关闭 spec decode。系统侧:新增 5 个核心源码文件(约 3400 行)与 7 个测试文件,注册点覆盖 config / processor / model registry / tool parser / reasoning parser 五处。团队侧:这是与 Meta 模型发布节奏绑定的合作式 PR,测试质量要求高(DarkLight1337 的整合要求、多轮 CI 重跑);共享 DFlash 路径的 qwen3_dflash.pydflash.py 改动影响所有 DFlash 用户,但改动向后兼容。影响程度:中等到大——新模型影响面隔离,但 spec decode 共享路径的问题暴露了框架级 buffer sizing 缺陷。

DFlash draft 缓冲区越界(多硬件复现) 共享 spec-decode 路径变更 解析器依赖复杂正则与 holdback 长上下文调度热路径 O(context) 结构化输出无工具场景缺口

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论