main
py 1,224 lines 43.9 KB
Raw
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: Apache-2.0
3 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4 """Correctness tests for Int4EmulationTritonExperts MoE backend.
5
6 Tests the weight dequantization helpers (_unpack_and_dequant_int4_gptq,
7 _unpack_and_dequant_int4_awq) and full MoE forward pass
8 (_process_weights_emulation_gptq, _process_weights_emulation_awq)
9 for both symmetric and asymmetric zero-point cases.
10
11 Run `pytest tests/kernels/quantization/test_int4_emulation_moe.py`.
12 """
13
14 import numpy
15 import pytest
16 import torch
17 import torch.nn.functional as F
18
19 from vllm.model_executor.layers.fused_moe.activation import MoEActivation
20 from vllm.model_executor.layers.fused_moe.config import (
21 FusedMoEConfig,
22 FusedMoEParallelConfig,
23 RoutingMethodType,
24 int4_w4a16_moe_quant_config,
25 )
26 from vllm.model_executor.layers.fused_moe.experts.int4_emulation_moe import (
27 Int4EmulationTritonExperts,
28 )
29 from vllm.model_executor.layers.fused_moe.oracle.int_wna16 import (
30 _process_weights_emulation_awq,
31 _process_weights_emulation_gptq,
32 _unpack_and_dequant_int4_awq,
33 _unpack_and_dequant_int4_gptq,
34 )
35 from vllm.model_executor.layers.quantization.utils.quant_utils import (
36 awq_pack,
37 gptq_pack,
38 )
39 from vllm.platforms import current_platform
40
41 pytestmark = pytest.mark.skipif(
42 not current_platform.is_cuda_alike(),
43 reason="Int4EmulationTritonExperts requires CUDA.",
44 )
45
46 device = "cuda"
47
48 # (E, K, N, group_size)
49 SHAPES = [
50 pytest.param(2, 64, 32, 32, id="tiny-gs32"),
51 pytest.param(4, 128, 64, 64, id="small-gs64"),
52 pytest.param(4, 256, 128, 128, id="medium-gs128"),
53 ]
54
55 # (E, K, N, top_k, group_size, num_tokens)
56 E2E_CONFIGS = [
57 pytest.param(4, 64, 32, 2, 32, 8, id="tiny"),
58 pytest.param(8, 128, 64, 2, 64, 16, id="small"),
59 ]
60
61
62 # ---------------------------------------------------------------------------
63 # Helpers
64 # ---------------------------------------------------------------------------
65
66
67 def _quantize_sym(w_fp: torch.Tensor, group_size: int):
68 """Quantize [K, N] float to int4 symmetric (uint4b8), return q and scale."""
69 K, N = w_fp.shape
70 assert K % group_size == 0
71 n_groups = K // group_size
72 w_grouped = w_fp.reshape(n_groups, group_size, N)
73 scale = w_grouped.abs().amax(dim=1) / 7.0
74 scale = scale.clamp(min=1e-6)
75 w_quant = (w_grouped / scale.unsqueeze(1)).round().clamp(-8, 7)
76 q = (w_quant + 8).to(torch.int32).reshape(K, N)
77 return q, scale
78
79
80 def _quantize_asym(w_fp: torch.Tensor, group_size: int):
81 """Quantize [K, N] float to uint4 asymmetric, return q, scale, zero."""
82 K, N = w_fp.shape
83 assert K % group_size == 0
84 n_groups = K // group_size
85 w_grouped = w_fp.reshape(n_groups, group_size, N)
86 wmin = w_grouped.amin(dim=1)
87 wmax = w_grouped.amax(dim=1)
88 scale = (wmax - wmin) / 15.0
89 scale = scale.clamp(min=1e-6)
90 zero = (-wmin / scale).round().clamp(0, 15).to(torch.int32)
91 w_quant = ((w_grouped - wmin.unsqueeze(1)) / scale.unsqueeze(1)).round()
92 q = w_quant.clamp(0, 15).to(torch.int32).reshape(K, N)
93 return q, scale, zero
94
95
96 def _dequantize_ref(
97 w_uint: torch.Tensor,
98 scale: torch.Tensor,
99 zero=None,
100 output_dtype: torch.dtype = torch.bfloat16,
101 ):
102 """Reference dequant for a single [K, N] slice."""
103 K, N = w_uint.shape
104 n_groups = scale.shape[0]
105 group_size = K // n_groups
106 w = w_uint.reshape(n_groups, group_size, N).to(output_dtype)
107 s = scale.unsqueeze(1).to(output_dtype)
108 if zero is None:
109 return ((w - 8) * s).reshape(K, N)
110 z = zero.unsqueeze(1).to(output_dtype)
111 return ((w - z) * s).reshape(K, N)
112
113
114 def _pack_gptq_zeros(zero: torch.Tensor, N: int) -> torch.Tensor:
115 """Pack [n_groups, N] zeros into GPTQ format [n_groups, N//8] int32."""
116 n_groups, _ = zero.shape
117 z = zero.to(torch.int32).cpu().numpy().astype(numpy.uint32)
118 packed = numpy.zeros((n_groups, N // 8), dtype=numpy.uint32)
119 for i in range(8):
120 packed |= z[:, i::8] << (i * 4)
121 return torch.from_numpy(packed.astype(numpy.int32)).to(device)
122
123
124 def _pack_awq_zeros(zero: torch.Tensor, N: int) -> torch.Tensor:
125 """Pack [n_groups, N] zeros into AWQ column format [n_groups, N//8] int32."""
126 n_groups, _ = zero.shape
127 interleave = numpy.array([0, 2, 4, 6, 1, 3, 5, 7])
128 z = zero.to(torch.int32).cpu().numpy().astype(numpy.uint32)
129 z_interleaved = z.reshape(-1, 8)[:, interleave].reshape(n_groups, N)
130 packed = numpy.zeros((n_groups, N // 8), dtype=numpy.uint32)
131 for i in range(8):
132 packed |= z_interleaved[:, i::8] << (i * 4)
133 return torch.from_numpy(packed.astype(numpy.int32)).to(device)
134
135
136 def _make_moe_config(E, K, N):
137 return FusedMoEConfig(
138 num_experts=E,
139 experts_per_token=2,
140 hidden_dim=K,
141 intermediate_size=N,
142 num_local_experts=E,
143 num_logical_experts=E,
144 moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
145 activation=MoEActivation.SILU,
146 in_dtype=torch.bfloat16,
147 device=device,
148 routing_method=RoutingMethodType.TopK,
149 max_num_tokens=512,
150 )
151
152
153 def _make_gptq_moe_weights(E, K, N, group_size, asym=False):
154 """Build GPTQ MoE weight tensors and per-expert float references."""
155 torch.manual_seed(7)
156 w13_fp = torch.randn(E, K, 2 * N, dtype=torch.float16, device=device)
157 w2_fp = torch.randn(E, N, K, dtype=torch.float16, device=device)
158
159 w13_list, w13s_list, w13z_list, w13_ref_list = [], [], [], []
160 w2_list, w2s_list, w2z_list, w2_ref_list = [], [], [], []
161
162 for e in range(E):
163 if asym:
164 q13, s13, z13 = _quantize_asym(w13_fp[e], group_size)
165 w13_list.append(gptq_pack(q13, 4, K, 2 * N))
166 w13s_list.append(s13)
167 w13z_list.append(_pack_gptq_zeros(z13, 2 * N))
168 w13_ref_list.append(_dequantize_ref(q13, s13, z13))
169 q2, s2, z2 = _quantize_asym(w2_fp[e], group_size)
170 w2_list.append(gptq_pack(q2, 4, N, K))
171 w2s_list.append(s2)
172 w2z_list.append(_pack_gptq_zeros(z2, K))
173 w2_ref_list.append(_dequantize_ref(q2, s2, z2))
174 else:
175 q13, s13 = _quantize_sym(w13_fp[e], group_size)
176 w13_list.append(gptq_pack(q13, 4, K, 2 * N))
177 w13s_list.append(s13)
178 w13z_list.append(None)
179 w13_ref_list.append(_dequantize_ref(q13, s13))
180 q2, s2 = _quantize_sym(w2_fp[e], group_size)
181 w2_list.append(gptq_pack(q2, 4, N, K))
182 w2s_list.append(s2)
183 w2z_list.append(None)
184 w2_ref_list.append(_dequantize_ref(q2, s2))
185
186 return (
187 torch.stack(w13_list),
188 torch.stack(w13s_list),
189 torch.stack(w13z_list) if asym else None,
190 torch.stack(w13_ref_list), # [E, K, 2N]
191 torch.stack(w2_list),
192 torch.stack(w2s_list),
193 torch.stack(w2z_list) if asym else None,
194 torch.stack(w2_ref_list), # [E, N, K]
195 )
196
197
198 def _make_awq_moe_weights(E, K, N, group_size, asym=False):
199 """Build AWQ MoE weight tensors and per-expert float references."""
200 torch.manual_seed(8)
201 w13_fp = torch.randn(E, K, 2 * N, dtype=torch.float16, device=device)
202 w2_fp = torch.randn(E, N, K, dtype=torch.float16, device=device)
203
204 w13_list, w13s_list, w13z_list, w13_ref_list = [], [], [], []
205 w2_list, w2s_list, w2z_list, w2_ref_list = [], [], [], []
206
207 for e in range(E):
208 if asym:
209 q13, s13, z13 = _quantize_asym(w13_fp[e], group_size)
210 w13_list.append(awq_pack(q13, 4, K, 2 * N))
211 w13s_list.append(s13)
212 w13z_list.append(_pack_awq_zeros(z13, 2 * N))
213 w13_ref_list.append(_dequantize_ref(q13, s13, z13))
214 q2, s2, z2 = _quantize_asym(w2_fp[e], group_size)
215 w2_list.append(awq_pack(q2, 4, N, K))
216 w2s_list.append(s2)
217 w2z_list.append(_pack_awq_zeros(z2, K))
218 w2_ref_list.append(_dequantize_ref(q2, s2, z2))
219 else:
220 q13, s13 = _quantize_sym(w13_fp[e], group_size)
221 w13_list.append(awq_pack(q13, 4, K, 2 * N))
222 w13s_list.append(s13)
223 w13z_list.append(None)
224 w13_ref_list.append(_dequantize_ref(q13, s13))
225 q2, s2 = _quantize_sym(w2_fp[e], group_size)
226 w2_list.append(awq_pack(q2, 4, N, K))
227 w2s_list.append(s2)
228 w2z_list.append(None)
229 w2_ref_list.append(_dequantize_ref(q2, s2))
230
231 return (
232 torch.stack(w13_list),
233 torch.stack(w13s_list),
234 torch.stack(w13z_list) if asym else None,
235 torch.stack(w13_ref_list), # [E, K, 2N]
236 torch.stack(w2_list),
237 torch.stack(w2s_list),
238 torch.stack(w2z_list) if asym else None,
239 torch.stack(w2_ref_list), # [E, N, K]
240 )
241
242
243 def _canonicalize_gptq_moe_inputs(
244 w13: torch.Tensor,
245 w2: torch.Tensor,
246 w13_scale: torch.Tensor,
247 w2_scale: torch.Tensor,
248 w13_qzeros: torch.Tensor | None,
249 w2_qzeros: torch.Tensor | None,
250 ):
251 """Convert GPTQ K-first tensors to the oracle's canonical N-first layout."""
252
253 def transpose(value: torch.Tensor | None) -> torch.Tensor | None:
254 return value.transpose(1, 2).contiguous() if value is not None else None
255
256 return (
257 w13.transpose(1, 2).contiguous(),
258 w2.transpose(1, 2).contiguous(),
259 w13_scale.transpose(1, 2).contiguous(),
260 w2_scale.transpose(1, 2).contiguous(),
261 transpose(w13_qzeros),
262 transpose(w2_qzeros),
263 )
264
265
266 def _run_emulation_forward(
267 experts, w13_bf16, w2_bf16, hidden_states, topk_weights, topk_ids, E, K, N
268 ):
269 ws13_size = hidden_states.shape[0] * topk_ids.shape[1] * max(N, K)
270 ws2_size = hidden_states.shape[0] * topk_ids.shape[1] * max(2 * N, K)
271 workspace13 = torch.zeros(ws13_size, dtype=hidden_states.dtype, device=device)
272 workspace2 = torch.zeros(ws2_size, dtype=hidden_states.dtype, device=device)
273 output = torch.zeros(
274 hidden_states.shape[0], K, dtype=hidden_states.dtype, device=device
275 )
276 experts.apply(
277 output=output,
278 hidden_states=hidden_states,
279 w1=w13_bf16,
280 w2=w2_bf16,
281 topk_weights=topk_weights,
282 topk_ids=topk_ids,
283 activation=MoEActivation.SILU,
284 global_num_experts=E,
285 expert_map=None,
286 a1q_scale=None,
287 a2_scale=None,
288 workspace13=workspace13,
289 workspace2=workspace2,
290 expert_tokens_meta=None,
291 apply_router_weight_on_input=False,
292 )
293 return output
294
295
296 # ---------------------------------------------------------------------------
297 # Tests: _unpack_and_dequant_int4_gptq
298 # ---------------------------------------------------------------------------
299
300
301 @pytest.mark.parametrize("E, K, N, group_size", SHAPES)
302 def test_gptq_unpack_symmetric(E, K, N, group_size):
303 """GPTQ symmetric unpacker matches reference."""
304 torch.manual_seed(0)
305 w_fp = torch.randn(E, K, N, dtype=torch.float16, device=device)
306
307 packed_list, scale_list, ref_list = [], [], []
308 for e in range(E):
309 q, s = _quantize_sym(w_fp[e], group_size)
310 packed_list.append(gptq_pack(q, 4, K, N))
311 scale_list.append(s)
312 ref_list.append(_dequantize_ref(q, s, output_dtype=torch.float32))
313
314 w_packed = torch.stack(packed_list).to(device)
315 scale = torch.stack(scale_list).to(device)
316 ref = torch.stack(ref_list).to(device)
317
318 out = _unpack_and_dequant_int4_gptq(
319 w_packed, scale, None, transpose_output=False, output_dtype=torch.float32
320 )
321
322 assert out.shape == (E, K, N)
323 assert torch.allclose(out, ref, atol=0), (
324 f"max diff: {(out - ref).abs().max().item()}"
325 )
326
327
328 @pytest.mark.parametrize("E, K, N, group_size", SHAPES)
329 def test_gptq_unpack_asymmetric(E, K, N, group_size):
330 """GPTQ asymmetric unpacker matches reference."""
331 torch.manual_seed(1)
332 w_fp = torch.randn(E, K, N, dtype=torch.float16, device=device)
333
334 packed_list, scale_list, zero_list, ref_list = [], [], [], []
335 for e in range(E):
336 q, s, z = _quantize_asym(w_fp[e], group_size)
337 packed_list.append(gptq_pack(q, 4, K, N))
338 scale_list.append(s)
339 zero_list.append(_pack_gptq_zeros(z, N))
340 ref_list.append(_dequantize_ref(q, s, z, output_dtype=torch.float32))
341
342 w_packed = torch.stack(packed_list).to(device)
343 scale = torch.stack(scale_list).to(device)
344 qzeros = torch.stack(zero_list).to(device)
345 ref = torch.stack(ref_list).to(device)
346
347 out = _unpack_and_dequant_int4_gptq(
348 w_packed, scale, qzeros, transpose_output=False, output_dtype=torch.float32
349 )
350
351 assert out.shape == (E, K, N)
352 assert torch.allclose(out, ref, atol=0), (
353 f"max diff: {(out - ref).abs().max().item()}"
354 )
355
356
357 @pytest.mark.parametrize("E, K, N, group_size", SHAPES)
358 def test_gptq_unpack_transpose(E, K, N, group_size):
359 """GPTQ transpose_output=True gives [E, N, K]."""
360 torch.manual_seed(2)
361 w_fp = torch.randn(E, K, N, dtype=torch.float16, device=device)
362
363 packed_list, scale_list = [], []
364 for e in range(E):
365 q, s = _quantize_sym(w_fp[e], group_size)
366 packed_list.append(gptq_pack(q, 4, K, N))
367 scale_list.append(s)
368
369 w_packed = torch.stack(packed_list).to(device)
370 scale = torch.stack(scale_list).to(device)
371
372 out_normal = _unpack_and_dequant_int4_gptq(w_packed, scale, None, False)
373 out_transposed = _unpack_and_dequant_int4_gptq(w_packed, scale, None, True)
374
375 assert out_transposed.shape == (E, N, K)
376 assert torch.allclose(
377 out_transposed, out_normal.permute(0, 2, 1).contiguous(), atol=0
378 )
379
380
381 # ---------------------------------------------------------------------------
382 # Tests: _unpack_and_dequant_int4_awq
383 # ---------------------------------------------------------------------------
384
385
386 @pytest.mark.parametrize("E, K, N, group_size", SHAPES)
387 def test_awq_unpack_symmetric(E, K, N, group_size):
388 """AWQ symmetric unpacker matches reference."""
389 torch.manual_seed(3)
390 w_fp = torch.randn(E, K, N, dtype=torch.float16, device=device)
391
392 packed_list, scale_list, ref_list = [], [], []
393 for e in range(E):
394 q, s = _quantize_sym(w_fp[e], group_size)
395 packed_list.append(awq_pack(q, 4, K, N))
396 scale_list.append(s)
397 ref_list.append(_dequantize_ref(q, s, output_dtype=torch.float32))
398
399 w_packed = torch.stack(packed_list).to(device)
400 scale = torch.stack(scale_list).to(device)
401 ref = torch.stack(ref_list).to(device)
402
403 out = _unpack_and_dequant_int4_awq(
404 w_packed, scale, None, transpose_output=False, output_dtype=torch.float32
405 )
406
407 assert out.shape == (E, K, N)
408 assert torch.allclose(out, ref, atol=0), (
409 f"max diff: {(out - ref).abs().max().item()}"
410 )
411
412
413 @pytest.mark.parametrize("E, K, N, group_size", SHAPES)
414 def test_awq_unpack_asymmetric(E, K, N, group_size):
415 """AWQ asymmetric unpacker matches reference."""
416 torch.manual_seed(4)
417 w_fp = torch.randn(E, K, N, dtype=torch.float16, device=device)
418
419 packed_list, scale_list, zero_list, ref_list = [], [], [], []
420 for e in range(E):
421 q, s, z = _quantize_asym(w_fp[e], group_size)
422 packed_list.append(awq_pack(q, 4, K, N))
423 scale_list.append(s)
424 zero_list.append(_pack_awq_zeros(z, N))
425 ref_list.append(_dequantize_ref(q, s, z, output_dtype=torch.float32))
426
427 w_packed = torch.stack(packed_list).to(device)
428 scale = torch.stack(scale_list).to(device)
429 qzeros = torch.stack(zero_list).to(device)
430 ref = torch.stack(ref_list).to(device)
431
432 out = _unpack_and_dequant_int4_awq(
433 w_packed, scale, qzeros, transpose_output=False, output_dtype=torch.float32
434 )
435
436 assert out.shape == (E, K, N)
437 assert torch.allclose(out, ref, atol=0), (
438 f"max diff: {(out - ref).abs().max().item()}"
439 )
440
441
442 @pytest.mark.parametrize("E, K, N, group_size", SHAPES)
443 def test_awq_unpack_transpose(E, K, N, group_size):
444 """AWQ transpose_output=True gives [E, N, K]."""
445 torch.manual_seed(5)
446 w_fp = torch.randn(E, K, N, dtype=torch.float16, device=device)
447
448 packed_list, scale_list = [], []
449 for e in range(E):
450 q, s = _quantize_sym(w_fp[e], group_size)
451 packed_list.append(awq_pack(q, 4, K, N))
452 scale_list.append(s)
453
454 w_packed = torch.stack(packed_list).to(device)
455 scale = torch.stack(scale_list).to(device)
456
457 out_normal = _unpack_and_dequant_int4_awq(w_packed, scale, None, False)
458 out_transposed = _unpack_and_dequant_int4_awq(w_packed, scale, None, True)
459
460 assert out_transposed.shape == (E, N, K)
461 assert torch.allclose(
462 out_transposed, out_normal.permute(0, 2, 1).contiguous(), atol=0
463 )
464
465
466 @pytest.mark.parametrize("E, K, N, group_size", SHAPES)
467 def test_awq_gptq_unpack_agree(E, K, N, group_size):
468 """AWQ and GPTQ unpackers produce identical values for the same weights."""
469 torch.manual_seed(6)
470 w_fp = torch.randn(E, K, N, dtype=torch.float16, device=device)
471
472 gptq_list, awq_list, scale_list = [], [], []
473 for e in range(E):
474 q, s = _quantize_sym(w_fp[e], group_size)
475 gptq_list.append(gptq_pack(q, 4, K, N))
476 awq_list.append(awq_pack(q, 4, K, N))
477 scale_list.append(s)
478
479 scale = torch.stack(scale_list).to(device)
480 out_gptq = _unpack_and_dequant_int4_gptq(
481 torch.stack(gptq_list).to(device), scale, None, False, torch.float32
482 )
483 out_awq = _unpack_and_dequant_int4_awq(
484 torch.stack(awq_list).to(device), scale, None, False, torch.float32
485 )
486
487 assert torch.allclose(out_gptq, out_awq, atol=0), (
488 f"max diff: {(out_gptq - out_awq).abs().max().item()}"
489 )
490
491
492 # ---------------------------------------------------------------------------
493 # Tests: _process_weights_emulation_{gptq,awq}
494 # ---------------------------------------------------------------------------
495
496
497 @pytest.mark.parametrize("E, K, N, group_size", SHAPES)
498 @pytest.mark.parametrize("asym", [False, True], ids=["sym", "asym"])
499 def test_gptq_process_weights_shapes_and_values(E, K, N, group_size, asym):
500 """_process_weights_emulation_gptq shapes and values match reference."""
501 w13, w13s, w13z, w13_ref, w2, w2s, w2z, w2_ref = _make_gptq_moe_weights(
502 E, K, N, group_size, asym
503 )
504 gptq_inputs = _canonicalize_gptq_moe_inputs(w13, w2, w13s, w2s, w13z, w2z)
505 result = _process_weights_emulation_gptq(*gptq_inputs)
506 w13_out, w2_out = result[0], result[1]
507
508 assert w13_out.shape == (E, 2 * N, K)
509 assert w2_out.shape == (E, K, N)
510 assert w13_out.dtype == torch.bfloat16
511 assert w2_out.dtype == torch.bfloat16
512
513 expected_w13 = w13_ref.permute(0, 2, 1)
514 expected_w2 = w2_ref.permute(0, 2, 1)
515
516 assert torch.allclose(w13_out.float(), expected_w13.float(), atol=0), (
517 f"w13 max diff: {(w13_out.float() - expected_w13.float()).abs().max().item()}"
518 )
519 assert torch.allclose(w2_out.float(), expected_w2.float(), atol=0), (
520 f"w2 max diff: {(w2_out.float() - expected_w2.float()).abs().max().item()}"
521 )
522
523
524 @pytest.mark.parametrize("E, K, N, group_size", SHAPES)
525 @pytest.mark.parametrize("asym", [False, True], ids=["sym", "asym"])
526 def test_awq_process_weights_shapes_and_values(E, K, N, group_size, asym):
527 """_process_weights_emulation_awq shapes and values match reference."""
528 w13, w13s, w13z, w13_ref, w2, w2s, w2z, w2_ref = _make_awq_moe_weights(
529 E, K, N, group_size, asym
530 )
531 result = _process_weights_emulation_awq(w13, w2, w13s, w2s, w13z, w2z)
532 w13_out, w2_out = result[0], result[1]
533
534 assert w13_out.shape == (E, 2 * N, K)
535 assert w2_out.shape == (E, K, N)
536 assert w13_out.dtype == torch.bfloat16
537 assert w2_out.dtype == torch.bfloat16
538
539 expected_w13 = w13_ref.permute(0, 2, 1)
540 expected_w2 = w2_ref.permute(0, 2, 1)
541
542 assert torch.allclose(w13_out.float(), expected_w13.float(), atol=0), (
543 f"w13 max diff: {(w13_out.float() - expected_w13.float()).abs().max().item()}"
544 )
545 assert torch.allclose(w2_out.float(), expected_w2.float(), atol=0), (
546 f"w2 max diff: {(w2_out.float() - expected_w2.float()).abs().max().item()}"
547 )
548
549
550 @pytest.mark.parametrize("E, K, N, group_size", SHAPES)
551 @pytest.mark.parametrize("asym", [False, True], ids=["sym", "asym"])
552 def test_gptq_awq_process_weights_agree(E, K, N, group_size, asym):
553 """AWQ and GPTQ process_weights produce identical dequantized tensors."""
554 torch.manual_seed(9)
555 w13_fp = torch.randn(E, K, 2 * N, dtype=torch.float16, device=device)
556 w2_fp = torch.randn(E, N, K, dtype=torch.float16, device=device)
557
558 g13_list, g13s_list, g13z_list = [], [], []
559 a13_list, a13s_list, a13z_list = [], [], []
560 g2_list, g2s_list, g2z_list = [], [], []
561 a2_list, a2s_list, a2z_list = [], [], []
562
563 for e in range(E):
564 if asym:
565 q13, s13, z13 = _quantize_asym(w13_fp[e], group_size)
566 q2, s2, z2 = _quantize_asym(w2_fp[e], group_size)
567 g13z_list.append(_pack_gptq_zeros(z13, 2 * N))
568 a13z_list.append(_pack_awq_zeros(z13, 2 * N))
569 g2z_list.append(_pack_gptq_zeros(z2, K))
570 a2z_list.append(_pack_awq_zeros(z2, K))
571 else:
572 q13, s13 = _quantize_sym(w13_fp[e], group_size)
573 q2, s2 = _quantize_sym(w2_fp[e], group_size)
574 g13z_list.append(None)
575 a13z_list.append(None)
576 g2z_list.append(None)
577 a2z_list.append(None)
578
579 g13_list.append(gptq_pack(q13, 4, K, 2 * N))
580 a13_list.append(awq_pack(q13, 4, K, 2 * N))
581 g13s_list.append(s13)
582 a13s_list.append(s13)
583 g2_list.append(gptq_pack(q2, 4, N, K))
584 a2_list.append(awq_pack(q2, 4, N, K))
585 g2s_list.append(s2)
586 a2s_list.append(s2)
587
588 gptq_inputs = _canonicalize_gptq_moe_inputs(
589 torch.stack(g13_list),
590 torch.stack(g2_list),
591 torch.stack(g13s_list),
592 torch.stack(g2s_list),
593 torch.stack(g13z_list) if asym else None,
594 torch.stack(g2z_list) if asym else None,
595 )
596 gptq_res = _process_weights_emulation_gptq(*gptq_inputs)
597 awq_res = _process_weights_emulation_awq(
598 torch.stack(a13_list),
599 torch.stack(a2_list),
600 torch.stack(a13s_list),
601 torch.stack(a2s_list),
602 torch.stack(a13z_list) if asym else None,
603 torch.stack(a2z_list) if asym else None,
604 )
605
606 assert torch.allclose(gptq_res[0].float(), awq_res[0].float(), atol=1e-3), (
607 f"w13 max diff: {(gptq_res[0] - awq_res[0]).float().abs().max().item()}"
608 )
609 assert torch.allclose(gptq_res[1].float(), awq_res[1].float(), atol=1e-3), (
610 f"w2 max diff: {(gptq_res[1] - awq_res[1]).float().abs().max().item()}"
611 )
612
613
614 # ---------------------------------------------------------------------------
615 # End-to-end MoE forward pass tests
616 # ---------------------------------------------------------------------------
617
618
619 @pytest.mark.parametrize("E, K, N, top_k, group_size, num_tokens", E2E_CONFIGS)
620 def test_gptq_vs_awq_forward_agree(E, K, N, top_k, group_size, num_tokens):
621 """GPTQ and AWQ emulation backends produce bit-identical forward outputs."""
622 torch.manual_seed(42)
623 moe_config = _make_moe_config(E, K, N)
624
625 w13_fp = torch.randn(E, K, 2 * N, dtype=torch.float16, device=device)
626 w2_fp = torch.randn(E, N, K, dtype=torch.float16, device=device)
627
628 g13_list, g13s_list, a13_list, a13s_list = [], [], [], []
629 g2_list, g2s_list, a2_list, a2s_list = [], [], [], []
630
631 for e in range(E):
632 q13, s13 = _quantize_sym(w13_fp[e], group_size)
633 q2, s2 = _quantize_sym(w2_fp[e], group_size)
634 g13_list.append(gptq_pack(q13, 4, K, 2 * N))
635 a13_list.append(awq_pack(q13, 4, K, 2 * N))
636 g13s_list.append(s13)
637 a13s_list.append(s13.clone())
638 g2_list.append(gptq_pack(q2, 4, N, K))
639 a2_list.append(awq_pack(q2, 4, N, K))
640 g2s_list.append(s2)
641 a2s_list.append(s2.clone())
642
643 gptq_inputs = _canonicalize_gptq_moe_inputs(
644 torch.stack(g13_list),
645 torch.stack(g2_list),
646 torch.stack(g13s_list),
647 torch.stack(g2s_list),
648 None,
649 None,
650 )
651 gptq_res = _process_weights_emulation_gptq(*gptq_inputs)
652 awq_res = _process_weights_emulation_awq(
653 torch.stack(a13_list),
654 torch.stack(a2_list),
655 torch.stack(a13s_list),
656 torch.stack(a2s_list),
657 None,
658 None,
659 )
660 w13_gptq, w2_gptq = gptq_res[0], gptq_res[1]
661 w13_awq, w2_awq = awq_res[0], awq_res[1]
662
663 dummy_scale = torch.ones(1, dtype=torch.float16, device=device)
664 experts_gptq = Int4EmulationTritonExperts(
665 moe_config, int4_w4a16_moe_quant_config(dummy_scale, dummy_scale)
666 )
667 experts_awq = Int4EmulationTritonExperts(
668 moe_config, int4_w4a16_moe_quant_config(dummy_scale, dummy_scale)
669 )
670
671 hidden_states = torch.randn(num_tokens, K, dtype=torch.bfloat16, device=device)
672 topk_weights = torch.softmax(
673 torch.randn(num_tokens, top_k, dtype=torch.float32, device=device), dim=-1
674 )
675 topk_ids = torch.stack(
676 [torch.randperm(E, device=device)[:top_k] for _ in range(num_tokens)]
677 ).to(torch.int32)
678
679 out_gptq = _run_emulation_forward(
680 experts_gptq, w13_gptq, w2_gptq, hidden_states, topk_weights, topk_ids, E, K, N
681 )
682 out_awq = _run_emulation_forward(
683 experts_awq, w13_awq, w2_awq, hidden_states, topk_weights, topk_ids, E, K, N
684 )
685
686 assert torch.allclose(out_gptq, out_awq, atol=0), (
687 f"max diff: {(out_gptq - out_awq).abs().max().item()}"
688 )
689
690
691 # ---------------------------------------------------------------------------
692 # EP (Expert Parallelism) tests
693 # ---------------------------------------------------------------------------
694
695 # (E, K, N, top_k, group_size, num_tokens, ep_size)
696 EP_CONFIGS = [
697 pytest.param(4, 64, 32, 2, 32, 8, 2, id="E4-ep2"),
698 pytest.param(8, 64, 32, 2, 32, 16, 4, id="E8-ep4"),
699 pytest.param(8, 128, 64, 2, 64, 16, 2, id="E8-ep2"),
700 ]
701
702
703 def _make_expert_map(global_num_experts: int, start: int, end: int) -> torch.Tensor:
704 """Build expert_map for a rank that owns experts [start, end)."""
705 expert_map = torch.full((global_num_experts,), -1, dtype=torch.int32, device=device)
706 expert_map[start:end] = torch.arange(end - start, dtype=torch.int32, device=device)
707 return expert_map
708
709
710 def _run_emulation_forward_ep(
711 experts,
712 w13_bf16,
713 w2_bf16,
714 hidden_states,
715 topk_weights,
716 topk_ids,
717 global_num_experts,
718 expert_map,
719 ):
720 """Run forward with EP expert_map; returns output tensor."""
721 T, K = hidden_states.shape
722 N = w2_bf16.shape[2]
723 ws13_size = T * topk_ids.shape[1] * max(N, K)
724 ws2_size = T * topk_ids.shape[1] * max(2 * N, K)
725 workspace13 = torch.zeros(ws13_size, dtype=hidden_states.dtype, device=device)
726 workspace2 = torch.zeros(ws2_size, dtype=hidden_states.dtype, device=device)
727 output = torch.zeros(T, K, dtype=hidden_states.dtype, device=device)
728 experts.apply(
729 output=output,
730 hidden_states=hidden_states,
731 w1=w13_bf16,
732 w2=w2_bf16,
733 topk_weights=topk_weights,
734 topk_ids=topk_ids,
735 activation=MoEActivation.SILU,
736 global_num_experts=global_num_experts,
737 expert_map=expert_map,
738 a1q_scale=None,
739 a2_scale=None,
740 workspace13=workspace13,
741 workspace2=workspace2,
742 expert_tokens_meta=None,
743 apply_router_weight_on_input=False,
744 )
745 return output
746
747
748 @pytest.mark.parametrize("E, K, N, top_k, group_size, num_tokens, ep_size", EP_CONFIGS)
749 @pytest.mark.parametrize("fmt", ["gptq", "awq"])
750 def test_ep_output_matches_no_ep(E, K, N, top_k, group_size, num_tokens, ep_size, fmt):
751 """EP simulation: sum of per-rank outputs equals the no-EP forward pass."""
752 assert E % ep_size == 0
753 num_local = E // ep_size
754
755 torch.manual_seed(20)
756
757 # Build all expert weights in BF16 (no-EP reference)
758 w13_fp = torch.randn(E, K, 2 * N, dtype=torch.float16, device=device) * 0.02
759 w2_fp = torch.randn(E, N, K, dtype=torch.float16, device=device) * 0.02
760
761 packed13_list, scales13_list, packed2_list, scales2_list = [], [], [], []
762 for e in range(E):
763 q13, s13 = _quantize_sym(w13_fp[e].float(), group_size)
764 q2, s2 = _quantize_sym(w2_fp[e].float(), group_size)
765 if fmt == "gptq":
766 packed13_list.append(gptq_pack(q13, 4, K, 2 * N))
767 packed2_list.append(gptq_pack(q2, 4, N, K))
768 else:
769 packed13_list.append(awq_pack(q13, 4, K, 2 * N))
770 packed2_list.append(awq_pack(q2, 4, N, K))
771 scales13_list.append(s13)
772 scales2_list.append(s2)
773
774 process_fn = (
775 _process_weights_emulation_gptq
776 if fmt == "gptq"
777 else _process_weights_emulation_awq
778 )
779 process_inputs = (
780 torch.stack(packed13_list),
781 torch.stack(packed2_list),
782 torch.stack(scales13_list),
783 torch.stack(scales2_list),
784 None,
785 None,
786 )
787 if fmt == "gptq":
788 process_inputs = _canonicalize_gptq_moe_inputs(*process_inputs)
789 res = process_fn(*process_inputs)
790 w13_all, w2_all = res[0], res[1] # [E, 2N, K], [E, K, N]
791
792 hidden_states = torch.randn(num_tokens, K, dtype=torch.bfloat16, device=device)
793 topk_weights = torch.softmax(
794 torch.randn(num_tokens, top_k, dtype=torch.float32, device=device), dim=-1
795 )
796 topk_ids = torch.stack(
797 [torch.randperm(E, device=device)[:top_k] for _ in range(num_tokens)]
798 ).to(torch.int32)
799
800 # No-EP reference
801 dummy_scale = torch.ones(1, dtype=torch.float16, device=device)
802 moe_config_full = FusedMoEConfig(
803 num_experts=E,
804 experts_per_token=top_k,
805 hidden_dim=K,
806 intermediate_size=N,
807 num_local_experts=E,
808 num_logical_experts=E,
809 moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
810 activation=MoEActivation.SILU,
811 in_dtype=torch.bfloat16,
812 device=device,
813 routing_method=RoutingMethodType.TopK,
814 max_num_tokens=512,
815 )
816 experts_ref = Int4EmulationTritonExperts(
817 moe_config_full, int4_w4a16_moe_quant_config(dummy_scale, dummy_scale)
818 )
819 out_no_ep = _run_emulation_forward(
820 experts_ref, w13_all, w2_all, hidden_states, topk_weights, topk_ids, E, K, N
821 )
822
823 # EP simulation: sum contributions from each rank
824 out_ep_sum = torch.zeros(num_tokens, K, dtype=torch.bfloat16, device=device)
825 for rank in range(ep_size):
826 start = rank * num_local
827 end = start + num_local
828 expert_map = _make_expert_map(E, start, end)
829 moe_config_ep = FusedMoEConfig(
830 num_experts=E,
831 experts_per_token=top_k,
832 hidden_dim=K,
833 intermediate_size=N,
834 num_local_experts=num_local,
835 num_logical_experts=E,
836 moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
837 activation=MoEActivation.SILU,
838 in_dtype=torch.bfloat16,
839 device=device,
840 routing_method=RoutingMethodType.TopK,
841 max_num_tokens=512,
842 )
843 experts_ep = Int4EmulationTritonExperts(
844 moe_config_ep, int4_w4a16_moe_quant_config(dummy_scale, dummy_scale)
845 )
846 out_rank = _run_emulation_forward_ep(
847 experts_ep,
848 w13_all[start:end],
849 w2_all[start:end],
850 hidden_states,
851 topk_weights,
852 topk_ids,
853 global_num_experts=E,
854 expert_map=expert_map,
855 )
856 out_ep_sum = out_ep_sum + out_rank
857
858 assert torch.allclose(out_ep_sum, out_no_ep, atol=1e-3), (
859 f"[{fmt}] EP sum max diff: {(out_ep_sum - out_no_ep).abs().max().item():.6f}"
860 )
861
862
863 @pytest.mark.parametrize("E, K, N, top_k, group_size, num_tokens, ep_size", EP_CONFIGS)
864 def test_ep_gptq_awq_agree(E, K, N, top_k, group_size, num_tokens, ep_size):
865 """With EP, GPTQ and AWQ emulation produce the same outputs per rank."""
866 assert E % ep_size == 0
867 num_local = E // ep_size
868
869 torch.manual_seed(21)
870 w13_fp = torch.randn(E, K, 2 * N, dtype=torch.float16, device=device) * 0.02
871 w2_fp = torch.randn(E, N, K, dtype=torch.float16, device=device) * 0.02
872
873 g13_list, g13s_list, g2_list, g2s_list = [], [], [], []
874 a13_list, a13s_list, a2_list, a2s_list = [], [], [], []
875 for e in range(E):
876 q13, s13 = _quantize_sym(w13_fp[e].float(), group_size)
877 q2, s2 = _quantize_sym(w2_fp[e].float(), group_size)
878 g13_list.append(gptq_pack(q13, 4, K, 2 * N))
879 a13_list.append(awq_pack(q13, 4, K, 2 * N))
880 g13s_list.append(s13)
881 a13s_list.append(s13.clone())
882 g2_list.append(gptq_pack(q2, 4, N, K))
883 a2_list.append(awq_pack(q2, 4, N, K))
884 g2s_list.append(s2)
885 a2s_list.append(s2.clone())
886
887 gptq_inputs = _canonicalize_gptq_moe_inputs(
888 torch.stack(g13_list),
889 torch.stack(g2_list),
890 torch.stack(g13s_list),
891 torch.stack(g2s_list),
892 None,
893 None,
894 )
895 gptq_res = _process_weights_emulation_gptq(*gptq_inputs)
896 awq_res = _process_weights_emulation_awq(
897 torch.stack(a13_list),
898 torch.stack(a2_list),
899 torch.stack(a13s_list),
900 torch.stack(a2s_list),
901 None,
902 None,
903 )
904 w13_gptq, w2_gptq = gptq_res[0], gptq_res[1]
905 w13_awq, w2_awq = awq_res[0], awq_res[1]
906
907 hidden_states = torch.randn(num_tokens, K, dtype=torch.bfloat16, device=device)
908 topk_weights = torch.softmax(
909 torch.randn(num_tokens, top_k, dtype=torch.float32, device=device), dim=-1
910 )
911 topk_ids = torch.stack(
912 [torch.randperm(E, device=device)[:top_k] for _ in range(num_tokens)]
913 ).to(torch.int32)
914
915 dummy_scale = torch.ones(1, dtype=torch.float16, device=device)
916
917 for rank in range(ep_size):
918 start = rank * num_local
919 end = start + num_local
920 expert_map = _make_expert_map(E, start, end)
921 moe_config_ep = FusedMoEConfig(
922 num_experts=E,
923 experts_per_token=top_k,
924 hidden_dim=K,
925 intermediate_size=N,
926 num_local_experts=num_local,
927 num_logical_experts=E,
928 moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
929 activation=MoEActivation.SILU,
930 in_dtype=torch.bfloat16,
931 device=device,
932 routing_method=RoutingMethodType.TopK,
933 max_num_tokens=512,
934 )
935 experts_gptq = Int4EmulationTritonExperts(
936 moe_config_ep, int4_w4a16_moe_quant_config(dummy_scale, dummy_scale)
937 )
938 experts_awq = Int4EmulationTritonExperts(
939 moe_config_ep, int4_w4a16_moe_quant_config(dummy_scale, dummy_scale)
940 )
941 out_gptq = _run_emulation_forward_ep(
942 experts_gptq,
943 w13_gptq[start:end],
944 w2_gptq[start:end],
945 hidden_states,
946 topk_weights,
947 topk_ids,
948 E,
949 expert_map,
950 )
951 out_awq = _run_emulation_forward_ep(
952 experts_awq,
953 w13_awq[start:end],
954 w2_awq[start:end],
955 hidden_states,
956 topk_weights,
957 topk_ids,
958 E,
959 expert_map,
960 )
961 assert torch.allclose(out_gptq, out_awq, atol=0), (
962 f"rank={rank} max diff: {(out_gptq - out_awq).abs().max().item()}"
963 )
964
965
966 @pytest.mark.parametrize("E, K, N, top_k, group_size, num_tokens, ep_size", EP_CONFIGS)
967 def test_ep_partial_rank_no_active_experts(
968 E, K, N, top_k, group_size, num_tokens, ep_size
969 ):
970 """A rank that owns no token-selected experts produces an all-zero output."""
971 assert E % ep_size == 0
972 num_local = E // ep_size
973
974 torch.manual_seed(22)
975 w13_fp = torch.randn(E, K, 2 * N, dtype=torch.float16, device=device) * 0.02
976 w2_fp = torch.randn(E, N, K, dtype=torch.float16, device=device) * 0.02
977
978 packed13_list, scales13_list, packed2_list, scales2_list = [], [], [], []
979 for e in range(E):
980 q13, s13 = _quantize_sym(w13_fp[e].float(), group_size)
981 q2, s2 = _quantize_sym(w2_fp[e].float(), group_size)
982 packed13_list.append(gptq_pack(q13, 4, K, 2 * N))
983 packed2_list.append(gptq_pack(q2, 4, N, K))
984 scales13_list.append(s13)
985 scales2_list.append(s2)
986
987 gptq_inputs = _canonicalize_gptq_moe_inputs(
988 torch.stack(packed13_list),
989 torch.stack(packed2_list),
990 torch.stack(scales13_list),
991 torch.stack(scales2_list),
992 None,
993 None,
994 )
995 res = _process_weights_emulation_gptq(*gptq_inputs)
996 w13_all, w2_all = res[0], res[1]
997
998 # Force topk_ids to only use experts in [0, num_local) — rank 0's slice
999 topk_ids = torch.zeros(num_tokens, top_k, dtype=torch.int32, device=device)
1000 topk_weights = torch.softmax(
1001 torch.randn(num_tokens, top_k, dtype=torch.float32, device=device), dim=-1
1002 )
1003 hidden_states = torch.randn(num_tokens, K, dtype=torch.bfloat16, device=device)
1004
1005 dummy_scale = torch.ones(1, dtype=torch.float16, device=device)
1006
1007 # Last rank owns experts [E-num_local, E), tokens only route to [0, num_local)
1008 last_rank = ep_size - 1
1009 start = last_rank * num_local
1010 end = E
1011 expert_map = _make_expert_map(E, start, end)
1012 moe_config_ep = FusedMoEConfig(
1013 num_experts=E,
1014 experts_per_token=top_k,
1015 hidden_dim=K,
1016 intermediate_size=N,
1017 num_local_experts=num_local,
1018 num_logical_experts=E,
1019 moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
1020 activation=MoEActivation.SILU,
1021 in_dtype=torch.bfloat16,
1022 device=device,
1023 routing_method=RoutingMethodType.TopK,
1024 max_num_tokens=512,
1025 )
1026 experts_ep = Int4EmulationTritonExperts(
1027 moe_config_ep, int4_w4a16_moe_quant_config(dummy_scale, dummy_scale)
1028 )
1029 out = _run_emulation_forward_ep(
1030 experts_ep,
1031 w13_all[start:end],
1032 w2_all[start:end],
1033 hidden_states,
1034 topk_weights,
1035 topk_ids,
1036 E,
1037 expert_map,
1038 )
1039 assert torch.all(out == 0), (
1040 f"Expected zeros for inactive rank, got max={out.abs().max().item()}"
1041 )
1042
1043
1044 @pytest.mark.parametrize("E, K, N, top_k, group_size, num_tokens, ep_size", EP_CONFIGS)
1045 def test_ep_sum_equals_full_forward(E, K, N, top_k, group_size, num_tokens, ep_size):
1046 """With fixed routing, EP rank outputs sum to the single-rank full forward."""
1047 assert E % ep_size == 0
1048 num_local = E // ep_size
1049
1050 torch.manual_seed(23)
1051 w13_fp = torch.randn(E, K, 2 * N, dtype=torch.float16, device=device) * 0.02
1052 w2_fp = torch.randn(E, N, K, dtype=torch.float16, device=device) * 0.02
1053
1054 packed13_list, scales13_list, packed2_list, scales2_list = [], [], [], []
1055 for e in range(E):
1056 q13, s13 = _quantize_sym(w13_fp[e].float(), group_size)
1057 q2, s2 = _quantize_sym(w2_fp[e].float(), group_size)
1058 packed13_list.append(gptq_pack(q13, 4, K, 2 * N))
1059 packed2_list.append(gptq_pack(q2, 4, N, K))
1060 scales13_list.append(s13)
1061 scales2_list.append(s2)
1062
1063 gptq_inputs = _canonicalize_gptq_moe_inputs(
1064 torch.stack(packed13_list),
1065 torch.stack(packed2_list),
1066 torch.stack(scales13_list),
1067 torch.stack(scales2_list),
1068 None,
1069 None,
1070 )
1071 res = _process_weights_emulation_gptq(*gptq_inputs)
1072 w13_all, w2_all = res[0], res[1]
1073
1074 # Fix routing so every token uses exactly 2 consecutive experts (round-robin)
1075 hidden_states = torch.randn(num_tokens, K, dtype=torch.bfloat16, device=device)
1076 topk_ids = torch.stack(
1077 [
1078 torch.tensor([(t * top_k + k) % E for k in range(top_k)], dtype=torch.int32)
1079 for t in range(num_tokens)
1080 ]
1081 ).to(device)
1082 topk_weights = torch.full((num_tokens, top_k), 1.0 / top_k, device=device)
1083
1084 dummy_scale = torch.ones(1, dtype=torch.float16, device=device)
1085
1086 # Full (no-EP) reference
1087 moe_config_full = FusedMoEConfig(
1088 num_experts=E,
1089 experts_per_token=top_k,
1090 hidden_dim=K,
1091 intermediate_size=N,
1092 num_local_experts=E,
1093 num_logical_experts=E,
1094 moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
1095 activation=MoEActivation.SILU,
1096 in_dtype=torch.bfloat16,
1097 device=device,
1098 routing_method=RoutingMethodType.TopK,
1099 max_num_tokens=512,
1100 )
1101 experts_full = Int4EmulationTritonExperts(
1102 moe_config_full, int4_w4a16_moe_quant_config(dummy_scale, dummy_scale)
1103 )
1104 out_full = _run_emulation_forward(
1105 experts_full, w13_all, w2_all, hidden_states, topk_weights, topk_ids, E, K, N
1106 )
1107
1108 # EP sum
1109 out_ep_sum = torch.zeros(num_tokens, K, dtype=torch.bfloat16, device=device)
1110 for rank in range(ep_size):
1111 start = rank * num_local
1112 end = start + num_local
1113 expert_map = _make_expert_map(E, start, end)
1114 moe_config_ep = FusedMoEConfig(
1115 num_experts=E,
1116 experts_per_token=top_k,
1117 hidden_dim=K,
1118 intermediate_size=N,
1119 num_local_experts=num_local,
1120 num_logical_experts=E,
1121 moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
1122 activation=MoEActivation.SILU,
1123 in_dtype=torch.bfloat16,
1124 device=device,
1125 routing_method=RoutingMethodType.TopK,
1126 max_num_tokens=512,
1127 )
1128 experts_ep = Int4EmulationTritonExperts(
1129 moe_config_ep, int4_w4a16_moe_quant_config(dummy_scale, dummy_scale)
1130 )
1131 out_rank = _run_emulation_forward_ep(
1132 experts_ep,
1133 w13_all[start:end],
1134 w2_all[start:end],
1135 hidden_states,
1136 topk_weights,
1137 topk_ids,
1138 E,
1139 expert_map,
1140 )
1141 out_ep_sum = out_ep_sum + out_rank
1142
1143 assert torch.allclose(out_ep_sum, out_full, atol=1e-3), (
1144 f"EP sum max diff: {(out_ep_sum - out_full).abs().max().item():.6f}"
1145 )
1146
1147
1148 @pytest.mark.parametrize("E, K, N, top_k, group_size, num_tokens", E2E_CONFIGS)
1149 @pytest.mark.parametrize("fmt", ["gptq", "awq"])
1150 def test_emulation_output_close_to_bf16_reference(
1151 E, K, N, top_k, group_size, num_tokens, fmt
1152 ):
1153 """Emulation output is close to a direct BF16 MoE forward."""
1154 torch.manual_seed(11)
1155 moe_config = _make_moe_config(E, K, N)
1156
1157 w13_fp = torch.randn(E, K, 2 * N, dtype=torch.bfloat16, device=device) * 0.02
1158 w2_fp = torch.randn(E, N, K, dtype=torch.bfloat16, device=device) * 0.02
1159
1160 packed13_list, scales13_list, packed2_list, scales2_list = [], [], [], []
1161 for e in range(E):
1162 q13, s13 = _quantize_sym(w13_fp[e].float(), group_size)
1163 q2, s2 = _quantize_sym(w2_fp[e].float(), group_size)
1164 if fmt == "gptq":
1165 packed13_list.append(gptq_pack(q13, 4, K, 2 * N))
1166 packed2_list.append(gptq_pack(q2, 4, N, K))
1167 else:
1168 packed13_list.append(awq_pack(q13, 4, K, 2 * N))
1169 packed2_list.append(awq_pack(q2, 4, N, K))
1170 scales13_list.append(s13)
1171 scales2_list.append(s2)
1172
1173 process_fn = (
1174 _process_weights_emulation_gptq
1175 if fmt == "gptq"
1176 else _process_weights_emulation_awq
1177 )
1178 process_inputs = (
1179 torch.stack(packed13_list),
1180 torch.stack(packed2_list),
1181 torch.stack(scales13_list),
1182 torch.stack(scales2_list),
1183 None,
1184 None,
1185 )
1186 if fmt == "gptq":
1187 process_inputs = _canonicalize_gptq_moe_inputs(*process_inputs)
1188 res = process_fn(*process_inputs)
1189 w13_bf16, w2_bf16 = res[0], res[1]
1190
1191 dummy_scale = torch.ones(1, dtype=torch.float16, device=device)
1192 experts = Int4EmulationTritonExperts(
1193 moe_config, int4_w4a16_moe_quant_config(dummy_scale, dummy_scale)
1194 )
1195
1196 hidden_states = torch.randn(num_tokens, K, dtype=torch.bfloat16, device=device)
1197 topk_weights = torch.softmax(
1198 torch.randn(num_tokens, top_k, dtype=torch.float32, device=device), dim=-1
1199 )
1200 topk_ids = torch.stack(
1201 [torch.randperm(E, device=device)[:top_k] for _ in range(num_tokens)]
1202 ).to(torch.int32)
1203
1204 out_emulation = _run_emulation_forward(
1205 experts, w13_bf16, w2_bf16, hidden_states, topk_weights, topk_ids, E, K, N
1206 )
1207
1208 ref = torch.zeros(num_tokens, K, dtype=torch.bfloat16, device=device)
1209 for m in range(num_tokens):
1210 acc = torch.zeros(K, dtype=torch.float32, device=device)
1211 for k in range(top_k):
1212 e = topk_ids[m, k].item()
1213 w = topk_weights[m, k].item()
1214 gate_up = hidden_states[m] @ w13_bf16[e].T
1215 gate, up = gate_up.chunk(2)
1216 act = F.silu(gate) * up
1217 acc += w * (act @ w2_bf16[e].T).float()
1218 ref[m] = acc.bfloat16()
1219
1220 rel_l2 = (
1221 torch.norm(out_emulation.float() - ref.float())
1222 / torch.norm(ref.float()).clamp(min=1e-6)
1223 ).item()
1224 assert rel_l2 < 0.15, f"[{fmt}] relative L2 = {rel_l2:.4f} (threshold 0.15)"