@@ -376,6 +376,7 @@ def test_per_token_loss_path(self, mock_finalize, mock_dp, mock_in_grid, mock_di
376376 s = _build_two_module_setup (llm_dp = 2 , encoder_dp = 4 )
377377 mock_in_grid .return_value = True
378378 mock_dp .side_effect = lambda grid : s .dp_by_grid [id (grid )]
379+ s .encoder_module .config .calculate_per_token_loss = True
379380
380381 num_tokens = torch .tensor (100 )
381382
@@ -405,6 +406,58 @@ def test_per_token_loss_path(self, mock_finalize, mock_dp, mock_in_grid, mock_di
405406 s .encoder_module .scale_gradients .assert_called_once_with (1.0 / 100 )
406407 s .llm_module .scale_gradients .assert_not_called ()
407408
409+ @pytest .mark .parametrize (
410+ ("language_per_token" , "encoder_per_token" , "expected_gradient" ),
411+ [
412+ (True , False , 4.0 ),
413+ (False , True , 200.0 ),
414+ ],
415+ )
416+ @patch (f"{ MODULE } .dist" )
417+ @patch (f"{ MODULE } .is_current_rank_in_grid" )
418+ @patch (f"{ MODULE } ._get_dp_size_from_grid" )
419+ @patch (f"{ MODULE } ._finalize_model_grads" )
420+ def test_mixed_per_token_configs_preserve_encoder_gradient_normalization (
421+ self ,
422+ mock_finalize ,
423+ mock_dp ,
424+ mock_in_grid ,
425+ mock_dist ,
426+ language_per_token ,
427+ encoder_per_token ,
428+ expected_gradient ,
429+ ):
430+ """Module-local DDP scaling must not change the global encoder gradient."""
431+ from megatron .bridge .training .megatron_mimo_parallel_utils import finalize_model_grads_multimodule
432+
433+ s = _build_two_module_setup (llm_dp = 2 , encoder_dp = 4 )
434+ mock_in_grid .return_value = True
435+ mock_dp .side_effect = lambda grid : s .dp_by_grid [id (grid )]
436+ s .llm_module .config .calculate_per_token_loss = language_per_token
437+ s .encoder_module .config .calculate_per_token_loss = encoder_per_token
438+
439+ # Simulate the value after MCore DDP reduction for a global encoder
440+ # gradient sum of 400. Non-per-token DDP averages over encoder DP;
441+ # per-token DDP leaves the global sum intact.
442+ encoder_gradient = torch .tensor (400.0 )
443+
444+ def finalize_with_module_local_ddp (model , ** kwargs ):
445+ if model [0 ] is s .encoder_module and not encoder_per_token :
446+ encoder_gradient .div_ (s .dp_by_grid [id (s .encoder_grid )])
447+
448+ mock_finalize .side_effect = finalize_with_module_local_ddp
449+ s .encoder_module .scale_gradients .side_effect = encoder_gradient .mul_
450+
451+ num_tokens = torch .tensor (100 ) if language_per_token else None
452+ finalize_model_grads_multimodule (
453+ [MagicMock ()],
454+ num_tokens ,
455+ infra = s .infra ,
456+ module_to_grid_tuple = s .module_to_grid_tuple ,
457+ )
458+
459+ assert encoder_gradient .item () == pytest .approx (expected_gradient )
460+
408461 @patch (f"{ MODULE } .dist" )
409462 @patch (f"{ MODULE } .is_current_rank_in_grid" )
410463 @patch (f"{ MODULE } ._get_dp_size_from_grid" )
@@ -420,6 +473,8 @@ def test_non_per_token_loss_path_applies_dp_compensation(self, mock_finalize, mo
420473 s = _build_two_module_setup (llm_dp = 2 , encoder_dp = 4 )
421474 mock_in_grid .return_value = True
422475 mock_dp .side_effect = lambda grid : s .dp_by_grid [id (grid )]
476+ s .llm_module .config .calculate_per_token_loss = False
477+ s .encoder_module .config .calculate_per_token_loss = False
423478
424479 finalize_model_grads_multimodule (
425480 [MagicMock ()],
0 commit comments