Prhub

#35114 [kernels] Reorganize ops/diffusion by operator domain behind a lazy facade

原始 PR 作者 BBuf 合并时间 2026-08-18 20:37 文件变更 167 提交数 7 评论 0 代码增减 +4808 / -4335

执行摘要

ops/diffusion 按算子域重组并加懒门面统一导出

PR body 明确指出 ops/diffusion 是 kernels/ops 中唯一按后端组织而非按算子组织的操作组,导致"norm + scale/shift"的六种实现散落在三个目录,回答"does this fusion exist on ROCm?"需要 grep 整个分组;同时该分组混入了 kernel、请求级 mount 策略、JIT C++/CUDA 扩展三类不同性质的东西,141 处外部 import 直接穿透叶模块,把布局钉死。懒门面是必须的而非风格选择:"the backends have disjoint heavy dependencies ... an eager re-export would make each an import-time requirement on every platform."入口协议从返回 None 改为 raise + 谓词,是因为"a forgotten is None check produced a wrong image rather than an exception, which is the worst available failure mode for a denoiser."

值得精读。该 PR 是 kernels 组织方式的架构级调整,核心看点有三:一是 PEP-562 懒门面的实现与"导出表 + 注册表 + 静态扫描守护"的组合,为多后端并存的算子库提供了可复制的组织范式;二是将"返回 None 表示不支持"改为"谓词 + raise"的入口协议,直接消除了扩散模型 denoiser 中静默出错的最坏失败模式;三是测试重组展示了大文件合并时如何用参数化消除真实重复、并识别非本域的测试归属。建议后续新增算子或 backend 时对照 README 选择矩阵与 test_import_surface 的约束进行扩展。

讨论亮点

该 PR 的 review 评论数为 0,但 7 个 commit 的提交信息揭示了演进中的关键问题与决策:

关于测试套件合并后的 CI lane 继承:"Merging N test files into one operator-domain suite also merges their CI lane registrations... test_layout.py picked up the B200 lane from the causal-Conv3d section, and the varlen<->USPAttention comparison folded in from test_varlen_uspattn_equivalence.py needs FlashAttention, which that image does ..."

关于 B200/HIP 专属测试 lane:"test_rope.py carried the B200 lane because the LTX-2 split-RoPE kernel is validated there... the fused_inplace_qknorm_rope cases in that file are held to the split baseline... three bit-exact assertions that hold on H200 do not hold on B200."

关于平台谓词性能:"is_cuda()/is_hip() in common/platform.py derived their answer through platform_key()... walks a chain of getattr(current_platform, f"is_{name}")() calls. Those predicates sit in per-call kernel guards... Delegate straight to that method instead."

关于 Sana 测试竞态:"test_sana_fused_ln_modulate_is_bit_exact fills x/scale/shift on the default stream, then enters a fresh torch.cuda.Stream() and reads them there without making the side stream wait... if a mismatch disables _SANA_LN_MOD permanently, every later parametrization also stops registering its signature -- so one lost race fails three of the four cases."

这些都属于实现过程中发现并解决的问题,全部已解决。

实现拆解

按以下 5 步拆解实现:

  1. 目录结构重组:将ops/diffusion下按后端划分的triton/cutedsl/flydsl/目录拆成按算子域划分的子包——norm/modulate/rope/activation/attention/layout/——后端改为文件名后缀(_triton_jit_cutedsl_flydsl_bitexact)。非算子的两类内容单独建目录:sites/承载请求级 mount 策略(重写 nn.Module 树),ext/承载 Hunyuan3D 光栅化/网格处理等无后端维度、无数值契约的扩展。kernel 函数体原样搬迁,包括layernorm_modulatermsnorm_scale_shift_bitexact的 SASS 级 bit-exact 复刻,完全不改动。

  2. 懒门面与导入面收敛__init__.py变为 PEP-562 懒门面,通过__getattr__按需解析 117 个导出符号与 38 个 KernelSpec 注册(此前只有 4 个)。这样 Triton、CUTLASS/CuTe-DSL、FlyDSL、MLX 等互斥重依赖不会在import sglang时全部加载。所有 141 个外部 import 点改为从门面导入,使内部布局可再次调整而无需改动调用点。

  3. 入口协议与平台分发统一can_fuse_*谓词全部折叠为can_use_*group_norm_silu_4dgroup_norm_silu_rowswan_rmsnorm_silu三个 kernel 从"不支持时返回 None"改为"调用方先查谓词、直接调用则 raise",避免错误的图像而非异常。scale_shiftnormrotaryrmsnorm_onepass四处手写的 import-time 平台判断链收敛到common.platform.select_impl一个接缝,并消除了三处对multimodal_gen.current_platform的上行 import。新增KernelBackend.FLYDSL及其BACKEND_METHODS条目,使 ROCm norm kernel 如实注册自己的出身。

  4. 测试重组与守护:37 个测试文件合并为 8 个——5 个非 diffusion kernel 测试迁回真实归属(两个量化测试到ops/quantization/,perf-logger 同步测试到profiling/),其余按算子域合并为一个套件,外加test_sites.py(gate 协议)与test_model_fast_paths.py(模型级接线)。合并时对真实重复做了参数化消除(LN+modulate 有五份近似的模型级拷贝、CuTe-DSL 套件两个类只差 gate 参数),测试主体保留。新增的test_import_surface.py用 AST 静态扫描守护导出表、注册表解析、门面惰性及"禁止外部代码 import 子模块"这两条不变量。

  5. 配套文档与 CI 修正README.md增加选择矩阵,说明九个看起来可互换实则不同的 norm 实现的数值契约与支持形状/布局。提交历史还包含三处配套修正:合并套件后的 CI lane 继承问题拆分(test_layout.py的 B200 lane 需要 FlashAttention)、B200/HIP 专属测试 lane 分离、平台谓词改为委托平台自身缓存的is_cuda()/is_hip(),以及 Sana 测试的跨流竞态修复。

文件 模块 状态 重要度
python/sglang/kernels/ops/diffusion/__init__.py 门面层 modified 7.92
python/sglang/kernels/ops/diffusion/common/platform.py 平台分发 modified 6.72
test/registered/kernels/ops/diffusion/test_import_surface.py 导入守护 added 7.99
test/registered/kernels/ops/diffusion/test_model_fast_paths.py 模型路径 added 7.97
test/registered/kernels/ops/diffusion/test_layout.py 布局测试 added 7.76
python/sglang/kernels/ops/diffusion/README.md 文档 modified 5.0

关键符号

select_impl can_use_wan_rmsnorm_silu can_use_usp_merge_heads can_use_ln_modulate can_use_modulate_scale_shift_cuda can_use_residual_gate_add_cuda mount_fused_ln_modulate BitExactFusionGate.accept_or_fallback _module_defines test_runtime_code_imports_only_through_the_facade fused_ln_modulate fused_ltx2_rms_norm_modulate

关键源码片段

test/registered/kernels/ops/diffusion/test_import_surface.py test-coverage

新增的布局守护测试,用 AST 静态扫描双向校验导出表与注册表、验证门面惰性、禁止包外代码深导入子模块,是防止本次重组后布局再次退化的关键机制。

# test/registered/kernels/ops/diffusion/test_import_surface.py
# 这类守护测试把“只允许通过门面导入”的约定固化成可执行的断言,
# 防止将来有人为了图省事直接 import 子模块导致布局再次被锁死。
​
​
def _module_defines(module_path: str) -> set[str]:
    """读取子模块源码,找出其在顶层绑定的名字,但不真正 import 它。    原因:import 会拉入 Triton / CuTe-DSL / FlyDSL 这些彼此互斥的重依赖,
    CPU CI lane 上没有它们;所以这里用 ast 静态解析替代 import。
    """
    path = _PACKAGE_DIR / (module_path.replace(".", "/") + ".py")
    if not path.exists():
        path = _PACKAGE_DIR / module_path.replace(".", "/") / "__init__.py"
    assert path.exists(), f"{PACKAGE}.{module_path} does not exist"
​
    names: set[str] = set()
    tree = ast.parse(path.read_text(encoding="utf-8"))
    for node in tree.body:
        if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
            names.add(node.name)
        elif isinstance(node, ast.Assign):
            names.update(t.id for t in node.targets if isinstance(t, ast.Name))
        elif isinstance(node, (ast.Import, ast.ImportFrom)):
            names.update((a.asname or a.name).split(".")[0] for a in node.names)
        elif isinstance(node, (ast.If, ast.Try)):
            # 平台条件式 rebind(例如 x = select_impl(...))依然绑定公开名字
            for inner in ast.walk(node):
                if isinstance(inner, (ast.FunctionDef, ast.ClassDef)):
                    names.add(inner.name)
                elif isinstance(inner, ast.Assign):
                    names.update(t.id for t in inner.targets if isinstance(t, ast.Name))
    return names
​
​
def test_runtime_code_imports_only_through_the_facade(root):
    """扫描整个仓库,禁止运行时代码 import diffusion 的子模块。    只有门面(sglang.kernels.ops.diffusion)是唯一对外合法入口,
    这样内部布局将来可以再移动,而不会破坏任何调用点。
    """
    root_dir = _REPO_ROOT / root
    if not root_dir.exists(): # 源码检出才检查
        pytest.skip(f"{root} not present in this install")
​
    offenders = []
    for path in root_dir.rglob("*.py"):
        rel = path.relative_to(_REPO_ROOT).as_posix()
        if rel.startswith("python/sglang/kernels/ops/diffusion/"):
            continue # 包内部子模块互相 import 是重组的本意
        if rel in _DEEP_IMPORT_ALLOWLIST:
            continue
        try:
            tree = ast.parse(path.read_text(encoding="utf-8"))
        except (SyntaxError, UnicodeDecodeError):
            continue
        for node in ast.walk(tree):
            if (
                isinstance(node, ast.ImportFrom)
                and node.module
                and node.module.startswith(f"{PACKAGE}.")
            ):
                offenders.append(f"{rel}:{node.lineno} imports {node.module}")
            elif isinstance(node, ast.Import):
                offenders.extend(
                    f"{rel}:{node.lineno} imports {a.name}"
                    for a in node.names
                    if a.name.startswith(f"{PACKAGE}.")
                )
    assert not offenders, (
        "import from sglang.kernels.ops.diffusion instead of a submodule"
    )
test/registered/kernels/ops/diffusion/test_layout.py test-coverage

数据搬运类 kernel 的合并套件,覆盖 USP merge-heads、Ulysses QKV pack、varlen pack/scatter、causal Conv3d cat+pad 等;由于这些 kernel 仅搬运数值,全部用 torch.equal 做 bitwise 断言。

# test/registered/kernels/ops/diffusion/test_layout.py
# 数据搬运 kernel 只移动数值,天然 bitwise 等价于它替换的 aten 链,
# 因此全文件用 torch.equal 而非 assert_close,容差会掩盖真实 bug。
​
​
def _build_mask(bs, s_txt, s_img, valid_txt_lens):
    """构造 varlen 测试用的 [B, S] 布尔掩码:文本段只保留前 vt 行,图像段全保留。"""
    mask = torch.zeros(bs, s_txt + s_img, dtype=torch.bool, device=DEVICE)
    for b, vt in enumerate(valid_txt_lens):
        mask[b, :vt] = True
        mask[b, s_txt:] = True
    return mask
​
​
@pytest.mark.parametrize("dtype", VARLEN_DTYPES)
@pytest.mark.parametrize("shape", VARLEN_SHAPES, ids=lambda s: s[0])
def test_varlen_pack_matches_index_select(dtype, shape):
    """fused_pack_qkv 必须与 index_select 严格一致,允许的形状矩阵覆盖生产 shape。"""
    _, bs, s_txt, s_img, num_heads, head_dim, valid_txt_lens = shape
    torch.manual_seed(0)
    s = s_txt + s_img
    indices, _ = _build_meta(_build_mask(bs, s_txt, s_img, valid_txt_lens))
​
    q, k, v = (
        torch.randn(bs, s, num_heads, head_dim, dtype=dtype, device=DEVICE)
        for _ in range(3)
    )
    fused = fused_pack_qkv(q, k, v, indices)
    for got, src in zip(fused, (q, k, v), strict=True):
        want = src.reshape(bs * s, num_heads, head_dim).index_select(0, indices)
        assert torch.equal(got, want)
​
​
def test_varlen_pack_handles_non_contiguous_input():
    """Q/K/V 可能以 (B, H, S, D) permute 形式到达,helper 必须自行 contig。"""
    torch.manual_seed(2)
    bs, s_txt, s_img, num_heads, head_dim = 2, 64, 128, 4, 64
    indices, _ = _build_meta(_build_mask(bs, s_txt, s_img, [32, 48]))
​
    pre = torch.randn(
        bs, num_heads, s_txt + s_img, head_dim, dtype=torch.bfloat16, device=DEVICE
    )
    q, k, v = (torch.randn_like(pre).permute(0, 2, 1, 3) for _ in range(3))
    assert not q.is_contiguous()
​
    fused = fused_pack_qkv(q, k, v, indices)
    for got, src in zip(fused, (q, k, v), strict=True):
        want = src.contiguous().flatten(0, 1).index_select(0, indices)
        assert torch.equal(got, want)

评论区精华

测试套件合并后的 CI lane 继承 测试

合并 N 个测试文件到域名套件时,每个部分继承了兄弟部分的 CI lane 注册,导致部分用例被放到依赖缺失的 runner 上:test_layout.py 因 causal-Conv3d 部分带上了 B200 lane,而 varlen<->USPAttention 对比需要 FlashAttention,该镜像未提供。

结论:拆分 lane 注册,按依赖隔离平台专属测试,避免测试被静默跳过或误跑。 · 已解决

B200 与 HIP 专属测试 lane 分离 测试

test_rope.py 因 LTX-2 split-RoPE 携带 B200 lane,但同文件的 fused_inplace_qknorm_rope 用例被约束到 split 基线,该分派在 Blackwell 上行为不同,三个在 H200 上成立的 bit-exact 断言在 B200 上不成立。

结论:将需要相反 runner 策略的两组用例拆分到不同文档与 lane。 · 已解决

平台谓词缓存与性能 性能

重构后 common/platform.py 的 is_cuda()/is_hip() 通过 platform_key() 推导,每次调用都走 getattr 链,而它们位于 try_fused_scaled_residual_add_exact 等热路径的逐次守卫中;原实现直接调用 lru_cache 的 current_platform.is_cuda()。

结论:平台谓词改为直接委托 current_platform 的缓存方法,避免每次 kernel guard 重新解析平台。 · 已解决

Sana 测试跨流竞态 正确性

test_sana_fused_ln_modulate_is_bit_exact 在默认流写 x/scale/shift,在新建 stream 读取且未同步;竞态导致 gate 的 first-sight torch.equal 检查失败,而 mismatch 会永久禁用 _SANA_LN_MOD,使后续三个参数化用例全部失败。

结论:修复流同步,避免 GPU 争用条件下偶发失败。 · 已解决

风险与影响

主要风险集中在导入面与测试面:

  • 跨模块大范围重构:167 个文件、141 处 import 改写,任何遗漏的深导入或导出表缺失都会在特定平台、特定模型运行时才暴露。虽然test_import_surface.py用 AST 静态扫描覆盖了导出表与合法 import 路径,但 allowlist 中仍有 3 个文件直接深导入(test 文件),属于特意放行。
  • 懒门面依赖解析__getattr__在首次访问时才解析符号,若导出表指向的模块路径错误,错误会延迟到模型调用那一刻才抛出,且只会发生在有对应后端的机器上,CPU/Apple 环境无法预演。
  • 平台分发统一common.platform.select_impl替代了原有的 import-time 平台 if 链,新接缝需要保证懒加载语义与原有行为完全一致;提交bb749c5专门修正了谓词缓存问题,说明此处容易踩性能与正确性坑。
  • 测试 lane 继承:合并套件后各部分的 CI lane 会互相继承,已出现过两次 lane 错配(B200 与 HIP 场景),若未来继续合入新的平台相关测试,需警惕同类问题。
  • 验证范围有限:精度验证仅在 1x H200 上完成,FlyDSL(ROCm gfx950)与 LTX-2 split-RoPE(B200)的 13 个跳过用例没有全平台覆盖;PR body 还披露了一个与本次改动无关的 pre-existing 失败test_diffusion_modelopt_fp8_scaled_mm.py,需在后续单独跟进。

对用户与运行时透明:kernel 数学逻辑零变更,外部 API 面(通过门面的导入)保持兼容。对开发者的影响是结构性的——新增 diffusion kernel 时必须遵循按算子域放置、后端写进文件名、经由门面导出、按can_use_*+raise 协议暴露、测试归入对应域名套件这几条约定,否则会被test_import_surface.py拦截。影响范围覆盖全部 32 个 consumer 模块(每个 DiT、VAE、layer 与 stage),所有 diffusion 相关 CI lane(CUDA/AMD/B200/nightly)均会运行重组合并后的测试套件。跨平台依赖被隔离到懒门面之后,未来接入新 backend(如 MLX)不再污染其他平台的 import 路径。

跨模块大范围重构 import 面 141 处改动 懒门面依赖静态检查 仅 H200 单机验证 存在无关的 pre-existing 失败

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论