[Diffusion][MiniMax-H3] Add SM90 Sage compute for SubBlock sparse attention (#37982)
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.attention.subblock_sage_fp8_sm90 import (
|
||||
_load_sparge_attention_sm90_ops,
|
||||
_routing_plan_to_block_map,
|
||||
)
|
||||
from sglang.multimodal_gen.configs.pipeline_configs.minimax_h3 import (
|
||||
MiniMaxH3PipelineConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.attention.backends.subblock_sparse_attn import (
|
||||
SubBlockSparseAttentionImpl,
|
||||
_get_subblock_sparse_attention_runner,
|
||||
_sm90_sage_fp8_sparse_attention,
|
||||
_sm90_sparse_attention,
|
||||
_sm100_sparse_attention,
|
||||
_sm120_sparse_attention,
|
||||
@@ -40,12 +46,87 @@ from sglang.multimodal_gen.runtime.platforms import (
|
||||
from sglang.multimodal_gen.runtime.platforms.cuda import (
|
||||
_SubBlockSparseAttentionBackendResolver,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.platforms.interface import DeviceCapability
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cpu_ci(est_time=9, suite="stage-a-test-cpu-intel")
|
||||
|
||||
|
||||
class TestSubBlockSageFp8PlanAdapter(CustomTestCase):
|
||||
def test_variable_counts_ignore_each_row_suffix(self):
|
||||
index = torch.tensor([[[[2, 0, 1, 3], [3, 1, 0, 2]]]], dtype=torch.int32)
|
||||
counts = torch.tensor([[[3, 1]]], dtype=torch.int32)
|
||||
|
||||
block_map = _routing_plan_to_block_map(index, counts, 4)
|
||||
|
||||
torch.testing.assert_close(
|
||||
block_map,
|
||||
torch.tensor([[[[True, True, True, False], [False, False, False, True]]]]),
|
||||
)
|
||||
|
||||
|
||||
class TestSubBlockSageFp8DependencyLoader(CustomTestCase):
|
||||
def setUp(self):
|
||||
_load_sparge_attention_sm90_ops.cache_clear()
|
||||
self.addCleanup(_load_sparge_attention_sm90_ops.cache_clear)
|
||||
|
||||
def test_missing_dependency_has_install_help(self):
|
||||
missing_modules = {
|
||||
name: None
|
||||
for name in (
|
||||
"spas_sage_attn",
|
||||
"spas_sage_attn._fused",
|
||||
"spas_sage_attn._qattn",
|
||||
"spas_sage_attn.utils",
|
||||
)
|
||||
}
|
||||
with (
|
||||
patch.dict(sys.modules, missing_modules),
|
||||
self.assertRaisesRegex(ImportError, "pip install.*SpargeAttn"),
|
||||
):
|
||||
_load_sparge_attention_sm90_ops()
|
||||
|
||||
def test_complete_dependency_exposes_required_ops(self):
|
||||
package = ModuleType("spas_sage_attn")
|
||||
package.__path__ = []
|
||||
fused = ModuleType("spas_sage_attn._fused")
|
||||
qattn = ModuleType("spas_sage_attn._qattn")
|
||||
utils = ModuleType("spas_sage_attn.utils")
|
||||
fused.transpose_pad_permute_cuda = Mock()
|
||||
fused.scale_fuse_quant_cuda = Mock()
|
||||
qattn.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_sm90 = (
|
||||
Mock()
|
||||
)
|
||||
utils.block_map_lut_triton = Mock()
|
||||
utils.get_vanilla_qk_quant = Mock()
|
||||
package._fused = fused
|
||||
package._qattn = qattn
|
||||
package.utils = utils
|
||||
|
||||
with patch.dict(
|
||||
sys.modules,
|
||||
{
|
||||
"spas_sage_attn": package,
|
||||
"spas_sage_attn._fused": fused,
|
||||
"spas_sage_attn._qattn": qattn,
|
||||
"spas_sage_attn.utils": utils,
|
||||
},
|
||||
):
|
||||
ops = _load_sparge_attention_sm90_ops()
|
||||
|
||||
self.assertEqual(
|
||||
ops,
|
||||
(
|
||||
utils.get_vanilla_qk_quant,
|
||||
utils.block_map_lut_triton,
|
||||
fused.transpose_pad_permute_cuda,
|
||||
fused.scale_fuse_quant_cuda,
|
||||
qattn.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_sm90,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestSubBlockSparseAttentionDispatch(CustomTestCase):
|
||||
def setUp(self):
|
||||
_get_subblock_sparse_attention_runner.cache_clear()
|
||||
@@ -70,6 +151,23 @@ class TestSubBlockSparseAttentionDispatch(CustomTestCase):
|
||||
|
||||
self.assertIs(runner, _sm100_sparse_attention)
|
||||
|
||||
def test_dispatches_sm90_sage_fp8_independently_from_bf16(self):
|
||||
device = torch.device("cuda:0")
|
||||
with patch("torch.cuda.get_device_capability", return_value=(9, 0)):
|
||||
bf16_runner = _get_subblock_sparse_attention_runner(device, "bf16")
|
||||
sage_runner = _get_subblock_sparse_attention_runner(device, "sage_fp8")
|
||||
|
||||
self.assertIs(bf16_runner, _sm90_sparse_attention)
|
||||
self.assertIs(sage_runner, _sm90_sage_fp8_sparse_attention)
|
||||
|
||||
def test_rejects_sm90_sage_fp8_on_sm100_until_adapter_is_wired(self):
|
||||
device = torch.device("cuda:0")
|
||||
with (
|
||||
patch("torch.cuda.get_device_capability", return_value=(10, 0)),
|
||||
self.assertRaisesRegex(RuntimeError, "currently targets SM90"),
|
||||
):
|
||||
_get_subblock_sparse_attention_runner(device, "sage_fp8")
|
||||
|
||||
def test_dispatches_sm120(self):
|
||||
device = torch.device("cuda:0")
|
||||
with patch("torch.cuda.get_device_capability", return_value=(12, 0)):
|
||||
@@ -134,6 +232,14 @@ class TestSubBlockSparseAttentionDispatch(CustomTestCase):
|
||||
self.assertIs(kwargs["q2k_block_nums"], block_counts)
|
||||
self.assertEqual(kwargs["softmax_scale"], 0.125)
|
||||
|
||||
def test_rejects_sage_fp8_on_sm120_until_adapter_is_wired(self):
|
||||
device = torch.device("cuda:0")
|
||||
with (
|
||||
patch("torch.cuda.get_device_capability", return_value=(12, 0)),
|
||||
self.assertRaisesRegex(RuntimeError, "currently targets SM90"),
|
||||
):
|
||||
_get_subblock_sparse_attention_runner(device, "sage_fp8")
|
||||
|
||||
def test_rejects_unsupported_compute_capability(self):
|
||||
device = torch.device("cuda:0")
|
||||
with patch("torch.cuda.get_device_capability", return_value=(10, 3)):
|
||||
@@ -145,6 +251,128 @@ class TestSubBlockSparseAttentionDispatch(CustomTestCase):
|
||||
|
||||
|
||||
class TestSubBlockSparseAttentionModalities(CustomTestCase):
|
||||
@staticmethod
|
||||
def _subblock_server_args(compute_mode: str):
|
||||
return SimpleNamespace(
|
||||
attention_backend="subblock_sparse_attn",
|
||||
attention_backend_config={"compute_mode": compute_mode},
|
||||
ring_degree=1,
|
||||
resolve_component_attention_backend=lambda *_names: (
|
||||
AttentionBackendEnum.SUBBLOCK_SPARSE_ATTN,
|
||||
"transformer",
|
||||
),
|
||||
)
|
||||
|
||||
def test_sage_fp8_dependency_is_checked_during_server_validation(self):
|
||||
config = MiniMaxH3PipelineConfig()
|
||||
server_args = self._subblock_server_args("sage_fp8")
|
||||
loader = Mock()
|
||||
with (
|
||||
patch.object(current_platform, "is_mps", return_value=False),
|
||||
patch.object(
|
||||
current_platform,
|
||||
"get_device_capability",
|
||||
return_value=DeviceCapability(9, 0),
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3."
|
||||
"get_global_forced_attn_backend",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"sglang.kernels.ops.attention.subblock_sage_fp8_sm90."
|
||||
"_load_sparge_attention_sm90_ops",
|
||||
loader,
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3."
|
||||
"get_attn_backend"
|
||||
),
|
||||
):
|
||||
config.validate_server_args(server_args)
|
||||
|
||||
loader.assert_called_once_with()
|
||||
|
||||
def test_missing_sage_fp8_dependency_fails_server_validation(self):
|
||||
config = MiniMaxH3PipelineConfig()
|
||||
server_args = self._subblock_server_args("sage_fp8")
|
||||
with (
|
||||
patch.object(current_platform, "is_mps", return_value=False),
|
||||
patch.object(
|
||||
current_platform,
|
||||
"get_device_capability",
|
||||
return_value=DeviceCapability(9, 0),
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3."
|
||||
"get_global_forced_attn_backend",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"sglang.kernels.ops.attention.subblock_sage_fp8_sm90."
|
||||
"_load_sparge_attention_sm90_ops",
|
||||
side_effect=ImportError("Install SpargeAttention"),
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3."
|
||||
"get_attn_backend"
|
||||
) as get_backend,
|
||||
self.assertRaisesRegex(ImportError, "Install SpargeAttention"),
|
||||
):
|
||||
config.validate_server_args(server_args)
|
||||
|
||||
get_backend.assert_not_called()
|
||||
|
||||
def test_bf16_does_not_require_sparge_attention(self):
|
||||
config = MiniMaxH3PipelineConfig()
|
||||
server_args = self._subblock_server_args("bf16")
|
||||
loader = Mock()
|
||||
with (
|
||||
patch.object(current_platform, "is_mps", return_value=False),
|
||||
patch(
|
||||
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3."
|
||||
"get_global_forced_attn_backend",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"sglang.kernels.ops.attention.subblock_sage_fp8_sm90."
|
||||
"_load_sparge_attention_sm90_ops",
|
||||
loader,
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3."
|
||||
"get_attn_backend"
|
||||
),
|
||||
):
|
||||
config.validate_server_args(server_args)
|
||||
|
||||
loader.assert_not_called()
|
||||
|
||||
def test_sage_fp8_rejects_non_sm90_during_server_validation(self):
|
||||
config = MiniMaxH3PipelineConfig()
|
||||
server_args = self._subblock_server_args("sage_fp8")
|
||||
with (
|
||||
patch.object(current_platform, "is_mps", return_value=False),
|
||||
patch.object(
|
||||
current_platform,
|
||||
"get_device_capability",
|
||||
return_value=DeviceCapability(10, 0),
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3."
|
||||
"get_global_forced_attn_backend",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"sglang.multimodal_gen.configs.pipeline_configs.minimax_h3."
|
||||
"get_attn_backend"
|
||||
) as get_backend,
|
||||
self.assertRaisesRegex(ValueError, "requires SM90.*found 10.0"),
|
||||
):
|
||||
config.validate_server_args(server_args)
|
||||
|
||||
get_backend.assert_not_called()
|
||||
|
||||
def test_transformer_subblock_with_ring_fails_admission(self):
|
||||
config = MiniMaxH3PipelineConfig()
|
||||
server_args = SimpleNamespace(
|
||||
@@ -400,7 +628,7 @@ class TestSubBlockSparseAttentionModalities(CustomTestCase):
|
||||
impl = object.__new__(SubBlockSparseAttentionImpl)
|
||||
impl.softmax_scale = 2**-0.5
|
||||
impl.causal = False
|
||||
impl.schedule = SimpleNamespace(sparsity=0.75)
|
||||
impl.schedule = SimpleNamespace(sparsity=0.75, compute_mode="bf16")
|
||||
plan = SimpleNamespace(
|
||||
index=torch.tensor(
|
||||
[[[[7, 1, 4], [6, 2, 0], [5, 0, 3]]]], dtype=torch.int32
|
||||
@@ -419,6 +647,7 @@ class TestSubBlockSparseAttentionModalities(CustomTestCase):
|
||||
|
||||
for runner, sparse_rows in (
|
||||
(_sm90_sparse_attention, ([1, 4, 7], [0, 3, 5])),
|
||||
(_sm90_sage_fp8_sparse_attention, ([7, 1, 4], [5, 0, 3])),
|
||||
(_sm100_sparse_attention, ([7, 1, 4], [5, 0, 3])),
|
||||
(_sm120_sparse_attention, ([7, 1, 4], [5, 0, 3])),
|
||||
):
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Correctness tests for native SM90 SubBlock Sage FP8 attention."""
|
||||
|
||||
import math
|
||||
import unittest
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
from sglang.test.test_utils import CustomTestCase
|
||||
|
||||
register_cuda_ci(est_time=45, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
|
||||
def _native_sm90_sage_available() -> bool:
|
||||
try:
|
||||
import spas_sage_attn._qattn as qattn
|
||||
except (ImportError, OSError):
|
||||
return False
|
||||
return hasattr(
|
||||
qattn,
|
||||
"qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_sm90",
|
||||
)
|
||||
|
||||
|
||||
requires_native_sm90_sage = unittest.skipUnless(
|
||||
torch.cuda.is_available()
|
||||
and torch.cuda.get_device_capability() == (9, 0)
|
||||
and _native_sm90_sage_available(),
|
||||
"requires SM90 and a compiled SpargeAttention installation",
|
||||
)
|
||||
|
||||
|
||||
def _masked_reference(q, k, v, index, counts, scale, key_block_size=128):
|
||||
logits = torch.einsum("bqhd,bkhd->bhqk", q.float(), k.float()) * scale
|
||||
mask = torch.zeros_like(logits, dtype=torch.bool)
|
||||
for b in range(q.shape[0]):
|
||||
for h in range(q.shape[2]):
|
||||
for qb in range(index.shape[2]):
|
||||
q_slice = slice(qb * 64, min((qb + 1) * 64, q.shape[1]))
|
||||
for slot in range(int(counts[b, h, qb])):
|
||||
kb = int(index[b, h, qb, slot])
|
||||
k_slice = slice(
|
||||
kb * key_block_size,
|
||||
min((kb + 1) * key_block_size, k.shape[1]),
|
||||
)
|
||||
mask[b, h, q_slice, k_slice] = True
|
||||
logits.masked_fill_(~mask, -float("inf"))
|
||||
p = torch.softmax(logits, dim=-1)
|
||||
return torch.einsum("bhqk,bkhd->bqhd", p, v.float()).to(torch.bfloat16)
|
||||
|
||||
|
||||
def _cosine(a, b):
|
||||
return float(
|
||||
torch.nn.functional.cosine_similarity(
|
||||
a.float().flatten(), b.float().flatten(), dim=0
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@requires_native_sm90_sage
|
||||
class TestSubBlockSageFp8NativeSm90(CustomTestCase):
|
||||
def test_full_budget_ragged_tail_reproduces_dense(self):
|
||||
from sglang.kernels.ops.attention.subblock_sage_fp8_sm90 import (
|
||||
subblock_sage_fp8_sm90_attention,
|
||||
)
|
||||
|
||||
torch.manual_seed(19)
|
||||
seq_len = 1024 + 37
|
||||
heads = 4
|
||||
shape = (1, seq_len, heads, 128)
|
||||
q = torch.randn(shape, device="cuda", dtype=torch.bfloat16)
|
||||
k = torch.randn_like(q)
|
||||
v = torch.randn_like(q)
|
||||
scale = 1.0 / math.sqrt(128)
|
||||
|
||||
query_blocks = math.ceil(seq_len / 64)
|
||||
key_blocks = math.ceil(seq_len / 128)
|
||||
index = (
|
||||
torch.arange(key_blocks, device="cuda", dtype=torch.int32)
|
||||
.view(1, 1, 1, key_blocks)
|
||||
.expand(1, heads, query_blocks, key_blocks)
|
||||
.contiguous()
|
||||
)
|
||||
output = subblock_sage_fp8_sm90_attention(q, k, v, index, key_blocks, scale)
|
||||
reference = torch.nn.functional.scaled_dot_product_attention(
|
||||
q.transpose(1, 2),
|
||||
k.transpose(1, 2),
|
||||
v.transpose(1, 2),
|
||||
scale=scale,
|
||||
).transpose(1, 2)
|
||||
|
||||
self.assertTrue(torch.isfinite(output.float()).all())
|
||||
self.assertGreater(_cosine(output, reference), 0.998)
|
||||
|
||||
def test_sparse_k128_plan_and_variable_counts(self):
|
||||
from sglang.kernels.ops.attention.subblock_sage_fp8_sm90 import (
|
||||
subblock_sage_fp8_sm90_attention,
|
||||
)
|
||||
|
||||
torch.manual_seed(23)
|
||||
seq_len = 1024 + 37
|
||||
heads = 4
|
||||
shape = (1, seq_len, heads, 128)
|
||||
q = torch.randn(shape, device="cuda", dtype=torch.bfloat16)
|
||||
k = torch.randn_like(q)
|
||||
v = torch.randn_like(q)
|
||||
scale = 1.0 / math.sqrt(128)
|
||||
|
||||
query_blocks = math.ceil(seq_len / 64)
|
||||
key_blocks = math.ceil(seq_len / 128)
|
||||
width = 4
|
||||
index = torch.stack(
|
||||
[
|
||||
torch.roll(torch.arange(key_blocks), shifts=query_block)[:width]
|
||||
for query_block in range(query_blocks)
|
||||
]
|
||||
)
|
||||
index = (
|
||||
index.to(device="cuda", dtype=torch.int32)
|
||||
.view(1, 1, query_blocks, width)
|
||||
.expand(1, heads, query_blocks, width)
|
||||
.contiguous()
|
||||
)
|
||||
counts = (
|
||||
((torch.arange(query_blocks, device="cuda", dtype=torch.int32) % width) + 1)
|
||||
.view(1, 1, query_blocks)
|
||||
.expand(1, heads, query_blocks)
|
||||
.contiguous()
|
||||
)
|
||||
|
||||
output = subblock_sage_fp8_sm90_attention(q, k, v, index, width, scale, counts)
|
||||
reference = _masked_reference(q, k, v, index, counts, scale)
|
||||
|
||||
self.assertTrue(torch.isfinite(output.float()).all())
|
||||
self.assertGreater(_cosine(output, reference), 0.997)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=3)
|
||||
Reference in New Issue
Block a user