| import pytest |
| import torch |
| import torch.nn.functional as F |
|
|
|
|
| def _get_sorted_idx(topk_idx: torch.Tensor, num_experts: int): |
| flatten_topk_idx = topk_idx.flatten() |
| sorted_ids = flatten_topk_idx.argsort() |
| exp_range = torch.arange(0, num_experts, device=topk_idx.device) |
| exp_tok_cnt = (flatten_topk_idx[None, :] == exp_range[:, None]).sum(1) |
| return sorted_ids, exp_tok_cnt |
|
|
|
|
| class TestFusedMoEKernelLauncher: |
|
|
| @pytest.fixture |
| def dtype(self): |
| yield torch.float16 |
|
|
| @pytest.fixture |
| def device(self): |
| yield torch.device('cuda') |
|
|
| @pytest.fixture |
| def N(self): |
| yield 128 |
|
|
| @pytest.fixture |
| def K(self): |
| yield 64 |
|
|
| @pytest.fixture |
| def M(self): |
| yield 256 |
|
|
| @pytest.fixture |
| def num_experts(self): |
| yield 64 |
|
|
| @pytest.fixture |
| def top_k(self): |
| yield 6 |
|
|
| @pytest.fixture |
| def A(self, M, K, device, dtype): |
| ret = torch.rand(M, K, device=device, dtype=dtype) |
| yield (ret - 0.5) / 2 |
|
|
| @pytest.fixture |
| def B(self, num_experts, N, K, device, dtype): |
| ret = torch.rand(num_experts, N, K, device=device, dtype=dtype) |
| yield (ret - 0.5) / 2 |
|
|
| @pytest.fixture |
| def bias(self, num_experts, N, device, dtype): |
| yield torch.rand(num_experts, N, device=device, dtype=dtype) - 0.5 |
|
|
| @pytest.fixture |
| def router_weights(self, M, num_experts, device, dtype): |
| yield torch.rand(M, num_experts, device=device, dtype=dtype) |
|
|
| @pytest.fixture |
| def topk_weights(self, router_weights, top_k): |
| yield router_weights.topk(top_k, dim=-1) |
|
|
| @pytest.fixture |
| def topk_idx(self, topk_weights): |
| yield topk_weights[1] |
|
|
| @pytest.fixture |
| def sort_and_cnt(self, topk_idx, num_experts): |
| yield _get_sorted_idx(topk_idx, num_experts) |
|
|
| @pytest.fixture |
| def sorted_idx(self, sort_and_cnt): |
| yield sort_and_cnt[0] |
|
|
| @pytest.fixture |
| def exp_tok_cnt(self, sort_and_cnt): |
| yield sort_and_cnt[1] |
|
|
| @pytest.fixture |
| def exp_end(self, exp_tok_cnt): |
| yield exp_tok_cnt.cumsum(0) |
|
|
| @pytest.fixture |
| def exp_start(self, exp_end, exp_tok_cnt): |
| yield exp_end - exp_tok_cnt |
|
|
| @pytest.fixture |
| def gt(self, A, B, bias, top_k, topk_idx): |
| M = A.size(0) |
| N = B.size(1) |
| E = B.size(0) |
| C = B.new_empty(M, top_k, N) |
| for eid in range(E): |
| EB = B[eid].t() |
| Ebias = bias[eid] |
| token_idx, k_idx = torch.where(topk_idx == eid) |
| if len(token_idx) == 0: |
| continue |
| EC = A[token_idx] @ EB + Ebias |
| C[token_idx, k_idx] = EC |
| yield C.flatten(0, 1) |
|
|
| @torch.inference_mode() |
| def test_launcher(self, A, B, bias, sorted_idx, exp_start, exp_end, top_k, M, gt): |
| from lmdeploy.pytorch.kernels.cuda.fused_moe import fused_moe_kernel_launcher |
| N = B.size(1) |
| C = B.new_empty(M * top_k, N) |
|
|
| fused_moe_kernel_launcher( |
| A, |
| B, |
| C, |
| sorted_idx, |
| exp_start, |
| exp_end, |
| bias=bias, |
| top_k=top_k, |
| num_tokens=M, |
| ) |
| torch.testing.assert_close(C, gt, atol=1e-3, rtol=1e-3) |
|
|
|
|
| def _mlp_forward(hidden_states, gate_proj, up_proj, down_proj): |
| gate = F.linear(hidden_states, gate_proj) |
| up = F.linear(hidden_states, up_proj) |
| return F.linear(F.silu(gate) * up, down_proj) |
|
|
|
|
| class TestFusedMoe: |
|
|
| @pytest.fixture |
| def dtype(self): |
| yield torch.float16 |
|
|
| @pytest.fixture |
| def device(self): |
| yield torch.device('cuda') |
|
|
| @pytest.fixture |
| def in_size(self): |
| yield 128 |
|
|
| @pytest.fixture |
| def seq_len(seq_len): |
| yield 128 |
|
|
| @pytest.fixture |
| def hidden_size(self): |
| yield 256 |
|
|
| @pytest.fixture |
| def out_size(self): |
| yield 128 |
|
|
| @pytest.fixture |
| def num_experts(self): |
| yield 64 |
|
|
| @pytest.fixture |
| def top_k(self): |
| yield 6 |
|
|
| @pytest.fixture |
| def renormalize(self): |
| yield True |
|
|
| @pytest.fixture |
| def hidden_states(self, seq_len, in_size, dtype, device): |
| ret = torch.rand(seq_len, in_size, dtype=dtype, device=device) |
| yield (ret - 0.5) / 2 |
|
|
| @pytest.fixture |
| def w1(self, num_experts, hidden_size, in_size, dtype, device): |
| ret = torch.rand(num_experts, hidden_size, in_size, dtype=dtype, device=device) |
| yield (ret - 0.5) / 2 |
|
|
| @pytest.fixture |
| def w2(self, num_experts, out_size, hidden_size, dtype, device): |
| ret = torch.rand(num_experts, out_size, hidden_size // 2, dtype=dtype, device=device) |
| yield (ret - 0.5) / 2 |
|
|
| @pytest.fixture |
| def router_logits(self, seq_len, num_experts, dtype, device): |
| yield torch.rand(seq_len, num_experts, dtype=dtype, device=device) |
|
|
| @pytest.fixture |
| def topk_logits(self, router_logits, top_k): |
| routing_weights = torch.softmax(router_logits, dim=-1, dtype=torch.float32) |
| yield torch.topk(routing_weights, top_k, dim=-1) |
|
|
| @pytest.fixture |
| def topk_weights(self, topk_logits): |
| yield topk_logits[0] |
|
|
| @pytest.fixture |
| def topk_idx(self, topk_logits): |
| yield topk_logits[1] |
|
|
| @pytest.fixture |
| def gt(self, hidden_states, w1, w2, topk_weights, topk_idx, renormalize): |
| if renormalize: |
| topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True) |
|
|
| seq_len = hidden_states.size(0) |
| out_size = w2.size(1) |
| output = hidden_states.new_zeros(seq_len, out_size) |
| num_experts = w1.size(0) |
| for eid in range(num_experts): |
| token_idx, k_idx = torch.where(topk_idx == eid) |
| gate_proj, up_proj = w1[eid].chunk(2, dim=0) |
| down_proj = w2[eid] |
| tmp_out = _mlp_forward(hidden_states[token_idx], gate_proj, up_proj, down_proj) |
| tmp_out = tmp_out * topk_weights[token_idx, k_idx, None] |
| output.index_add_(0, token_idx, tmp_out.to(output.dtype)) |
| yield output |
|
|
| @torch.inference_mode() |
| def test_fused_moe(self, hidden_states, w1, w2, topk_weights, topk_idx, top_k, renormalize, gt): |
| from lmdeploy.pytorch.kernels.cuda.fused_moe import fused_moe |
| output = fused_moe(hidden_states, w1, w2, topk_weights, topk_idx, topk=top_k, renormalize=renormalize) |
| torch.testing.assert_close(output, gt, atol=1e-3, rtol=1e-3) |
|
|
|
|
| class TestFusedMoeW8A8(TestFusedMoe): |
|
|
| @pytest.fixture |
| def quant_states(self, hidden_states): |
| from lmdeploy.pytorch.kernels.cuda.w8a8_triton_kernels import per_token_quant_int8 |
| states_i8, states_scale = per_token_quant_int8(hidden_states, 1e-7) |
| yield states_i8, states_scale |
|
|
| def quant_weight(self, w): |
| from lmdeploy.pytorch.kernels.cuda.w8a8_triton_kernels import per_channel_quant |
| num_experts, num_outs, _ = w.shape |
| w = w.flatten(0, -2) |
| w_i8, w_scale = per_channel_quant(w, torch.int8) |
| w_i8 = w_i8.view(num_experts, num_outs, -1) |
| w_scale = w_scale.view(num_experts, num_outs, -1) |
| return w_i8, w_scale |
|
|
| @pytest.fixture |
| def quant_w1(self, w1): |
| w_i8, w_scale = self.quant_weight(w1) |
| yield w_i8, w_scale |
|
|
| @pytest.fixture |
| def quant_w2(self, w2): |
| w_i8, w_scale = self.quant_weight(w2) |
| yield w_i8, w_scale |
|
|
| @torch.inference_mode() |
| def test_fused_moe(self, quant_states, quant_w1, quant_w2, topk_weights, topk_idx, top_k, renormalize, gt): |
| from lmdeploy.pytorch.kernels.cuda.w8a8_fused_moe import fused_moe_w8a8 |
| state_i8, state_scale = quant_states |
| w1_i8, w1_scale = quant_w1 |
| w2_i8, w2_scale = quant_w2 |
|
|
| output = fused_moe_w8a8(state_i8, |
| state_scale, |
| w1_i8, |
| w1_scale, |
| w2_i8, |
| w2_scale, |
| topk_weights=topk_weights, |
| topk_ids=topk_idx, |
| topk=top_k, |
| out_dtype=torch.float16, |
| renormalize=renormalize) |
| torch.testing.assert_close(output, gt, atol=5e-3, rtol=1e-3) |
|
|