[NPU] [Diffusion] Support MiniMax H3 on Ascend NPU's (#33569)

Co-authored-by: ronnie_zheng <zl19940307@163.com>
This commit is contained in:
Артем Савкин
2026-08-25 09:44:14 +03:00
committed by GitHub
co-authored by ronnie_zheng
parent b7f9fca26e
commit 61b67316d8
21 changed files with 1147 additions and 693 deletions
+1
View File
@@ -55,6 +55,7 @@ RUN apt-get update -y && apt upgrade -y && apt-get install -y \
clang \
locales \
ccache \
ffmpeg \
openssl \
libssl-dev \
pkg-config \
@@ -369,6 +369,70 @@ values. SGLang rejects a request outside that coverage instead of silently
changing conditioning. Cache mode supports the matching unquantized checkpoint
only.
### Serve MiniMax-H3 on Ascend NPUs
For Ascend NPU, follow the
[NPU installation guide](/docs/hardware-platforms/ascend-npus/getting-started/installation)
before starting the server.
The Ascend commands below explicitly enable the Cache-DiT configuration used
for the reported performance measurements. Remove these `SGLANG_CACHE_DIT_*`
variables to use lossless denoising. See the **Ascend NPU topology comparison**
in the Benchmarks section for the measured eight- and four-NPU latency.
The measured latency configuration also passes `--dit-cpu-offload false` to
keep the transformer resident on the NPUs. Omit this flag when lower device
memory usage is more important than avoiding CPU-to-NPU transfer latency.
For an eight-NPU host, the validated topology is TP2 + SP4 with Laser
Attention. Use Ascend Flash Attention by replacing `laser_attn` with `fa`.
```bash 8-NPU
SGLANG_CACHE_DIT_ENABLED=true \
SGLANG_CACHE_DIT_FN=2 \
SGLANG_CACHE_DIT_BN=1 \
SGLANG_CACHE_DIT_WARMUP=4 \
SGLANG_CACHE_DIT_RDT=0.4 \
SGLANG_CACHE_DIT_MC=4 \
SGLANG_CACHE_DIT_TAYLORSEER=true \
SGLANG_CACHE_DIT_TS_ORDER=2 \
HCCL_BUFFSIZE=256 sglang serve \
--model-path MiniMaxAI/MiniMax-H3 \
--model-type diffusion \
--model-variant fl2va \
--dit-cpu-offload false \
--num-gpus 8 \
--tp-size 2 \
--sp-degree 4 \
--attention-backend laser_attn \
--port 30088 \
--component-residency text_encoder=layerwise-offload
```
For a four-NPU host, use TP2 + SP2:
```bash 4-NPU
SGLANG_CACHE_DIT_ENABLED=true \
SGLANG_CACHE_DIT_FN=2 \
SGLANG_CACHE_DIT_BN=1 \
SGLANG_CACHE_DIT_WARMUP=4 \
SGLANG_CACHE_DIT_RDT=0.4 \
SGLANG_CACHE_DIT_MC=4 \
SGLANG_CACHE_DIT_TAYLORSEER=true \
SGLANG_CACHE_DIT_TS_ORDER=2 \
HCCL_BUFFSIZE=256 sglang serve \
--model-path MiniMaxAI/MiniMax-H3 \
--model-type diffusion \
--model-variant fl2va \
--dit-cpu-offload false \
--num-gpus 4 \
--tp-size 2 \
--sp-degree 2 \
--attention-backend laser_attn \
--port 30088 \
--component-residency text_encoder=layerwise-offload
```
## 4. Generate video and audio
MiniMax-H3 uses the asynchronous OpenAI-compatible video endpoint. Choose a
@@ -1087,10 +1151,27 @@ the configurations with collected measurements:
| B200 | 8× Ulysses8 resident | 4× FSDP + Ulysses4 |
| H200 | 4× Ulysses4 resident | 4× FSDP + Ulysses4; 4× TP2 + Ulysses2; 2 nodes × 8× Ulysses8×Ring2 cross-node |
| H100 | 4× TP2 + Ulysses2 resident | 4× TP4 + Ulysses1; 4× FSDP + Ulysses4 |
| Ascend NPU | 8 NPUs, TP2 + SP4, Laser Attention | 4 NPUs, TP2 + SP2, Laser Attention |
| MI300X / MI355X | 8× Ulysses8 resident | 1×, 2×, and 4× scaling runs |
| RTX 5090 | 2× TP2 + layerwise offload | — |
| RTX 4090 24 GB | 1× layerwise offload + `kitchen_int8` | Approximate attention backends are opt-in |
### Ascend NPU topology comparison
Both topologies used Laser Attention and the explicit Cache-DiT configuration
from the Ascend launch commands, with `--dit-cpu-offload false` keeping the DiT
resident. The measured workload was one 5-second T2VA request at 1344×768,
124 frames, 24 fps, and 50 inference steps.
| NPU count | Topology | End-to-end latency |
| ---: | --- | ---: |
| 8 | TP2 + SP4 | **55.07 s** |
| 4 | TP2 + SP2 | **103.57 s** |
These are individual end-to-end measurements for each topology, not averages.
The eight-NPU topology had 46.8% lower end-to-end latency than the four-NPU
topology.
### B300 precision and encoder placement
A 12-configuration sweep on a single 8× B300 host, covering both checkpoint
@@ -9,18 +9,26 @@ from sglang.multimodal_gen.runtime.layers.attention.backends.sdpa import SDPABac
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
# Import to use torch.ops.attentions, install package with sgl_kernel_npu
try:
import attentions # noqa: F401
except ImportError as e:
logger.warning_once(
"The 'attentions' library is not installed. Laser Attention is unavailable. "
"Installing this library may improve performance on NPU. "
"See: sgl-project/sgl-kernel-npu"
)
raise ImportError(
(
"The required 'attentions' package is not installed. "
"The package can be installed with sgl_kernel_npu"
"Install it from sgl-project/sgl-kernel-npu."
)
) from e
logger = init_logger(__name__)
# The current NPU kernel stores QK scores and V in FP16 even for BF16 inputs.
_BF16_LASER_SCALE = 256.0
class LaserAttentionBackend(AttentionBackend):
@@ -102,14 +110,32 @@ class LaserAttentionImpl(AttentionImpl):
return torch.nn.functional.pad(input_tensor, pad_list)
def _la_preprocess_input(
self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
*,
preserve_bf16_range: bool = False,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, float, float]:
# Currently BSND input layout is not supported
q = query.transpose(1, 2)
k = key.transpose(1, 2)
v = value.transpose(1, 2)
q_scale = 1.0
k_scale = 1.0
value_scale = 1.0
if preserve_bf16_range:
q_scale = _BF16_LASER_SCALE if q.dtype == torch.bfloat16 else 1.0
k_scale = _BF16_LASER_SCALE if k.dtype == torch.bfloat16 else 1.0
value_scale = _BF16_LASER_SCALE if v.dtype == torch.bfloat16 else 1.0
if q.dtype != torch.float16:
q = q.mul(1.0 / q_scale).to(torch.float16)
if k.dtype != torch.float16:
k = k.mul(1.0 / k_scale).to(torch.float16)
if v.dtype != torch.float16:
v = v.mul(1.0 / value_scale).to(torch.float16)
elif q.dtype != torch.float16:
q = q.to(torch.float16)
k = k.to(torch.float16)
v = v.to(torch.float16)
@@ -118,7 +144,7 @@ class LaserAttentionImpl(AttentionImpl):
k = self._pad(k)
v = self._pad(v)
return q, k, v
return q, k, v, q_scale * k_scale, value_scale
def _la_postprocess_output(
self,
@@ -141,6 +167,7 @@ class LaserAttentionImpl(AttentionImpl):
value: torch.Tensor,
head_num: int,
pre_tokens: int,
scale_value: float,
) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops.attentions.la(
query=query,
@@ -149,7 +176,7 @@ class LaserAttentionImpl(AttentionImpl):
atten_mask=None,
alibi_mask=None,
drop_mask=None,
scale_value=self.softmax_scale,
scale_value=scale_value,
head_num=head_num,
input_layout="BNSD",
keep_prob=1.0,
@@ -164,6 +191,17 @@ class LaserAttentionImpl(AttentionImpl):
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
return self._forward_dense(query, key, value, attn_metadata)
def _forward_dense(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
attn_metadata: AttentionMetadata,
*,
preserve_bf16_range: bool = False,
) -> torch.Tensor:
q_seqlen, head_dim = query.shape[1], query.shape[3]
kv_seqlen = key.shape[1]
@@ -182,10 +220,91 @@ class LaserAttentionImpl(AttentionImpl):
kv_seqlen // self.seq_len_pad_base + 1
) * self.seq_len_pad_base - kv_seqlen
q, k, v = self._la_preprocess_input(query, key, value)
_, la_output = self._laser_attention(q, k, v, q.shape[1], pre_tokens)
q, k, v, qk_scale, value_scale = self._la_preprocess_input(
query,
key,
value,
preserve_bf16_range=preserve_bf16_range,
)
_, la_output = self._laser_attention(
q,
k,
v,
q.shape[1],
pre_tokens,
self.softmax_scale * qk_scale,
)
if value_scale != 1.0:
la_output.mul_(value_scale)
output = self._la_postprocess_output(
la_output, query.dtype, q_seqlen, head_dim
)
return output
def forward_varlen(
self,
query: torch.Tensor,
key: torch.Tensor,
value: torch.Tensor,
*,
cu_seqlens: torch.Tensor,
max_seqlen: int,
cu_seqlens_host: tuple[int, ...] | None = None,
) -> torch.Tensor:
del max_seqlen
bounds = (
cu_seqlens_host
if cu_seqlens_host is not None
else tuple(int(item) for item in cu_seqlens.tolist())
)
# MiniMax-H3 is the current Laser varlen caller and encodes one real
# segment followed by alignment padding as [0, used, padded].
padding_start = (
bounds[1]
if len(bounds) == 3 and bounds[0] == 0 and bounds[-1] == query.shape[0]
else None
)
# Packed segments are independent; a single dense call would let real
# tokens attend alignment padding in MiniMax-H3.
# MiniMax-H3 packs one real segment followed by alignment padding.
# Delay allocation until Laser releases its temporary tensors and
# avoid allocating/copying the result when no padding was added.
if (
padding_start is not None
and padding_start > 0
and bounds[0] == 0
and bounds[1] == padding_start
):
segment = self._forward_dense(
query[:padding_start].unsqueeze(0),
key[:padding_start].unsqueeze(0),
value[:padding_start].unsqueeze(0),
None,
preserve_bf16_range=True,
)[0]
if padding_start == query.shape[0]:
return segment
output = torch.empty_like(query)
output[:padding_start].copy_(segment)
output[padding_start:].zero_()
return output
output = torch.empty_like(query)
for start, stop in zip(bounds[:-1], bounds[1:]):
if padding_start is not None and start >= padding_start:
break
if start == stop:
continue
segment = self._forward_dense(
query[start:stop].unsqueeze(0),
key[start:stop].unsqueeze(0),
value[start:stop].unsqueeze(0),
None,
preserve_bf16_range=padding_start is not None,
)
output[start:stop].copy_(segment[0])
if padding_start is not None:
output[padding_start:].zero_()
return output
@@ -37,6 +37,7 @@ _is_musa = current_platform.is_musa()
_is_cpu = current_platform.is_cpu()
_is_xpu = current_platform.is_xpu()
_use_rocm_flydsl = get_bool_env_var("SGLANG_USE_ROCM_FLYDSL")
_has_attentions = False
if _is_cuda or _is_xpu:
from sgl_kernel import fused_add_rmsnorm, rmsnorm
@@ -47,6 +48,20 @@ if _is_npu:
fused_rmsnorm_without_weight,
)
try:
import attentions # noqa: F401
_has_attentions = True
except ImportError:
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__) # pylint: disable=invalid-name
logger.warning_once(
"The 'attentions' library is not installed. Falling back to native layernorm. "
"Installing this library may improve performance on NPU. "
"See: sgl-project/sgl-kernel-npu"
)
if _is_musa:
from sgl_kernel import fused_add_rmsnorm
@@ -452,20 +467,7 @@ class FP32LayerNorm(CustomOp, nn.LayerNorm):
)
self._forward_method = self.dispatch_forward()
if _is_npu:
try:
import attentions # noqa: F401
except ImportError:
from sglang.multimodal_gen.runtime.utils.logging_utils import (
init_logger,
)
logger = init_logger(__name__) # pylint: disable=invalid-name
logger.warning(
"The 'attentions' library is not installed. Falling back to native layernorm. "
"Installing this library may improve performance on NPU."
"See: sgl-project/sgl-kernel-npu"
)
if _is_npu and not _has_attentions:
self._forward_method = self.forward_native
def _cached_fp32_param(
@@ -478,9 +478,11 @@ class ComponentResidencyManager:
if should_keep:
return
strategy = self.strategy_for(use.component_name, module)
was_on_cuda = self._module_on_cuda(module)
was_on_supported_device = self._module_on_supported_device(module)
strategy.finish_use(module, use, self.state)
self._empty_cache_after_large_release(use, strategy, module, was_on_cuda)
self._empty_cache_after_large_release(
use, strategy, module, was_on_supported_device
)
def finish_request(self) -> None:
self.finish_active_use(prefetch_next=False)
@@ -503,9 +505,11 @@ class ComponentResidencyManager:
not self._is_single_dit_component(component_name) or keep_single_dit
)
strategy = self.strategy_for(component_name, module)
was_on_cuda = self._module_on_cuda(module)
was_on_supported_device = self._module_on_supported_device(module)
strategy.finish_request(module, use, self.state, preferred=preferred)
self._empty_cache_after_large_release(use, strategy, module, was_on_cuda)
self._empty_cache_after_large_release(
use, strategy, module, was_on_supported_device
)
def stage_name(self, stage: ComponentResidencyStage) -> str:
return self._stage_names_by_id.get(id(stage), stage.__class__.__name__)
@@ -634,21 +638,33 @@ class ComponentResidencyManager:
buffer = next(module.buffers(), None)
return buffer.device.type if buffer is not None else None
def _module_on_cuda(self, module: nn.Module | None) -> bool:
return self._module_device(module) == "cuda"
def _module_on_supported_device(self, module: nn.Module | None) -> bool:
is_supported_platform = (
current_platform.is_cuda()
or current_platform.is_rocm()
or current_platform.is_npu()
)
return is_supported_platform and current_platform.is_device_type(
self._module_device(module)
)
def _empty_cache_after_large_release(
self,
use: ComponentUse,
strategy: ComponentResidencyStrategy,
module: nn.Module,
was_on_cuda: bool,
was_on_supported_device: bool,
) -> None:
if not use.memory_intensive:
return
released_cuda_storage = was_on_cuda and not self._module_on_cuda(module)
released_device_storage = (
was_on_supported_device and not self._module_on_supported_device(module)
)
released_layerwise_storage = isinstance(strategy, LayerwiseOffloadStrategy)
if not (released_cuda_storage or released_layerwise_storage):
should_empty_component_cache = (
released_device_storage and not current_platform.is_npu()
)
if not (should_empty_component_cache or released_layerwise_storage):
return
if not torch.get_device_module().is_available():
return
@@ -13,6 +13,8 @@ from sglang.kernels.ops.activation.activation import (
silu_and_mul_with_activation_rounding,
)
from sglang.kernels.ops.diffusion import try_fused_scaled_residual_add_exact
from sglang.multimodal_gen.runtime.layers.activation import SiluAndMul
from sglang.multimodal_gen.runtime.platforms import current_platform
from .attention import Attention
from .vit_utils import _env_flag, _vit_torch_compile_kwargs
@@ -62,6 +64,12 @@ class FeedForward(nn.Module):
else:
raise ValueError(f"Unsupported activation function: {activation_fn}")
self.silu_and_mul = (
SiluAndMul()
if use_gated and activation_fn == "silu" and current_platform.is_npu()
else None
)
self.w2 = nn.Linear(inner_dim, dim_out, bias=bias)
self._compile_forward_enabled = _env_flag(
"MINIMAX_H3_VAE_DECODER_VIT_FF_TORCH_COMPILE", "0"
@@ -83,6 +91,8 @@ class FeedForward(nn.Module):
and hidden_states.shape[-1] % 32 == 0
):
hidden_states = silu_and_mul_with_activation_rounding(hidden_states)
elif self.silu_and_mul is not None:
hidden_states = self.silu_and_mul(hidden_states)
else:
gate, hidden_states = hidden_states.chunk(2, dim=-1)
hidden_states = self.act_fn(gate).mul_(hidden_states)
@@ -1,6 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import shutil
from sglang.multimodal_gen.configs.pipeline_configs.minimax_h3 import (
MiniMaxH3PipelineConfig,
)
@@ -24,6 +26,7 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.m
MiniMaxH3PartitionAdmissionStage,
MiniMaxH3ReleaseMetadata,
)
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.server_args import ServerArgs
@@ -46,6 +49,24 @@ class MiniMaxH3Pipeline(LoRAPipeline, ComposedPipelineBase):
"transformer",
]
def __init__(self, *args, **kwargs):
# TODO: Enable this check on ROCm after adding ffmpeg to the AMD Docker
# image and CI dependency installer.
if not current_platform.is_rocm():
missing_media_tools = [
executable
for executable in ("ffmpeg", "ffprobe")
if shutil.which(executable) is None
]
if missing_media_tools:
raise RuntimeError(
"MiniMax H3 requires ffmpeg and ffprobe for media processing "
"and validated output delivery; missing executables: "
f"{', '.join(missing_media_tools)}. Install the ffmpeg system "
"package before starting SGLang."
)
super().__init__(*args, **kwargs)
@staticmethod
def model_subfolder_for_variant(variant: str) -> str:
if not isinstance(variant, str) or not variant.strip():
@@ -26,6 +26,7 @@ from sglang.multimodal_gen.configs.models.vaes.minimax_h3_video import (
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.packed_tokens import (
minimax_h3_patchify_video_latent,
)
from sglang.multimodal_gen.runtime.platforms import current_platform
MINIMAX_H3_KEYFRAME_ENCODE_SEED = 42
MINIMAX_H3_KEYFRAME_PATCH_SIZE = (1, 2, 2)
@@ -37,16 +38,37 @@ def minimax_h3_scoped_encode_rng(seed: int, device: torch.device | None = None):
The encode recipes seed the default torch generators right before a
posterior-sampled VAE encode. Forking restores the process-global CPU and
CUDA generators after the encode while preserving the exact sampled result.
device generators after the encode while preserving the exact sampled
result.
"""
devices: list[torch.device] = []
if device is not None and device.type == "cuda" and torch.cuda.is_available():
device_module = None
device_type = None
is_supported_backend = (
current_platform.is_cuda()
or current_platform.is_rocm()
or current_platform.is_npu()
)
if (
device is not None
and is_supported_backend
and device.type == current_platform.device_type
):
device_module = torch.get_device_module(device)
if device_module.is_available():
devices = [device]
with torch.random.fork_rng(devices=devices):
device_type = current_platform.device_type
fork_rng_context = (
torch.random.fork_rng(devices=devices)
if device_type is None
else torch.random.fork_rng(devices=devices, device_type=device_type)
)
with fork_rng_context:
torch.default_generator.manual_seed(int(seed))
for forked_device in devices:
with torch.cuda.device(forked_device):
torch.cuda.manual_seed(int(seed))
assert device_module is not None
with device_module.device(forked_device):
device_module.manual_seed(int(seed))
yield
@@ -29,7 +29,8 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages.validators import (
)
from sglang.multimodal_gen.runtime.server_args import ServerArgs
from sglang.multimodal_gen.runtime.utils.precision import (
autocast_enabled,
autocast_context,
autocast_enabled_for_device,
resolve_decode_precision,
resolve_precision,
)
@@ -293,9 +294,8 @@ class MiniMaxH3DecodingStage(DecodingStage):
audio_vae_dtype = resolve_precision(
server_args, "audio_vae", precision_attr="audio_vae_precision"
)
audio_autocast_enabled = (
audio_latent.device.type == "cuda"
and autocast_enabled(audio_vae_dtype, server_args.disable_autocast)
audio_autocast_enabled = autocast_enabled_for_device(
audio_latent, audio_vae_dtype, server_args.disable_autocast
)
autocast_context = (
torch.autocast(
@@ -352,22 +352,16 @@ class MiniMaxH3DecodingStage(DecodingStage):
name="video_vae",
)
video_vae_dtype = resolve_decode_precision(server_args, "video_vae")
visual_autocast_enabled = (
visual_latent.device.type == "cuda"
and autocast_enabled(video_vae_dtype, server_args.disable_autocast)
visual_autocast_enabled = autocast_enabled_for_device(
visual_latent, video_vae_dtype, server_args.disable_autocast
)
if visual_autocast_enabled:
selected_video_vae.prepare_decoder_autocast_weights(video_vae_dtype)
autocast_context = (
torch.autocast(
device_type="cuda",
dtype=video_vae_dtype,
with autocast_context(
video_vae_dtype,
server_args.disable_autocast,
enabled=visual_autocast_enabled,
)
if visual_latent.is_cuda
else nullcontext()
)
with autocast_context:
):
video_decode = self._get_vae_decode_fn(
selected_video_vae,
server_args,
@@ -620,8 +620,15 @@ class MiniMaxH3DenoisingStage(DenoisingStage):
ctx = _resolve_full_loop_context(batch)
if not (current_platform.is_cuda() or current_platform.is_mps()):
raise RuntimeError("MiniMax H3 full-loop denoise requires CUDA or MPS")
if not (
current_platform.is_cuda()
or current_platform.is_mps()
or current_platform.is_npu()
):
raise RuntimeError(
"MiniMax H3 full-loop denoise requires CUDA, MPS, or Ascend NPU"
)
device = current_platform.get_local_torch_device()
sigmas_video = [float(v) for v in ctx.sigmas["video"]]
self._maybe_enable_cache_dit_and_torch_compile(
@@ -188,6 +188,10 @@ class Platform:
"""Stateless version of :func:`torch.cuda.is_available`."""
return self._enum in (PlatformEnum.CUDA, PlatformEnum.ROCM, PlatformEnum.MUSA)
def is_device_type(self, device_type: str | None) -> bool:
"""Return whether a device type belongs to this platform."""
return device_type == self.device_type
@lru_cache(maxsize=1)
def is_mps(self) -> bool:
return self._enum == PlatformEnum.MPS
@@ -953,6 +953,23 @@ class ServerArgs(DisaggServerArgsMixin):
text_backend,
)
self.component_attention_backends["text_encoder"] = "torch_sdpa"
from sglang.multimodal_gen.configs.pipeline_configs.minimax_h3 import (
MiniMaxH3PipelineConfig,
)
if (
self.backend != Backend.DIFFUSERS
and isinstance(self.pipeline_config, MiniMaxH3PipelineConfig)
and self.attention_backend == "laser_attn"
and "text_encoder" not in self.component_attention_backends
):
# Laser Attention is used only by the MiniMax-H3 transformer.
# SDPA is faster than Ascend FA for its Qwen3-VL text encoder.
logger.info(
"Automatically set torch_sdpa backend for the MiniMax H3 text "
"encoder; laser_attn applies to the transformer"
)
self.component_attention_backends["text_encoder"] = "torch_sdpa"
if self.ring_degree > 1:
if (
@@ -107,6 +107,14 @@ def autocast_enabled(dtype: torch.dtype, disable_autocast: bool) -> bool:
)
def autocast_enabled_for_device(
tensor: torch.Tensor, dtype: torch.dtype, disable_autocast: bool
) -> bool:
return tensor.device.type == current_platform.device_type and autocast_enabled(
dtype, disable_autocast
)
def autocast_context(
dtype: torch.dtype,
disable_autocast: bool,
@@ -46,7 +46,7 @@ def _all_cases() -> list[DiffusionTestCase]:
def _baseline_path() -> Path:
import sglang.multimodal_gen.test.server.testcase_configs as cfg
return cfg.get_perf_baseline_path()
return cfg.get_perf_baseline_update_path()
def _openai_client(port: int) -> OpenAI:
File diff suppressed because it is too large Load Diff
@@ -27,6 +27,7 @@ JOYAI_IMAGE_EDIT_WEIGHTS_PATH = use_modelscope(
)
LTX_2_WEIGHTS_PATH = use_modelscope("Lightricks/LTX-2")
MOVA_360_WEIGHTS_PATH = use_modelscope("openmoss/MOVA-360p")
MINIMAX_H3_WEIGHTS_PATH = use_modelscope("MiniMax/MiniMax-H3")
QWEN_IMAGE_WEIGHTS_PATH = use_modelscope("Qwen/Qwen-Image")
WAN2_1_T2V_1_3B_DIFFUSERS_WEIGHTS_PATH = use_modelscope(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
@@ -145,7 +146,69 @@ TWO_NPU_CASES: list[DiffusionTestCase] = [
prompt=T2V_PROMPT,
),
),
# === Text+Image to Video+Audio (TI2V)
# === Text to Video+Audio (T2VA)
DiffusionTestCase(
"minimax_h3_t2va_2npu",
DiffusionServerArgs(
model_path=MINIMAX_H3_WEIGHTS_PATH,
modality="video",
num_gpus=2,
tp_size=2,
extras=[
"--model-variant",
"fl2va",
"--dit-cpu-offload",
"false",
"--sp-degree",
"1",
"--attention-backend",
"laser_attn",
"--component-residency",
"text_encoder=layerwise-offload",
],
env_vars={
"SGLANG_CACHE_DIT_ENABLED": "true",
"SGLANG_CACHE_DIT_FN": "2",
"SGLANG_CACHE_DIT_BN": "1",
"SGLANG_CACHE_DIT_WARMUP": "4",
"SGLANG_CACHE_DIT_RDT": "0.4",
"SGLANG_CACHE_DIT_MC": "4",
"SGLANG_CACHE_DIT_TAYLORSEER": "true",
"SGLANG_CACHE_DIT_TS_ORDER": "2",
"HCCL_BUFFSIZE": "256",
},
),
DiffusionSamplingParams(
prompt=(
"At night, while their owner sleeps in a bedroom, three cats "
"march in loudly playing tiny brass instruments, then abruptly "
"file out."
),
output_size="1344x768",
seconds=5,
output_format="mp4",
num_outputs_per_prompt=1,
extras={
"task": "t2va",
"conditions": [],
"target": {
"short_edge": 768,
"aspect_ratio": "16:9",
"duration_seconds": 5.0,
},
"num_inference_steps": 50,
"flow_shift": 12.0,
"audio_flow_shift": 3.0,
"seed": 1101,
},
),
run_perf_check=True,
run_consistency_check=True,
run_component_accuracy_check=False,
run_models_api_check=False,
run_t2v_input_reference_check=False,
),
# === Text+Image to Video+Audio (TI2VA)
DiffusionTestCase(
"ltx_2_ti2va_2npu",
DiffusionServerArgs(
@@ -45,7 +45,7 @@ from sglang.multimodal_gen.test.server.testcase_configs import (
PerformanceSummary,
ScenarioConfig,
get_model_task_type_for_server_args,
get_perf_baseline_path,
get_perf_baseline_update_path,
)
from sglang.multimodal_gen.test.test_utils import (
SGL_TEST_FILES_CI_DATA_REVISION,
@@ -246,7 +246,7 @@ def diffusion_server(case: DiffusionTestCase) -> ServerContext:
logger.error(
f'\n{"=" * 60}\n'
f'Add "estimated_full_test_time_s" to scenario "{case.id}":\n\n'
f"File: {get_perf_baseline_path()}\n\n"
f"File: {get_perf_baseline_update_path()}\n\n"
f' "{case.id}": {{\n'
f" ...\n"
f' "estimated_full_test_time_s": {_measured_full_time:.1f}\n'
@@ -445,7 +445,7 @@ class DiffusionServerBase:
self._dump_baseline_for_testcase(case, summary, missing_scenario)
if missing_scenario:
pytest.fail(
f"Testcase '{case.id}' not found in {get_perf_baseline_path()}"
f"Testcase '{case.id}' not found in {get_perf_baseline_update_path()}"
)
return
@@ -459,7 +459,7 @@ class DiffusionServerBase:
self._dump_baseline_for_testcase(case, summary, missing_scenario)
pytest.fail(
f"Testcase '{case.id}' is missing a load/runtime peak VRAM "
f"baseline in {get_perf_baseline_path()}"
f"baseline in {get_perf_baseline_update_path()}"
)
try:
validator.validate_peak_vram(
@@ -521,7 +521,9 @@ class DiffusionServerBase:
scenario = BASELINE_CONFIG.scenarios.get(case.id)
if scenario is None:
pytest.fail(f"Testcase '{case.id}' not found in {get_perf_baseline_path()}")
pytest.fail(
f"Testcase '{case.id}' not found in {get_perf_baseline_update_path()}"
)
validator = PerformanceValidator(
scenario=scenario,
@@ -544,7 +546,7 @@ class DiffusionServerBase:
if scenario.load_peak_vram_mb is None or scenario.runtime_peak_vram_mb is None:
pytest.fail(
f"Testcase '{case.id}' is missing a load/runtime peak VRAM "
f"baseline in {get_perf_baseline_path()}; measured "
f"baseline in {get_perf_baseline_update_path()}; measured "
f"load={summary.load_peak_vram_mb:.0f}MiB, "
f"runtime={summary.runtime_peak_vram_mb:.0f}MiB"
)
@@ -689,7 +691,7 @@ class DiffusionServerBase:
)
action = "add" if missing_scenario else "update"
output = f"""
{action} this baseline in the "scenarios" section of {get_perf_baseline_path()}:
{action} this baseline in the "scenarios" section of {get_perf_baseline_update_path()}:
"{case.id}": {json.dumps(baseline, indent=4)}
@@ -899,6 +899,14 @@ def get_perf_baseline_path(platform: str | None = None) -> Path:
return PERF_BASELINE_DIR / PERF_BASELINE_FILE_BY_PLATFORM[baseline_platform]
def get_perf_baseline_update_path() -> Path:
if current_platform.is_npu():
return Path(__file__).parent / "ascend" / "perf_baselines_npu.json"
if current_platform.is_musa():
return Path(__file__).parent / "musa" / "perf_baselines_musa.json"
return get_perf_baseline_path()
def _make_modelopt_ci_case(
case_id: str,
*,
@@ -45,7 +45,7 @@ SGL_TEST_FILES_CI_DATA_REVISION = "15b30030ef980756788ab40072f9223fe21a5526"
# The NPU pin is kept as a separate branch so ascend GT can be bumped independently
# when it's regenerated on its own cadence.
if current_platform.is_npu():
SGL_TEST_FILES_CI_DATA_REVISION = "8e3d717e65fb87339c2974382a092a731669f884"
SGL_TEST_FILES_CI_DATA_REVISION = "7df858ead07940ff4d9489230fa9f040dd186789"
SGL_TEST_FILES_CONSISTENCY_GT_ROOT = (
"https://raw.githubusercontent.com/"
@@ -13,11 +13,21 @@ import torch
from sglang.multimodal_gen.runtime.managers.forward_context import get_forward_context
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3 import (
keyframe_encoding,
material_io,
reference_encoding,
)
def test_keyframe_rng_supports_cpu_and_default_device():
initial_state = torch.random.get_rng_state()
for device in (None, torch.device("cpu")):
with keyframe_encoding.minimax_h3_scoped_encode_rng(42, device):
assert torch.initial_seed() == 42
torch.testing.assert_close(torch.random.get_rng_state(), initial_state)
def test_ffprobe_falls_back_when_stream_side_data_is_unknown(monkeypatch):
material_io._ffprobe_entries = None
calls = []
@@ -19,6 +19,7 @@ apt update -y && apt install -y \
clang \
locales \
ccache \
ffmpeg \
libgl1-mesa-glx \
libgl1-mesa-dri \
ca-certificates \