[NPU] [Diffusion] Support MiniMax H3 on Ascend NPU's (#33569)
Co-authored-by: ronnie_zheng <zl19940307@163.com>
This commit is contained in:
co-authored by
ronnie_zheng
parent
b7f9fca26e
commit
61b67316d8
@@ -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():
|
||||
|
||||
+27
-5
@@ -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
|
||||
|
||||
|
||||
|
||||
+10
-16
@@ -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,
|
||||
|
||||
+9
-2
@@ -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 \
|
||||
|
||||
Reference in New Issue
Block a user