[AMD][Diffusion] Migrate FlyDSL fused norm kernels to the v0.3.0 stable API (#36349)

Co-authored-by: Phil Li <haicli@amd.com>
This commit is contained in:
YC Yen-Ching Tseng
2026-09-02 23:03:08 -07:00
committed by GitHub
co-authored by Phil Li
parent 030d7e7e9b
commit 1fb85053e7
2 changed files with 541 additions and 516 deletions
@@ -9,20 +9,22 @@ Oracle: an fp32 reference chain, with a tolerance -- the kernel keeps fp32
statistics but reorders the reduction.
"""
import os
import subprocess
import sys
import pytest
import torch
import torch.nn.functional as F
from sglang.test.ci.ci_register import register_amd_ci
register_amd_ci(est_time=30, stage="jit-kernel-unit", runner_config="amd")
register_amd_ci(est_time=60, stage="jit-kernel-unit", runner_config="amd")
pytestmark = pytest.mark.skipif(not torch.cuda.is_available(), reason="GPU required")
DEVICE = "cuda"
FLYDSL_MODULE = "sglang.kernels.ops.diffusion.norm.fused_residual_norm_flydsl"
FLYDSL_D = 5120
FLYDSL_EPS = 1e-6
@@ -32,67 +34,259 @@ def _require_rocm():
pytest.skip("ROCm/HIP required for FlyDSL kernels")
def _flydsl_reference(residual, x, gate, weight, bias, scale, shift, norm_type, eps):
if residual is not None:
x = (residual.float() + x.float() * gate.float()).to(torch.bfloat16)
residual_out = x
else:
residual_out = None
if norm_type == "layer":
normed = F.layer_norm(x.float(), (FLYDSL_D,), weight, bias, eps)
else:
var = x.float().pow(2).mean(-1, keepdim=True)
normed = x.float() * torch.rsqrt(var + eps) * weight.float()
y = (normed * (1.0 + scale.float()) + shift.float()).to(torch.bfloat16)
return y, residual_out
def _flydsl_ops():
"""Resolve the FlyDSL exports, skipping when the installed FlyDSL is too old.
Resolved inside each test: the FlyDSL compiler only exists on ROCm, and the
facade imports the submodule the moment an export is named -- a module-level
import would fail collection of this whole file on CUDA.
@pytest.mark.parametrize("with_residual", [False, True])
@pytest.mark.parametrize(
"norm_type,B,L",
[("rms", 1, 16), ("rms", 2, 16), ("layer", 2, 16), ("rms", 1, 90000)],
)
def test_flydsl_norm_scale_shift(with_residual, norm_type, B, L):
The kernel module raises ImportError when the stable FlyDSL surface it needs
is absent, which is also how ``layernorm.py`` detects that it must fall back
to the native path. A runner image predating that surface should skip here
rather than report a kernel regression.
"""
_require_rocm()
# Imported inside the test: the FlyDSL compiler only exists on ROCm, and
# the facade resolves an export the moment it is named -- a module-level
# import here would fail collection of this whole file on CUDA.
from sglang.kernels.ops.diffusion import (
flydsl_fused_residual_norm_scale_shift,
flydsl_norm_scale_shift,
)
try:
from sglang.kernels.ops.diffusion import (
flydsl_fused_residual_norm_scale_shift,
flydsl_norm_scale_shift,
)
except ImportError as exc:
pytest.skip(f"FlyDSL unavailable or too old for the fused norm kernels: {exc}")
return flydsl_fused_residual_norm_scale_shift, flydsl_norm_scale_shift
def _mk(shape, dtype=torch.bfloat16):
return torch.randn(*shape, device=DEVICE, dtype=dtype)
def _ref_rms_norm(x_f32, eps):
var = x_f32.pow(2).mean(-1, keepdim=True)
return x_f32 * torch.rsqrt(var + eps)
def _apply_affine(normed, weight, bias, norm_type):
"""Reproduce the kernel's affine stage.
The kernel applies weight and bias together, and skips both when weight is
absent -- a standalone bias is not reachable through the public op.
"""
if weight is None:
return normed
out = normed * weight.float()
if norm_type == "layer" and bias is not None:
out = out + bias.float()
return out
def _ref_norm(x_bf16, weight, bias, norm_type, eps):
base = x_bf16.float()
if norm_type == "layer":
mean = base.mean(-1, keepdim=True)
var = base.var(-1, keepdim=True, unbiased=False)
normed = (base - mean) * torch.rsqrt(var + eps)
else:
normed = _ref_rms_norm(base, eps)
return _apply_affine(normed, weight, bias, norm_type)
def _ref_fused_residual_norm_ss(
residual, x, gate, weight, bias, scale, shift, norm_type, eps
):
ref_res = residual.float() + x.float() * (gate.float() if gate is not None else 1)
ref_res_bf16 = ref_res.to(torch.bfloat16)
normed = _ref_norm(ref_res_bf16, weight, bias, norm_type, eps)
y = (normed * (1.0 + scale.float()) + shift.float()).to(torch.bfloat16)
return y, ref_res_bf16
def _ref_norm_ss(x, weight, bias, scale, shift, norm_type, eps):
normed = _ref_norm(x, weight, bias, norm_type, eps)
return (normed * (1.0 + scale.float()) + shift.float()).to(torch.bfloat16)
@pytest.fixture(autouse=True)
def _seed():
torch.manual_seed(42)
shape = (B, L, FLYDSL_D)
x = torch.randn(shape, device=DEVICE, dtype=torch.bfloat16)
weight = torch.randn(FLYDSL_D, device=DEVICE, dtype=torch.float32)
# norm_type, B, L, has_gate, has_weight, scale/shift layout
FUSED_CASES = [
("rms", 1, 16, True, True, "bcast"),
("rms", 2, 16, True, True, "bcast"),
("layer", 2, 16, True, True, "bcast"),
("rms", 1, 90000, True, True, "bcast"),
# gate-free path: a distinct compiled specialization
("rms", 2, 16, False, True, "bcast"),
("layer", 2, 16, False, True, "bcast"),
# no-affine path: the production default
("rms", 2, 16, True, False, "bcast"),
("layer", 2, 16, True, False, "bcast"),
# per-row scale/shift exercises the non-zero row stride
("rms", 2, 16, True, True, "perrow"),
("layer", 2, 16, True, True, "perrow"),
]
@pytest.mark.parametrize("norm_type,B,L,has_gate,has_weight,ss", FUSED_CASES)
def test_flydsl_fused_residual_norm_scale_shift(
norm_type, B, L, has_gate, has_weight, ss
):
fused_op, _ = _flydsl_ops()
ss_shape = (B, 1, FLYDSL_D) if ss == "bcast" else (B, L, FLYDSL_D)
residual = _mk((B, L, FLYDSL_D))
x = _mk((B, L, FLYDSL_D))
gate = _mk((B, 1, FLYDSL_D)) if has_gate else None
weight = _mk((FLYDSL_D,), torch.float32) if has_weight else None
bias = (
torch.randn(FLYDSL_D, device=DEVICE, dtype=torch.float32)
if norm_type == "layer"
_mk((FLYDSL_D,), torch.float32)
if (has_weight and norm_type == "layer")
else None
)
scale = torch.randn(B, 1, FLYDSL_D, device=DEVICE, dtype=torch.bfloat16)
shift = torch.randn(B, 1, FLYDSL_D, device=DEVICE, dtype=torch.bfloat16)
scale = _mk(ss_shape)
shift = _mk(ss_shape)
if with_residual:
residual = torch.randn(shape, device=DEVICE, dtype=torch.bfloat16)
gate = torch.randn(B, 1, FLYDSL_D, device=DEVICE, dtype=torch.bfloat16)
y, res = flydsl_fused_residual_norm_scale_shift(
residual, x, gate, weight, bias, scale, shift, norm_type, FLYDSL_EPS
)
y_ref, res_ref = _flydsl_reference(
residual, x, gate, weight, bias, scale, shift, norm_type, FLYDSL_EPS
)
torch.testing.assert_close(res, res_ref, atol=5e-2, rtol=5e-2)
else:
y = flydsl_norm_scale_shift(
x, weight, bias, scale, shift, norm_type, FLYDSL_EPS
)
y_ref, _ = _flydsl_reference(
None, x, None, weight, bias, scale, shift, norm_type, FLYDSL_EPS
)
y, res_out = fused_op(
residual, x, gate, weight, bias, scale, shift, norm_type, FLYDSL_EPS
)
y_ref, res_ref = _ref_fused_residual_norm_ss(
residual, x, gate, weight, bias, scale, shift, norm_type, FLYDSL_EPS
)
torch.testing.assert_close(res_out, res_ref, atol=5e-2, rtol=5e-2)
torch.testing.assert_close(y, y_ref, atol=1.0, rtol=5e-2)
NSS_CASES = [
("rms", 2, 16, True, "bcast"),
("layer", 2, 16, True, "bcast"),
("rms", 1, 90000, True, "bcast"),
("layer", 1, 90000, True, "bcast"),
# no-affine path
("rms", 2, 16, False, "bcast"),
("layer", 2, 16, False, "bcast"),
# per-row scale/shift
("rms", 2, 16, True, "perrow"),
("layer", 2, 16, True, "perrow"),
]
@pytest.mark.parametrize("norm_type,B,L,has_weight,ss", NSS_CASES)
def test_flydsl_norm_scale_shift(norm_type, B, L, has_weight, ss):
_, nss_op = _flydsl_ops()
ss_shape = (B, 1, FLYDSL_D) if ss == "bcast" else (B, L, FLYDSL_D)
x = _mk((B, L, FLYDSL_D))
weight = _mk((FLYDSL_D,), torch.float32) if has_weight else None
bias = (
_mk((FLYDSL_D,), torch.float32)
if (has_weight and norm_type == "layer")
else None
)
scale = _mk(ss_shape)
shift = _mk(ss_shape)
y = nss_op(x, weight, bias, scale, shift, norm_type, FLYDSL_EPS)
y_ref = _ref_norm_ss(x, weight, bias, scale, shift, norm_type, FLYDSL_EPS)
torch.testing.assert_close(y, y_ref, atol=1.0, rtol=5e-2)
def test_flydsl_fused_frame_gate_4d_scale_shift():
"""4D (B, NF, 1, D) scale/shift is expanded to per-row by _prep_slices."""
fused_op, _ = _flydsl_ops()
B, L, NF = 2, 16, 4
residual, x = _mk((B, L, FLYDSL_D)), _mk((B, L, FLYDSL_D))
gate = _mk((B, 1, FLYDSL_D))
weight = _mk((FLYDSL_D,), torch.float32)
scale4, shift4 = _mk((B, NF, 1, FLYDSL_D)), _mk((B, NF, 1, FLYDSL_D))
y, res_out = fused_op(
residual, x, gate, weight, None, scale4, shift4, "rms", FLYDSL_EPS
)
def expand(t):
return t.expand(B, NF, L // NF, FLYDSL_D).reshape(B, L, FLYDSL_D)
y_ref, res_ref = _ref_fused_residual_norm_ss(
residual,
x,
gate,
weight,
None,
expand(scale4),
expand(shift4),
"rms",
FLYDSL_EPS,
)
torch.testing.assert_close(res_out, res_ref, atol=5e-2, rtol=5e-2)
torch.testing.assert_close(y, y_ref, atol=1.0, rtol=5e-2)
def test_flydsl_multi_iteration_dim():
"""D=10240 drives NUM_ITERS=2, exercising more than one register-cached tile."""
_, nss_op = _flydsl_ops()
big_d, B, L = 10240, 1, 16
x = _mk((B, L, big_d))
weight = _mk((big_d,), torch.float32)
scale, shift = _mk((B, 1, big_d)), _mk((B, 1, big_d))
y = nss_op(x, weight, None, scale, shift, "rms", FLYDSL_EPS)
y_ref = _ref_norm_ss(x, weight, None, scale, shift, "rms", FLYDSL_EPS)
torch.testing.assert_close(y, y_ref, atol=1.0, rtol=5e-2)
def test_flydsl_compile_cache_reuse_across_row_counts_and_layouts():
"""Guard for the compile-cache/shape-specialization hazard.
The cache key is (D, is_rms, has_gate, has_weight) and deliberately excludes
the row count and whether scale/shift are broadcast. One compiled kernel must
therefore stay correct when both of those change between calls.
"""
_, nss_op = _flydsl_ops()
weight = _mk((FLYDSL_D,), torch.float32)
for L, ss in ((16, "bcast"), (90000, "bcast"), (16, "perrow"), (64, "bcast")):
ss_shape = (1, 1, FLYDSL_D) if ss == "bcast" else (1, L, FLYDSL_D)
x = _mk((1, L, FLYDSL_D))
scale, shift = _mk(ss_shape), _mk(ss_shape)
y = nss_op(x, weight, None, scale, shift, "rms", FLYDSL_EPS)
y_ref = _ref_norm_ss(x, weight, None, scale, shift, "rms", FLYDSL_EPS)
torch.testing.assert_close(
y, y_ref, atol=1.0, rtol=5e-2, msg=f"L={L} layout={ss}"
)
def test_flydsl_imports_without_flydsl_source_tree():
"""The module must resolve against the installed FlyDSL wheel alone.
A FlyDSL source checkout on PYTHONPATH makes its `kernels` package
importable and would hide an accidental source-tree dependency, so re-import
in a subprocess with those entries stripped from the path.
"""
_flydsl_ops()
clean = [
d
for d in sys.path
if d
and not os.path.isfile(os.path.join(d, "kernels", "common", "buffer_ops.py"))
]
env = dict(os.environ, PYTHONPATH=os.pathsep.join(clean))
code = (
"import importlib, sys;"
f"m = importlib.import_module('{FLYDSL_MODULE}');"
"assert 'kernels' not in sys.modules, 'leaked FlyDSL source-tree kernels package';"
"print('OK', m.FLYDSL_NORM_MIN_ALIGNED_DIM)"
)
r = subprocess.run(
[sys.executable, "-c", code], capture_output=True, text=True, env=env
)
assert r.returncode == 0, f"stdout={r.stdout}\nstderr={r.stderr}"
assert f"OK {FLYDSL_D}" in r.stdout, r.stdout
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v"]))