Prhub

#35809 fix(gemma4): set lm_head_is_tied for Gemma4UnifiedForConditionalGeneration

原始 PR 作者 fechaMe 合并时间 2026-09-16 06:16 文件变更 1 提交数 5 评论 1 代码增减 +2 / -1

执行摘要

补齐 lm_head_is_tied,修复 Gemma4 统一架构模型启动崩溃

用户报告(#38673)在 0.5.18 与 0.5.19 上启动 google/gemma-4-12B-it 均失败:scheduler 在 CUDA graph 捕获阶段抛出 AttributeError: 'Gemma4UnifiedForConditionalGeneration' object has no attribute 'lm_head_is_tied'。根因是 #22498 将 Gemma4ForConditionalGeneration.forward() 中原本内联计算的 tied 条件改为读取实例属性,而 Gemma4UnifiedForConditionalGeneration 为适配 encoder-free unified 架构刻意跳过父类 __init__,从未初始化该属性,导致首次 forward 即崩溃、服务无法启动。

值得快速阅读其背景:这是典型的“重构把内联判断改为实例属性后,子类漏初始化”回归,有助于理解继承与初始化顺序的隐式契约风险;但实现本身无需精读。值得关注的设计决策包括:修复直接复用 __init__ 已解析的配置计算属性(不做重复猜测),以及维护者将 PR 收敛为最小修复的合入判断。

讨论亮点

审核过程没有文字性 review 评论,kpham-sgl 直接 APPROVED。唯一公开讨论是作者在 issue #38673 评论中 @kpham-sgl 和 @pyc96 请求 review,并重申根因:Gemma4UnifiedForConditionalGeneration 从不设置 lm_head_is_tied,而继承的 forward() 自 #22498 起读取该属性。最终合并 commit 由 kpham-sgl 完成并移除了 12B MMMU eval 注册与说明注释,反映维护者希望以最小改动范围合入该修复。

实现拆解

  1. 变更入口:唯一改动文件为 python/sglang/srt/models/gemma4_unified.py,位置在 Gemma4UnifiedForConditionalGeneration.__init__ 的 lm_head 初始化段。
  2. 核心改动:将原先内联于 if 语句的判断 self.pp_group.world_size == 1 and text_tie 提升为显式属性 self.lm_head_is_tied = self.pp_group.world_size == 1 and text_tie,再以该属性作为分支条件选择 lm_head(绑定 embed_tokens、ParallelLMHead 或 PPMissingLayer)。布尔表达式与原逻辑完全等价,因此单卡绑定、分布式末 rank 独立头、中间 rank 占位层的行为均保持不变,同时补齐了父类 forward() 期望的属性。
  3. 配套改动与测试:作者最早的提交曾向 VLM MMMU eval 注册 google/gemma-4-12B-it 并基于 H100 实测写入阈值,但最终由维护者 kpham-sgl 在合入提交中移除该注册与说明性注释,将 PR 收敛为纯修复;最终合入不含任何测试文件变更。
文件 模块 状态 重要度
python/sglang/srt/models/gemma4_unified.py 模型实现 modified 5.37

关键符号

Gemma4UnifiedForConditionalGeneration.__init__

关键源码片段

python/sglang/srt/models/gemma4_unified.py data-contract

唯一变更文件;为 encoder-free unified 架构补充父类 forward() 依赖的 lm_head_is_tied 属性,直接修复 gemma-4-12B-it 启动崩溃。

# Gemma4UnifiedForConditionalGeneration.__init__ 统一架构(无 vision tower)初始化片段。
# 该子类跳过 Gemma4ForConditionalGeneration.__init__,但继承其 forward();
# 父类 forward() 自 #22498 起读取 self.lm_head_is_tied 来选择 logits 输出头,
# 因此这里必须先补齐属性,否则启动阶段 CUDA graph 捕获就会 AttributeError。
​
self.language_model = Gemma4TextModel(
    text_config,
    quant_config,
    prefix=add_prefix("language_model", add_prefix("model", prefix)),
)
​
text_tie = getattr(text_config, "tie_word_embeddings", True)
self.lm_head_is_tied = self.pp_group.world_size == 1 and text_tie
if self.lm_head_is_tied:
    # 权重绑定且未走 pipeline 并行:直接复用 embed_tokens,省一份 lm_head 参数。
    self.lm_head = self.language_model.embed_tokens
elif self.pp_group.is_last_rank:
    # 分布式场景下仅在最后一 rank 创建独立输出头。
    self.lm_head = ParallelLMHead(
        text_config.vocab_size,
        text_config.hidden_size,
        quant_config=quant_config,
        prefix=add_prefix("lm_head", prefix),
    )
else:
    # 中间 rank 使用占位层,由最后一 rank 持有真实权重。
    self.lm_head = PPMissingLayer()
​
self.logits_processor = LogitsProcessor(text_config)
self.capture_aux_hidden_states = False

评论区精华

请求 review 并重申根因 question

fechaMe 在 issue #38673 评论中 @kpham-sgl 与 @pyc96,说明 Gemma4UnifiedForConditionalGeneration 从不设置 lm_head_is_tied,而继承的 forward() 自 #22498 起读取该属性,导致当前 main 上 gemma-4-12B-it 启动失败。

结论:kpham-sgl 批准并合入;最终提交由其完成,移除了 12B MMMU eval 注册与说明注释,保持最小修复。 · 已解决

风险与影响

改动将内联布尔条件提升为实例属性,逻辑完全等价,对 world_size 与 tie_word_embeddings 的所有组合行为不变,回归风险极低。主要风险是缺少针对该属性的单元测试:lm_head_is_tied 与父类 forward() 的契约目前只靠注释约定,未来若 forward() 对 tied 语义再作调整(例如引入新的判定条件),__init__ 中的重复计算可能漂移而不被测试发现。同时,该属性只覆盖文本 lm_head,多模态投影路径不受影响;无性能与安全影响。

影响范围集中在 Gemma 4 的 encoder-free unified 架构(当前唯一公开尺寸为 google/gemma-4-12B-it),修复其 CUDA graph 启动阻断;对其他模型族、API、配置与 checkpoint 均无影响。改动仅 1 个文件 3 行,对团队而言是低风险热修复。对用户而言,该模型可从 0.5.18/0.5.19 无法启动恢复为可正常服务。

缺少单元测试覆盖 继承属性契约脆弱 模型启动核心路径

关联 Issue

#38673 Title: [Bug] Gemma4UnifiedForConditionalGeneration crashes on CUDA graph capture: 'lm_head_is_tied' not set

完整报告

参与讨论