[Kernel] Applies routed_scaling_factor internally (#47408)

Signed-off-by: Jee Jee Li <jeejeelee@inferact.ai>

Jee Jee Li committed Jul 7, 2026 at 17:00 UTC 5d23ca47ab597d9a725df1dbf14c9e7877072407
8 files changed +56 -44
csrc/libtorch_stable/moe/moe_ops.h
+2 -1
@@ -15,7 +15,8 @@ void topk_sigmoid(torch::stable::Tensor& topk_weights,
15 torch::stable::Tensor& topk_indices,
16 torch::stable::Tensor& token_expert_indices,
17 torch::stable::Tensor& gating_output, bool renormalize,
18 - std::optional<torch::stable::Tensor> bias);
18 + std::optional<torch::stable::Tensor> bias,
19 + double routed_scaling_factor);
20
21 void topk_softplus_sqrt(
22 torch::stable::Tensor& topk_weights, torch::stable::Tensor& topk_indices,
csrc/libtorch_stable/moe/topk_softmax_kernels.cu
+42 -36
@@ -173,7 +173,8 @@ __launch_bounds__(TPB) __global__ void moeTopK(
173 const int start_expert,
174 const int end_expert,
175 const bool renormalize,
176 - const float* bias)
176 + const float* bias,
177 + const double routed_scaling_factor)
178 {
179
180 using cub_kvp = cub::KeyValuePair<int, float>;
@@ -241,14 +242,16 @@ __launch_bounds__(TPB) __global__ void moeTopK(
242 __syncthreads();
243 }
244
244 - // Renormalize the k weights for this row to sum to 1, if requested.
245 - if (renormalize) {
246 - if (threadIdx.x == 0) {
245 + // Apply renormalization and routed scaling factor to final weights.
246 + if (threadIdx.x == 0) {
247 + float scale = static_cast<float>(routed_scaling_factor);
248 + if (renormalize) {
249 const float denom = selected_sum > 0.f ? selected_sum : 1.f;
248 - for (int k_idx = 0; k_idx < k; ++k_idx) {
249 - const int idx = k * block_row + k_idx;
250 - output[idx] = output[idx] / denom;
251 - }
250 + scale /= denom;
251 + }
252 + for (int k_idx = 0; k_idx < k; ++k_idx) {
253 + const int idx = k * block_row + k_idx;
254 + output[idx] = output[idx] * scale;
255 }
256 }
257 }
@@ -274,7 +277,7 @@ template <int VPT, int NUM_EXPERTS, int WARPS_PER_CTA, int BYTES_PER_LDG, int WA
277 __launch_bounds__(WARPS_PER_CTA* WARP_SIZE_PARAM) __global__
278 void topkGating(const InputType* input, const bool* finished, float* output, const int num_rows, IndType* indices,
279 int* source_rows, const int k, const int start_expert, const int end_expert, const bool renormalize,
277 - const float* bias)
280 + const float* bias, const double routed_scaling_factor)
281 {
282 static_assert(std::is_same_v<InputType, float> || std::is_same_v<InputType, __nv_bfloat16> ||
283 std::is_same_v<InputType, __half>,
@@ -570,17 +573,17 @@ __launch_bounds__(WARPS_PER_CTA* WARP_SIZE_PARAM) __global__
573 }
574 }
575
573 - // Renormalize the k weights for this row to sum to 1, if requested.
574 - if (renormalize) {
575 - if (thread_group_idx == 0)
576 - {
577 - const float denom = selected_sum > 0.f ? selected_sum : 1.f;
578 - for (int k_idx = 0; k_idx < k; ++k_idx)
579 - {
580 - const int idx = k * thread_row + k_idx;
581 - output[idx] = output[idx] / denom;
582 - }
583 - }
576 + // Apply renormalization and routed scaling factor to final weights.
577 + if (thread_group_idx == 0) {
578 + float scale = static_cast<float>(routed_scaling_factor);
579 + if (renormalize) {
580 + const float denom = selected_sum > 0.f ? selected_sum : 1.f;
581 + scale /= denom;
582 + }
583 + for (int k_idx = 0; k_idx < k; ++k_idx) {
584 + const int idx = k * thread_row + k_idx;
585 + output[idx] = output[idx] * scale;
586 + }
587 }
588 }
589
@@ -602,7 +605,7 @@ struct TopkConstants
605 template <int EXPERTS, int WARPS_PER_TB, int WARP_SIZE_PARAM, int MAX_BYTES_PER_LDG, typename IndType, typename InputType, ScoringFunc SF>
606 void topkGatingLauncherHelper(const InputType* input, const bool* finished, float* output, IndType* indices,
607 int* source_row, const int num_rows, const int k, const int start_expert, const int end_expert, const bool renormalize,
605 - const float* bias, cudaStream_t stream)
608 + const float* bias, const double routed_scaling_factor, cudaStream_t stream)
609 {
610 static constexpr int BYTES_PER_LDG = MIN(MAX_BYTES_PER_LDG, sizeof(InputType) * EXPERTS);
611 using Constants = detail::TopkConstants<EXPERTS, BYTES_PER_LDG, WARP_SIZE_PARAM, InputType>;
@@ -613,7 +616,7 @@ void topkGatingLauncherHelper(const InputType* input, const bool* finished, floa
616
617 dim3 block_dim(WARP_SIZE_PARAM, WARPS_PER_TB);
618 topkGating<VPT, EXPERTS, WARPS_PER_TB, BYTES_PER_LDG, WARP_SIZE_PARAM, IndType, InputType, SF><<<num_blocks, block_dim, 0, stream>>>(
616 - input, finished, output, num_rows, indices, source_row, k, start_expert, end_expert, renormalize, bias);
619 + input, finished, output, num_rows, indices, source_row, k, start_expert, end_expert, renormalize, bias, routed_scaling_factor);
620 }
621
622 #ifndef USE_ROCM
@@ -624,7 +627,7 @@ void topkGatingLauncherHelper(const InputType* input, const bool* finished, floa
627 IndType, InputType, SF>( \
628 gating_output, nullptr, topk_weights, topk_indices, \
629 token_expert_indices, num_tokens, topk, 0, num_experts, renormalize, \
627 - bias, stream);
630 + bias, routed_scaling_factor, stream);
631 #else
632 #define LAUNCH_TOPK(NUM_EXPERTS, WARPS_PER_TB, MAX_BYTES) \
633 if (WARP_SIZE == 64) { \
@@ -632,13 +635,13 @@ void topkGatingLauncherHelper(const InputType* input, const bool* finished, floa
635 IndType, InputType, SF>( \
636 gating_output, nullptr, topk_weights, topk_indices, \
637 token_expert_indices, num_tokens, topk, 0, num_experts, renormalize, \
635 - bias, stream); \
638 + bias, routed_scaling_factor, stream); \
639 } else if (WARP_SIZE == 32) { \
640 topkGatingLauncherHelper<NUM_EXPERTS, WARPS_PER_TB, 32, MAX_BYTES, \
641 IndType, InputType, SF>( \
642 gating_output, nullptr, topk_weights, topk_indices, \
643 token_expert_indices, num_tokens, topk, 0, num_experts, renormalize, \
641 - bias, stream); \
644 + bias, routed_scaling_factor, stream); \
645 } else { \
646 assert(false && \
647 "Unsupported warp size. Only 32 and 64 are supported for ROCm"); \
@@ -657,6 +660,7 @@ void topkGatingKernelLauncher(
660 const int topk,
661 const bool renormalize,
662 const float* bias,
663 + const double routed_scaling_factor,
664 cudaStream_t stream) {
665 static constexpr int WARPS_PER_TB = 4;
666 static constexpr int BYTES_PER_LDG_POWER_OF_2 = 16;
@@ -732,7 +736,7 @@ void topkGatingKernelLauncher(
736 }
737 moeTopK<TPB><<<num_tokens, TPB, 0, stream>>>(
738 workspace, nullptr, topk_weights, topk_indices, token_expert_indices,
735 - num_experts, topk, 0, num_experts, renormalize, bias);
739 + num_experts, topk, 0, num_experts, renormalize, bias, routed_scaling_factor);
740 }
741 }
742 }
@@ -750,6 +754,7 @@ void dispatch_topk_launch(
754 torch::stable::Tensor& softmax_workspace,
755 int num_tokens, int num_experts, int topk, bool renormalize,
756 std::optional<torch::stable::Tensor> bias,
757 + double routed_scaling_factor,
758 cudaStream_t stream)
759 {
760 const float* bias_ptr = nullptr;
@@ -772,7 +777,7 @@ void dispatch_topk_launch(
777 token_expert_indices.mutable_data_ptr<int>(),
778 softmax_workspace.mutable_data_ptr<float>(),
779 num_tokens, num_experts, topk, renormalize,
775 - bias_ptr, stream);
780 + bias_ptr, routed_scaling_factor, stream);
781 } else if (topk_indices.scalar_type() == torch::headeronly::ScalarType::UInt32) {
782 vllm::moe::topkGatingKernelLauncher<uint32_t, ComputeType, SF>(
783 reinterpret_cast<const ComputeType*>(gating_output.const_data_ptr()),
@@ -781,7 +786,7 @@ void dispatch_topk_launch(
786 token_expert_indices.mutable_data_ptr<int>(),
787 softmax_workspace.mutable_data_ptr<float>(),
788 num_tokens, num_experts, topk, renormalize,
784 - bias_ptr, stream);
789 + bias_ptr, routed_scaling_factor, stream);
790 } else {
791 STD_TORCH_CHECK(topk_indices.scalar_type() == torch::headeronly::ScalarType::Long);
792 vllm::moe::topkGatingKernelLauncher<int64_t, ComputeType, SF>(
@@ -791,7 +796,7 @@ void dispatch_topk_launch(
796 token_expert_indices.mutable_data_ptr<int>(),
797 softmax_workspace.mutable_data_ptr<float>(),
798 num_tokens, num_experts, topk, renormalize,
794 - bias_ptr, stream);
799 + bias_ptr, routed_scaling_factor, stream);
800 }
801 }
802
@@ -820,15 +825,15 @@ void topk_softmax(
825 if (gating_output.scalar_type() == torch::headeronly::ScalarType::Float) {
826 dispatch_topk_launch<float, vllm::moe::SCORING_SOFTMAX>(gating_output, topk_weights, topk_indices,
827 token_expert_indices, softmax_workspace, num_tokens, num_experts, topk, renormalize,
823 - bias, stream);
828 + bias, 1.0, stream);
829 } else if (gating_output.scalar_type() == torch::headeronly::ScalarType::Half) {
830 dispatch_topk_launch<__half, vllm::moe::SCORING_SOFTMAX>(gating_output, topk_weights, topk_indices,
831 token_expert_indices, softmax_workspace, num_tokens, num_experts, topk, renormalize,
827 - bias, stream);
832 + bias, 1.0, stream);
833 } else if (gating_output.scalar_type() == torch::headeronly::ScalarType::BFloat16) {
834 dispatch_topk_launch<__nv_bfloat16, vllm::moe::SCORING_SOFTMAX>(gating_output, topk_weights, topk_indices,
835 token_expert_indices, softmax_workspace, num_tokens, num_experts, topk, renormalize,
831 - bias, stream);
836 + bias, 1.0, stream);
837 } else {
838 STD_TORCH_CHECK(false, "Unsupported gating_output data type: ", gating_output.scalar_type());
839 }
@@ -840,7 +845,8 @@ void topk_sigmoid(
845 torch::stable::Tensor& token_expert_indices, // [num_tokens, topk]
846 torch::stable::Tensor& gating_output, // [num_tokens, num_experts]
847 bool renormalize,
843 - std::optional<torch::stable::Tensor> bias)
848 + std::optional<torch::stable::Tensor> bias,
849 + double routed_scaling_factor)
850 {
851 const int num_experts = gating_output.size(-1);
852 const auto num_tokens = gating_output.numel() / num_experts;
@@ -859,15 +865,15 @@ void topk_sigmoid(
865 if (gating_output.scalar_type() == torch::headeronly::ScalarType::Float) {
866 dispatch_topk_launch<float, vllm::moe::SCORING_SIGMOID>(gating_output, topk_weights, topk_indices,
867 token_expert_indices, workspace, num_tokens, num_experts, topk, renormalize,
862 - bias, stream);
868 + bias, routed_scaling_factor, stream);
869 } else if (gating_output.scalar_type() == torch::headeronly::ScalarType::Half) {
870 dispatch_topk_launch<__half, vllm::moe::SCORING_SIGMOID>(gating_output, topk_weights, topk_indices,
871 token_expert_indices, workspace, num_tokens, num_experts, topk, renormalize,
866 - bias, stream);
872 + bias, routed_scaling_factor, stream);
873 } else if (gating_output.scalar_type() == torch::headeronly::ScalarType::BFloat16) {
874 dispatch_topk_launch<__nv_bfloat16, vllm::moe::SCORING_SIGMOID>(gating_output, topk_weights, topk_indices,
875 token_expert_indices, workspace, num_tokens, num_experts, topk, renormalize,
870 - bias, stream);
876 + bias, routed_scaling_factor, stream);
877 } else {
878 STD_TORCH_CHECK(false, "Unsupported gating_output data type: ", gating_output.scalar_type());
879 }
csrc/libtorch_stable/moe/torch_bindings.cpp
+2 -2
@@ -13,8 +13,8 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_moe_C, m) {
13 // Apply topk sigmoid to the gating outputs.
14 m.def(
15 "topk_sigmoid(Tensor! topk_weights, Tensor! topk_indices, Tensor! "
16 - "token_expert_indices, Tensor gating_output, bool renormalize, Tensor? "
17 - "bias) -> ()");
16 + "token_expert_indices, Tensor gating_output, bool renormalize, "
17 + "Tensor? bias, float routed_scaling_factor) -> ()");
18
19 m.def(
20 "topk_softplus_sqrt(Tensor! topk_weights, Tensor! topk_indices, Tensor! "
vllm/_custom_ops.py
+2
@@ -2244,6 +2244,7 @@ def topk_sigmoid(
2244 gating_output: torch.Tensor,
2245 renormalize: bool = False,
2246 e_score_correction_bias: torch.Tensor | None = None,
2247 + routed_scaling_factor: float = 1.0,
2248 ) -> None:
2249 torch.ops._moe_C.topk_sigmoid(
2250 topk_weights,
@@ -2252,6 +2253,7 @@ def topk_sigmoid(
2253 gating_output,
2254 renormalize,
2255 e_score_correction_bias,
2256 + routed_scaling_factor,
2257 )
2258
2259
vllm/model_executor/layers/fused_moe/router/fused_topk_bias_router.py
+3 -2
@@ -43,6 +43,7 @@ def vllm_topk_sigmoid(
43 gating_output: torch.Tensor,
44 renormalize: bool = False,
45 e_score_correction_bias: torch.Tensor | None = None,
46 + routed_scaling_factor: float = 1.0,
47 ) -> tuple[torch.Tensor, ...]:
48 ops.topk_sigmoid(
49 topk_weights,
@@ -51,6 +52,7 @@ def vllm_topk_sigmoid(
52 gating_output,
53 renormalize,
54 e_score_correction_bias,
55 + routed_scaling_factor,
56 )
57
58 return topk_weights, topk_indices
@@ -216,9 +218,8 @@ def fused_topk_bias(
218 gating_output,
219 renormalize,
220 e_score_correction_bias,
221 + routed_scaling_factor,
222 )
220 - if routed_scaling_factor != 1.0:
221 - topk_weights *= routed_scaling_factor
223 return topk_weights, topk_ids
224 elif scoring_func == "sqrtsoftplus":
225 return vllm_topk_softplus_sqrt(
vllm/model_executor/models/deepseek_v2.py
+4 -2
@@ -280,6 +280,7 @@ class DeepseekV2MoE(nn.Module):
280 parallel_config: ParallelConfig,
281 quant_config: QuantizationConfig | None = None,
282 prefix: str = "",
283 + apply_routed_scale_to_output: bool = False,
284 ):
285 super().__init__()
286 self.tp_size = get_tensor_model_parallel_world_size()
@@ -372,9 +373,8 @@ class DeepseekV2MoE(nn.Module):
373 topk_group=getattr(config, "topk_group", 1),
374 prefix=f"{prefix}.experts",
375 scoring_func=getattr(config, "scoring_func", "softmax"),
375 - # aiter applies routed_scaling_factor internally
376 routed_scaling_factor=self.routed_scaling_factor,
377 - apply_routed_scale_to_output=not self.is_rocm_aiter_moe_enabled,
377 + apply_routed_scale_to_output=apply_routed_scale_to_output,
378 e_score_correction_bias=self.gate.e_score_correction_bias,
379 enable_eplb=self.enable_eplb,
380 num_redundant_experts=self.n_redundant_experts,
@@ -1246,6 +1246,8 @@ class DeepseekV2DecoderLayer(nn.Module):
1246 parallel_config=parallel_config,
1247 quant_config=quant_config,
1248 prefix=f"{prefix}.mlp",
1249 + # aiter applies routed_scaling_factor internally
1250 + apply_routed_scale_to_output=not rocm_aiter_ops.is_fused_moe_enabled(),
1251 )
1252 else:
1253 self.mlp = DeepseekV2MLP(
vllm/models/deepseek_v32/nvidia/model.py
+1
@@ -77,6 +77,7 @@ class DeepseekV32DecoderLayer(torch.nn.Module):
77 parallel_config=parallel_config,
78 quant_config=quant_config,
79 prefix=f"{prefix}.mlp",
80 + apply_routed_scale_to_output=False,
81 )
82 # Defer the MoE cross-rank all-reduce; it is fused into the next
83 # layer's input_layernorm (or the final norm) via
vllm/models/minimax_m3/nvidia/model.py
-1
@@ -258,7 +258,6 @@ class MiniMaxM3MoE(nn.Module):
258 swiglu_alpha=config.swiglu_alpha,
259 swiglu_beta=config.swiglu_beta,
260 routed_scaling_factor=self.routed_scaling_factor,
261 - apply_routed_scale_to_output=True,
261 router_logits_dtype=self.gate.out_dtype,
262 shared_experts=self.shared_experts,
263 quant_config=quant_config,