# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import pytest import torch import vllm.model_executor.kernels.mhc # noqa: F401 from vllm.model_executor.kernels.mhc.tilelang import ( _tilelang_hc_prenorm_gemm, _torch_hc_prenorm_gemm, ) from vllm.model_executor.layers.mhc import HAS_TILELANG_MHC from vllm.platforms import current_platform from vllm.utils.torch_utils import set_random_seed DEVICE = current_platform.device_type def sinkhorn_normalize_ref(x: torch.Tensor, repeat: int, eps: float) -> torch.Tensor: x = x.softmax(-1) + eps x = x / (x.sum(-2, keepdim=True) + eps) for _ in range(repeat - 1): x = x / (x.sum(-1, keepdim=True) + eps) x = x / (x.sum(-2, keepdim=True) + eps) return x def mhc_pre_ref( residual: torch.Tensor, fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, rms_eps: float, hc_pre_eps: float, hc_sinkhorn_eps: float, hc_post_mult_value: float, sinkhorn_repeat: int, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: """mHC pre reference kernel from tilelang repo: https://github.com/tile-ai/tilelang/blob/d135bd1cd2d2eee74fbb41dd0a0831a427194c86/examples/deepseek_mhc/example_mhc_pre.py#L303""" hc_mult = residual.shape[-2] residual_flat = residual.flatten(-2, -1).float() sqrsum = residual_flat.square().sum(-1) mixes = ( residual_flat @ fn.T * (sqrsum.unsqueeze(-1) / fn.shape[-1] + rms_eps).rsqrt() ) hc_scale = torch.cat( [ hc_scale[0].expand(hc_mult), hc_scale[1].expand(hc_mult), hc_scale[2].expand(hc_mult * hc_mult), ], ) mixes = mixes * hc_scale + hc_base pre_mix = mixes[:, :hc_mult].sigmoid().unsqueeze(-1) + hc_pre_eps post_mix = ( mixes[:, hc_mult : 2 * hc_mult].sigmoid() * hc_post_mult_value ).unsqueeze(-1) res_mix = mixes[:, 2 * hc_mult :].view(-1, hc_mult, hc_mult) res_mix = sinkhorn_normalize_ref( res_mix, repeat=sinkhorn_repeat, eps=hc_sinkhorn_eps ) layer_input = (residual * pre_mix).sum(-2).bfloat16() return post_mix, res_mix, layer_input def mhc_post_ref( x: torch.Tensor, residual: torch.Tensor, post_layer_mix: torch.Tensor, comb_res_mix: torch.Tensor, ) -> torch.Tensor: """mHC post reference kernel from tilelang repo: https://github.com/tile-ai/tilelang/blob/d135bd1cd2d2eee74fbb41dd0a0831a427194c86/examples/deepseek_mhc/example_mhc_post.py#L68""" term2 = torch.bmm(comb_res_mix.mT, residual.float()) return (x.float().unsqueeze(-2) * post_layer_mix + term2).bfloat16() def hc_head_ref( residual: torch.Tensor, fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, rms_eps: float, hc_eps: float, ) -> torch.Tensor: residual_flat = residual.flatten(-2).float() residual_norm = residual_flat * torch.rsqrt( residual_flat.square().mean(dim=-1, keepdim=True) + rms_eps ) pre_mix = torch.nn.functional.linear(residual_norm, fn) pre_mix = torch.sigmoid(pre_mix * hc_scale + hc_base) + hc_eps return torch.sum(pre_mix.unsqueeze(-1) * residual.float(), dim=-2).bfloat16() @pytest.mark.skipif( not HAS_TILELANG_MHC, reason="TileLang MHC support required", ) @pytest.mark.parametrize("num_tokens", [1, 4, 8, 128]) @pytest.mark.parametrize("hidden_size", [4096, 7168]) @pytest.mark.parametrize("hc_mult", [4]) def test_mhc_pre_tilelang(num_tokens, hidden_size, hc_mult): torch.set_default_device(DEVICE) set_random_seed(0) residual = torch.randn((num_tokens, hc_mult, hidden_size), dtype=torch.bfloat16) hc_mult2 = hc_mult * hc_mult hc_mult3 = 2 * hc_mult + hc_mult2 fn = ( torch.randn((hc_mult3, hc_mult, hidden_size), dtype=torch.float) * 1e-4 * (1 + torch.arange(hc_mult).mul(0.01).view(1, -1, 1)) ).flatten(1, 2) hc_scale = torch.randn((3,), dtype=torch.float) * 0.1 hc_base = torch.randn((hc_mult3,), dtype=torch.float) * 0.1 hc_sinkhorn_eps = hc_pre_eps = rms_eps = 1e-6 sinkhorn_repeat = 20 hc_post_alpha = 1.0 ref = mhc_pre_ref( residual, fn, hc_scale, hc_base, rms_eps, hc_pre_eps, hc_sinkhorn_eps, hc_post_alpha, sinkhorn_repeat, ) out = torch.ops.vllm.mhc_pre_tilelang( residual, fn, hc_scale, hc_base, rms_eps, hc_pre_eps, hc_sinkhorn_eps, hc_post_alpha, sinkhorn_repeat, ) for actual, expected in zip(out, ref, strict=True): torch.testing.assert_close(actual, expected, atol=5e-2, rtol=1e-2) @pytest.mark.skipif( not HAS_TILELANG_MHC, reason="TileLang MHC support required", ) @pytest.mark.parametrize( ("num_tokens", "hidden_size"), [ (1, 1280), (512, 1280), (2048, 1280), (1, 4096), (64, 4096), (512, 4096), (2048, 4096), (1, 7168), (64, 7168), (512, 7168), (2048, 7168), ], ) def test_hc_prenorm_gemm_tilelang(num_tokens, hidden_size): torch.set_default_device(DEVICE) set_random_seed(0) hc_mult = 4 hc_mult3 = 2 * hc_mult + hc_mult * hc_mult x = torch.randn((num_tokens, hc_mult * hidden_size), dtype=torch.bfloat16) fn = torch.randn((hc_mult3, hc_mult * hidden_size), dtype=torch.float32) * 1e-4 out_ref = torch.empty((1, num_tokens, hc_mult3), dtype=torch.float32) sqrsum_ref = torch.empty((1, num_tokens), dtype=torch.float32) out = torch.empty_like(out_ref) sqrsum = torch.empty_like(sqrsum_ref) _torch_hc_prenorm_gemm(x, fn, out_ref, sqrsum_ref) _tilelang_hc_prenorm_gemm(x, fn, out, sqrsum, hidden_size, hc_mult) torch.testing.assert_close(out, out_ref, atol=1e-5, rtol=1e-4) torch.testing.assert_close(sqrsum, sqrsum_ref, atol=8.0, rtol=5e-4) @pytest.mark.skipif( not HAS_TILELANG_MHC, reason="TileLang MHC support required", ) @pytest.mark.parametrize("num_tokens", [1, 4, 8, 128]) @pytest.mark.parametrize("hidden_size", [4096, 7168]) @pytest.mark.parametrize("hc_mult", [4]) def test_mhc_post_tilelang(num_tokens, hidden_size, hc_mult): torch.set_default_device(DEVICE) set_random_seed(0) x = torch.randn((num_tokens, hidden_size), dtype=torch.bfloat16) residual = torch.randn((num_tokens, hc_mult, hidden_size), dtype=torch.bfloat16) post_layer_mix = torch.randn((num_tokens, hc_mult, 1), dtype=torch.float32) comb_res_mix = torch.randn((num_tokens, hc_mult, hc_mult), dtype=torch.float32) ref = mhc_post_ref(x, residual, post_layer_mix, comb_res_mix) out = torch.ops.vllm.mhc_post_tilelang( x, residual, post_layer_mix, comb_res_mix, ) torch.testing.assert_close(out, ref, atol=5e-2, rtol=1e-2) @pytest.mark.skipif( not HAS_TILELANG_MHC, reason="TileLang MHC support required", ) @pytest.mark.parametrize("num_tokens", [1, 4, 8, 128]) @pytest.mark.parametrize("hidden_size", [4096, 7168]) @pytest.mark.parametrize("hc_mult", [4]) def test_mhc_fused_post_pre(num_tokens, hidden_size, hc_mult): torch.set_default_device(DEVICE) set_random_seed(0) x = torch.randn((num_tokens, hidden_size), dtype=torch.bfloat16) residual = torch.randn((num_tokens, hc_mult, hidden_size), dtype=torch.bfloat16) post_layer_mix = torch.randn((num_tokens, hc_mult, 1), dtype=torch.float32) comb_res_mix = torch.randn((num_tokens, hc_mult, hc_mult), dtype=torch.float32) hc_mult2 = hc_mult * hc_mult hc_mult3 = hc_mult * 2 + hc_mult2 fn = ( torch.randn((hc_mult3, hc_mult, hidden_size), dtype=torch.float) * 1e-4 * (1 + torch.arange(hc_mult).mul(0.01).view(1, -1, 1)) ).flatten(1, 2) hc_scale = torch.randn((3,), dtype=torch.float) * 0.1 hc_base = torch.randn((hc_mult3,), dtype=torch.float) * 0.1 hc_sinkhorn_eps = hc_pre_eps = rms_eps = 1e-6 sinkhorn_repeat = 20 hc_post_alpha = 1.0 def run_ref(): residual_ref = mhc_post_ref(x, residual, post_layer_mix, comb_res_mix) post_mix_ref, res_mix_ref, layer_input_ref = mhc_pre_ref( residual_ref, fn, hc_scale, hc_base, rms_eps, hc_pre_eps, hc_sinkhorn_eps, hc_post_alpha, sinkhorn_repeat, ) return residual_ref, post_mix_ref, res_mix_ref, layer_input_ref residual_ref, post_mix_ref, res_mix_ref, layer_input_ref = run_ref() residual, post_mix, res_mix, x = torch.ops.vllm.mhc_fused_post_pre_tilelang( x, residual, post_layer_mix, comb_res_mix, fn, hc_scale, hc_base, rms_eps, hc_pre_eps, hc_sinkhorn_eps, hc_post_alpha, sinkhorn_repeat, ) torch.testing.assert_close(residual, residual_ref, atol=1e-2, rtol=1e-2) torch.testing.assert_close(post_mix, post_mix_ref, atol=1e-2, rtol=1e-2) torch.testing.assert_close(res_mix, res_mix_ref, atol=1e-2, rtol=1e-2) torch.testing.assert_close(x, layer_input_ref, atol=1e-2, rtol=1e-2) @pytest.mark.skipif( not current_platform.is_rocm(), reason="ROCm required", ) @pytest.mark.parametrize("num_tokens", [1, 4, 8, 128]) @pytest.mark.parametrize("hidden_size", [4096, 7168]) @pytest.mark.parametrize("hc_mult", [4]) def test_hc_head_triton(num_tokens, hidden_size, hc_mult): torch.set_default_device(DEVICE) set_random_seed(0) residual = torch.randn((num_tokens, hc_mult, hidden_size), dtype=torch.bfloat16) fn = torch.randn((hc_mult, hc_mult * hidden_size), dtype=torch.float32) * 1e-4 hc_scale = torch.randn((1,), dtype=torch.float32) * 0.1 hc_base = torch.randn((hc_mult,), dtype=torch.float32) * 0.1 rms_eps = hc_eps = 1e-6 out = torch.empty((num_tokens, hidden_size), dtype=torch.bfloat16) out.fill_(float("nan")) result = torch.ops.vllm.hc_head_triton( residual, fn, hc_scale, hc_base, out, hidden_size, rms_eps, hc_eps, hc_mult, ) assert result is None assert not torch.isnan(out).any() out_ref = hc_head_ref(residual, fn, hc_scale, hc_base, rms_eps, hc_eps) torch.testing.assert_close(out, out_ref, atol=5e-2, rtol=1e-2) @pytest.mark.skipif( not HAS_TILELANG_MHC, reason="TileLang MHC support required", ) @pytest.mark.parametrize("num_tokens", [1, 4, 8, 128]) @pytest.mark.parametrize("hidden_size", [4096, 7168]) @pytest.mark.parametrize("hc_mult", [4]) def test_hc_head_tilelang(num_tokens, hidden_size, hc_mult): torch.set_default_device(DEVICE) set_random_seed(0) residual = torch.randn((num_tokens, hc_mult, hidden_size), dtype=torch.bfloat16) fn = torch.randn((hc_mult, hc_mult * hidden_size), dtype=torch.float32) * 1e-4 hc_scale = torch.randn((1,), dtype=torch.float32) * 0.1 hc_base = torch.randn((hc_mult,), dtype=torch.float32) * 0.1 rms_eps = hc_eps = 1e-6 out = torch.ops.vllm.hc_head_fused_kernel_tilelang( residual, fn, hc_scale, hc_base, rms_eps, hc_eps, ) assert out.shape == (num_tokens, hidden_size) assert out.dtype == torch.bfloat16 assert not torch.isnan(out).any() out_ref = hc_head_ref(residual, fn, hc_scale, hc_base, rms_eps, hc_eps) torch.testing.assert_close(out, out_ref, atol=5e-2, rtol=1e-2)