[Bugfix] Route Music3 float32 attention to SDPA (#7354)

Signed-off-by: liuyao0322 <yaoliu548926@gmail.com>
This commit is contained in:
leo authored and GitHub committed 2026-09-15 18:08:06 -07:00
1 parent 5527f3f407
commit 45a3e77351
2 files changed
+150 -14

No files matched your search

@@ -0,0 +1,117 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Attention dtype routing for MiniMax Music 3's float32 acoustic stage."""
from __future__ import annotations
import pytest
import torch
from torch import nn
from vllm_omni.model_executor.models.minimax_music3 import dit
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
_FLOAT32_INCOMPATIBLE_BACKENDS = [
"FLASH_ATTN",
"FLASH_ATTN_HUB",
"FLASH_ATTN_3_HUB",
"CUDNN_ATTN",
"FLASHINFER_ATTN",
]
class _BackendType:
def __init__(self, name: str) -> None:
self.name = name
def get_name(self) -> str:
return self.name
class _Backend(nn.Module):
def __init__(self, *, name: str, explicit: bool) -> None:
super().__init__()
self.attn_backend = _BackendType(name)
self.backend_explicit = explicit
self.calls = 0
def forward(self, q, k, v):
del k, v
self.calls += 1
return torch.full_like(q, 7)
def _attention(monkeypatch, *, backend_name: str, explicit: bool):
backend = _Backend(name=backend_name, explicit=explicit)
monkeypatch.setattr(dit, "_build_native_attention", lambda **_kwargs: backend)
return dit.Attention(8, head_dim=4), backend
@pytest.mark.parametrize(
"backend_name",
_FLOAT32_INCOMPATIBLE_BACKENDS,
)
def test_float32_uses_sdpa_for_automatic_incompatible_backend(monkeypatch, backend_name):
attention, backend = _attention(monkeypatch, backend_name=backend_name, explicit=False)
q = torch.randn(2, 5, 2, 4)
actual = attention._attend(q, q, q)
expected = torch.nn.functional.scaled_dot_product_attention(
q.transpose(1, 2),
q.transpose(1, 2),
q.transpose(1, 2),
is_causal=False,
scale=attention.softmax_scale,
).transpose(1, 2)
torch.testing.assert_close(actual, expected)
assert backend.calls == 0
assert attention.backend_name == f"TORCH_SDPA(float32)/{backend_name}(lower precision)"
@pytest.mark.parametrize("backend_name", _FLOAT32_INCOMPATIBLE_BACKENDS)
@pytest.mark.parametrize(
("dtype", "explicit"),
[
(torch.float16, False),
(torch.bfloat16, False),
(torch.float32, True),
],
)
def test_lower_precision_or_explicit_backend_remains_authoritative(monkeypatch, backend_name, dtype, explicit):
attention, backend = _attention(monkeypatch, backend_name=backend_name, explicit=explicit)
q = torch.zeros(1, 3, 2, 4, dtype=dtype)
actual = attention._attend(q, q, q)
assert backend.calls == 1
torch.testing.assert_close(actual, torch.full_like(q, 7))
def test_float32_uses_sdpa_when_native_attention_is_unavailable(monkeypatch):
monkeypatch.setattr(dit, "_build_native_attention", lambda **_kwargs: None)
attention = dit.Attention(8, head_dim=4)
q = torch.randn(1, 3, 2, 4)
actual = attention._attend(q, q, q)
expected = torch.nn.functional.scaled_dot_product_attention(
q.transpose(1, 2),
q.transpose(1, 2),
q.transpose(1, 2),
is_causal=False,
scale=attention.softmax_scale,
).transpose(1, 2)
torch.testing.assert_close(actual, expected)
assert attention.backend_name == "TORCH_SDPA"
def test_float32_keeps_compatible_automatic_backend(monkeypatch):
attention, backend = _attention(monkeypatch, backend_name="SDPA", explicit=False)
q = torch.zeros(1, 3, 2, 4)
actual = attention._attend(q, q, q)
assert backend.calls == 1
torch.testing.assert_close(actual, torch.full_like(q, 7))
@@ -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
"""Condition encoder and flow-matching transformer for MiniMax Music 3.
Stage 1 turns the AR stage's conditioning frames into a DAC latent in two
@@ -16,13 +16,13 @@ a single ``to_qkv`` so each block runs one GEMM instead of three.
:func:`remap_transformer_state` performs that fusion, and is the only place
the checkpoint's layout is reinterpreted.
Attention runs through the project's own diffusion attention layer, so this
model picks up the same backend selection, sequence-parallel plumbing and
KV-quant policy as every other transformer in the tree. That layer resolves
its backend from the diffusion config, which an ``LLM_GENERATION`` stage such
as this one does not set; it tolerates that and falls through to the platform
default. If it cannot be built at all the block runs plain SDPA instead, a
choice made once when the blocks are built and never inside ``forward``.
Attention uses the project's diffusion attention layer when its selected
backend supports the runtime dtype. This ``LLM_GENERATION`` stage does not set
a diffusion backend and therefore gets the platform default. CUDA commonly
resolves that default to FlashAttention, cuDNN, or FlashInfer, so the model
routes its native float32 decode through SDPA while retaining the selected
backend for supported lower-precision inputs. An explicitly selected backend
remains authoritative.
"""
from __future__ import annotations
@@ -75,6 +75,19 @@ _TRANSFORMER_IN_DIM = DIT_LATENT_CHANNELS * 2 + _CONDITION_DIM
# reaches exactly zero, so the noise term never fully vanishes.
_SIGMA_FLOOR = 1e-6
# These backends reject float32 Q/K/V. MiniMax Music 3 decodes in float32, so
# automatically selected incompatible backends must not receive its attention
# tensors. This includes cuDNN and FlashInfer selected on Blackwell. Explicit
# backend choices retain the shared layer's fail-fast contract instead of
# being silently replaced here.
_FLOAT32_UNSUPPORTED_BACKENDS = {
"FLASH_ATTN",
"FLASH_ATTN_HUB",
"FLASH_ATTN_3_HUB",
"CUDNN_ATTN",
"FLASHINFER_ATTN",
}
class FourierFeatures(nn.Module):
"""Random-Fourier timestep features: ``[B, 1] -> [B, out_features]``."""
@@ -149,10 +162,7 @@ def _build_native_attention(*, num_heads: int, head_size: int, softmax_scale: fl
except Exception as exc: # noqa: BLE001 - any failure means: use SDPA
_NATIVE_ATTENTION_UNAVAILABLE = repr(exc)
logger.warning(
"MiniMax Music 3 DiT could not build the diffusion attention layer "
"(%s); running torch SDPA instead. The stage decodes in float32, "
"for which the diffusion layer dispatches to SDPA anyway, so this "
"changes performance bookkeeping and not the audio.",
"MiniMax Music 3 DiT could not build the diffusion attention layer (%s); running torch SDPA instead.",
exc,
)
return None
@@ -180,10 +190,19 @@ class Attention(nn.Module):
softmax_scale=self.softmax_scale,
prefix=prefix,
)
self.backend_name = self.backend.attn_backend.get_name() if self.backend is not None else "TORCH_SDPA"
native_backend_name = self.backend.attn_backend.get_name() if self.backend is not None else None
self._auto_float32_sdpa = (
self.backend is not None
and not getattr(self.backend, "backend_explicit", False)
and native_backend_name in _FLOAT32_UNSUPPORTED_BACKENDS
)
if self._auto_float32_sdpa:
self.backend_name = f"TORCH_SDPA(float32)/{native_backend_name}(lower precision)"
else:
self.backend_name = native_backend_name or "TORCH_SDPA"
def _attend(self, q: Tensor, k: Tensor, v: Tensor) -> Tensor:
if self.backend is not None:
if self.backend is not None and not (q.dtype == torch.float32 and self._auto_float32_sdpa):
return self.backend(q, k, v)
q, k, v = (t.transpose(1, 2) for t in (q, k, v))
out = F.scaled_dot_product_attention(q, k, v, is_causal=False, scale=self.softmax_scale)