Skip to content

Commit 2f19b94

Browse files
author
jaysonyu
committed
[CI] Add marlin backend unit tests for Hackathon 10th No.39
1 parent cb7a171 commit 2f19b94

1 file changed

Lines changed: 283 additions & 0 deletions

File tree

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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

Comments
 (0)