[Perf] Fuse AdaLayerNorm CUDA eager path with Triton (#8231)
Signed-off-by: GUOGUO <xwdong1998@163.com> Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com>
This commit is contained in:
1 parent
182b77da4c
commit
ee9ab9ea88
8 files changed
+1255
-14
No files matched your search
@@ -105,7 +105,7 @@ steps:
|
||||
|
||||
- label: "Diffusion · Other Test"
|
||||
commands:
|
||||
- timeout 15m pytest -sv tests/diffusion/*.py tests/diffusion/ar_diffusion -m 'core_model and cuda' --run-level "core_model"
|
||||
- timeout 15m pytest -sv tests/diffusion/*.py tests/diffusion/ar_diffusion tests/diffusion/layers/test_adalayernorm_*.py -m 'core_model and cuda' --run-level "core_model"
|
||||
mirror_hardwares: l4_1
|
||||
|
||||
- group: ":card_index_dividers: Tiny Model Tests [Single GPU]"
|
||||
|
||||
@@ -80,9 +80,13 @@ parallelism at `--usp 4` so the two CFG branches cover all 8 GPUs.
|
||||
- `--use-hsdp` is recommended for memory efficiency on the 14B DiT backbone.
|
||||
- `--vae-patch-parallel-size 8` is recommended for the standard 8-card setup;
|
||||
disabling VAE patch parallelism can significantly increase VAE latency.
|
||||
- The CUDA Triton fused AdaLayerNorm path is not available in the current
|
||||
release. The `VLLM_OMNI_ENABLE_TRITON_ADALN` toggle is reserved for that
|
||||
future implementation and should not be used until the kernel lands.
|
||||
- CUDA eager execution uses the fused Triton AdaLayerNorm path for supported
|
||||
contiguous 3D inputs with shared `(C,)`, `(1, C)`, or `(1, 1, C)` modulation,
|
||||
or per-sample `(B, 1, C)` modulation, including supported strided chunk views.
|
||||
Unsupported inputs use the native path; per-token `(B, L, C)` modulation
|
||||
used by TI2V remains native.
|
||||
- During `torch.compile`, AdaLayerNorm uses the native operation chain, which
|
||||
Inductor can fuse. The eager Triton path remains available after compilation.
|
||||
- In an 8x NVIDIA H20 test with the official model, `num_inference_steps=4`,
|
||||
`flow_shift=12.0`, `--use-hsdp`, `--cfg-parallel-size 2`, `--usp 4`, and
|
||||
`--vae-patch-parallel-size 8`, an internal Triton AdaLayerNorm prototype
|
||||
@@ -181,20 +185,20 @@ for complete client examples and request formats.
|
||||
#### Notes
|
||||
|
||||
- **Key flags:**
|
||||
- `--omni` — enables vLLM-Omni diffusion serving.
|
||||
- `--use-hsdp` — enables Hybrid Sharded Data Parallelism for the DiT model
|
||||
- `--omni` — enables vLLM-Omni diffusion serving.
|
||||
- `--use-hsdp` — enables Hybrid Sharded Data Parallelism for the DiT model
|
||||
weights.
|
||||
- `--usp <N>` — Unified Sequence Parallelism degree.
|
||||
- `--cfg <N>` — Classifier-Free Guidance parallelism; set to 2 for models
|
||||
- `--usp <N>` — Unified Sequence Parallelism degree.
|
||||
- `--cfg <N>` — Classifier-Free Guidance parallelism; set to 2 for models
|
||||
that require negative-prompt computation, omit for distilled models.
|
||||
- `--vae-patch-parallel-size 8` — parallelizes VAE decoding across all 8
|
||||
- `--vae-patch-parallel-size 8` — parallelizes VAE decoding across all 8
|
||||
cards.
|
||||
- `--vae-use-tiling` — enables tiled VAE decoding to reduce peak memory.
|
||||
- `--vae-use-tiling` — enables tiled VAE decoding to reduce peak memory.
|
||||
- **Performance tips:**
|
||||
- Installing mindie-sd and enabling Laser Attention
|
||||
- Installing mindie-sd and enabling Laser Attention
|
||||
(`MINDIE_SD_FA_TYPE=ascend_laser_attention`) provides up to ~40%
|
||||
performance improvement at 720p resolution due to long-sequence attention
|
||||
optimization.
|
||||
- **Known limitations:**
|
||||
- `MULTI_STREAM_MEMORY_REUSE=2` is required on NPU when using HSDP/FSDP2
|
||||
- `MULTI_STREAM_MEMORY_REUSE=2` is required on NPU when using HSDP/FSDP2
|
||||
due to a multi-stream memory reuse bug. This is not needed on CUDA.
|
||||
@@ -0,0 +1,110 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Compile and CUDA-graph smoke tests for the fused AdaLayerNorm fast path.
|
||||
|
||||
The serving stack reaches AdaLayerNorm.forward_cuda through CustomOp dispatch
|
||||
from compiled diffusion workloads (lazy regional torch.compile on repeated
|
||||
blocks) and can capture CUDA graphs around pipeline stages. These smokes
|
||||
verify that torch.compile uses the native chain and leaves the eager fused
|
||||
path eligible, and that the fused path can be captured/replayed inside a CUDA
|
||||
graph with outputs matching native. They are smoke tests, not performance
|
||||
measurements.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.layers.adalayernorm import (
|
||||
_FAILED_ADALN_KEYS,
|
||||
AdaLayerNorm,
|
||||
_adaln_fused_forward,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cuda]
|
||||
|
||||
|
||||
def _make(bs=2, seq=512, hidden=3072, dtype=torch.bfloat16, seed=0, framewise=False):
|
||||
if framewise:
|
||||
hidden = 2240
|
||||
m = AdaLayerNorm(hidden).to(device="cuda", dtype=dtype)
|
||||
g = torch.Generator(device="cuda").manual_seed(seed)
|
||||
if framewise:
|
||||
x = torch.randn(bs, 4, seq // 4, hidden, generator=g, device="cuda", dtype=dtype)
|
||||
modulation = torch.randn(bs, 4, 6, hidden, generator=g, device="cuda", dtype=dtype)
|
||||
scale, shift = modulation.chunk(6, dim=2)[:2]
|
||||
return m, x, scale, shift
|
||||
x = torch.randn(bs, seq, hidden, generator=g, device="cuda", dtype=dtype)
|
||||
scale = torch.randn(bs, 1, hidden, generator=g, device="cuda", dtype=dtype)
|
||||
shift = torch.randn(bs, 1, hidden, generator=g, device="cuda", dtype=dtype)
|
||||
return m, x, scale, shift
|
||||
|
||||
|
||||
@pytest.mark.parametrize("framewise", [False, True])
|
||||
def test_compile_smoke(framewise):
|
||||
# torch.compile over the guarded forward_cuda: with the is_compiling
|
||||
# guard the compiled region takes the NATIVE chain, so this compares
|
||||
# inductor's own LN+modulation fusion against the eager 4-kernel chain.
|
||||
# Cross-implementation BF16 outputs differ at rounding level (up to a few
|
||||
# ulps at O(1-8) magnitudes), hence the loose tolerance. The smoke
|
||||
# verifies the compiled workload stays functional - no crash, finite
|
||||
# outputs, no request failure.
|
||||
m, x, scale, shift = _make(framewise=framewise)
|
||||
compiled = torch.compile(m.forward_cuda, dynamic=False, fullgraph=True)
|
||||
out1 = compiled(x, scale, shift)
|
||||
out2 = compiled(x, scale, shift)
|
||||
native = m.forward_native(x, scale, shift)
|
||||
torch.testing.assert_close(out1.float(), native.float(), atol=5e-2, rtol=2e-2)
|
||||
torch.testing.assert_close(out2.float(), native.float(), atol=5e-2, rtol=2e-2)
|
||||
runtime_key = (
|
||||
x.device.index,
|
||||
x.shape[-1],
|
||||
x.dtype,
|
||||
m.layernorm.weight is not None,
|
||||
m.layernorm.bias is not None,
|
||||
True,
|
||||
True,
|
||||
)
|
||||
assert runtime_key not in _FAILED_ADALN_KEYS, "compile must not cache an eager launch failure"
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is not None, "compile must leave the eager fused path eligible"
|
||||
torch.testing.assert_close(fused.float(), native.float(), atol=2e-2, rtol=2e-2)
|
||||
assert runtime_key not in _FAILED_ADALN_KEYS
|
||||
|
||||
|
||||
@pytest.mark.parametrize("framewise", [False, True])
|
||||
def test_cuda_graph_capture_replay_smoke(framewise):
|
||||
# CUDA graph capture must prove the FUSED path itself is captured: call
|
||||
# _adaln_fused_forward inside the capture and assert it returned a tensor
|
||||
# (None would mean the guard/fallback silently degraded the graph to the
|
||||
# native chain). Warm the JIT on a side stream so capture contains no
|
||||
# compile-time allocations, then replay and compare against the native
|
||||
# fallback on the same static input buffers.
|
||||
m, x, scale, shift = _make(framewise=framewise)
|
||||
s = torch.cuda.Stream()
|
||||
s.wait_stream(torch.cuda.current_stream())
|
||||
with torch.cuda.stream(s):
|
||||
for _ in range(3):
|
||||
fused_warm = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused_warm is not None
|
||||
torch.cuda.current_stream().wait_stream(s)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
out_graph = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out_graph is not None
|
||||
graph.replay()
|
||||
torch.accelerator.synchronize()
|
||||
native = m.forward_native(x, scale, shift)
|
||||
torch.testing.assert_close(out_graph.float(), native.float(), atol=2e-2, rtol=2e-2)
|
||||
|
||||
# Replay must consume NEW input values: mutate the captured buffers and
|
||||
# verify the replay output tracks them (a no-op replay would pass the
|
||||
# check above trivially).
|
||||
x2, scale2, shift2 = torch.randn_like(x), torch.randn_like(scale), torch.randn_like(shift)
|
||||
x.copy_(x2)
|
||||
scale.copy_(scale2)
|
||||
shift.copy_(shift2)
|
||||
graph.replay()
|
||||
torch.accelerator.synchronize()
|
||||
native2 = m.forward_native(x, scale, shift)
|
||||
torch.testing.assert_close(out_graph.float(), native2.float(), atol=2e-2, rtol=2e-2)
|
||||
assert not torch.equal(native, native2), "replay must consume the new input"
|
||||
@@ -0,0 +1,234 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Candidate-added tests for the fused Triton AdaLayerNorm CUDA path.
|
||||
|
||||
Complements (never replaces) the frozen suite: exercises the fused fast path
|
||||
and the forward_native fallback branches of forward_cuda. Uses the same
|
||||
tolerance scheme as the frozen suite; golden = forward_native real behavior.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.layers.adalayernorm import (
|
||||
AdaLayerNorm,
|
||||
_adaln_fused_forward,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cuda]
|
||||
|
||||
TOL_STRICT = {"bf16": (2e-2, 2e-2), "fp32": (1e-3, 1e-3)}
|
||||
# Matches the frozen suite: the double-rounded golden chain deviates from a
|
||||
# pure-fp32 reference more than the strict band, so the fp32-reference check
|
||||
# uses the same loose band as the frozen tests.
|
||||
TOL_LOOSE = {"bf16": (5e-2, 2e-2), "fp32": (5e-3, 5e-3)}
|
||||
DTYPES = [torch.bfloat16, torch.float32]
|
||||
|
||||
|
||||
def fp32_reference(x, scale, shift, eps):
|
||||
xf = x.float()
|
||||
xn = torch.nn.functional.layer_norm(xf, (x.shape[-1],), None, None, eps)
|
||||
return xn * (1 + scale.float().reshape(1, 1, -1)) + shift.float().reshape(1, 1, -1)
|
||||
|
||||
|
||||
def make_module(hidden, affine, eps, device, dtype):
|
||||
m = AdaLayerNorm(hidden, elementwise_affine=affine, eps=eps)
|
||||
if affine:
|
||||
m = m.to(dtype)
|
||||
return m.to(device)
|
||||
|
||||
|
||||
def assert_close(a, b, dtype, loose=False):
|
||||
atol, rtol = (TOL_LOOSE if loose else TOL_STRICT)["bf16" if dtype == torch.bfloat16 else "fp32"]
|
||||
torch.testing.assert_close(a.float(), b.float(), atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
def make_inputs(bs, seq, hidden, dtype, device, seed=0):
|
||||
g = torch.Generator(device=device).manual_seed(seed)
|
||||
x = torch.randn(bs, seq, hidden, generator=g, device=device, dtype=dtype)
|
||||
scale = torch.randn(1, hidden, generator=g, device=device, dtype=dtype)
|
||||
shift = torch.randn(1, hidden, generator=g, device=device, dtype=dtype)
|
||||
return x, scale, shift
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("affine", [False, True])
|
||||
@pytest.mark.parametrize("bs,seq,hidden", [(1, 512, 3072), (2, 128, 1536), (1, 3, 1000)])
|
||||
@torch.no_grad()
|
||||
def test_fused_fast_path_matches_native(dtype, affine, bs, seq, hidden):
|
||||
# hidden=1000 exercises BLOCK_C masking (next_pow2(1000)=1024 > 1000)
|
||||
device = "cuda"
|
||||
m = make_module(hidden, affine, 1e-6, device, dtype)
|
||||
x, scale, shift = make_inputs(bs, seq, hidden, dtype, device)
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is not None, "supported inputs must take the fused path so BLOCK_C masking is exercised"
|
||||
out = fused
|
||||
assert out.shape == x.shape and out.dtype == dtype and out.device == x.device
|
||||
assert_close(out, m.forward_native(x, scale, shift), dtype)
|
||||
assert_close(out, fp32_reference(x, scale, shift, 1e-6), dtype, loose=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_fused_noncontiguous_x(dtype):
|
||||
# Non-contiguous x routes to the forward_native fallback (the fused kernel
|
||||
# requires contiguous x); result must still match native exactly.
|
||||
device = "cuda"
|
||||
hidden = 1536
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
g = torch.Generator(device=device).manual_seed(3)
|
||||
x_big = torch.randn(1, 1024, hidden, generator=g, device=device, dtype=dtype)
|
||||
x = x_big[:, ::2, :]
|
||||
scale = torch.randn(1, hidden, generator=g, device=device, dtype=dtype)
|
||||
shift = torch.randn(1, hidden, generator=g, device=device, dtype=dtype)
|
||||
out = m.forward_cuda(x, scale, shift)
|
||||
assert_close(out, m.forward_native(x, scale, shift), dtype)
|
||||
assert_close(out, fp32_reference(x.contiguous(), scale, shift, 1e-6), dtype, loose=True)
|
||||
|
||||
|
||||
def test_normalized_shape_mismatch_preserves_native_error():
|
||||
m = AdaLayerNorm(16, elementwise_affine=False).cuda()
|
||||
x = torch.randn(1, 2, 32, device="cuda")
|
||||
scale = torch.zeros(32, device="cuda")
|
||||
shift = torch.zeros_like(scale)
|
||||
assert _adaln_fused_forward(m, x, scale, shift) is None
|
||||
with pytest.raises(RuntimeError) as native_error:
|
||||
m.forward_native(x, scale, shift)
|
||||
with pytest.raises(RuntimeError) as cuda_error:
|
||||
m.forward_cuda(x, scale, shift)
|
||||
assert str(cuda_error.value) == str(native_error.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("grad_target", ["x", "scale", "shift", "weight", "bias"])
|
||||
def test_grad_fallback_matches_native_backward(grad_target):
|
||||
m = make_module(32, grad_target in ("weight", "bias"), 1e-6, "cuda", torch.float32)
|
||||
m.requires_grad_(False)
|
||||
x, scale, shift = make_inputs(1, 2, 32, torch.float32, "cuda", seed=12)
|
||||
targets = {"x": x, "scale": scale, "shift": shift, "weight": m.layernorm.weight, "bias": m.layernorm.bias}
|
||||
target = targets[grad_target]
|
||||
target.requires_grad_(True)
|
||||
with torch.enable_grad():
|
||||
assert _adaln_fused_forward(m, x, scale, shift) is None
|
||||
native = m.forward_native(x, scale, shift)
|
||||
out = m.forward_cuda(x, scale, shift)
|
||||
assert out.requires_grad
|
||||
torch.testing.assert_close(out, native)
|
||||
upstream = torch.randn_like(out)
|
||||
native_grad = torch.autograd.grad(native, target, upstream)[0]
|
||||
cuda_grad = torch.autograd.grad(out, target, upstream)[0]
|
||||
torch.testing.assert_close(cuda_grad, native_grad)
|
||||
|
||||
|
||||
def test_no_grad_keeps_fused_path_for_grad_requiring_tensors():
|
||||
m = make_module(32, True, 1e-6, "cuda", torch.float32)
|
||||
x, scale, shift = make_inputs(1, 2, 32, torch.float32, "cuda", seed=14)
|
||||
for tensor in (x, scale, shift):
|
||||
tensor.requires_grad_(True)
|
||||
with torch.no_grad():
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is not None
|
||||
assert not fused.requires_grad
|
||||
torch.testing.assert_close(fused, m.forward_native(x, scale, shift), atol=1e-3, rtol=1e-3)
|
||||
|
||||
|
||||
def test_1d_scale_shift_fast_path():
|
||||
# (C,) scale/shift broadcast per-channel exactly like (1, C)
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, torch.bfloat16)
|
||||
g = torch.Generator(device=device).manual_seed(10)
|
||||
x = torch.randn(1, 256, hidden, generator=g, device=device, dtype=torch.bfloat16)
|
||||
scale = torch.randn(hidden, generator=g, device=device, dtype=torch.bfloat16)
|
||||
shift = torch.randn(hidden, generator=g, device=device, dtype=torch.bfloat16)
|
||||
out = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out is not None, "(C,) scale/shift must take the fused path"
|
||||
assert_close(out, m.forward_native(x, scale, shift), torch.bfloat16)
|
||||
assert_close(out, fp32_reference(x, scale, shift, 1e-6), torch.bfloat16, loose=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_per_row_scale_falls_back_to_native(dtype):
|
||||
# (L, C) scale with B=1 is a valid per-row torch broadcast; it is not
|
||||
# per-channel, so forward_cuda must route it to forward_native.
|
||||
device = "cuda"
|
||||
hidden = 512
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
g = torch.Generator(device=device).manual_seed(4)
|
||||
x = torch.randn(1, 64, hidden, generator=g, device=device, dtype=dtype)
|
||||
scale = torch.randn(64, hidden, generator=g, device=device, dtype=dtype)
|
||||
shift = torch.randn(64, hidden, generator=g, device=device, dtype=dtype)
|
||||
out = m.forward_cuda(x, scale, shift)
|
||||
ref = m.forward_native(x, scale, shift)
|
||||
assert out.dtype == ref.dtype
|
||||
assert_close(out, ref, dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_mixed_dtype_falls_back_to_native(dtype):
|
||||
# scale/shift dtype != x.dtype: native type-promotes; fused path requires
|
||||
# matching dtypes, so this must go through forward_native.
|
||||
other = torch.float32 if dtype == torch.bfloat16 else torch.bfloat16
|
||||
device = "cuda"
|
||||
hidden = 256
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
g = torch.Generator(device=device).manual_seed(5)
|
||||
x = torch.randn(1, 32, hidden, generator=g, device=device, dtype=dtype)
|
||||
scale = torch.randn(1, hidden, generator=g, device=device, dtype=other)
|
||||
shift = torch.randn(1, hidden, generator=g, device=device, dtype=other)
|
||||
out = m.forward_cuda(x, scale, shift)
|
||||
ref = m.forward_native(x, scale, shift)
|
||||
assert out.dtype == ref.dtype
|
||||
torch.testing.assert_close(out, ref)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_zero_dim_scale_falls_back_to_native(dtype):
|
||||
# 0-d scalar modulation is a valid torch broadcast; not per-channel.
|
||||
device = "cuda"
|
||||
hidden = 256
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
g = torch.Generator(device=device).manual_seed(6)
|
||||
x = torch.randn(1, 32, hidden, generator=g, device=device, dtype=dtype)
|
||||
scale = torch.tensor(0.5, device=device, dtype=dtype)
|
||||
shift = torch.tensor(-0.25, device=device, dtype=dtype)
|
||||
out = m.forward_cuda(x, scale, shift)
|
||||
assert_close(out, m.forward_native(x, scale, shift), dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_per_sample_scale_raises_like_native(dtype):
|
||||
# Frozen behavior: B>1 with (B, C) scale/shift -> RuntimeError.
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
g = torch.Generator(device=device).manual_seed(7)
|
||||
x = torch.randn(4, 64, hidden, generator=g, device=device, dtype=dtype)
|
||||
scale = torch.randn(4, hidden, generator=g, device=device, dtype=dtype)
|
||||
shift = torch.randn(4, hidden, generator=g, device=device, dtype=dtype)
|
||||
with pytest.raises(RuntimeError):
|
||||
m.forward_native(x, scale, shift)
|
||||
with pytest.raises(RuntimeError):
|
||||
m.forward_cuda(x, scale, shift)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_fused_determinism_repeat_calls(dtype):
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, scale, shift = make_inputs(1, 1024, hidden, dtype, device, seed=8)
|
||||
out1 = _adaln_fused_forward(m, x, scale, shift)
|
||||
out2 = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out1 is not None and out2 is not None, "determinism must exercise the fused path"
|
||||
assert torch.equal(out1, out2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("eps", [1e-5, 1e-3])
|
||||
def test_fused_eps_variants(eps):
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, eps, device, torch.bfloat16)
|
||||
x, scale, shift = make_inputs(1, 256, hidden, torch.bfloat16, device, seed=9)
|
||||
out = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out is not None, "supported eps variants must take the fused path"
|
||||
assert_close(out, m.forward_native(x, scale, shift), torch.bfloat16)
|
||||
assert_close(out, fp32_reference(x, scale, shift, eps), torch.bfloat16, loose=True)
|
||||
@@ -0,0 +1,135 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Direct tests for the synchronous launch-failure fallback (C3a).
|
||||
|
||||
Simulates a backend-specific Triton launch failure (the launcher raises
|
||||
synchronously) and verifies the two-layer response: the failed config is
|
||||
recorded, the caller gets the correct native result, the same config is not
|
||||
retried, and a different constexpr variant stays eligible.
|
||||
|
||||
The failed-key set is module-global; the autouse fixture snapshots and
|
||||
restores it so tests are self-contained and independent of execution order.
|
||||
Collection is safe on non-Triton environments: the Triton-only kernel symbol
|
||||
is resolved lazily inside each test and skips if unavailable.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.layers import adalayernorm as adaln_mod
|
||||
from vllm_omni.diffusion.layers.adalayernorm import (
|
||||
_FAILED_ADALN_KEYS,
|
||||
AdaLayerNorm,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cuda]
|
||||
|
||||
# Unique channels + eps so the failed keys used here cannot collide with
|
||||
# other tests in the same session.
|
||||
_HIDDEN = 2048
|
||||
_EPS = 1e-5
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_failed_keys():
|
||||
saved = set(_FAILED_ADALN_KEYS)
|
||||
_FAILED_ADALN_KEYS.clear()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_FAILED_ADALN_KEYS.clear()
|
||||
_FAILED_ADALN_KEYS.update(saved)
|
||||
|
||||
|
||||
def _make(bs=1):
|
||||
m = AdaLayerNorm(_HIDDEN, eps=_EPS).to(device="cuda", dtype=torch.bfloat16)
|
||||
g = torch.Generator(device="cuda").manual_seed(31)
|
||||
x = torch.randn(bs, 512, _HIDDEN, generator=g, device="cuda", dtype=torch.bfloat16)
|
||||
scale = torch.randn(1, _HIDDEN, generator=g, device="cuda", dtype=torch.bfloat16)
|
||||
shift = torch.randn(1, _HIDDEN, generator=g, device="cuda", dtype=torch.bfloat16)
|
||||
return m, x, scale, shift
|
||||
|
||||
|
||||
def _get_kernel():
|
||||
kernel = getattr(adaln_mod, "_adaln_scale_shift_layernorm_kernel", None)
|
||||
if kernel is None:
|
||||
pytest.skip("Triton AdaLayerNorm kernel unavailable")
|
||||
return kernel
|
||||
|
||||
|
||||
def test_shared_launch_failure_fallback_and_cache(monkeypatch):
|
||||
kernel = _get_kernel()
|
||||
m, x, scale, shift = _make(1)
|
||||
native = m.forward_native(x, scale, shift)
|
||||
|
||||
calls = {"n": 0}
|
||||
|
||||
def raising_run(self, *args, **kwargs):
|
||||
calls["n"] += 1
|
||||
raise RuntimeError("simulated synchronous launch failure")
|
||||
|
||||
monkeypatch.setattr(type(kernel), "run", raising_run)
|
||||
|
||||
# Phase 1: the launch raises synchronously; the caller must receive the
|
||||
# correct native result and the failed key must be recorded.
|
||||
out = m.forward_cuda(x, scale, shift)
|
||||
torch.testing.assert_close(out.float(), native.float())
|
||||
expected_key = (x.device.index, _HIDDEN, x.dtype, False, False, False, False)
|
||||
assert expected_key in _FAILED_ADALN_KEYS
|
||||
assert calls["n"] == 1 # the fused launcher was attempted exactly once
|
||||
|
||||
# Phase 2: the same config must NOT retry the launcher (failed-key
|
||||
# cache) and still return the correct native result.
|
||||
calls["n"] = 0
|
||||
out2 = m.forward_cuda(x, scale, shift)
|
||||
assert calls["n"] == 0
|
||||
torch.testing.assert_close(out2.float(), native.float())
|
||||
|
||||
|
||||
def test_shared_failure_then_per_sample_variant_eligible(monkeypatch):
|
||||
# The failed-key cache is per constexpr variant: after the shared
|
||||
# (False, False) variant fails, the per-sample (True, True) variant must
|
||||
# still attempt the launcher and succeed.
|
||||
kernel = _get_kernel()
|
||||
m, x, shared_scale, shared_shift = _make(2)
|
||||
shared_native = m.forward_native(x, shared_scale, shared_shift)
|
||||
shared_key = (x.device.index, _HIDDEN, x.dtype, False, False, False, False)
|
||||
per_sample_key = (x.device.index, _HIDDEN, x.dtype, False, False, True, True)
|
||||
calls = {"n": 0}
|
||||
|
||||
def raising_run(self, *args, **kwargs):
|
||||
calls["n"] += 1
|
||||
raise RuntimeError("simulated synchronous shared launch failure")
|
||||
|
||||
# Establish the shared failure in this test; the autouse fixture clears
|
||||
# failures from other tests, so their cache entries cannot prove isolation.
|
||||
with monkeypatch.context() as shared_patch:
|
||||
shared_patch.setattr(type(kernel), "run", raising_run)
|
||||
shared_out = m.forward_cuda(x, shared_scale, shared_shift)
|
||||
torch.testing.assert_close(shared_out.float(), shared_native.float())
|
||||
assert calls["n"] == 1
|
||||
assert shared_key in _FAILED_ADALN_KEYS
|
||||
assert per_sample_key not in _FAILED_ADALN_KEYS
|
||||
|
||||
m.forward_cuda(x, shared_scale, shared_shift)
|
||||
assert calls["n"] == 1, "the cached shared failure must not retry"
|
||||
|
||||
g = torch.Generator(device="cuda").manual_seed(37)
|
||||
scale = torch.randn(2, 1, _HIDDEN, generator=g, device="cuda", dtype=torch.bfloat16)
|
||||
shift = torch.randn(2, 1, _HIDDEN, generator=g, device="cuda", dtype=torch.bfloat16)
|
||||
calls["n"] = 0
|
||||
|
||||
def counting_run(self, *args, **kwargs):
|
||||
calls["n"] += 1
|
||||
return real_run(self, *args, **kwargs)
|
||||
|
||||
real_run = type(kernel).run
|
||||
monkeypatch.setattr(type(kernel), "run", counting_run)
|
||||
|
||||
out = adaln_mod._adaln_fused_forward(m, x, scale, shift)
|
||||
assert out is not None, "the per-sample variant must remain eligible after a shared failure"
|
||||
native = m.forward_native(x, scale, shift)
|
||||
torch.testing.assert_close(out.float(), native.float(), atol=2e-2, rtol=2e-2)
|
||||
assert calls["n"] == 1
|
||||
assert shared_key in _FAILED_ADALN_KEYS
|
||||
assert per_sample_key not in _FAILED_ADALN_KEYS
|
||||
@@ -0,0 +1,462 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""P0 correctness matrix for the fused AdaLayerNorm CUDA path.
|
||||
|
||||
Covers: fp32/bf16/fp16, B>1 (shared and per-sample modulation), C in
|
||||
{1536, 3072, 4096}, varying L, elementwise_affine False/True with
|
||||
NON-DEFAULT weight/bias, eps variants, legal non-contiguous inputs,
|
||||
broadcast scale/shift shapes ((C,), (1, C), (1, 1, C), (B, 1, C)),
|
||||
fallback completeness (fp64, oversized hidden size, cross-device
|
||||
parameters), large-offset/small-variance stability, and the frozen
|
||||
B > 1 (B, C) native broadcast-error contract.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.layers.adalayernorm import (
|
||||
AdaLayerNorm,
|
||||
_adaln_fused_forward,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cuda]
|
||||
|
||||
TOL_STRICT = {"bf16": (2e-2, 2e-2), "fp16": (1e-2, 1e-2), "fp32": (1e-3, 1e-3)}
|
||||
TOL_LOOSE = {"bf16": (5e-2, 2e-2), "fp16": (2e-2, 1e-2), "fp32": (5e-3, 5e-3)}
|
||||
DTYPES = [torch.bfloat16, torch.float16, torch.float32]
|
||||
|
||||
|
||||
def tol_key(dtype):
|
||||
if dtype == torch.bfloat16:
|
||||
return "bf16"
|
||||
if dtype == torch.float16:
|
||||
return "fp16"
|
||||
return "fp32"
|
||||
|
||||
|
||||
def fp32_reference(x, scale, shift, eps, weight=None, bias=None):
|
||||
xf = x.float()
|
||||
w = weight.float() if weight is not None else None
|
||||
b = bias.float() if bias is not None else None
|
||||
xn = torch.nn.functional.layer_norm(xf, (x.shape[-1],), w, b, eps)
|
||||
return xn * (1 + scale.float()[:, None]) + shift.float()[:, None]
|
||||
|
||||
|
||||
def make_module(hidden, elementwise_affine, eps, device, dtype, nondefault_affine=False):
|
||||
m = AdaLayerNorm(hidden, elementwise_affine=elementwise_affine, eps=eps)
|
||||
if elementwise_affine:
|
||||
m = m.to(dtype)
|
||||
if nondefault_affine:
|
||||
with torch.no_grad():
|
||||
g = torch.Generator(device="cpu").manual_seed(42)
|
||||
m.layernorm.weight.copy_(0.5 + torch.rand(hidden, generator=g))
|
||||
m.layernorm.bias.copy_(0.2 * torch.randn(hidden, generator=g))
|
||||
return m.to(device)
|
||||
|
||||
|
||||
def make_inputs(bs, seq, hidden, dtype, device, seed=0, mod_shape=None):
|
||||
g = torch.Generator(device=device).manual_seed(seed)
|
||||
x = torch.randn(bs, seq, hidden, generator=g, device=device, dtype=dtype)
|
||||
ms = mod_shape or (1, hidden)
|
||||
scale = torch.randn(ms, generator=g, device=device, dtype=dtype)
|
||||
shift = torch.randn(ms, generator=g, device=device, dtype=dtype)
|
||||
return x, scale, shift
|
||||
|
||||
|
||||
def assert_close(a, b, dtype, loose=False):
|
||||
atol, rtol = (TOL_LOOSE if loose else TOL_STRICT)[tol_key(dtype)]
|
||||
torch.testing.assert_close(a.float(), b.float(), atol=atol, rtol=rtol)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("chunks", [2, 6])
|
||||
@pytest.mark.parametrize("bs,frames,spatial", [(1, 21, 880), (2, 3, 5), (2, 1, 5), (1, 3, 5)])
|
||||
def test_matrix_framewise_modulation(dtype, chunks, bs, frames, spatial):
|
||||
hidden = 2240
|
||||
m = make_module(hidden, False, 1e-6, "cuda", dtype)
|
||||
g = torch.Generator(device="cuda").manual_seed(61)
|
||||
x = torch.randn(bs, frames, spatial, hidden, generator=g, device="cuda", dtype=dtype)
|
||||
modulation = torch.randn(bs, frames, chunks, hidden, generator=g, device="cuda", dtype=dtype)
|
||||
scale, shift = modulation.chunk(chunks, dim=2)[:2]
|
||||
# Sana's block/final modulation uses chunk views spanning frames.
|
||||
assert scale.shape == (bs, frames, 1, hidden)
|
||||
assert scale.stride(1) == chunks * hidden
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is not None, "framewise chunk views must take the fused path"
|
||||
assert fused.shape == x.shape and fused.dtype == dtype
|
||||
assert_close(fused, m.forward_native(x, scale, shift), dtype)
|
||||
ref = torch.nn.functional.layer_norm(x.float(), (hidden,), eps=m.eps)
|
||||
ref = ref * (1 + scale.float()) + shift.float()
|
||||
assert_close(fused, ref, dtype, loose=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shared_scale,shared_shift", [(False, True), (True, False), (True, True)])
|
||||
@torch.no_grad()
|
||||
def test_matrix_framewise_shared_modulation(shared_scale, shared_shift):
|
||||
dtype = torch.bfloat16
|
||||
hidden = 2240
|
||||
m = make_module(hidden, True, 1e-6, "cuda", dtype, nondefault_affine=True)
|
||||
x = torch.randn(2, 3, 5, hidden, device="cuda", dtype=dtype)
|
||||
scale = torch.randn((hidden,) if shared_scale else (2, 3, 1, hidden), device="cuda", dtype=dtype)
|
||||
shift = torch.randn((1, 1, 1, hidden) if shared_shift else (2, 3, 1, hidden), device="cuda", dtype=dtype)
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is not None
|
||||
assert_close(fused, m.forward_native(x, scale, shift), dtype)
|
||||
|
||||
|
||||
def test_matrix_framewise_irregular_group_stride_falls_back():
|
||||
dtype = torch.bfloat16
|
||||
hidden = 2240
|
||||
m = make_module(hidden, False, 1e-6, "cuda", dtype)
|
||||
x = torch.randn(2, 3, 5, hidden, device="cuda", dtype=dtype)
|
||||
modulation = torch.randn(3, 2, 2, hidden, device="cuda", dtype=dtype).transpose(0, 1)
|
||||
scale, shift = modulation.chunk(2, dim=2)
|
||||
assert scale.stride(0) != x.shape[1] * scale.stride(1)
|
||||
assert _adaln_fused_forward(m, x, scale, shift) is None
|
||||
assert_close(m.forward_cuda(x, scale, shift), m.forward_native(x, scale, shift), dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
def test_matrix_sana_patch_embed_reaches_framewise_fused_path(dtype):
|
||||
from vllm_omni.diffusion.models.sana_wm.sana_wm_transformer import SanaWmPatchEmbedMS3D
|
||||
|
||||
hidden = 2240
|
||||
patch = SanaWmPatchEmbedMS3D((1, 1, 1), 8, hidden).to(device="cuda", dtype=dtype)
|
||||
latents = torch.randn(2, 8, 3, 2, 3, device="cuda", dtype=dtype)
|
||||
with torch.no_grad():
|
||||
projected = patch.proj(latents)
|
||||
tokens, (frames, height, width) = patch.project_with_shape(latents)
|
||||
torch.testing.assert_close(tokens, projected.flatten(2).transpose(1, 2), atol=0, rtol=0)
|
||||
assert tokens.is_contiguous()
|
||||
x = tokens.reshape(2, frames, height * width, hidden)
|
||||
modulation = torch.randn(2, frames, 6, hidden, device="cuda", dtype=dtype)
|
||||
scale, shift = modulation.chunk(6, dim=2)[:2]
|
||||
m = make_module(hidden, False, 1e-6, "cuda", dtype)
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is not None, "real Sana patch features must reach the fused path"
|
||||
assert_close(fused, m.forward_native(x, scale, shift), dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("affine", [False, True])
|
||||
@pytest.mark.parametrize(
|
||||
"bs,seq,hidden", [(1, 4096, 3072), (2, 1024, 1536), (3, 128, 4096), (1, 1, 3072), (1, 8192, 1536)]
|
||||
)
|
||||
@torch.no_grad()
|
||||
def test_matrix_main(dtype, affine, bs, seq, hidden):
|
||||
device = "cuda"
|
||||
m = make_module(hidden, affine, 1e-6, device, dtype)
|
||||
x, scale, shift = make_inputs(bs, seq, hidden, dtype, device)
|
||||
out_cuda = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out_cuda is not None, "supported matrix inputs must take the fused path"
|
||||
out_native = m.forward_native(x, scale, shift)
|
||||
assert out_cuda.shape == x.shape and out_cuda.dtype == dtype and out_cuda.device == x.device
|
||||
assert_close(out_cuda, out_native, dtype, loose=False)
|
||||
w = m.layernorm.weight if affine else None
|
||||
b = m.layernorm.bias if affine else None
|
||||
assert_close(out_cuda, fp32_reference(x, scale, shift, 1e-6, w, b), dtype, loose=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@torch.no_grad()
|
||||
def test_matrix_weight_bias_nondefault(dtype):
|
||||
# weight/bias semantics with non-identity initialization:
|
||||
# out = (w * ln(x) + b) * (1 + scale) + shift
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, True, 1e-6, device, dtype, nondefault_affine=True)
|
||||
x, scale, shift = make_inputs(1, 1024, hidden, dtype, device, seed=13)
|
||||
out_cuda = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out_cuda is not None, "non-default affine parameters must take the fused path"
|
||||
out_native = m.forward_native(x, scale, shift)
|
||||
assert_close(out_cuda, out_native, dtype)
|
||||
ref = fp32_reference(x, scale, shift, 1e-6, m.layernorm.weight, m.layernorm.bias)
|
||||
assert_close(out_cuda, ref, dtype, loose=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@pytest.mark.parametrize("eps", [1e-5, 1e-3, 1e-2])
|
||||
def test_matrix_eps_variants(dtype, eps):
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, eps, device, dtype)
|
||||
x, scale, shift = make_inputs(1, 512, hidden, dtype, device, seed=3)
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is not None, "supported eps variants must take the fused path"
|
||||
assert_close(fused, m.forward_native(x, scale, shift), dtype)
|
||||
assert_close(
|
||||
fused,
|
||||
fp32_reference(x, scale, shift, eps),
|
||||
dtype,
|
||||
loose=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@pytest.mark.parametrize("mod_shape", ["C", "1x1xC", "1xC"])
|
||||
def test_matrix_broadcast_scale_shift(dtype, mod_shape):
|
||||
# (C,), (1, 1, C) and (1, C) are all legal per-channel broadcasts against x (B, L, C)
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, _, _ = make_inputs(2, 512, hidden, dtype, device, seed=5)
|
||||
g = torch.Generator(device=device).manual_seed(6)
|
||||
if mod_shape == "C":
|
||||
scale = torch.randn(hidden, generator=g, device=device, dtype=dtype)
|
||||
shift = torch.randn(hidden, generator=g, device=device, dtype=dtype)
|
||||
elif mod_shape == "1x1xC":
|
||||
scale = torch.randn(1, 1, hidden, generator=g, device=device, dtype=dtype)
|
||||
shift = torch.randn(1, 1, hidden, generator=g, device=device, dtype=dtype)
|
||||
else:
|
||||
scale = torch.randn(1, hidden, generator=g, device=device, dtype=dtype)
|
||||
shift = torch.randn(1, hidden, generator=g, device=device, dtype=dtype)
|
||||
out_cuda = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out_cuda is not None, f"shared modulation layout {mod_shape} must take the fused path"
|
||||
out_native = m.forward_native(x, scale, shift)
|
||||
assert_close(out_cuda, out_native, dtype)
|
||||
s = scale.float().reshape(1, 1, hidden) if scale.ndim == 1 else scale.float()
|
||||
sh = shift.float().reshape(1, 1, hidden) if shift.ndim == 1 else shift.float()
|
||||
ref = fp32_reference(x, s.reshape(-1, hidden)[0:1], sh.reshape(-1, hidden)[0:1], 1e-6)
|
||||
assert_close(out_cuda, ref, dtype, loose=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
def test_matrix_per_sample_modulation(dtype):
|
||||
# Qwen-Image's _modulate produces (B, 1, C): one modulation row per sample.
|
||||
# B > 1 must take the fused path (not fall back to native) and match native.
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, _, _ = make_inputs(4, 512, hidden, dtype, device, seed=7, mod_shape=(4, hidden))
|
||||
g = torch.Generator(device=device).manual_seed(8)
|
||||
scale = torch.randn(4, 1, hidden, generator=g, device=device, dtype=dtype)
|
||||
shift = torch.randn(4, 1, hidden, generator=g, device=device, dtype=dtype)
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is not None, "(B, 1, C) per-sample modulation must take the fused path"
|
||||
out_cuda = fused
|
||||
out_native = m.forward_native(x, scale, shift)
|
||||
assert_close(out_cuda, out_native, dtype)
|
||||
# fp32_reference's [:, None] expects a 2D (B, C) modulation row; the
|
||||
# (B, 1, C) tensor reshapes to it losslessly.
|
||||
ref = fp32_reference(x, scale.reshape(x.shape[0], hidden), shift.reshape(x.shape[0], hidden), 1e-6)
|
||||
assert_close(out_cuda, ref, dtype, loose=True)
|
||||
# Per-sample: different rows must receive different modulation.
|
||||
row_diff = (out_cuda[0] - out_cuda[1]).abs().max().item()
|
||||
assert row_diff > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
def test_matrix_chunk_view_modulation(dtype):
|
||||
# Wan2.2-style producer: the modulation projection is (B, 6, C) and the
|
||||
# consumer chunks it along dim=1. The resulting (B, 1, C) views are
|
||||
# NON-CONTIGUOUS (row stride 6*C) - the fused path must consume them
|
||||
# directly via the explicit row stride, without a contiguous copy.
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, _, _ = make_inputs(4, 512, hidden, dtype, device, seed=27)
|
||||
g = torch.Generator(device=device).manual_seed(28)
|
||||
source = torch.randn(4, 6, hidden, generator=g, device=device, dtype=dtype) * 0.1
|
||||
chunks = source.chunk(6, dim=1)
|
||||
scale, shift = chunks[1], chunks[2]
|
||||
assert scale.shape == (4, 1, hidden) and shift.shape == (4, 1, hidden)
|
||||
assert not scale.is_contiguous()
|
||||
assert scale.stride(-1) == 1
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is not None, "chunk-view modulation must take the fused path"
|
||||
out_native = m.forward_native(x, scale, shift)
|
||||
assert_close(fused, out_native, dtype)
|
||||
ref = fp32_reference(x, scale.reshape(4, hidden), shift.reshape(4, hidden), 1e-6)
|
||||
assert_close(fused, ref, dtype, loose=True)
|
||||
|
||||
|
||||
def test_matrix_3d_modulation_fallback():
|
||||
# Wan2.2 TI2V-style per-token modulation: scale/shift (B, L, C) full 3D.
|
||||
# This is intentionally outside the fused kernel contract - the fast path
|
||||
# must be rejected (None) and forward_cuda must match native.
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, torch.bfloat16)
|
||||
x, scale, shift = make_inputs(2, 128, hidden, torch.bfloat16, device, seed=29, mod_shape=(2, 128, hidden))
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is None, "(B, L, C) per-token modulation must be rejected"
|
||||
out = m.forward_cuda(x, scale, shift)
|
||||
native = m.forward_native(x, scale, shift)
|
||||
torch.testing.assert_close(out.float(), native.float(), atol=2e-2, rtol=2e-2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
def test_matrix_production_widths(dtype):
|
||||
# Real consumer widths: Sana-WM 2240 (BLOCK_C 4096, 45.3% mask) and
|
||||
# Wan2.2 A14B 5120 (BLOCK_C 8192, the max supported block, 16-warp
|
||||
# compiled variant). BF16 + elementwise_affine=False.
|
||||
device = "cuda"
|
||||
for hidden in (2240, 5120):
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, scale, shift = make_inputs(1, 512, hidden, dtype, device, seed=31)
|
||||
fused = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert fused is not None, f"hidden={hidden} must take the fused path"
|
||||
out_native = m.forward_native(x, scale, shift)
|
||||
assert_close(fused, out_native, dtype)
|
||||
assert_close(fused, fp32_reference(x, scale, shift, 1e-6), dtype, loose=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
def test_matrix_noncontiguous_fallback(dtype):
|
||||
# Non-contiguous inputs must fall back to native and stay correct
|
||||
# (the fused kernel only serves the contiguous fast path).
|
||||
device = "cuda"
|
||||
hidden = 1536
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x_big, scale, shift = make_inputs(1, 1024, hidden, dtype, device, seed=7)
|
||||
x = x_big[:, ::2, :]
|
||||
assert not x.is_contiguous()
|
||||
out_cuda = m.forward_cuda(x, scale, shift)
|
||||
out_native = m.forward_native(x, scale, shift)
|
||||
assert_close(out_cuda, out_native, dtype)
|
||||
assert_close(out_cuda, fp32_reference(x.contiguous(), scale, shift, 1e-6), dtype, loose=True)
|
||||
|
||||
|
||||
def test_matrix_fallback_fp64():
|
||||
# fp64 is not in the kernel's supported dtype list -> must fall back to
|
||||
# native and stay correct (fallback completeness).
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, torch.float64)
|
||||
x, scale, shift = make_inputs(1, 256, hidden, torch.float64, device, seed=9)
|
||||
out_cuda = m.forward_cuda(x, scale, shift)
|
||||
out_native = m.forward_native(x, scale, shift)
|
||||
torch.testing.assert_close(out_cuda, out_native)
|
||||
|
||||
|
||||
def test_matrix_fallback_oversized_hidden():
|
||||
# hidden sizes whose next_power_of_2 exceeds the kernel's supported block
|
||||
# bound must fall back to native and stay correct.
|
||||
device = "cuda"
|
||||
hidden = 10000 # next_power_of_2 = 16384 > MAX_BLOCK_C
|
||||
m = make_module(hidden, False, 1e-6, device, torch.bfloat16)
|
||||
x, scale, shift = make_inputs(1, 128, hidden, torch.bfloat16, device, seed=10)
|
||||
out_cuda = m.forward_cuda(x, scale, shift)
|
||||
out_native = m.forward_native(x, scale, shift)
|
||||
torch.testing.assert_close(out_cuda, out_native)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
def test_matrix_zero_scale_shift_identity(dtype):
|
||||
# With scale = shift = 0 the output must equal a pure LayerNorm.
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, _, _ = make_inputs(1, 256, hidden, dtype, device, seed=19)
|
||||
scale = torch.zeros(1, hidden, device=device, dtype=dtype)
|
||||
shift = torch.zeros(1, hidden, device=device, dtype=dtype)
|
||||
out_cuda = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out_cuda is not None, "zero modulation must take the fused path"
|
||||
assert_close(out_cuda, m.forward_native(x, scale, shift), dtype)
|
||||
assert_close(out_cuda, m.layernorm(x), dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
def test_matrix_determinism(dtype):
|
||||
# Two calls on the same input must agree bitwise (a hard constraint for
|
||||
# any cached/direct-launch scheme).
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, scale, shift = make_inputs(1, 4096, hidden, dtype, device, seed=11)
|
||||
out1 = _adaln_fused_forward(m, x, scale, shift)
|
||||
out2 = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out1 is not None and out2 is not None, "determinism must exercise the fused path"
|
||||
assert torch.equal(out1, out2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
def test_matrix_multi_batch_shared_modulation(dtype):
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, scale, shift = make_inputs(4, 512, hidden, dtype, device, seed=15)
|
||||
out = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out is not None, "multi-batch shared modulation must take the fused path"
|
||||
assert_close(out, m.forward_native(x, scale, shift), dtype)
|
||||
for b in range(4):
|
||||
single = _adaln_fused_forward(m, x[b : b + 1], scale, shift)
|
||||
assert single is not None
|
||||
assert_close(out[b : b + 1], single, dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
def test_matrix_bc_modulation_preserves_native_error(dtype):
|
||||
# Frozen native contract: (B, C) modulation with B > 1 raises
|
||||
# RuntimeError (torch broadcasts B against L). The fused path preserves
|
||||
# this behavior - it neither supports nor silently changes it.
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, scale, shift = make_inputs(4, 64, hidden, dtype, device, seed=17, mod_shape=(4, hidden))
|
||||
with pytest.raises(RuntimeError):
|
||||
m.forward_native(x, scale, shift)
|
||||
with pytest.raises(RuntimeError):
|
||||
m.forward_cuda(x, scale, shift)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
def test_matrix_cross_device_fallback(dtype):
|
||||
# Raw pointers go to one Triton kernel, which does no cross-device
|
||||
# checking: the fast-path guard must reject cross-device parameters so
|
||||
# they fall back to native, which raises torch's own cross-device error
|
||||
# instead of crashing in the kernel.
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, scale, shift = make_inputs(1, 512, hidden, dtype, device, seed=21)
|
||||
cpu_scale = scale.to("cpu")
|
||||
with pytest.raises(RuntimeError):
|
||||
m.forward_cuda(x, cpu_scale, shift)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16, torch.float32])
|
||||
def test_matrix_large_offset_small_variance(dtype):
|
||||
"""A large constant offset with a small variance must stay stable.
|
||||
|
||||
Mirrors the fused_adaptive_group_norm_silu case: x ~ 10000 +/- 0.1 has
|
||||
mean ~1e4 and variance ~1e-2, so a plain fp32 sum of large-offset values
|
||||
rounds away the low-order bits that the variance is built from. The fused
|
||||
kernel centers the row on its first element before the tree sum
|
||||
(shift-invariant two-pass), keeping mean/variance accurate.
|
||||
|
||||
For fp32 the meaningful reference is an fp64 computation, NOT the native
|
||||
eager chain: at mean/std = 1e5 the native chain itself carries a
|
||||
~0.07 absolute deviation from the fp64 truth (its own fp32 dynamic-range
|
||||
floor), while the fused kernel stays within ~2e-6. For fp16/bf16 inputs
|
||||
the +/-0.1 perturbation quantizes away at magnitude 1e4, so the
|
||||
meaningful assertion is that outputs stay finite and match native.
|
||||
"""
|
||||
device = "cuda"
|
||||
hidden = 3072
|
||||
m = make_module(hidden, False, 1e-6, device, dtype)
|
||||
x, scale, shift = make_inputs(1, 4096, hidden, dtype, device, seed=23)
|
||||
# Apply the large constant offset AFTER generation: x ~ 10000 +/- 0.1 is
|
||||
# the cancellation case that motivates the shift-invariant two-pass.
|
||||
x = x * 0.1 + 10000
|
||||
out_cuda = _adaln_fused_forward(m, x, scale, shift)
|
||||
assert out_cuda is not None, "large-offset stability must exercise the fused path"
|
||||
out_native = m.forward_native(x, scale, shift)
|
||||
assert torch.isfinite(out_cuda).all()
|
||||
|
||||
# fp64 reference computed from the SAME fp32 input the implementations see
|
||||
# (the kernel cannot recover bits that fp32 quantization never stored).
|
||||
x64 = x.double()
|
||||
mean64 = x64.mean(dim=-1, keepdim=True)
|
||||
var64 = x64.var(dim=-1, unbiased=False, keepdim=True)
|
||||
ref64 = (x64 - mean64) * torch.rsqrt(var64 + 1e-6) * (1 + scale.double()[:, None, :]) + shift.double()[:, None, :]
|
||||
|
||||
if dtype is torch.float32:
|
||||
# fp32 fused output vs the fp64 truth of the same input: within the
|
||||
# fp32 representation floor (~2e-6 measured). The native eager chain
|
||||
# deviates ~0.07 on the same input (its own dynamic-range floor) -
|
||||
# documented, not asserted.
|
||||
torch.testing.assert_close(out_cuda.double(), ref64, atol=1e-4, rtol=1e-4)
|
||||
else:
|
||||
# bf16/fp16 quantize the perturbation away: no NaN, matches native.
|
||||
assert_close(out_cuda, out_native, dtype, loose=True)
|
||||
@@ -1,3 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from collections.abc import Callable
|
||||
from importlib.util import find_spec
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -5,6 +9,8 @@ import torch
|
||||
import torch.nn as nn
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.linear import ReplicatedLinear
|
||||
from vllm.triton_utils import HAS_TRITON as _HAS_TRITON
|
||||
from vllm.triton_utils import tl, triton
|
||||
|
||||
from vllm_omni.diffusion.layers.custom_op import CustomOp
|
||||
from vllm_omni.diffusion.layers.norm import LayerNorm
|
||||
@@ -16,6 +22,282 @@ logger = init_logger(__name__)
|
||||
|
||||
_HAS_MINDIESD = find_spec("mindiesd") is not None
|
||||
|
||||
# Fast-path bound: one Triton program covers the whole normalized dimension, so
|
||||
# BLOCK_C = next_power_of_2(C) grows with the hidden size. The diffusion models
|
||||
# consuming AdaLayerNorm use hidden sizes 2240 (Sana-WM), 3072/3584 (Wan2.2,
|
||||
# Qwen-Image) and 5120 (Wan2.2 A14B), i.e. BLOCK_C up to 8192, which compiles
|
||||
# and runs efficiently on H200 (measured; C=5120 -> 30-111 us depending on L).
|
||||
# Beyond that, masked-lane waste and register pressure grow without a measured
|
||||
# benefit, so the fast path falls back to forward_native.
|
||||
_MAX_BLOCK_C = 8192
|
||||
|
||||
_ADALN_CONFIGS: dict = {}
|
||||
_ADALN_DTYPES = (torch.bfloat16, torch.float16, torch.float32)
|
||||
# Runtime configs whose Triton compile/launch failed synchronously; they fall
|
||||
# back to forward_native and are not retried (mirrors the LTX2
|
||||
# residual-AdaLN failed-key cache in residual_adaln.py).
|
||||
_FAILED_ADALN_KEYS: set = set()
|
||||
_adaln_fused_forward: Callable[["AdaLayerNorm", torch.Tensor, torch.Tensor, torch.Tensor], torch.Tensor | None] | None
|
||||
|
||||
|
||||
if _HAS_TRITON:
|
||||
|
||||
@triton.jit
|
||||
def _adaln_scale_shift_layernorm_kernel(
|
||||
x_ptr,
|
||||
scale_ptr,
|
||||
shift_ptr,
|
||||
out_ptr,
|
||||
weight_ptr,
|
||||
bias_ptr,
|
||||
eps,
|
||||
channels,
|
||||
seq_len,
|
||||
scale_row_stride,
|
||||
shift_row_stride,
|
||||
has_weight: tl.constexpr,
|
||||
has_bias: tl.constexpr,
|
||||
is_half: tl.constexpr,
|
||||
per_sample_scale: tl.constexpr,
|
||||
per_sample_shift: tl.constexpr,
|
||||
block_c: tl.constexpr,
|
||||
):
|
||||
"""One program per LayerNorm row: out = ln(x) * (1 + scale) + shift.
|
||||
|
||||
Rows enumerate contiguous (B, L, C) or (B, F, S, C) input.
|
||||
sample_idx = row // seq_len selects a sample, or a flattened
|
||||
batch/frame group with seq_len = S. Per-group modulation is
|
||||
(B, 1, C) or (B, F, 1, C); explicit row strides support projection
|
||||
chunk views. Shared modulation addresses one channel vector.
|
||||
(B, C) at B > 1 is a native-path broadcast error and never reaches
|
||||
this kernel.
|
||||
|
||||
Mean/variance use a shift-invariant two-pass: the row is centered on
|
||||
its first element (x0) before the fp32 tree sum, so for inputs with a
|
||||
large constant offset and small variance the summation and the
|
||||
x - mean cancellation stay accurate where a plain fp32 sum of
|
||||
large-offset values loses the low-order bits (the reason
|
||||
fused_adaptive_group_norm_silu uses Welford/Chan). Reductions
|
||||
accumulate in fp32 (matches the golden path, which computes
|
||||
F.layer_norm on x.float()). For half-precision outputs the golden
|
||||
chain rounds after every torch op (LN result, 1+scale, product, sum),
|
||||
so the kernel replicates exactly those roundings - keeping the output
|
||||
bit-faithful to the frozen semantics. fp32 outputs have no
|
||||
intermediate rounding in the golden path and use a plain fp32 chain.
|
||||
block_c covers the whole normalized dim (masked); it is never
|
||||
autotuned. Non-contiguous last-dim inputs never reach this kernel
|
||||
(routed to forward_native upstream).
|
||||
"""
|
||||
row = tl.program_id(0).to(tl.int64)
|
||||
sample_idx = row // seq_len
|
||||
cols = tl.arange(0, block_c)
|
||||
mask = cols < channels
|
||||
x = tl.load(x_ptr + row * channels + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
# Center on the first element before summing: for x ~ offset + noise
|
||||
# this keeps every value entering the tree sum at noise magnitude.
|
||||
x0 = tl.load(x_ptr + row * channels).to(tl.float32)
|
||||
xs = tl.where(mask, x - x0, 0.0)
|
||||
mean_xs = tl.sum(xs, axis=0) / channels
|
||||
# xm = xs - mean_xs is exactly x - (x0 + mean_xs): the row mean enters
|
||||
# only through this centering, and the shift keeps it accurate.
|
||||
xm = tl.where(mask, xs - mean_xs, 0.0)
|
||||
var = tl.sum(xm * xm, axis=0) / channels
|
||||
xn = xm * tl.rsqrt(var + eps)
|
||||
if has_weight:
|
||||
xn = xn * tl.load(weight_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
if has_bias:
|
||||
xn = xn + tl.load(bias_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
if per_sample_scale:
|
||||
s = tl.load(scale_ptr + sample_idx * scale_row_stride + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
else:
|
||||
s = tl.load(scale_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
if per_sample_shift:
|
||||
sh = tl.load(shift_ptr + sample_idx * shift_row_stride + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
else:
|
||||
sh = tl.load(shift_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
out_dtype = out_ptr.dtype.element_ty
|
||||
if is_half:
|
||||
# Golden bf16/fp16 chain: round after each op, exactly as the
|
||||
# sequence of torch elementwise kernels does (fp32 internal math).
|
||||
ln16 = xn.to(out_dtype)
|
||||
t1 = (1.0 + s).to(out_dtype)
|
||||
t2 = (ln16.to(tl.float32) * t1.to(tl.float32)).to(out_dtype)
|
||||
y = (t2.to(tl.float32) + sh).to(out_dtype)
|
||||
else:
|
||||
y = xn * (1.0 + s) + sh
|
||||
# Preserve the input shape and store output rows contiguously.
|
||||
tl.store(out_ptr + row * channels + cols, y.to(out_dtype), mask=mask)
|
||||
|
||||
def _adaln_modulation_mode(t: torch.Tensor, x: torch.Tensor):
|
||||
"""Classify scale/shift against 3D/4D x for the fused kernel.
|
||||
|
||||
"shared" every leading dim is 1: (C,), (1, C), (1, 1, C)
|
||||
"per_sample" (B, 1, C) or (B, F, 1, C): one row per sample/frame
|
||||
None anything else - native torch broadcasting on the
|
||||
forward_native path handles it (or raises, for the
|
||||
frozen B > 1 (B, C) RuntimeError)
|
||||
"""
|
||||
shape = t.shape
|
||||
if t.ndim < 1 or t.ndim > x.ndim or shape[-1] != x.shape[-1]:
|
||||
return None
|
||||
if all(d == 1 for d in shape[:-1]):
|
||||
return "shared"
|
||||
if x.ndim == 3 and t.ndim == 3 and shape == (x.shape[0], 1, x.shape[-1]):
|
||||
return "per_sample"
|
||||
if x.ndim == 4 and t.ndim == 4 and shape == (x.shape[0], x.shape[1], 1, x.shape[-1]):
|
||||
# Batch/frame groups must be addressable with one row stride, including chunk views.
|
||||
if shape[0] > 1 and shape[1] > 1 and t.stride(0) != shape[1] * t.stride(1):
|
||||
return None
|
||||
return "per_sample"
|
||||
return None
|
||||
|
||||
def _adaln_param_matches(x: torch.Tensor, p: torch.Tensor) -> bool:
|
||||
return (
|
||||
p.dtype is x.dtype
|
||||
and p.is_cuda
|
||||
and p.device == x.device
|
||||
and p.ndim == 1
|
||||
and p.shape[0] == x.shape[-1]
|
||||
and p.is_contiguous()
|
||||
)
|
||||
|
||||
def _adaln_inputs_supported(module: "AdaLayerNorm", x: torch.Tensor, scale: torch.Tensor, shift: torch.Tensor):
|
||||
"""Strict fast-path eligibility (mirrors the LTX2 residual-AdaLN
|
||||
guard pattern): every tensor the kernel dereferences must be a dense
|
||||
CUDA tensor on x's device with a supported layout and dtype; anything
|
||||
else falls back to forward_native, which reproduces torch semantics
|
||||
exactly - including the frozen B > 1 (B, C) RuntimeError. Raw pointers
|
||||
go to one kernel, which does no cross-device checking. Runs native
|
||||
under torch.compile so regional compilation is not disrupted."""
|
||||
if torch.compiler.is_compiling():
|
||||
return None
|
||||
if (
|
||||
not _HAS_TRITON
|
||||
or x.device.type != "cuda"
|
||||
or x.ndim not in (3, 4)
|
||||
or x.numel() == 0
|
||||
or not x.is_contiguous()
|
||||
):
|
||||
return None
|
||||
if x.dtype not in _ADALN_DTYPES:
|
||||
return None
|
||||
if scale.dtype is not x.dtype or shift.dtype is not x.dtype:
|
||||
return None
|
||||
if not (scale.is_cuda and shift.is_cuda):
|
||||
return None
|
||||
if scale.device != x.device or shift.device != x.device:
|
||||
return None
|
||||
mode_s = _adaln_modulation_mode(scale, x)
|
||||
mode_h = _adaln_modulation_mode(shift, x)
|
||||
if mode_s is None or mode_h is None:
|
||||
return None
|
||||
# The kernel addresses the modulation rows densely per channel and
|
||||
# strides per sample: the last dim must be unit-stride; other dims
|
||||
# are handled by the explicit row stride (chunk views supported).
|
||||
# 0-dim (scalar) modulation is classified None above and falls back.
|
||||
if scale.stride(-1) != 1 or shift.stride(-1) != 1:
|
||||
return None
|
||||
if x.data_ptr() % 16 or scale.data_ptr() % 16 or shift.data_ptr() % 16:
|
||||
return None
|
||||
channels = x.shape[-1]
|
||||
ln = module.layernorm
|
||||
if tuple(ln.normalized_shape) != (channels,):
|
||||
return None
|
||||
if torch.is_grad_enabled() and any(
|
||||
t is not None and t.requires_grad for t in (x, scale, shift, ln.weight, ln.bias)
|
||||
):
|
||||
return None
|
||||
block_c = triton.next_power_of_2(channels)
|
||||
if block_c > _MAX_BLOCK_C:
|
||||
return None
|
||||
for p in (ln.weight, ln.bias):
|
||||
if p is not None and not _adaln_param_matches(x, p):
|
||||
return None
|
||||
return channels, block_c, mode_s == "per_sample", mode_h == "per_sample"
|
||||
|
||||
def _adaln_fused_forward_impl(module: "AdaLayerNorm", x: torch.Tensor, scale: torch.Tensor, shift: torch.Tensor):
|
||||
"""Fused fast path. Returns the output tensor, or None when the inputs
|
||||
are unsupported or the optimized path failed synchronously (the caller
|
||||
then uses forward_native). The runtime fallback is best-effort: it
|
||||
catches synchronous Triton compile/launch errors, not asynchronous
|
||||
CUDA execution faults."""
|
||||
supported = _adaln_inputs_supported(module, x, scale, shift)
|
||||
if supported is None:
|
||||
return None
|
||||
channels, block_c, per_sample_scale, per_sample_shift = supported
|
||||
seq_len = x.shape[-2]
|
||||
rows = x.numel() // channels
|
||||
out = torch.empty(x.shape, dtype=x.dtype, device=x.device)
|
||||
ln = module.layernorm
|
||||
weight = ln.weight
|
||||
bias = ln.bias
|
||||
has_weight = weight is not None
|
||||
has_bias = bias is not None
|
||||
is_half = x.dtype is not torch.float32
|
||||
cfg = _ADALN_CONFIGS.get(block_c)
|
||||
if cfg is None:
|
||||
num_warps = 4 if block_c <= 1024 else (8 if block_c <= 4096 else 16)
|
||||
cfg = (block_c, num_warps)
|
||||
_ADALN_CONFIGS[block_c] = cfg
|
||||
# Step across frames when F > 1, otherwise across batches.
|
||||
scale_row_stride = scale.stride(1) if scale.ndim == 4 and scale.shape[1] > 1 else scale.stride(0)
|
||||
shift_row_stride = shift.stride(1) if shift.ndim == 4 and shift.shape[1] > 1 else shift.stride(0)
|
||||
# Dummy pointer args for disabled branches: never dereferenced because
|
||||
# the loads are constexpr-pruned when has_weight/has_bias is False.
|
||||
args = (
|
||||
x,
|
||||
scale,
|
||||
shift,
|
||||
out,
|
||||
weight if has_weight else scale,
|
||||
bias if has_bias else scale,
|
||||
module.eps,
|
||||
channels,
|
||||
seq_len,
|
||||
scale_row_stride,
|
||||
shift_row_stride,
|
||||
has_weight,
|
||||
has_bias,
|
||||
is_half,
|
||||
per_sample_scale,
|
||||
per_sample_shift,
|
||||
block_c,
|
||||
)
|
||||
# The key carries every field that selects the compiled binary
|
||||
# (dtype covers is_half; per-sample flags and affine presence select
|
||||
# constexpr variants; eps is a runtime scalar). A failure in one
|
||||
# variant must not disable the others.
|
||||
runtime_key = (
|
||||
x.device.index,
|
||||
channels,
|
||||
x.dtype,
|
||||
has_weight,
|
||||
has_bias,
|
||||
per_sample_scale,
|
||||
per_sample_shift,
|
||||
)
|
||||
if runtime_key in _FAILED_ADALN_KEYS:
|
||||
return None
|
||||
try:
|
||||
_adaln_scale_shift_layernorm_kernel[(rows,)](*args, num_warps=cfg[1])
|
||||
except Exception as exc: # noqa: BLE001 - fail closed after optimized-path failure
|
||||
_FAILED_ADALN_KEYS.add(runtime_key)
|
||||
logger.warning(
|
||||
"Disabling the AdaLayerNorm fused fast path on %s after failure: %s",
|
||||
x.device,
|
||||
exc,
|
||||
)
|
||||
return None
|
||||
return out
|
||||
|
||||
_adaln_fused_forward = _adaln_fused_forward_impl
|
||||
|
||||
|
||||
else:
|
||||
# No Triton: the fused fast path is unavailable; forward_cuda falls back
|
||||
# to forward_native below.
|
||||
_adaln_fused_forward = None
|
||||
|
||||
|
||||
class AdaLayerNorm(CustomOp):
|
||||
"""
|
||||
@@ -36,6 +318,11 @@ class AdaLayerNorm(CustomOp):
|
||||
scale: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
if _adaln_fused_forward is None:
|
||||
return self.forward_native(x, scale, shift)
|
||||
out = _adaln_fused_forward(self, x, scale, shift)
|
||||
if out is not None:
|
||||
return out
|
||||
return self.forward_native(x, scale, shift)
|
||||
|
||||
def forward_hip(
|
||||
@@ -46,6 +333,14 @@ class AdaLayerNorm(CustomOp):
|
||||
) -> torch.Tensor:
|
||||
return self.forward_native(x, scale, shift)
|
||||
|
||||
def forward_musa(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
scale: torch.Tensor,
|
||||
shift: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
return self.forward_native(x, scale, shift)
|
||||
|
||||
def forward_npu(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""SANA-WM Stage-1 transformer.
|
||||
|
||||
Native vLLM-Omni port of the NVlabs SANA-WM DiT. Modules are built eagerly at
|
||||
@@ -563,7 +563,8 @@ class SanaWmPatchEmbedMS3D(nn.Module):
|
||||
def project_with_shape(self, latents: torch.Tensor) -> tuple[torch.Tensor, tuple[int, int, int]]:
|
||||
hidden_states = self.norm(self.proj(latents))
|
||||
_, _, frames, height, width = hidden_states.shape
|
||||
return hidden_states.flatten(2).transpose(1, 2), (frames, height, width)
|
||||
# Use a contiguous token/channel layout for framewise AdaLayerNorm.
|
||||
return hidden_states.flatten(2).transpose(1, 2).contiguous(), (frames, height, width)
|
||||
|
||||
def forward(self, latents: torch.Tensor) -> torch.Tensor:
|
||||
return self.project_with_shape(latents)[0]
|
||||
|
||||
Reference in new issue
Block a user