Prhub

#2716 fix: align MLA RoPE types with model configs

原始 PR 作者 zyzshishui 合并时间 2026-08-23 19:14 文件变更 9 提交数 2 评论 3 代码增减 +12 / -0

执行摘要

为 GLM-4.7-Flash 与 JoyAI-LLM-Flash 显式声明 plain RoPE,修复 MLA 误用 YaRN

PR body 明确指出:python miles/utils/external_utils/model_args_utils.py glm4.7-flash 此前输出的参数只有 --multi-latent-attention 而没有 --rope-type,该遗漏被 load_model_args() 原样传播进 launcher 与转换命令;随后 set_default_megatron_args()multi_latent_attention 启用时把缺失的 rope_type 映射为 yarn,最终让 MLA 实现构造出 YarnRotaryEmbedding 而非 RotaryEmbedding。这个兜底对 DeepSeek、Kimi 等确实使用 YaRN 的定义是合适的,但与 GLM-4.7-Flash、GLM-5/5.1/5.2 及 JoyAI-LLM-Flash 五个 HuggingFace 官方 checkpoint 描述的 plain/default RoPE 不符,属于静默的行为错配。

值得精读,尤其适合关注模型配置契约与训练框架隐式默认值交互的读者。本 PR 虽然只有 +12 行的最终改动,但完整呈现了一个隐蔽的配置错配 bug 的根因链条:模型定义遗漏参数 → 参数加载原样传递 → Megatron 侧隐式兜底 → 运行时构造错误的嵌入实现。值得借鉴的两个设计决策:一是“显式优于隐式”,在框架存在默认值填充逻辑时,模型定义应显式声明;二是合并前的 split-scope 纪律,未验证的 GLM-5 部分被果断剥离,避免把不确定性的风险带入主分支。

讨论亮点

本 PR 没有 inline review 评论,核心讨论集中在 Issue 评论区与 PR body 的 Note 段落:

  • 合并者 guapisolo 在 Issue 评论中提到“Letting my agent to do ablation and see the scale of rope issue”,先用自动化手段评估 rope 问题的影响规模,再决定变更范围。

  • 在第二个 commit 的说明中,guapisolo 明确解释了收缩范围的理由:

    Commit b9f61310 will remove the GLM5/5.1/5.2 plain-RoPE launcher and generated snapshot changes from this PR because the current GLM5 adapter has not been validated for that override, while preserving the GLM-4.7 and JoyAI changes.

  • PR body 的 Note 是本次最重要的技术论证:

    GLM-5 DSAMLASelfAttention.forward() unconditionally unpacks (rotary_pos_emb, mscale), matching the two-value return contract of YarnRotaryEmbedding.forward(). Selecting --rope-type rope instead constructs RotaryEmbedding, whose forward() returns one tensor, so the existing GLM-5 path can fail during unpacking.

  • 结论:GLM-5 的适配必须与 attention 调用点一起改造后才能合并,本次先合并已验证的 GLM-4.7-Flash 与 JoyAI-LLM-Flash,GLM-5 作为 follow-up 保留。

实现拆解

本 PR 的完整实现过程如下:

  1. 定位根因链路:从 miles/utils/external_utils/model_args_utils.py 的复现命令入手,确认缺失 --rope-type 是问题起点;load_model_args() 原样传递该遗漏,set_default_megatron_args() 再以 yarn 兜底,最终 config.rope_type 驱动 MLA 位置编码构造分支。四个环节中只要模型定义显式声明 rope_type 即可切断错误传播。

  2. 修改两个模型定义:在 scripts/models/glm4.7-flash.pyscripts/models/joyai-llm-flash.pymodel_args() 返回串中,紧跟 --position-embedding-type rope 之后各插入一行 --rope-type rope。这是 data-contract 层面的修复:不触碰 Megatron 侧逻辑,也不改全局 MLA 默认值,仅让这两个模型的参数契约与其官方 checkpoint 对齐。

  3. 同步刷新 7 个快照文件:覆盖三类快照——tests/snapshots/model_args/ 下的参数展开结果、tests/snapshots/launch_scripts/sh/examples/infra_features/p2p_weight_transfer/ 下的 shell launcher(GLM-4.7-Flash 2 节点 profile 场景)、以及 tests/snapshots/launch_scripts/py/scripts/run_glm47_flash.py/run_joy_ai_llm_flash.py/ 下 Python launcher 的 prepare/execute 各两组。快照更新验证了 --rope-type rope 能正确传播到 launcher 与 torchrun/ray job submit 命令的最终形态。

  4. 收缩合并范围(第二个 commit):合并者 guapisolo 将 GLM-5/5.1/5.2 的 plain-RoPE 变更从 PR 中移除,原因写在 commit message 与 issue 评论中:当前 GLM5 adapter 未验证 generic plain-RoPE override,直接切换会把 YarnRotaryEmbeddingforward() 返回两个值)换成 RotaryEmbeddingforward() 返回一个值),使 DSAMLASelfAttention.forward() 中无条件执行的 (rotary_pos_emb, mscale) 解包直接失败。GLM-4.7 与 JoyAI 的经验证部分保留。

  5. 验证与配套:除快照外没有新增专门单元测试文件;合并者通过定向 GLM5 snapshot 检查(20 个 model-argument、28 个 Python-launcher、2 个 shell-launcher 用例)与 git diff --check 确认移除变更后仍保持一致性。这是以快照为回归护栏、以数据契约为修复对象的典型配套方式。

文件 模块 状态 重要度
scripts/models/glm4.7-flash.py 模型定义 modified 4.65
scripts/models/joyai-llm-flash.py 模型定义 modified 4.65
tests/snapshots/model_args/glm4.7-flash.txt 快照测试 modified 1.89
tests/snapshots/model_args/joyai-llm-flash.txt 快照测试 modified 1.89
tests/snapshots/launch_scripts/py/scripts/run_glm47_flash.py/prepare.txt 快照测试 modified 1.3
tests/snapshots/launch_scripts/py/scripts/run_glm47_flash.py/execute.txt 快照测试 modified 1.3
tests/snapshots/launch_scripts/py/scripts/run_joy_ai_llm_flash.py/prepare.txt 快照测试 modified 1.3
tests/snapshots/launch_scripts/py/scripts/run_joy_ai_llm_flash.py/execute.txt 快照测试 modified 1.3
tests/snapshots/launch_scripts/sh/examples/infra_features/p2p_weight_transfer/run-glm4.7-flash-2node-profile.sh.txt 快照测试 modified 1.54

关键符号

model_args (scripts/models/glm4.7-flash.py) model_args (scripts/models/joyai-llm-flash.py)

关键源码片段

scripts/models/glm4.7-flash.py data-contract

核心修复文件之一:在 `model_args()` 中紧跟 `--position-embedding-type rope` 后新增 `--rope-type rope`,切断 `set_default_megatron_args()` 对缺失 `rope_type` 的 yarn 隐式兜底。

# scripts/models/glm4.7-flash.py 中的 model_args(),修复后版本
# 修复要点:此前只声明了 --position-embedding-type rope 却没有 --rope-type,
# 一旦启用 --multi-latent-attention,set_default_megatron_args() 会把缺失的
# rope_type 隐式填充为 yarn,与官方 checkpoint 的 plain/default RoPE 不一致。
def model_args() -> str:
    return (
        f"--moe-layer-freq [0]*{N_DENSE_LAYERS}+[1]*{N_MOE_LAYERS} "
        f"--num-experts {MOE_ROUTED_EXPERTS} "
        f"--moe-shared-expert-intermediate-size {MOE_SHARED_EXPERT_INTERMEDIATE_SIZE} "
        f"--moe-router-topk {MOE_ACTIVE_ROUTED_EXPERTS} "
        "--moe-grouped-gemm "
        "--moe-permute-fusion "
        f"--moe-ffn-hidden-size {MOE_FFN_HIDDEN} "
        "--moe-router-score-function sigmoid "
        "--moe-router-pre-softmax "
        "--moe-router-enable-expert-bias "
        "--moe-router-bias-update-rate 0 "
        "--moe-router-load-balancing-type seq_aux_loss "
        "--moe-router-topk-scaling-factor 1.8 "
        "--moe-aux-loss-coeff 0 "
        "--moe-router-dtype fp32 "
        "--make-vocab-size-divisible-by 64 "
        f"--num-layers {N_DENSE_LAYERS + N_MOE_LAYERS} "
        f"--hidden-size {NHIDDEN} "
        f"--ffn-hidden-size {FFN_HIDDEN} "
        f"--num-attention-heads {NHEADS} "
        "--disable-bias-linear "
        "--add-qkv-bias "
        "--swiglu "
        "--untie-embeddings-and-output-weights "
        "--position-embedding-type rope "
        "--rope-type rope " # 显式声明 plain RoPE,阻断 yarn 隐式默认值
        "--no-position-embedding "
        "--normalization RMSNorm "
        "--norm-epsilon 1e-5 "
        "--qk-layernorm "
        "--multi-latent-attention "
        "--q-lora-rank 768 "
        "--kv-lora-rank 512 "
        "--qk-head-dim 192 "
        "--v-head-dim 256 "
        "--kv-channels 192 "
        "--qk-pos-emb-head-dim 64 "
        "--vocab-size 154880 "
        "--rotary-base 1000000 "
        "--no-rope-fusion "
        "--mtp-num-layers 1 "
    )
scripts/models/joyai-llm-flash.py data-contract

核心修复文件之二:同样的 data-contract 修复,为 JoyAI-LLM-Flash 的 MLA 配置显式声明 `--rope-type rope`,避免其误用 YaRN。

# scripts/models/joyai-llm-flash.py 中的 model_args(),修复后版本
# 与 GLM-4.7-Flash 相同:在 MLA 参数组之前显式声明 --rope-type rope,
# 否则该模型会从 set_default_megatron_args() 继承 yarn 兜底值。
def model_args(nlayers: int | None = None, rotary_base: str | None = None) -> str:
    nlayers = nlayers if nlayers is not None else int(os.environ.get("MODEL_ARGS_NUM_LAYERS") or 40)
    rotary_base = rotary_base if rotary_base is not None else os.environ.get("MODEL_ARGS_ROTARY_BASE") or "32000000"
    return (
        "--disable-bias-linear "
        f"--num-layers {nlayers} "
        "--hidden-size 2048 "
        "--ffn-hidden-size 7168 "
        "--num-attention-heads 32 "
        "--kv-channels 128 "
        "--normalization RMSNorm "
        "--position-embedding-type rope "
        "--rope-type rope " # 显式声明 plain RoPE,与官方 checkpoint 对齐
        "--norm-epsilon 1e-6 "
        "--swiglu "
        "--untie-embeddings-and-output-weights "
        "--vocab-size 129280 "
        "--multi-latent-attention "
        "--q-lora-rank 1536 "
        "--kv-lora-rank 512 "
        "--qk-head-dim 128 "
        "--qk-pos-emb-head-dim 64 "
        "--v-head-dim 128 "
        "--qk-layernorm "
        f"--rotary-base {rotary_base} "
        "--mscale 1.0 "
        "--mscale-all-dim 1.0 "
        "--attention-softmax-in-fp32 "
        "--no-rope-fusion "
        "--num-experts 256 "
        f"--moe-layer-freq {moe_layer_freq(nlayers=nlayers, first_k_dense_replace=FIRST_K_DENSE_REPLACE)} "
        "--moe-ffn-hidden-size 768 "
        "--moe-router-topk 8 "
        "--moe-shared-expert-intermediate-size 768 "
        "--moe-router-pre-softmax "
        "--moe-router-score-function sigmoid "
        "--moe-router-enable-expert-bias "
        "--moe-router-load-balancing-type seq_aux_loss "
        "--moe-token-dispatcher-type alltoall "
        "--moe-aux-loss-coeff 0 "
        "--moe-router-bias-update-rate 0 "
        "--moe-router-group-topk 1 "
        "--moe-router-num-groups 1 "
        "--moe-grouped-gemm "
        # 其余 MoE 路由参数与原始定义一致,此处省略
    )

评论区精华

GLM-5 系列是否纳入本次 plain-RoPE 修复 设计

guapisolo 先让 agent 做 ablation 评估 rope issue 的规模,随后在 commit b9f613 中移除了 GLM-5/5.1/5.2 的变更。PR body 的 Note 给出核心技术原因:GLM-5 的 `DSAMLASelfAttention.forward()` 无条件解包 `(rotary_pos_emb, mscale)`,匹配 `YarnRotaryEmbedding.forward()` 的双返回值契约;换成 `RotaryEmbedding` 后 `forward()` 只返回一个 tensor,解包会失败。

结论:GLM-5 变更被移除,仅保留已验证的 GLM-4.7 与 JoyAI 部分;GLM-5 需要 attention 调用点一起适配后才能安全合并。 · 已解决

缺失 rope_type 时 yarn 兜底的契约风险 正确性

PR body 根因分析指出:`set_default_megatron_args()` 在 `multi_latent_attention` 启用时把缺失的 `rope_type` 映射为 `yarn`,对 DeepSeek/Kimi 合适,但对使用 plain RoPE 的 GLM 与 JoyAI 是错误的。修复选择在模型定义侧显式声明,而不是修改全局默认。

结论:采用数据契约层面的修复:仅给受影响模型加 `--rope-type rope`,不改全局 MLA 默认值,不影响显式 YaRN 配置。 · 已解决

风险与影响

  1. 位置编码语义切换风险(核心)scripts/models/glm4.7-flash.pyscripts/models/joyai-llm-flash.py 切换后,Megatron 侧将从 YarnRotaryEmbedding 变为 RotaryEmbedding。PR 未改动这两个模型对应的 attention adapter,如果它们也像 GLM-5 那样对 rotary 输出做双值解包,会在运行时报错;不过合并前 GLM-4.7 与 JoyAI 的快照与定向校验已通过,风险相对可控。

  2. 与既有训练产物兼容性:若此前用隐式 yarn 配置启动过训练或生成过 checkpoint,切换为 plain RoPE 后位置嵌入初始化不同,续训或加载旧权重会出现位置编码语义漂移,需要重新对齐或重新初始化。

  3. 测试覆盖缺口:没有新增直接针对 rope_type 传播的单元测试文件,回归保护完全依赖快照。快照覆盖了 model_args、shell launcher、Python launcher 的 prepare/execute 全链路,但仍缺少对 set_default_megatron_args() 兜底逻辑本身的单元断言。

  4. 派生变体继承:pruned 与 LoRA 变体自动继承修正值,但若某些派生脚本存在自己的参数覆盖逻辑,存在被二次覆盖的隐患;本次快照未覆盖所有派生组合。

对用户的影响:运行 GLM-4.7-Flash 与 JoyAI-LLM-Flash 的训练、转换流程时,生成的 launcher 命令会多出 --rope-type rope,位置编码从隐式 YaRN 切换为 plain RoPE,更贴近 HuggingFace 官方 checkpoint 的语义。对系统的影响:全局 MLA 默认值与显式 YaRN 配置(DeepSeek、Kimi)完全不受影响,改动是模型级、局部化的数据契约修复。对团队的影响:确立了“MLA 模型定义必须显式声明 rope_type”的约定,并示范了在未验证的模型变体面前收缩合并范围的协作方式;GLM-5 系列留下明确的 follow-up 任务。

隐式默认值兜底链路 位置编码语义切换 GLM-5 适配未完成 缺少新增单元测试

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论