@@ -123,13 +123,14 @@ def _make_valid_meta_inputs() -> dict:
123123def _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