@@ -1051,6 +1051,50 @@ def test_router_padding_masking(self):
10511051class TestPositionwiseConvFFMoE :
10521052 """Test the PositionwiseConvFFMoE class."""
10531053
1054+ # Golden expected values captured from the original sequential implementation
1055+ # with set_seed(42), d_model=8, d_ffn=32, batch_size=2, seq_len=10.
1056+ # Each entry: (num_experts, top_k, bias, padding, expected_output_sum,
1057+ # expected_logits_sum, expected_first_expert_indices,
1058+ # expected_output_first_4_elements)
1059+ _GOLDEN_VALUES = {
1060+ "E4_top1_nobias_nopad" : {
1061+ "output_sum" : 1.4807705879211426 ,
1062+ "logits_sum" : - 1.4070085287094116 ,
1063+ "first_idx" : [2 ],
1064+ "output_0_0" : [- 0.031408119946718216 , 0.14679314196109772 , - 0.021915754303336143 , 0.12932124733924866 ],
1065+ },
1066+ "E4_top2_nobias_nopad" : {
1067+ "output_sum" : 1.6757168769836426 ,
1068+ "logits_sum" : - 1.4070085287094116 ,
1069+ "first_idx" : [2 , 0 ],
1070+ "output_0_0" : [- 0.12169260531663895 , - 0.0002511143684387207 , - 0.09718462079763412 , - 0.017200887203216553 ],
1071+ },
1072+ "E8_top1_nobias_pad" : {
1073+ "output_sum" : 3.700800895690918 ,
1074+ "logits_sum" : 8.973369598388672 ,
1075+ "first_idx" : [4 ],
1076+ "output_0_0" : [- 0.28720206022262573 , 0.20689748227596283 , 0.40565282106399536 , - 0.021458642557263374 ],
1077+ },
1078+ "E8_top2_nobias_pad" : {
1079+ "output_sum" : 3.0652523040771484 ,
1080+ "logits_sum" : 8.973369598388672 ,
1081+ "first_idx" : [4 , 1 ],
1082+ "output_0_0" : [- 0.2828606963157654 , 0.1696583479642868 , 0.2753525376319885 , - 0.041214004158973694 ],
1083+ },
1084+ "E4_top2_bias_pad" : {
1085+ "output_sum" : - 1.1706292629241943 ,
1086+ "logits_sum" : - 1.2999919652938843 ,
1087+ "first_idx" : [1 , 3 ],
1088+ "output_0_0" : [- 0.10531097650527954 , 0.14638465642929077 , - 0.1260562241077423 , - 0.11432743072509766 ],
1089+ },
1090+ "E16_top1_bias_pad" : {
1091+ "output_sum" : - 0.7250787019729614 ,
1092+ "logits_sum" : 6.78455924987793 ,
1093+ "first_idx" : [8 ],
1094+ "output_0_0" : [0.480951189994812 , - 0.3138628602027893 , - 0.010073505342006683 , - 0.05126545578241348 ],
1095+ },
1096+ }
1097+
10541098 @classmethod
10551099 def setup_class (cls ):
10561100 cls .d_model = 8
@@ -1165,6 +1209,124 @@ def test_moe_ffn_different_expert_counts(self):
11651209
11661210 assert output .shape == x .shape
11671211
1212+ def test_gradient_flow (self ):
1213+ """Backward pass must produce non-zero gradients for router and expert weights."""
1214+ set_seed (42 )
1215+ moe_ffn = PositionwiseConvFFMoE (
1216+ d_model = self .d_model ,
1217+ d_ffn = self .d_ffn ,
1218+ p_dropout = 0.0 ,
1219+ num_experts = self .num_experts ,
1220+ top_k_experts = self .top_k_experts ,
1221+ kernel_size = 1 ,
1222+ )
1223+ moe_ffn .train ()
1224+
1225+ x = torch .randn (self .batch_size , self .seq_len , self .d_model , requires_grad = True )
1226+ x_mask = torch .ones (self .batch_size , self .seq_len )
1227+ output , _ , _ , _ = moe_ffn (x , x_mask )
1228+ loss = output .sum ()
1229+ loss .backward ()
1230+
1231+ assert x .grad is not None and x .grad .abs ().sum () > 0 , "Input grad must be non-zero"
1232+ assert moe_ffn .router .router .weight .grad is not None , "Router weight grad must exist"
1233+ assert moe_ffn .router .router .weight .grad .abs ().sum () > 0 , "Router weight grad must be non-zero"
1234+
1235+ has_expert_grad = False
1236+ for expert in moe_ffn .experts :
1237+ for name in ('proj' , 'o_net' ):
1238+ g = expert [name ].conv .weight .grad
1239+ if g is not None and g .abs ().sum () > 0 :
1240+ has_expert_grad = True
1241+ break
1242+ assert has_expert_grad , "At least one expert weight must receive a gradient"
1243+
1244+ def test_all_padding_produces_zeros (self ):
1245+ """When x_mask is all zeros, output and routing info must be all zeros."""
1246+ set_seed (42 )
1247+ moe_ffn = PositionwiseConvFFMoE (
1248+ d_model = self .d_model ,
1249+ d_ffn = self .d_ffn ,
1250+ p_dropout = 0.0 ,
1251+ num_experts = self .num_experts ,
1252+ top_k_experts = self .top_k_experts ,
1253+ kernel_size = 1 ,
1254+ )
1255+ moe_ffn .eval ()
1256+
1257+ x = torch .randn (self .batch_size , self .seq_len , self .d_model )
1258+ x_mask = torch .zeros (self .batch_size , self .seq_len )
1259+
1260+ with torch .no_grad ():
1261+ output , router_logits , router_probs , expert_indices = moe_ffn (x , x_mask )
1262+
1263+ assert torch .all (output == 0 ), "Output must be all zeros when fully padded"
1264+ assert torch .all (router_logits == 0 ), "Router logits must be all zeros when fully padded"
1265+ assert torch .all (expert_indices == - 1 ), "Expert indices must be -1 when fully padded"
1266+
1267+ @pytest .mark .parametrize (
1268+ "num_experts,top_k,use_bias,use_padding" ,
1269+ [
1270+ (4 , 1 , False , False ),
1271+ (4 , 2 , False , False ),
1272+ (8 , 1 , False , True ),
1273+ (8 , 2 , False , True ),
1274+ (4 , 2 , True , True ),
1275+ (16 , 1 , True , True ),
1276+ ],
1277+ ids = [
1278+ "E4_top1_nobias_nopad" ,
1279+ "E4_top2_nobias_nopad" ,
1280+ "E8_top1_nobias_pad" ,
1281+ "E8_top2_nobias_pad" ,
1282+ "E4_top2_bias_pad" ,
1283+ "E16_top1_bias_pad" ,
1284+ ],
1285+ )
1286+ def test_forward_golden_values (self , num_experts , top_k , use_bias , use_padding , request ):
1287+ """Verify forward() output matches golden expected values.
1288+
1289+ Golden values were captured from the original sequential implementation.
1290+ Any refactoring of forward() must reproduce these exact values (within
1291+ floating-point tolerance) to guarantee numerical equivalence.
1292+ """
1293+ golden = self ._GOLDEN_VALUES [request .node .callspec .id ]
1294+
1295+ set_seed (42 )
1296+ moe_ffn = PositionwiseConvFFMoE (
1297+ d_model = self .d_model ,
1298+ d_ffn = self .d_ffn ,
1299+ p_dropout = 0.0 ,
1300+ num_experts = num_experts ,
1301+ top_k_experts = top_k ,
1302+ kernel_size = 1 ,
1303+ bias = use_bias ,
1304+ )
1305+ moe_ffn .eval ()
1306+
1307+ x = torch .randn (self .batch_size , self .seq_len , self .d_model )
1308+ x_mask = torch .ones (self .batch_size , self .seq_len )
1309+ if use_padding :
1310+ x_mask [0 , 7 :] = 0
1311+ x_mask [1 , 5 :] = 0
1312+
1313+ with torch .no_grad ():
1314+ output , router_logits , router_probs , expert_indices = moe_ffn (x , x_mask )
1315+
1316+ assert (
1317+ expert_indices [0 , 0 ].tolist () == golden ["first_idx" ]
1318+ ), f"Expert indices mismatch: got { expert_indices [0 , 0 ].tolist ()} , expected { golden ['first_idx' ]} "
1319+ assert torch .allclose (
1320+ router_logits .sum (), torch .tensor (golden ["logits_sum" ]), atol = 1e-5
1321+ ), f"Logits sum mismatch: got { router_logits .sum ().item ()} , expected { golden ['logits_sum' ]} "
1322+ assert torch .allclose (
1323+ output .sum (), torch .tensor (golden ["output_sum" ]), atol = 1e-5
1324+ ), f"Output sum mismatch: got { output .sum ().item ()} , expected { golden ['output_sum' ]} "
1325+ expected_elems = torch .tensor (golden ["output_0_0" ])
1326+ assert torch .allclose (
1327+ output [0 , 0 , :4 ], expected_elems , atol = 1e-5
1328+ ), f"Output[0,0,:4] mismatch: got { output [0 , 0 , :4 ].tolist ()} , expected { golden ['output_0_0' ]} "
1329+
11681330
11691331@pytest .mark .unit
11701332class TestTransformerLayerWithMoE :
0 commit comments