Skip to content

Commit 158e09a

Browse files
committed
[ExecuTorch][llm] Add numerical kernel tests for quantized_moe_ffn
Pull Request resolved: #21123 Add TestQuantizedMoeFfnOp: 5 tests comparing custom op output against a Python q-dq reference (sigmoid+bias, sigmoid no-bias, softmax, single-token, route_scale=0.0). ghstack-source-id: 409183076 @exported-using-ghexport Differential Revision: [D102381999](https://our.internmc.facebook.com/intern/diff/D102381999/)
1 parent 043e1bf commit 158e09a

1 file changed

Lines changed: 112 additions & 8 deletions

File tree

extension/llm/custom_ops/test_quantized_moe.py

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,14 @@ def _make_valid_meta_inputs() -> dict:
123123
def _moe_forward_with_qdq_weights(
124124
moe: MOEFeedForward, x: torch.Tensor, group_size: int
125125
) -> torch.Tensor:
126-
"""Run `moe.forward(x)` after replacing each per-expert weight with its
127-
INT4 group-quantize/dequant round-trip. This is the apples-to-apples
128-
reference for the custom op's output.
129-
130-
We deepcopy the real module (so any attributes added by future
131-
`MOEFeedForward.__init__` / `ConditionalFeedForward.__init__` changes
132-
are preserved) and only overwrite the per-expert weights in-place.
126+
"""Independent q-dq reference for `llama::quantized_moe_ffn`.
127+
128+
Applies the same INT4 group-quantize/dequant round-trip to each expert
129+
weight, then reproduces the op's routing contract (see op_moe.cpp) in
130+
eager PyTorch: sigmoid or softmax scoring, expert-bias-shifted top-k
131+
selection, un-biased weight renormalization scaled by route_scale for the
132+
sigmoid path (softmax path softmaxes the top-k raw scores). Expert compute
133+
reuses the q-dq `ConditionalFeedForward`.
133134
"""
134135
moe_q = copy.deepcopy(moe)
135136

@@ -152,7 +153,110 @@ def _qdq_per_expert(w_eFD: torch.Tensor) -> torch.Tensor:
152153
w2_DF_qdq = _qdq_per_expert(w2_DF)
153154
cond_q.w2.copy_(w2_DF_qdq.transpose(-2, -1).contiguous())
154155

155-
return moe_q.forward(x)
156+
scores = moe.gate(x) # [T, E]
157+
k = moe.num_activated_experts
158+
if moe.score_func == "sigmoid":
159+
s = torch.sigmoid(scores)
160+
sel = s + moe.expert_bias if getattr(moe, "use_expert_bias", False) else s
161+
idx = torch.topk(sel, k, dim=-1).indices
162+
weights = torch.gather(s, -1, idx)
163+
weights = (
164+
weights * moe.route_scale / (weights.sum(dim=-1, keepdim=True) + 1e-20)
165+
)
166+
else:
167+
idx = torch.topk(scores, k, dim=-1).indices
168+
weights = torch.gather(scores, -1, idx).softmax(dim=-1)
169+
170+
expert_outs = moe_q.cond_ffn(x, idx) # [T, K, D]
171+
return torch.einsum("tkd,tk->td", expert_outs, weights)
172+
173+
174+
@_REQUIRES_TORCHAO_KERNEL_LIBRARY
175+
class TestQuantizedMoeFfnOp(unittest.TestCase):
176+
"""Numerical correctness vs a Python q-dq reference."""
177+
178+
def setUp(self) -> None:
179+
torch.manual_seed(0)
180+
181+
def _check_against_qdq_reference(
182+
self,
183+
*,
184+
score_func: str,
185+
use_expert_bias: bool,
186+
route_scale: float,
187+
num_tokens: int = 8,
188+
atol: float = 5e-3,
189+
) -> None:
190+
dim, hidden_dim = 32, 32
191+
num_experts, num_activated_experts = 4, 2
192+
group_size = 32
193+
194+
moe = _build_moe_eager(
195+
dim=dim,
196+
hidden_dim=hidden_dim,
197+
num_experts=num_experts,
198+
num_activated_experts=num_activated_experts,
199+
score_func=score_func,
200+
use_expert_bias=use_expert_bias,
201+
route_scale=route_scale,
202+
)
203+
x = torch.randn(num_tokens, dim)
204+
205+
with torch.no_grad():
206+
ref = _moe_forward_with_qdq_weights(moe, x, group_size)
207+
208+
qmodel = MOEFeedForward.__new__(MOEFeedForward)
209+
torch.nn.Module.__init__(qmodel)
210+
qmodel.gate = moe.gate
211+
qmodel.cond_ffn = moe.cond_ffn
212+
qmodel.dim = moe.dim
213+
qmodel.num_activated_experts = moe.num_activated_experts
214+
qmodel.score_func = moe.score_func
215+
qmodel.route_scale = moe.route_scale
216+
# `MOEFeedForward` always registers `expert_bias` (None when
217+
# disabled); the transform reads the buffer, so mirror that here.
218+
qmodel.expert_bias = moe.expert_bias if moe.use_expert_bias else None
219+
220+
wrapper = torch.nn.Module()
221+
wrapper.block_sparse_moe = qmodel
222+
replace_moe_with_quantized_op(wrapper, group_size=group_size, weight_nbit=4)
223+
test = wrapper.block_sparse_moe(x)
224+
225+
diff = (ref - test).abs()
226+
self.assertTrue(
227+
diff.max().item() < atol,
228+
f"max abs diff {diff.max().item()} > atol {atol} (mean={diff.mean().item()})",
229+
)
230+
231+
def test_sigmoid_with_bias_route_scale_2p5(self) -> None:
232+
self._check_against_qdq_reference(
233+
score_func="sigmoid", use_expert_bias=True, route_scale=2.5
234+
)
235+
236+
def test_sigmoid_no_bias(self) -> None:
237+
self._check_against_qdq_reference(
238+
score_func="sigmoid", use_expert_bias=False, route_scale=1.0
239+
)
240+
241+
def test_softmax(self) -> None:
242+
self._check_against_qdq_reference(
243+
score_func="softmax", use_expert_bias=False, route_scale=1.0
244+
)
245+
246+
def test_single_token(self) -> None:
247+
self._check_against_qdq_reference(
248+
score_func="sigmoid",
249+
use_expert_bias=True,
250+
route_scale=2.5,
251+
num_tokens=1,
252+
)
253+
254+
def test_route_scale_zero(self) -> None:
255+
self._check_against_qdq_reference(
256+
score_func="sigmoid",
257+
use_expert_bias=False,
258+
route_scale=0.0,
259+
)
156260

157261

158262
@_REQUIRES_TORCHAO_KERNEL_LIBRARY

0 commit comments

Comments
 (0)