[MUSA][17/N] ci: Add MUSA diffusion, sgl-kernel tests, and CI workflow support (#20672)

Co-authored-by: ximin.chen <ximin.chen@mthreads.com>
Co-authored-by: R0CKSTAR <xiaodong.ye@mthreads.com>
This commit is contained in:
johnnycxm
2026-05-07 20:45:21 -07:00
committed by GitHub
co-authored by ximin.chen R0CKSTAR
parent 15e6572f21
commit cdf5771f91
14 changed files with 1548 additions and 28 deletions
@@ -0,0 +1,285 @@
# SPDX-License-Identifier: Apache-2.0
"""Tests for MUSA-specific RMSNorm custom op.
These tests call forward_musa directly and compare against forward_native
as the reference implementation.
"""
import pytest
import torch
# We need the MUSA platform to be available for these tests.
# Skip the entire module if MUSA is not available.
_musa_available = hasattr(torch, "musa") and torch.musa.is_available()
pytestmark = pytest.mark.skipif(not _musa_available, reason="MUSA device not available")
# Use a fixed seed for reproducibility
SEED = 42
def get_musa_device():
return torch.device("musa:0")
class TestRMSNorm:
"""Tests for RMSNorm.forward_musa vs forward_native."""
@pytest.fixture(autouse=True)
def setup(self):
self.device = get_musa_device()
def _make_norm(self, hidden_size, eps=1e-6, var_hidden_size=None):
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm
norm = RMSNorm(hidden_size, eps=eps, var_hidden_size=var_hidden_size)
norm = norm.to(self.device)
return norm
# --- Basic correctness: no residual ---
@pytest.mark.parametrize(
"hidden_size",
[64, 128, 256, 512, 1024, 2048],
)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
def test_no_residual_matches_native(self, hidden_size, dtype):
"""forward_musa without residual should match forward_native."""
norm = self._make_norm(hidden_size)
torch.manual_seed(SEED)
x = torch.randn(4, hidden_size, dtype=dtype, device=self.device)
out_musa = norm.forward_musa(x.clone())
out_native = norm.forward_native(x.clone())
atol = 1e-2 if dtype in (torch.float16, torch.bfloat16) else 1e-4
rtol = 1e-2 if dtype in (torch.float16, torch.bfloat16) else 1e-4
torch.testing.assert_close(out_musa, out_native, atol=atol, rtol=rtol)
# --- With residual ---
@pytest.mark.parametrize(
"hidden_size",
[64, 128, 256, 512, 1024],
)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
def test_with_residual_matches_native(self, hidden_size, dtype):
"""forward_musa with residual should match forward_native."""
norm = self._make_norm(hidden_size)
torch.manual_seed(SEED)
x = torch.randn(4, hidden_size, dtype=dtype, device=self.device)
residual = torch.randn(4, hidden_size, dtype=dtype, device=self.device)
# Clone inputs since forward_musa modifies them in-place
x_musa, res_musa = x.clone(), residual.clone()
x_native, res_native = x.clone(), residual.clone()
out_musa, res_out_musa = norm.forward_musa(x_musa, res_musa)
out_native, res_out_native = norm.forward_native(x_native, res_native)
atol = 1e-2 if dtype in (torch.float16, torch.bfloat16) else 1e-4
rtol = 1e-2 if dtype in (torch.float16, torch.bfloat16) else 1e-4
torch.testing.assert_close(out_musa, out_native, atol=atol, rtol=rtol)
torch.testing.assert_close(res_out_musa, res_out_native, atol=atol, rtol=rtol)
# --- 3D input shapes ---
@pytest.mark.parametrize(
"shape",
[
(1, 1, 128),
(2, 8, 128),
(4, 16, 256),
(2, 32, 512),
],
ids=lambda s: f"shape={'x'.join(map(str, s))}",
)
@pytest.mark.parametrize("dtype", [torch.float16, torch.float32])
def test_3d_input_no_residual(self, shape, dtype):
"""forward_musa should handle 3D inputs correctly."""
hidden_size = shape[-1]
norm = self._make_norm(hidden_size)
torch.manual_seed(SEED)
x = torch.randn(shape, dtype=dtype, device=self.device)
out_musa = norm.forward_musa(x.clone())
out_native = norm.forward_native(x.clone())
atol = 1e-2 if dtype == torch.float16 else 1e-4
rtol = 1e-2 if dtype == torch.float16 else 1e-4
torch.testing.assert_close(out_musa, out_native, atol=atol, rtol=rtol)
@pytest.mark.parametrize(
"shape",
[
(2, 8, 128),
(4, 16, 256),
],
ids=lambda s: f"shape={'x'.join(map(str, s))}",
)
def test_3d_input_with_residual(self, shape):
"""forward_musa should handle 3D inputs with residual correctly."""
hidden_size = shape[-1]
norm = self._make_norm(hidden_size)
torch.manual_seed(SEED)
dtype = torch.float32
x = torch.randn(shape, dtype=dtype, device=self.device)
residual = torch.randn(shape, dtype=dtype, device=self.device)
x_musa, res_musa = x.clone(), residual.clone()
x_native, res_native = x.clone(), residual.clone()
out_musa, res_out_musa = norm.forward_musa(x_musa, res_musa)
out_native, res_out_native = norm.forward_native(x_native, res_native)
torch.testing.assert_close(out_musa, out_native, atol=1e-4, rtol=1e-4)
torch.testing.assert_close(res_out_musa, res_out_native, atol=1e-4, rtol=1e-4)
# --- Non-contiguous input ---
def test_non_contiguous_input_no_residual(self):
"""forward_musa should handle non-contiguous inputs (makes them contiguous)."""
hidden_size = 128
norm = self._make_norm(hidden_size)
# Create non-contiguous tensor via slicing
x_base = torch.randn(8, hidden_size, dtype=torch.float32, device=self.device)
x = x_base[::2] # shape (4, 128), non-contiguous
assert not x.is_contiguous()
out_musa = norm.forward_musa(x.clone())
out_native = norm.forward_native(x.clone())
torch.testing.assert_close(out_musa, out_native, atol=1e-4, rtol=1e-4)
def test_non_contiguous_input_with_residual(self):
"""forward_musa should handle non-contiguous inputs with residual."""
hidden_size = 128
norm = self._make_norm(hidden_size)
x_base = torch.randn(8, hidden_size, dtype=torch.float32, device=self.device)
x = x_base[::2] # non-contiguous
assert not x.is_contiguous()
residual = torch.randn(4, hidden_size, dtype=torch.float32, device=self.device)
x_musa, res_musa = x.clone(), residual.clone()
x_native, res_native = x.clone(), residual.clone()
out_musa, res_out_musa = norm.forward_musa(x_musa, res_musa)
out_native, res_out_native = norm.forward_native(x_native, res_native)
torch.testing.assert_close(out_musa, out_native, atol=1e-4, rtol=1e-4)
torch.testing.assert_close(res_out_musa, res_out_native, atol=1e-4, rtol=1e-4)
# --- Output properties ---
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
def test_output_dtype_preserved_no_residual(self, dtype):
"""Output dtype should match input dtype when no residual."""
norm = self._make_norm(128)
x = torch.randn(4, 128, dtype=dtype, device=self.device)
out = norm.forward_musa(x)
assert out.dtype == dtype, f"Expected {dtype}, got {out.dtype}"
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
def test_output_dtype_preserved_with_residual(self, dtype):
"""Output and residual dtype should match input dtype."""
norm = self._make_norm(128)
x = torch.randn(4, 128, dtype=dtype, device=self.device)
residual = torch.randn(4, 128, dtype=dtype, device=self.device)
out, res_out = norm.forward_musa(x, residual)
assert out.dtype == dtype, f"Expected {dtype}, got {out.dtype}"
assert res_out.dtype == dtype, f"Expected {dtype}, got {res_out.dtype}"
def test_output_device_preserved(self):
"""Output should remain on the same MUSA device."""
norm = self._make_norm(128)
x = torch.randn(4, 128, dtype=torch.float32, device=self.device)
out = norm.forward_musa(x)
assert out.device == x.device
# --- Epsilon sensitivity ---
@pytest.mark.parametrize("eps", [1e-5, 1e-6, 1e-8])
def test_different_epsilon(self, eps):
"""Different epsilon values should produce consistent results."""
norm = self._make_norm(128, eps=eps)
torch.manual_seed(SEED)
x = torch.randn(4, 128, dtype=torch.float32, device=self.device)
out_musa = norm.forward_musa(x.clone())
out_native = norm.forward_native(x.clone())
torch.testing.assert_close(out_musa, out_native, atol=1e-4, rtol=1e-4)
# --- Weight initialization ---
def test_custom_weight(self):
"""RMSNorm with non-default weights should still match native."""
norm = self._make_norm(128)
# Set custom weights
with torch.no_grad():
norm.weight.fill_(2.0)
torch.manual_seed(SEED)
x = torch.randn(4, 128, dtype=torch.float32, device=self.device)
out_musa = norm.forward_musa(x.clone())
out_native = norm.forward_native(x.clone())
torch.testing.assert_close(out_musa, out_native, atol=1e-4, rtol=1e-4)
def test_random_weight(self):
"""RMSNorm with random weights should still match native."""
norm = self._make_norm(256)
with torch.no_grad():
norm.weight.copy_(torch.randn(256, device=self.device))
torch.manual_seed(SEED)
x = torch.randn(8, 256, dtype=torch.float32, device=self.device)
out_musa = norm.forward_musa(x.clone())
out_native = norm.forward_native(x.clone())
torch.testing.assert_close(out_musa, out_native, atol=1e-4, rtol=1e-4)
# --- Dispatch test ---
def test_dispatch_calls_forward_musa(self):
"""On MUSA platform, dispatch_forward should select forward_musa."""
from sglang.multimodal_gen.runtime.platforms import current_platform
if current_platform.is_musa():
norm = self._make_norm(128)
assert norm._forward_method == norm.forward_musa
# --- Large hidden size ---
def test_large_hidden_size(self):
"""Test with a large hidden size (float32)."""
hidden_size = 4096
norm = self._make_norm(hidden_size)
torch.manual_seed(SEED)
x = torch.randn(2, hidden_size, dtype=torch.float32, device=self.device)
out_musa = norm.forward_musa(x.clone())
out_native = norm.forward_native(x.clone())
torch.testing.assert_close(out_musa, out_native, atol=1e-4, rtol=1e-4)
def test_large_hidden_size_half(self):
"""Test with a large hidden size (float16)."""
hidden_size = 4096
norm = self._make_norm(hidden_size)
torch.manual_seed(SEED)
x = torch.randn(2, hidden_size, dtype=torch.float16, device=self.device)
out_musa = norm.forward_musa(x.clone())
out_native = norm.forward_native(x.clone())
torch.testing.assert_close(out_musa, out_native, atol=1e-2, rtol=1e-2)
# --- Single token ---
def test_single_token(self):
"""Test with a single token (batch_size=1)."""
norm = self._make_norm(128)
x = torch.randn(1, 128, dtype=torch.float32, device=self.device)
out_musa = norm.forward_musa(x.clone())
out_native = norm.forward_native(x.clone())
torch.testing.assert_close(out_musa, out_native, atol=1e-4, rtol=1e-4)
def test_single_token_with_residual(self):
"""Test with a single token and residual."""
norm = self._make_norm(128)
x = torch.randn(1, 128, dtype=torch.float32, device=self.device)
residual = torch.randn(1, 128, dtype=torch.float32, device=self.device)
x_musa, res_musa = x.clone(), residual.clone()
x_native, res_native = x.clone(), residual.clone()
out_musa, res_out_musa = norm.forward_musa(x_musa, res_musa)
out_native, res_out_native = norm.forward_native(x_native, res_native)
torch.testing.assert_close(out_musa, out_native, atol=1e-4, rtol=1e-4)
torch.testing.assert_close(res_out_musa, res_out_native, atol=1e-4, rtol=1e-4)
@@ -0,0 +1,119 @@
# SPDX-License-Identifier: Apache-2.0
"""Tests for MUSA-specific SiluAndMul custom op.
These tests call forward_musa directly and compare against forward_native
as the reference implementation.
"""
import pytest
import torch
# We need the MUSA platform to be available for these tests.
# Skip the entire module if MUSA is not available.
_musa_available = hasattr(torch, "musa") and torch.musa.is_available()
pytestmark = pytest.mark.skipif(not _musa_available, reason="MUSA device not available")
# Use a fixed seed for reproducibility
SEED = 42
def get_musa_device():
return torch.device("musa:0")
class TestSiluAndMul:
"""Tests for SiluAndMul.forward_musa vs forward_native."""
@pytest.fixture(autouse=True)
def setup(self):
from sglang.multimodal_gen.runtime.layers.activation import SiluAndMul
self.op = SiluAndMul()
self.device = get_musa_device()
# --- Shape parametrization ---
@pytest.mark.parametrize(
"shape",
[
(1, 64), # minimal 2D
(4, 128), # small 2D
(32, 256), # medium 2D
(128, 1024), # large 2D
(1, 1, 64), # minimal 3D
(2, 8, 128), # small 3D
(4, 16, 512), # medium 3D
(2, 32, 2048), # large 3D
],
ids=lambda s: f"shape={'x'.join(map(str, s))}",
)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
def test_forward_matches_native(self, shape, dtype):
"""forward_musa output should match forward_native within tolerance."""
torch.manual_seed(SEED)
x = torch.randn(shape, dtype=dtype, device=self.device)
x_native = x.clone().detach()
out_musa = self.op.forward_musa(x)
out_native = self.op.forward_native(x_native)
expected_last_dim = shape[-1] // 2
assert out_musa.shape == out_native.shape
assert out_musa.shape[-1] == expected_last_dim
atol = 1e-2 if dtype in (torch.float16, torch.bfloat16) else 1e-5
rtol = 1e-2 if dtype in (torch.float16, torch.bfloat16) else 1e-5
torch.testing.assert_close(out_musa, out_native, atol=atol, rtol=rtol)
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16, torch.float32])
def test_output_dtype_preserved(self, dtype):
"""Output dtype should match input dtype."""
x = torch.randn(4, 128, dtype=dtype, device=self.device)
out = self.op.forward_musa(x)
assert out.dtype == dtype, f"Expected {dtype}, got {out.dtype}"
def test_output_device_preserved(self):
"""Output should remain on the same MUSA device."""
x = torch.randn(4, 128, dtype=torch.float16, device=self.device)
out = self.op.forward_musa(x)
assert out.device == x.device
def test_zeros_input(self):
"""silu(0) * 0 = 0, so output should be all zeros."""
x = torch.zeros(4, 128, dtype=torch.float32, device=self.device)
out = self.op.forward_musa(x)
torch.testing.assert_close(
out, torch.zeros(4, 64, dtype=torch.float32, device=self.device)
)
def test_large_values(self):
"""Test with large magnitude inputs to check numerical stability."""
x = torch.randn(4, 128, dtype=torch.float32, device=self.device) * 100.0
out_musa = self.op.forward_musa(x)
out_native = self.op.forward_native(x.clone())
torch.testing.assert_close(out_musa, out_native, atol=1e-2, rtol=1e-2)
def test_non_contiguous_input(self):
"""forward_musa should handle non-contiguous inputs correctly."""
# Create non-contiguous tensor via transpose
x_base = torch.randn(128, 4, dtype=torch.float32, device=self.device)
x = x_base.t() # shape (4, 128), non-contiguous
assert not x.is_contiguous()
out_musa = self.op.forward_musa(x)
out_native = self.op.forward_native(x.clone())
torch.testing.assert_close(out_musa, out_native, atol=1e-5, rtol=1e-5)
def test_single_element_last_dim(self):
"""Edge case: last dim = 2 (d=1)."""
x = torch.randn(4, 2, dtype=torch.float32, device=self.device)
out_musa = self.op.forward_musa(x)
out_native = self.op.forward_native(x.clone())
assert out_musa.shape[-1] == 1
torch.testing.assert_close(out_musa, out_native, atol=1e-5, rtol=1e-5)
def test_dispatch_calls_forward_musa(self):
"""On MUSA platform, dispatch_forward should select forward_musa."""
from sglang.multimodal_gen.runtime.platforms import current_platform
if current_platform.is_musa():
assert self.op._forward_method == self.op.forward_musa
@@ -0,0 +1,268 @@
"""
Test runner for multimodal_gen MUSA suites that manages partitioned execution.
Usage:
python3 run_suite_musa.py --suite <suite_name> --partition-id <id> --total-partitions <num>
Example:
python3 run_suite_musa.py --suite 1-gpu-musa --partition-id 0 --total-partitions 2
"""
import argparse
import os
import subprocess
import sys
from pathlib import Path
import tabulate
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
SUITES = {
"1-gpu-musa": [
"musa/test_server_a_musa.py",
"musa/test_server_b_musa.py",
],
"2-gpu-musa": [
"musa/test_server_2_gpu_a_musa.py",
],
}
def parse_args():
parser = argparse.ArgumentParser(description="Run multimodal_gen MUSA test suite")
parser.add_argument(
"--suite",
type=str,
required=True,
choices=list(SUITES.keys()),
help="The test suite to run (valid names are defined in SUITES)",
)
parser.add_argument(
"--partition-id",
type=int,
default=0,
help="Index of the current partition (for parallel execution)",
)
parser.add_argument(
"--total-partitions",
type=int,
default=1,
help="Total number of partitions",
)
parser.add_argument(
"--base-dir",
type=str,
default="server",
help="Base directory for tests relative to this script's parent",
)
parser.add_argument(
"-k",
"--filter",
type=str,
default=None,
help="Pytest filter expression (passed to pytest -k)",
)
parser.add_argument(
"--continue-on-error",
action="store_true",
default=False,
help="Continue running remaining tests even if one fails.",
)
return parser.parse_args()
def collect_test_items(files, filter_expr=None):
"""Collect test item node IDs from the given files using pytest --collect-only."""
cmd = [sys.executable, "-m", "pytest", "--collect-only", "-q"]
if filter_expr:
cmd.extend(["-k", filter_expr])
cmd.extend(files)
print(f"Collecting tests with command: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode not in (0, 5):
error_msg = (
f"pytest --collect-only failed with exit code {result.returncode}\n"
f"Command: {' '.join(cmd)}\n"
)
if result.stderr:
error_msg += f"stderr:\n{result.stderr}\n"
if result.stdout:
error_msg += f"stdout:\n{result.stdout}\n"
logger.error(error_msg)
raise RuntimeError(error_msg)
if result.returncode == 5:
print(
"No tests were collected (exit code 5). This may be expected with filters."
)
test_items = []
for line in result.stdout.strip().split("\n"):
line = line.strip()
if line and "::" in line and not line.startswith(("=", "-", " ")):
test_id = line.split()[0] if " " in line else line
if "::" in test_id:
test_items.append(test_id)
print(f"Collected {len(test_items)} test items")
return test_items
def run_pytest(files, filter_expr=None, exitfirst=False):
if not files:
print("No files to run.")
return 0
base_cmd = [sys.executable, "-m", "pytest", "-s", "-v"]
if exitfirst:
base_cmd.append("-x")
if filter_expr:
base_cmd.extend(["-k", filter_expr])
max_retries = 6
for i in range(max_retries + 1):
cmd = list(base_cmd)
if i > 0:
cmd.append("--last-failed")
cmd.extend(files)
if i > 0:
print(
f"Performance assertion failed. Retrying ({i}/{max_retries}) with --last-failed..."
)
print(f"Running command: {' '.join(cmd)}")
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0,
)
output_bytes = bytearray()
while True:
chunk = process.stdout.read(4096)
if not chunk:
break
sys.stdout.buffer.write(chunk)
sys.stdout.buffer.flush()
output_bytes.extend(chunk)
process.wait()
returncode = process.returncode
if returncode == 0:
return 0
if returncode == 5:
print(
"No tests collected (exit code 5). This is expected when filters "
"deselect all tests in a partition. Treating as success."
)
return 0
full_output = output_bytes.decode("utf-8", errors="replace")
is_perf_assertion = (
"multimodal_gen/test/server/test_server_utils.py" in full_output
and "AssertionError" in full_output
)
is_flaky_ci_assertion = (
"SafetensorError" in full_output
or "FileNotFoundError" in full_output
or "TimeoutError" in full_output
)
is_oom_error = (
"out of memory" in full_output.lower()
or "oom killer" in full_output.lower()
)
if not (is_perf_assertion or is_flaky_ci_assertion or is_oom_error):
return returncode
print("Max retry exceeded")
return returncode
def main():
args = parse_args()
current_file_path = Path(__file__).resolve()
test_root_dir = current_file_path.parent
target_dir = test_root_dir / args.base_dir
if not target_dir.exists():
print(f"Error: Target directory {target_dir} does not exist.")
sys.exit(1)
suite_files_rel = SUITES[args.suite]
suite_files_abs = []
for rel_path in suite_files_rel:
abs_path = target_dir / rel_path
if not abs_path.exists():
print(f"Warning: Test file {rel_path} not found in {target_dir}. Skipping.")
continue
suite_files_abs.append(str(abs_path))
if not suite_files_abs:
print(f"No valid test files found for suite '{args.suite}'.")
sys.exit(0)
all_test_items = collect_test_items(suite_files_abs, filter_expr=args.filter)
if not all_test_items:
print(f"No test items found for suite '{args.suite}'.")
sys.exit(0)
my_items = [
item
for i, item in enumerate(all_test_items)
if i % args.total_partitions == args.partition_id
]
partition_info = (
f"{args.partition_id + 1}/{args.total_partitions} "
f"(0-based id={args.partition_id})"
)
rows = [[args.suite, partition_info]]
msg = (
tabulate.tabulate(rows, headers=["Suite", "Partition"], tablefmt="psql") + "\n"
)
msg += f"Enabled {len(my_items)} test(s):\n"
for item in my_items:
msg += f" - {item}\n"
print(msg, flush=True)
print(
f"Suite: {args.suite} | Partition: {args.partition_id}/{args.total_partitions}"
)
print(f"Selected {len(suite_files_abs)} files:")
for file_path in suite_files_abs:
print(f" - {os.path.basename(file_path)}")
if not my_items:
print("No items assigned to this partition. Exiting success.")
sys.exit(0)
print(f"Running {len(my_items)} items in this shard: {', '.join(my_items)}")
exit_code = run_pytest(my_items, exitfirst=not args.continue_on_error)
msg = (
"\n"
+ tabulate.tabulate(rows, headers=["Suite", "Partition"], tablefmt="psql")
+ "\n"
)
msg += f"Executed {len(my_items)} test(s):\n"
for item in my_items:
msg += f" - {item}\n"
print(msg, flush=True)
sys.exit(exit_code)
if __name__ == "__main__":
main()
@@ -0,0 +1,264 @@
{
"metadata":{
"model":"Diffusion Server",
"hardware":"CI S5000 pool",
"description":"Reference numbers captured from the CI diffusion server baseline run"
},
"scenarios":{
"qwen_image_t2i_musa":{
"stages_ms":{
"InputValidationStage":0.09,
"TextEncodingStage":658.4,
"LatentPreparationStage":0.33,
"TimestepPreparationStage":24.39,
"DenoisingStage":36196.6,
"DecodingStage":40.44
},
"denoise_step_ms":{
"0":666.68,
"1":732.33,
"2":721.29,
"3":729.27,
"4":725.05,
"5":721.71,
"6":722.22,
"7":725.93,
"8":724.94,
"9":724.14,
"10":730.43,
"11":719.92,
"12":726.24,
"13":722.04,
"14":727.68,
"15":720.31,
"16":721.75,
"17":725.65,
"18":720.23,
"19":724.12,
"20":726.35,
"21":723.27,
"22":731.58,
"23":724.97,
"24":721.48,
"25":722.0,
"26":722.37,
"27":719.81,
"28":721.64,
"29":724.81,
"30":723.9,
"31":725.42,
"32":719.86,
"33":728.04,
"34":728.55,
"35":723.13,
"36":722.0,
"37":730.11,
"38":724.06,
"39":728.35,
"40":728.04,
"41":726.62,
"42":728.47,
"43":728.11,
"44":728.59,
"45":721.5,
"46":724.59,
"47":729.26,
"48":726.05,
"49":721.13
},
"expected_e2e_ms":37190.98,
"expected_avg_denoise_ms":723.72,
"expected_median_denoise_ms":724.7
},
"wan2_1_t2v_1.3b_musa":{
"stages_ms":{
"InputValidationStage":0.12,
"TextEncodingStage":1097.75,
"LatentPreparationStage":0.24,
"TimestepPreparationStage":5.66,
"DenoisingStage":47399.84,
"DecodingStage":946.08,
"per_frame_generation":null
},
"denoise_step_ms":{
"0":783.06,
"1":970.52,
"2":939.72,
"3":947.58,
"4":941.44,
"5":955.26,
"6":960.39,
"7":951.84,
"8":959.68,
"9":953.33,
"10":940.87,
"11":958.5,
"12":952.7,
"13":933.4,
"14":952.0,
"15":951.6,
"16":947.04,
"17":939.28,
"18":956.88,
"19":960.1,
"20":949.73,
"21":954.77,
"22":959.98,
"23":947.37,
"24":957.51,
"25":953.39,
"26":953.73,
"27":959.57,
"28":942.59,
"29":958.05,
"30":952.76,
"31":952.76,
"32":950.6,
"33":948.76,
"34":957.53,
"35":940.86,
"36":958.11,
"37":940.9,
"38":949.1,
"39":951.81,
"40":948.61,
"41":957.28,
"42":951.41,
"43":953.09,
"44":955.69,
"45":941.93,
"46":952.96,
"47":953.5,
"48":939.25,
"49":942.69
},
"expected_e2e_ms":50007.17,
"expected_avg_denoise_ms":947.83,
"expected_median_denoise_ms":952.35
},
"wan2_2_t2v_a14b_2gpu_musa": {
"stages_ms": {
"per_frame_generation": null,
"TimestepPreparationStage": 4.8,
"DenoisingStage": 281769.53,
"DecodingStage": 1728.76,
"InputValidationStage": 0.13,
"TextEncodingStage": 1098.56,
"LatentPreparationStage": 0.33
},
"denoise_step_ms": {
"0": 6262.73,
"1": 7103.05,
"2": 7039.74,
"3": 7052.97,
"4": 7046.74,
"5": 7059.9,
"6": 7048.75,
"7": 7054.99,
"8": 7052.75,
"9": 7059.59,
"10": 7056.92,
"11": 7053.61,
"12": 7063.48,
"13": 7045.34,
"14": 7051.22,
"15": 7051.25,
"16": 7048.83,
"17": 7053.91,
"18": 7060.47,
"19": 7056.4,
"20": 7055.39,
"21": 7052.72,
"22": 7054.84,
"23": 7058.17,
"24": 7052.26,
"25": 7057.8,
"26": 7362.31,
"27": 7053.57,
"28": 7044.42,
"29": 7044.03,
"30": 7056.59,
"31": 7045.81,
"32": 7051.59,
"33": 7048.66,
"34": 7050.84,
"35": 7048.57,
"36": 7056.21,
"37": 7056.08,
"38": 7053.32,
"39": 7047.35
},
"expected_e2e_ms": 285727.55,
"expected_avg_denoise_ms": 7043.96,
"expected_median_denoise_ms": 7056.07
},
"wan2_1_i2v_14b_480P_2gpu_musa": {
"stages_ms": {
"InputValidationStage": 10.35,
"TextEncodingStage": 1098.55,
"ImageEncodingStage": 730.6,
"ImageVAEEncodingStage": 1188.4,
"LatentPreparationStage": 0.5,
"TimestepPreparationStage": 7.99,
"DenoisingStage": 134355.99,
"DecodingStage": 1200.0,
"per_frame_generation": null
},
"denoise_step_ms": {
"0": 2441.69,
"1": 2711.82,
"2": 2701.26,
"3": 2692.91,
"4": 2686.94,
"5": 2685.35,
"6": 2689.45,
"7": 2685.49,
"8": 2687.74,
"9": 2690.0,
"10": 2691.31,
"11": 2691.65,
"12": 2698.52,
"13": 2689.37,
"14": 2690.18,
"15": 2691.48,
"16": 2695.74,
"17": 2691.77,
"18": 2689.77,
"19": 2690.6,
"20": 2686.84,
"21": 2694.9,
"22": 2689.56,
"23": 2696.38,
"24": 2689.13,
"25": 2686.38,
"26": 2689.53,
"27": 2695.91,
"28": 2691.9,
"29": 2691.55,
"30": 2700.09,
"31": 2691.45,
"32": 2696.8,
"33": 2689.42,
"34": 2695.87,
"35": 2690.31,
"36": 2687.65,
"37": 2697.62,
"38": 2683.46,
"39": 2692.41,
"40": 2699.33,
"41": 2695.25,
"42": 2691.16,
"43": 2687.13,
"44": 2692.92,
"45": 2690.4,
"46": 2696.53,
"47": 2689.51,
"48": 2692.06,
"49": 2680.93
},
"expected_e2e_ms": 138624.98,
"expected_avg_denoise_ms": 2686.91,
"expected_median_denoise_ms": 2691.24
}
}
}
@@ -0,0 +1,28 @@
"""
MUSA-specific 2-GPU diffusion performance test.
"""
from __future__ import annotations
import pytest
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.test.server.musa.testcase_configs_musa import (
TWO_GPU_MUSA_CASES_A,
)
from sglang.multimodal_gen.test.server.test_server_common import ( # noqa: F401
DiffusionServerBase,
diffusion_server,
)
from sglang.multimodal_gen.test.server.testcase_configs import DiffusionTestCase
logger = init_logger(__name__)
class TestDiffusionServerTwoGpuMusaA(DiffusionServerBase):
"""Performance tests for 2-GPU diffusion cases on MUSA."""
@pytest.fixture(params=TWO_GPU_MUSA_CASES_A, ids=lambda c: c.id)
def case(self, request) -> DiffusionTestCase:
"""Provide a DiffusionTestCase for each 2-GPU MUSA test."""
return request.param
@@ -0,0 +1,28 @@
"""
MUSA-specific diffusion performance test (1-GPU).
"""
from __future__ import annotations
import pytest
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.test.server.musa.testcase_configs_musa import (
ONE_GPU_MUSA_CASES_A,
)
from sglang.multimodal_gen.test.server.test_server_common import ( # noqa: F401
DiffusionServerBase,
diffusion_server,
)
from sglang.multimodal_gen.test.server.testcase_configs import DiffusionTestCase
logger = init_logger(__name__)
class TestDiffusionServerOneGpuMusaImage(DiffusionServerBase):
"""Performance tests for 1-GPU diffusion cases on MUSA"""
@pytest.fixture(params=ONE_GPU_MUSA_CASES_A, ids=lambda c: c.id)
def case(self, request) -> DiffusionTestCase:
"""Provide a DiffusionTestCase for each 1-GPU MUSA test."""
return request.param
@@ -0,0 +1,28 @@
"""
MUSA-specific diffusion performance test (1-GPU).
"""
from __future__ import annotations
import pytest
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.test.server.musa.testcase_configs_musa import (
ONE_GPU_MUSA_CASES_B,
)
from sglang.multimodal_gen.test.server.test_server_common import ( # noqa: F401
DiffusionServerBase,
diffusion_server,
)
from sglang.multimodal_gen.test.server.testcase_configs import DiffusionTestCase
logger = init_logger(__name__)
class TestDiffusionServerOneGpuMusaVideo(DiffusionServerBase):
"""Performance tests for 1-GPU diffusion cases on MUSA"""
@pytest.fixture(params=ONE_GPU_MUSA_CASES_B, ids=lambda c: c.id)
def case(self, request) -> DiffusionTestCase:
"""Provide a DiffusionTestCase for each 1-GPU MUSA test."""
return request.param
@@ -0,0 +1,53 @@
from __future__ import annotations
from sglang.multimodal_gen.test.server.testcase_configs import (
T2V_PROMPT,
DiffusionSamplingParams,
DiffusionServerArgs,
DiffusionTestCase,
T2I_sampling_params,
TI2V_sampling_params,
)
ONE_GPU_MUSA_CASES_A: list[DiffusionTestCase] = [
DiffusionTestCase(
"qwen_image_t2i_musa",
DiffusionServerArgs(
model_path="Qwen/Qwen-Image",
modality="image",
),
T2I_sampling_params,
run_consistency_check=False,
),
]
ONE_GPU_MUSA_CASES_B: list[DiffusionTestCase] = [
DiffusionTestCase(
"wan2_1_t2v_1.3b_musa",
DiffusionServerArgs(
model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
modality="video",
custom_validator="video",
),
DiffusionSamplingParams(
prompt=T2V_PROMPT,
),
run_consistency_check=False,
),
]
TWO_GPU_MUSA_CASES_A: list[DiffusionTestCase] = [
DiffusionTestCase(
"wan2_1_i2v_14b_480P_2gpu_musa",
DiffusionServerArgs(
model_path="Wan-AI/Wan2.1-I2V-14B-480P-Diffusers",
modality="video",
custom_validator="video",
num_gpus=2,
),
TI2V_sampling_params,
run_consistency_check=False,
),
]
@@ -492,6 +492,8 @@ def _with_default_num_gpus(
# Load global configuration
BASELINE_CONFIG = BaselineConfig.load(
Path(__file__).with_name("perf_baselines.json")
).update(Path(__file__).parent / "ascend" / "perf_baselines_npu.json")
BASELINE_CONFIG = (
BaselineConfig.load(Path(__file__).with_name("perf_baselines.json"))
.update(Path(__file__).parent / "ascend" / "perf_baselines_npu.json")
.update(Path(__file__).parent / "musa" / "perf_baselines_musa.json")
)