Prhub

#43575 [feat] add GlmgaProcessor specific logits in `glm4_1v.py`

原始 PR 作者 JaredforReal 合并时间 2026-05-29 10:56 文件变更 3 提交数 26 评论 13 代码增减 +346 / -33

执行摘要

新增 GLMGA 模型推理支持并改进 GLM-4.6V 视频处理

PR 描述指出需要为 GLMGA 多模态模型添加 serving 支持,并提升 GLM-4.6V-Flash 在现有 Glm4vForConditionalGeneration 框架内的兼容性。具体来说,GLMGA 模型使用独立的图像/视频处理器,需要特殊的占位符构造和提示更新逻辑。

建议读者关注:

  • _to_video_metadata 的设计权衡:如何安全地筛选额外键。
  • GLM4_6VVideoBackend.compute_frames_index_to_sample 的帧采样策略,确保与 HuggingFace 一致。
  • 令牌预算计算中的潜在低估问题,可能需要后续调整。
  • 自动化 review 提出的风险点虽然未完全解决,但合并表明团队已评估风险可控。
讨论亮点

Review 中主要讨论了以下问题:

  • VideoMetadata 转换风险(gemini-code-assist):_to_video_metadata 使用 **kwargs 传递额外键(如 timestamps)会导致 TypeError,因为 HuggingFace VideoMetadata 是固定字段的 dataclass。
  • 时间戳属性不存在(gemini-code-assist):在 _construct_video_placeholder_glmga 中访问 metadata.timestamps 会引发 AttributeError,因为 VideoMetadata 没有该属性。
  • 视频列表与元数据长度不匹配(gemini-code-assist):在 _get_glmga_processor_inputs 中,非元组视频项未添加到 hf_video_metadata,导致长度不一致。
  • 除零风险(Copilot):GLM4_6VVideoBackend.compute_frames_index_to_sampleoriginal_fps 可能为 0,导致 duration_per_frameduration 计算中出现除零。
  • 令牌预算低估(Copilot):get_mm_max_tokens_per_itemmax_vision_tokens 的计算可能将边缘长度视为面积,导致严重低估。
  • 断言语句风险(Copilot):iter_mm_grid_thw 中的 assert 在生产环境中可能硬崩溃,建议改为可操作的错误检查。
  • 处理权重置(Copilot):多个视频时 do_sample_frames 可能被覆盖,需要明确处理。
  • 这些评论大多来自自动化工具,最终审核者 Isotr0py 批准了 PR,表明大部分问题可能已在后续提交中解决或属于设计权衡。

实现拆解

实现分为四个主要步骤:

  1. GLMGA 处理器集成(glm4_1v.py):添加 _is_glmga_model() 通过子处理器类名检测 GLMGA 变体;添加 _to_video_metadata() 安全地将词典转换为 VideoMetadata(过滤掉 do_sample_frames 等额外键)。
  2. 令牌预算计算(glm4_1v.py):新增 get_mm_max_tokens_per_item() 方法,用于 GLMGA 和 GLM-4.6V,从处理器配置动态计算视频令牌上限,包括时空合并影响和时间戳令牌开销。
  3. GLM4_6V 视频后端(video.py):注册 GLM4_6VVideoBackend,实现自定义帧采样逻辑,匹配 HuggingFace 的 GlmgaVideoProcessor.sample_frames():固定 fps=2,最大帧 640,使用 math.floor 上采样公式,并确保偶数帧去重。
  4. 每帧视频嵌入处理(glm4_1v.py):更新 _mm_embed_links() 以支持基于嵌入范围的逐帧网格维度生成,适用于 GLM-4.6V/GLMGA 视频的视觉嵌入放置。
  5. 测试注册表更新(tests/models/registry.py):为 Glm4vForConditionalGeneration 添加 extras,包含 "4.6V": "zai-org/GLM-4.6V-Flash",便于测试。
文件 模块 状态 重要度
vllm/model_executor/models/glm4_1v.py 模型执行器 modified 9.05
vllm/multimodal/video.py 多模态 modified 8.34
tests/models/registry.py 测试注册表 modified 4.27

关键符号

_to_video_metadata get_mm_max_tokens_per_item _is_glmga_model _get_video_second_idx_glmga _get_direct_path_inputs get_video_replacement_glm46v GLM4_6VVideoBackend._prepare_source GLM4_6VVideoBackend.compute_frames_index_to_sample GLM4_6VVideoBackend.load_bytes

关键源码片段

vllm/model_executor/models/glm4_1v.py core-logic

核心模型文件,新增 GLMGA 处理器检测、视频元数据处理、令牌预算计算和占位符构造逻辑,是 GLMGA 推理路径的主要入口。

# vllm/model_executor/models/glm4_1v.py(新增片段)import transformers
from packaging.version import Version# 检测当前 transformers 版本是否支持 GLMGA 处理器(>=5.10.0.dev0)
TRANSFORMERS_WITH_GA = Version(transformers.__version__) >= Version("5.10.0.dev0")
​
​
def _to_video_metadata(metadata: Mapping[str, Any]) -> VideoMetadata:
    """
    将原始元数据字典转换为 HuggingFace VideoMetadata。
    过滤掉 `do_sample_frames` 等 VideoMetadata 不接受的额外键,
    避免因未知字段导致 TypeError。
    """
    return VideoMetadata(
        **{k: metadata[k] for k in metadata if k != "do_sample_frames"}
    )
​
​
def _is_glmga_model(processor: object) -> bool:
    """
    通过检查 processor 的 image_processor / video_processor
    子处理器类名是否包含 "Glmga" 来识别 GLMGA 变体。
    """
    for attr in ("image_processor", "video_processor"):
        sub = getattr(processor, attr, None)
        if sub and "Glmga" in type(sub).__name__:
            return True
    return False
​
​
def get_mm_max_tokens_per_item(self, seq_len, mm_counts):
    """
    计算每项多模态数据的令牌预算上限。
    对于 GLMGA,视频令牌基于 spatial_merge_size、patch_size 和 temporal_patch_size
    动态计算。
    """
    processor = self.get_hf_processor()
    # GLM-4.1V 使用 Glm4vProcessor,直接返回 None
    if isinstance(processor, Glm4vProcessor):
        return None
​
    result: dict[str, int] = {}
​
    if mm_counts.get("image", 0) > 0:
        result["image"] = self.get_max_image_tokens()
​
    if mm_counts.get("video", 0) > 0:
        video_processor = self.get_video_processor()
        max_pixels = video_processor.size["longest_edge"]
​
        vision_config = self.get_hf_config().vision_config
        temporal_patch_size = vision_config.temporal_patch_size
        patch_size = vision_config.patch_size
        merge_size = vision_config.spatial_merge_size
​
        # 计算每帧视觉令牌数(注意:这里可能低估,见 review 讨论)
        max_vision_tokens = max_pixels // (
            temporal_patch_size * patch_size**2 * merge_size**2
        )
​
        # GLMGA 支持最多 640 帧
        max_grid_t = 640 // temporal_patch_size
​
        tokenizer = self.get_tokenizer()
        max_ts_tokens = max(
            len(tokenizer.encode(f"{t:.1f} seconds", add_special_tokens=False))
            for t in range(min(max_grid_t, 300))
        )
​
        result["video"] = max_vision_tokens + max_grid_t * (2 + max_ts_tokens) + 2
​
    return result

评论区精华

VideoMetadata 转换风险 正确性

gemini-code-assist 指出 `_to_video_metadata` 使用 `**kwargs` 传递额外键(如 `timestamps`)会导致 `TypeError`,因为 HuggingFace `VideoMetadata` 是固定字段的 dataclass。

结论:后续代码中添加了对 `do_sample_frames` 的过滤,但未处理 `timestamps` 等键;最终 PR 合并,可能设计上允许有限的额外键或已通过其他方式规避。 · 已解决

时间戳属性不存在 正确性

gemini-code-assist 指出在 `_construct_video_placeholder_glmga` 中访问 `metadata.timestamps` 会引发 `AttributeError`,因为 `VideoMetadata` 没有该属性。

结论:该问题未在可见代码中显式修复,但可能该路径在合并前已被调整或移除(从最终代码看,`_construct_video_placeholder_glmga` 已不存在)。 · 已解决

视频列表与元数据长度不匹配 正确性

gemini-code-assist 指出 `_get_glmga_processor_inputs` 中非元组视频项未添加到 `hf_video_metadata`,导致长度不一致。

结论:该评论未获直接回复,但最终代码中该函数被重构为 `_get_direct_path_inputs`,可能已处理此问题。 · 已解决

帧采样除零风险 正确性

Copilot 指出 `GLM4_6VVideoBackend.compute_frames_index_to_sample` 中 `original_fps` 可能为 0,导致 `duration_per_frame` 和 `duration` 计算中出现除零。

结论:代码中未添加显式保护(如 `if original_fps <= 0` 的回退逻辑),但 `_prepare_source` 已对 duration 进行估算,可能在实际中极少遇到 FPS=0 的输入。 · unresolved

令牌预算低估 正确性

Copilot 指出 `get_mm_max_tokens_per_item` 中 `max_vision_tokens` 的计算将边缘长度视为面积,可能导致严重低估。

结论:评论未直接解决,但该计算可能只是一个粗略上限,实际中由后续逻辑校正;或者已在后序提交中调整(最终代码保留该计算)。 · unresolved

断言语句风险 测试

Copilot 指出 `iter_mm_grid_thw` 中的 `assert` 在生产环境可能硬崩溃,建议改为可操作的异常。

结论:最终代码中仍保留 `assert`,但合并说明团队认为其在目标使用场景下足够安全。 · unresolved

处理权重置 设计

Copilot 指出多个视频时 `do_sample_frames` 可能被覆盖,需要明确处理。

结论:代码通过 `_get_direct_path_inputs` 将所有视频的 `do_sample_frames` 合并到一个 kwarg,意味着所有视频必须一致;未验证一致性。 · unresolved

时间戳列表构建开销 性能

Copilot 建议在 `compute_frames_index_to_sample` 中避免构建完整 `timestamps` 列表,因为可以用 `frame_index / original_fps` 替代。

结论:最终代码保留列表构建,但对于实际视频帧数(通常 <= 数千)开销可接受。 · unresolved

风险与影响

技术风险包括:

  • VideoMetadata 兼容性风险:如果视频元数据包含 timestamps 等额外字段,_to_video_metadata 可能抛出 TypeError,影响 GLMGA 路径。
  • 帧采样除零风险GLM4_6VVideoBackend.compute_frames_index_to_sampleoriginal_fps 为 0 时会导致除零异常,对于无效 FPS 的视频可能崩溃。
  • 令牌预算不准确get_mm_max_tokens_per_item 中的计算可能低估视频令牌预算,导致序列长度分配不足。
  • 断言语句导致意外中断iter_mm_grid_thw 中的 assert 在 Python 优化模式下被跳过,但在调试模式下可能因处理器输出微小差异而崩溃。
  • 视频列表与元数据不同步_get_glmga_processor_inputs 中非元组视频项可能导致元数据缺失,引发后续错误。

影响范围:

  • 模型支持:新增 GLMGA 模型(如 GLM-4.6V-Flash)的推理能力,扩展了多模态模型覆盖。
  • 系统稳定性:如果元数据或帧采样触发异常,可能影响整个推理服务。但风险多集中在 GLMGA 专用路径,不影响其他模型。
  • 开发与维护:GLM4_6VVideoBackend 是通用视频后端的专用变体,增加了代码维护成本。
  • 测试覆盖:测试注册表更新确保了 GLM-4.6V 模型的测试可用性。
多模态数据处理风险 除零风险 令牌预算可能低估 断言在生产环境可能崩溃 元数据兼容性风险

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论