|
| 1 | +# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +import importlib.util |
| 16 | +import sys |
| 17 | +import types |
| 18 | +from pathlib import Path |
| 19 | +from types import SimpleNamespace |
| 20 | +from unittest.mock import Mock |
| 21 | + |
| 22 | +import numpy as np |
| 23 | +import paddle |
| 24 | +import pytest |
| 25 | + |
| 26 | + |
| 27 | +MODULE_NAME = "fastdeploy.model_executor.layers.moe.fused_moe_marlin_backend" |
| 28 | +MODULE_PATH = ( |
| 29 | + Path(__file__).resolve().parents[2] |
| 30 | + / "fastdeploy" |
| 31 | + / "model_executor" |
| 32 | + / "layers" |
| 33 | + / "moe" |
| 34 | + / "fused_moe_marlin_backend.py" |
| 35 | +) |
| 36 | + |
| 37 | + |
| 38 | +def _package(name): |
| 39 | + module = types.ModuleType(name) |
| 40 | + module.__path__ = [] |
| 41 | + return module |
| 42 | + |
| 43 | + |
| 44 | +def _load_marlin_backend(monkeypatch): |
| 45 | + fastdeploy_mod = _package("fastdeploy") |
| 46 | + model_executor_mod = _package("fastdeploy.model_executor") |
| 47 | + layers_mod = _package("fastdeploy.model_executor.layers") |
| 48 | + ops_mod = _package("fastdeploy.model_executor.ops") |
| 49 | + gpu_mod = types.ModuleType("fastdeploy.model_executor.ops.gpu") |
| 50 | + moe_pkg_mod = _package("fastdeploy.model_executor.layers.moe") |
| 51 | + moe_mod = types.ModuleType("fastdeploy.model_executor.layers.moe.moe") |
| 52 | + quant_pkg_mod = _package("fastdeploy.model_executor.layers.quantization") |
| 53 | + quant_base_mod = types.ModuleType("fastdeploy.model_executor.layers.quantization.quant_base") |
| 54 | + |
| 55 | + class QuantMethodBase: |
| 56 | + pass |
| 57 | + |
| 58 | + quant_base_mod.QuantMethodBase = QuantMethodBase |
| 59 | + gpu_mod.MoeWna16MarlinGemmApi = Mock() |
| 60 | + gpu_mod.tritonmoe_preprocess_func = Mock() |
| 61 | + gpu_mod.moe_topk_select = Mock() |
| 62 | + gpu_mod.gptq_marlin_repack = Mock() |
| 63 | + moe_mod.get_moe_scores = Mock() |
| 64 | + |
| 65 | + fastdeploy_mod.model_executor = model_executor_mod |
| 66 | + model_executor_mod.layers = layers_mod |
| 67 | + model_executor_mod.ops = ops_mod |
| 68 | + layers_mod.moe = moe_pkg_mod |
| 69 | + layers_mod.quantization = quant_pkg_mod |
| 70 | + ops_mod.gpu = gpu_mod |
| 71 | + |
| 72 | + modules = { |
| 73 | + "fastdeploy": fastdeploy_mod, |
| 74 | + "fastdeploy.model_executor": model_executor_mod, |
| 75 | + "fastdeploy.model_executor.layers": layers_mod, |
| 76 | + "fastdeploy.model_executor.layers.moe": moe_pkg_mod, |
| 77 | + "fastdeploy.model_executor.layers.moe.moe": moe_mod, |
| 78 | + "fastdeploy.model_executor.layers.quantization": quant_pkg_mod, |
| 79 | + "fastdeploy.model_executor.layers.quantization.quant_base": quant_base_mod, |
| 80 | + "fastdeploy.model_executor.ops": ops_mod, |
| 81 | + "fastdeploy.model_executor.ops.gpu": gpu_mod, |
| 82 | + } |
| 83 | + for name, module in modules.items(): |
| 84 | + monkeypatch.setitem(sys.modules, name, module) |
| 85 | + monkeypatch.delitem(sys.modules, MODULE_NAME, raising=False) |
| 86 | + |
| 87 | + spec = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH) |
| 88 | + module = importlib.util.module_from_spec(spec) |
| 89 | + monkeypatch.setitem(sys.modules, MODULE_NAME, module) |
| 90 | + spec.loader.exec_module(module) |
| 91 | + return module, gpu_mod, moe_mod |
| 92 | + |
| 93 | + |
| 94 | +class _DummyMoELayer(paddle.nn.Layer): |
| 95 | + def __init__(self, hidden_size=32, moe_intermediate_size=16, num_local_experts=2): |
| 96 | + super().__init__() |
| 97 | + self.num_local_experts = num_local_experts |
| 98 | + self.num_experts = num_local_experts |
| 99 | + self.hidden_size = hidden_size |
| 100 | + self.moe_intermediate_size = moe_intermediate_size |
| 101 | + self.top_k = 2 |
| 102 | + self.topk_method = "topk" |
| 103 | + self.n_group = 1 |
| 104 | + self.topk_group = 1 |
| 105 | + self.routed_scaling_factor = 1.0 |
| 106 | + self.renormalize = True |
| 107 | + self.gate_correction_bias = paddle.zeros([num_local_experts], dtype="float32") |
| 108 | + |
| 109 | + def extract_moe_ffn_weights(self, state_dict): |
| 110 | + return state_dict["up"], state_dict["down"], None, None |
| 111 | + |
| 112 | + |
| 113 | +def test_scale_permutations_are_stable(monkeypatch): |
| 114 | + marlin, _, _ = _load_marlin_backend(monkeypatch) |
| 115 | + |
| 116 | + scale_perm, scale_perm_single = marlin.get_scale_perms() |
| 117 | + |
| 118 | + assert len(scale_perm) == 64 |
| 119 | + assert len(scale_perm_single) == 32 |
| 120 | + assert scale_perm[:10] == [0, 8, 16, 24, 32, 40, 48, 56, 1, 9] |
| 121 | + assert scale_perm[-8:] == [7, 15, 23, 31, 39, 47, 55, 63] |
| 122 | + assert scale_perm_single[:16] == [0, 1, 8, 9, 16, 17, 24, 25, 2, 3, 10, 11, 18, 19, 26, 27] |
| 123 | + |
| 124 | + |
| 125 | +def test_marlin_permute_scales_grouped_and_single_channel(monkeypatch): |
| 126 | + marlin, _, _ = _load_marlin_backend(monkeypatch) |
| 127 | + scale_perm, scale_perm_single = marlin.get_scale_perms() |
| 128 | + |
| 129 | + grouped = paddle.arange(128, dtype="int64").reshape([2, 64]) |
| 130 | + grouped_out = marlin.marlin_permute_scales(grouped, size_k=128, size_n=16, group_size=64) |
| 131 | + grouped_expected = grouped.reshape([-1, len(scale_perm)])[:, scale_perm].reshape([-1, 16]) |
| 132 | + np.testing.assert_array_equal(grouped_out.numpy(), grouped_expected.numpy()) |
| 133 | + |
| 134 | + per_channel = paddle.arange(64, dtype="int64").reshape([2, 32]) |
| 135 | + per_channel_out = marlin.marlin_permute_scales(per_channel, size_k=32, size_n=32, group_size=-1) |
| 136 | + per_channel_expected = per_channel.reshape([-1, len(scale_perm_single)])[:, scale_perm_single].reshape([-1, 32]) |
| 137 | + np.testing.assert_array_equal(per_channel_out.numpy(), per_channel_expected.numpy()) |
| 138 | + |
| 139 | + |
| 140 | +def test_marlin_moe_permute_scales_handles_each_expert(monkeypatch): |
| 141 | + marlin, _, _ = _load_marlin_backend(monkeypatch) |
| 142 | + _, scale_perm_single = marlin.get_scale_perms() |
| 143 | + |
| 144 | + scales = paddle.arange(128, dtype="float32").reshape([2, 2, 32]) |
| 145 | + out = marlin.marlin_moe_permute_scales(scales, size_k=32, size_n=32, group_size=-1) |
| 146 | + |
| 147 | + expected = paddle.stack( |
| 148 | + [expert.reshape([-1, len(scale_perm_single)])[:, scale_perm_single].reshape([2, 32]) for expert in scales], |
| 149 | + axis=0, |
| 150 | + ) |
| 151 | + assert list(out.shape) == [2, 2, 32] |
| 152 | + np.testing.assert_array_equal(out.numpy(), expected.numpy()) |
| 153 | + |
| 154 | + |
| 155 | +def test_gptq_marlin_moe_repack_invokes_kernel_per_expert(monkeypatch): |
| 156 | + marlin, gpu_mod, _ = _load_marlin_backend(monkeypatch) |
| 157 | + calls = [] |
| 158 | + |
| 159 | + def fake_repack(weight, perm, size_k, size_n, num_bits): |
| 160 | + calls.append((weight.numpy().copy(), perm.numpy().copy(), size_k, size_n, num_bits)) |
| 161 | + return paddle.full([size_k // 16, size_n * (num_bits // 2)], len(calls), dtype=weight.dtype) |
| 162 | + |
| 163 | + gpu_mod.gptq_marlin_repack = fake_repack |
| 164 | + q_weight = paddle.arange(32, dtype="int32").reshape([2, 2, 8]) |
| 165 | + perm = paddle.arange(6, dtype="int32").reshape([2, 3]) |
| 166 | + |
| 167 | + out = marlin.gptq_marlin_moe_repack(q_weight, perm, size_k=32, size_n=4, num_bits=4) |
| 168 | + |
| 169 | + assert len(calls) == 2 |
| 170 | + assert list(out.shape) == [2, 2, 8] |
| 171 | + np.testing.assert_array_equal(out[0].numpy(), np.ones([2, 8], dtype=np.int32)) |
| 172 | + np.testing.assert_array_equal(out[1].numpy(), np.full([2, 8], 2, dtype=np.int32)) |
| 173 | + np.testing.assert_array_equal(calls[0][0], q_weight[0].numpy()) |
| 174 | + np.testing.assert_array_equal(calls[1][1], perm[1].numpy()) |
| 175 | + |
| 176 | + with pytest.raises(AssertionError): |
| 177 | + marlin.gptq_marlin_moe_repack(q_weight, perm, size_k=17, size_n=4, num_bits=4) |
| 178 | + |
| 179 | + |
| 180 | +def test_create_weights_registers_expected_marlin_parameters(monkeypatch): |
| 181 | + marlin, _, _ = _load_marlin_backend(monkeypatch) |
| 182 | + layer = _DummyMoELayer(hidden_size=32, moe_intermediate_size=16, num_local_experts=2) |
| 183 | + method = marlin.MarlinWeightOnlyMoEMethod() |
| 184 | + |
| 185 | + method.create_weights(layer) |
| 186 | + |
| 187 | + assert list(layer.up_gate_proj_weight.shape) == [2, 2, 64] |
| 188 | + assert list(layer.down_proj_weight.shape) == [2, 1, 64] |
| 189 | + assert list(layer.up_gate_proj_weight_scale.shape) == [2, 1, 32] |
| 190 | + assert list(layer.down_proj_weight_scale.shape) == [2, 1, 32] |
| 191 | + assert layer.up_gate_proj_weight.dtype == paddle.int32 |
| 192 | + assert layer.down_proj_weight.dtype == paddle.int32 |
| 193 | + assert layer.up_gate_proj_weight_scale.dtype == paddle.float32 |
| 194 | + assert layer.down_proj_weight_scale.dtype == paddle.float32 |
| 195 | + |
| 196 | + |
| 197 | +def test_process_loaded_weights_quantizes_and_sets_parameters(monkeypatch): |
| 198 | + marlin, gpu_mod, _ = _load_marlin_backend(monkeypatch) |
| 199 | + |
| 200 | + def fake_repack(weight, _perm, size_k, size_n, num_bits): |
| 201 | + del weight |
| 202 | + return paddle.full([size_k // 16, size_n * (num_bits // 2)], 3, dtype="int32") |
| 203 | + |
| 204 | + gpu_mod.gptq_marlin_repack = fake_repack |
| 205 | + layer = _DummyMoELayer(hidden_size=32, moe_intermediate_size=16, num_local_experts=2) |
| 206 | + method = marlin.MarlinWeightOnlyMoEMethod() |
| 207 | + method.create_weights(layer) |
| 208 | + |
| 209 | + up_weights = [ |
| 210 | + paddle.arange(1, 32 * 32 + 1, dtype="float32").reshape([32, 32]) + expert_idx |
| 211 | + for expert_idx in range(layer.num_local_experts) |
| 212 | + ] |
| 213 | + down_weights = [ |
| 214 | + paddle.arange(1, 16 * 32 + 1, dtype="float32").reshape([16, 32]) + expert_idx |
| 215 | + for expert_idx in range(layer.num_local_experts) |
| 216 | + ] |
| 217 | + |
| 218 | + method.process_loaded_weights(layer, {"up": up_weights, "down": down_weights}) |
| 219 | + |
| 220 | + assert list(layer.up_gate_proj_weight.shape) == [2, 2, 64] |
| 221 | + assert list(layer.down_proj_weight.shape) == [2, 1, 64] |
| 222 | + assert paddle.all(layer.up_gate_proj_weight == 3).item() |
| 223 | + assert paddle.all(layer.down_proj_weight == 3).item() |
| 224 | + assert paddle.all(paddle.isfinite(layer.up_gate_proj_weight_scale)).item() |
| 225 | + assert paddle.all(paddle.isfinite(layer.down_proj_weight_scale)).item() |
| 226 | + |
| 227 | + with pytest.raises(AssertionError): |
| 228 | + method.process_loaded_weights(layer, {"up": [paddle.ones([4, 4])], "down": down_weights}) |
| 229 | + |
| 230 | + |
| 231 | +def test_apply_uses_marlin_gemm_and_hook_for_topk_path(monkeypatch): |
| 232 | + marlin, gpu_mod, _ = _load_marlin_backend(monkeypatch) |
| 233 | + layer = SimpleNamespace( |
| 234 | + top_k=2, |
| 235 | + moe_intermediate_size=8, |
| 236 | + hidden_size=16, |
| 237 | + num_experts=4, |
| 238 | + topk_method="topk", |
| 239 | + gate_correction_bias=paddle.zeros([4], dtype="float32"), |
| 240 | + up_gate_proj_weight=paddle.ones([4, 1, 32], dtype="int32"), |
| 241 | + up_gate_proj_weight_scale=paddle.ones([4, 1, 16], dtype="float32"), |
| 242 | + down_proj_weight=paddle.ones([4, 1, 32], dtype="int32"), |
| 243 | + down_proj_weight_scale=paddle.ones([4, 1, 16], dtype="float32"), |
| 244 | + ) |
| 245 | + method = marlin.MarlinWeightOnlyMoEMethod() |
| 246 | + x = paddle.ones([3, layer.hidden_size], dtype="float32") |
| 247 | + topk_ids = paddle.to_tensor([[0, 1], [1, 2], [2, 3]], dtype="int32") |
| 248 | + topk_weights = paddle.ones([3, layer.top_k], dtype="float32") |
| 249 | + hook = Mock() |
| 250 | + |
| 251 | + gpu_mod.moe_topk_select.return_value = (topk_ids, topk_weights) |
| 252 | + marlin.tritonmoe_preprocess_func = Mock( |
| 253 | + return_value=( |
| 254 | + paddle.arange(6, dtype="int32"), |
| 255 | + paddle.arange(layer.num_experts, dtype="int32"), |
| 256 | + paddle.to_tensor([6], dtype="int32"), |
| 257 | + ) |
| 258 | + ) |
| 259 | + marlin.MoeWna16MarlinGemmApi = Mock( |
| 260 | + side_effect=[ |
| 261 | + (paddle.ones([6, layer.moe_intermediate_size * 2], dtype="float32"),), |
| 262 | + (paddle.ones([6, layer.hidden_size], dtype="float32"),), |
| 263 | + ] |
| 264 | + ) |
| 265 | + |
| 266 | + out = method.apply(layer, x, gate=lambda _x: paddle.ones([3, layer.num_experts]), topk_ids_hookfunc=hook) |
| 267 | + |
| 268 | + assert list(out.shape) == [3, layer.hidden_size] |
| 269 | + hook.assert_called_once() |
| 270 | + np.testing.assert_array_equal(hook.call_args.kwargs["topk_ids"].numpy(), topk_ids.numpy()) |
| 271 | + assert marlin.MoeWna16MarlinGemmApi.call_count == 2 |
| 272 | + first_call = marlin.MoeWna16MarlinGemmApi.call_args_list[0].kwargs |
| 273 | + second_call = marlin.MoeWna16MarlinGemmApi.call_args_list[1].kwargs |
| 274 | + assert first_call["top_k"] == layer.top_k |
| 275 | + assert first_call["mul_topk_weights"] is False |
| 276 | + assert first_call["size_m"] == x.shape[0] |
| 277 | + assert first_call["size_n"] == layer.moe_intermediate_size * 2 |
| 278 | + assert first_call["size_k"] == layer.hidden_size |
| 279 | + assert second_call["top_k"] == 1 |
| 280 | + assert second_call["mul_topk_weights"] is True |
| 281 | + assert second_call["size_m"] == x.shape[0] * layer.top_k |
| 282 | + assert second_call["size_n"] == layer.hidden_size |
| 283 | + assert second_call["size_k"] == layer.moe_intermediate_size |
0 commit comments