Prhub

#19484 [CPU] Add Qwen3.5 model optimization for CPU

原始 PR 作者 jianan-gu 合并时间 2026-04-27 01:12 文件变更 20 提交数 43 评论 66 代码增减 +768 / -209

执行摘要

CPU 优化 Qwen3.5 系列:TP padding 和新 kernel

PR body 明确指出需要为 Qwen3.5 系列添加 CPU 优化的支持,包括 dtype 支持、融合 kernel、TP padding 和日志改进。评论中提到这是 Intel CPU 上 Qwen3.5 系列启动的最后一个 PR。

值得精读,尤其是 resolve_head_dim 的提取、pad_loaded_weight 的谨慎设计、以及 TP padding 在 CPU 下的交互。Review 中涵盖的多模态配置边界条件和 assert 陷阱对所有贡献者都有参考价值。

讨论亮点

Review 中聚焦在多个正确性问题上:JustinTong 指出 head_dim 解析在多模态子配置下可能 UnboundLocalError(update_config.py),建议兼容 d_model,作者接受并提取为 resolve_head_dim;pad_loaded_weight 的比例分割可能导致无声截断,添加 assert 后修复;多处 assert 使用了永真元组 (cond, msg,) 导致不触发,已全部修正;CPU-without-AMX 的 tie_word_embeddings 条件缺陷导致 lm_head 可能未初始化,改为对称条件;日志在循环内重复且级别不当,已提升循环外并改为 warning;异常处理从 bare except 改为 except Exception 以兼顾 RuntimeError 和 AssertionError。还有测试未注册 CPU CI 的问题被推迟到后续 PR。

实现拆解

  1. 新增 CPU kernel:在 sgl-kernel/csrc/cpu/model/qwen3.cpp 中添加 fused_qkvzba_split_reshape_cat_contiguous_cpu,用于连续布局的 QKV+BA 拆分,同时添加 dtype 一致性检查;
  2. 数据类型扩展:在 sgl-kernel/csrc/cpu/mamba/fla.cpp 中将 fused_sigmoid_gating_delta_rule_updatefused_gdn_gating 泛化以支持 bf16 A_log,并新增标量回退分支;
  3. TP padding 机制:在 update_config.py 中重构 get_num_heads_padding_size,新增 resolve_head_dim 辅助函数;在 weight_utils.py 中新增 pad_loaded_weight 函数,并在 sharded_weight_loader 中扩展 padding 条件。这些更改使 CPU 在不均衡 TP 场景下能正确 padding;
  4. 模型层适配:修改 qwen3_vl.pyqwen3_5.py 等模型,注入 CPU 特定逻辑(AMX 检测、padded_context_dim 传递、tie_word_embeddings 条件调整);
  5. 工具与日志:在 common.py 中添加 log_debug_on_rank0,并改进 log_info_on_rank0 的异常处理;
  6. 测试配套:在 test/srt/cpu/test_qwen3.py 中新增 test_fused_qkvzba_split_reshape_cat_contiguous 和辅助函数。
文件 模块 状态 重要度
python/sglang/srt/configs/update_config.py 配置层 modified 8.62
python/sglang/srt/model_loader/weight_utils.py 权重加载 modified 7.22
sgl-kernel/csrc/cpu/model/qwen3.cpp CPU Kernel modified 6.96
python/sglang/srt/utils/common.py 工具函数 modified 6.62
sgl-kernel/csrc/cpu/mamba/fla.cpp CPU Kernel modified 6.61
python/sglang/srt/models/qwen3_vl.py 视觉模型 modified 6.3
python/sglang/srt/models/qwen3_5.py 模型定义 modified 6.19
test/srt/cpu/test_qwen3.py 测试 modified 5.8

关键符号

get_num_heads_padding_size resolve_head_dim update_config pad_loaded_weight fused_qkvzba_split_reshape_cat_contiguous_cpu fused_sigmoid_gating_delta_rule_update fused_gdn_gating_kernel log_debug_on_rank0

关键源码片段

python/sglang/srt/configs/update_config.py core-logic

核心配置逻辑变更,重构了 head_dim 解析和 padding 大小计算,新增 resolve_head_dim 辅助函数

def resolve_head_dim(cfg, num_heads, is_text_config):
    # 默认通过 hidden_size 和 num_heads 计算 head_dim,兼容 d_model 属性
    hidden_size = getattr(cfg, "hidden_size", getattr(cfg, "d_model", None))
    head_dim = hidden_size // num_heads if hidden_size else None
    # 文本配置优先使用 qk_head_dim 或 head_dim 属性
    if is_text_config:
        if hasattr(cfg.hf_config, "qk_head_dim"):
            head_dim = cfg.hf_config.qk_head_dim
        elif hasattr(cfg.hf_text_config, "head_dim"):
            head_dim = cfg.hf_text_config.head_dim
        elif hasattr(cfg.hf_config, "head_dim"):
            head_dim = cfg.hf_config.head_dim
    else:
        # 非文本配置(vision/audio)直接使用 head_dim 属性
        if hasattr(cfg, "head_dim"):
            head_dim = cfg.head_dim
    return head_dim
​
​
def get_num_heads_padding_size(tp_size, weight_block_size, head_dim=None):
    # 当 head_dim 为 None 时(来自非文本配置),使用简单规则
    if head_dim is None:
        pad_size = (
            tp_size * 2
            if tp_size % 2 == 1 and weight_block_size is not None
            else tp_size
        )
        return pad_size
    pad_size = tp_size
    # 考虑 weight_block_size 对齐要求,用 lcm 扩展 pad_size
    if weight_block_size is not None and head_dim % weight_block_size[0] != 0:
        import math
        pad_size = tp_size * (
            math.lcm(head_dim, weight_block_size[0]) // weight_block_size[0]
        )
    return pad_size
python/sglang/srt/model_loader/weight_utils.py data-contract

新增 pad_loaded_weight 函数用于 TP padding 时的权重补齐,并扩展 sharded_weight_loader 的 padding 条件

def pad_loaded_weight(loaded_weight, output_dim, output_sizes):
    # 当 loaded_weight 小于 output_sizes 时进行零填充,用于 TP 非对齐场景
    total_output_size = sum(output_sizes)
    raw_output_size = loaded_weight.size(output_dim)
    if total_output_size > raw_output_size:
        # 按比例分割 raw 部分,必须保证总和相等(否则报错)
        weight_split_size = [
            int(output_size / total_output_size * raw_output_size)
            for output_size in output_sizes
        ]
        assert (
            sum(weight_split_size) == raw_output_size
        ), f"Padding failed: sum splits {sum(weight_split_size)} != raw size {raw_output_size}"
        split_weight = loaded_weight.split_with_sizes(weight_split_size, dim=output_dim)
        padded_parts = []
        for i, output_size in enumerate(output_sizes):
            pad_size = output_size - weight_split_size[i]
            if pad_size > 0:
                pad_shape = list(loaded_weight.size())
                pad_shape[output_dim] = pad_size
                pad_tensor = torch.zeros(pad_shape, dtype=loaded_weight.dtype)
                padded_parts.append(torch.cat([split_weight[i], pad_tensor], dim=output_dim))
            else:
                padded_parts.append(split_weight[i])
        return torch.cat(padded_parts, dim=output_dim)
    else:
        return loaded_weight

评论区精华

head_dim 解析兼容多模态配置 正确性

JustinTong 指出修复前 head_dim 仅在文本 head 不对齐时赋值,多模态子配置下 UnboundLocalError,建议使用 getattr 兼容 d_model

结论:已采纳并提取为 resolve_head_dim 函数 · 已解决

pad_loaded_weight 分割舍入错误 正确性

JustinTong 指出比例分割可能导致无声截断,建议 assert 总和并处理余数

结论:添加了 assert 并修正了 qwen3_5.py 和 mamba.py 中的类似代码 · 已解决

assert 格式错误 正确性

JustinTong 指出多处 assert 使用了元组形式 (cond, msg,) 永真,以及 Qwen3.cpp 缺少分号

结论:已修正所有出现点 · 已解决

CPU-without-AMX tie_word_embeddings 条件 正确性

JustinTong 指出条件 not _is_cpu and not _is_cpu_amx_available 等效于 not _is_cpu,漏掉 CPU-no-AMX 导致 lm_head 可能未初始化

结论:改为 and not (_is_cpu and _is_cpu_amx_available),并补全权重拷贝 · 已解决

日志重复与级别 性能

JustinTong 指出 mamba.py 中日志在循环内导致重复,建议提升到循环外并建议使用 warning 级别

结论:已修正:日志移出循环,级别改为 warning · 已解决

异常处理范围 正确性

JustinTong 指出 log_info_on_rank0 使用 except RuntimeError 无法捕获 AssertionError

结论:改为 except Exception 并优化消息格式 · 已解决

测试未注册到 CPU CI 测试

JustinTong 指出新测试未注册 CPU CI,建议搬移到 registered 目录并参数化 TP 大小

结论:作者表示将在后续 PR 中跟进(WIP refactoring) · unresolved

风险与影响

pad_loaded_weight 的整数分割即使有 assert 在极端比例下也可能触发异常导致加载失败;resolve_head_dim 若返回 None 可能导致后续 padding 逻辑异常;新 kernel 的测试缺少多 TP 尺寸覆盖;log_debug_on_rank0 的异常处理修改可能暴露不期望的异常;sharded_weight_loader 中 padding 条件扩展只在 CPU 激活,可能被误解为 CPU 通用代码。

对 CPU 用户可以运行 Qwen3.5 系列(含视觉模型)并获得 TP 非对齐支持;对系统增加了 SGL-Kernel 和权重加载的 CPU 专用路径;对团队提高了配置更新与日志可维护性,但增加了多条件分支的维护负担。

split 舍入 assert 可能失败 未测试多 TP 尺寸 多模态 head_dim 缺省值 assert 格式历史风险

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论