@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / 0852527647
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 13, 14:30 UTC

[Perf][DeepSeek] Add sigmoid+bias fusion to fused_grouped_topk from TRTLLM (#28124)

Signed-off-by: mgoin <mgoin64@gmail.com> Co-authored-by: Robert Shaw <114415538+robertgshaw2-redhat@users.noreply.github.com> Co-authored-by: Wentao Ye <44945378+yewentao256@users.noreply.github.com>

Michael Goin committed Nov 8, 2025 at 10:20 UTC 085252764710fdb42ac180983ef4c37da0ed72d3
5 files changed +149 -75
csrc/moe/grouped_topk_kernels.cu
+99 -54
@@ -427,11 +427,29 @@ __device__ inline bool is_finite(const T val) {
427 #endif
428 }
429
430 +// Scoring function enums
431 +enum ScoringFunc {
432 + SCORING_NONE = 0, // no activation function
433 + SCORING_SIGMOID = 1 // apply sigmoid
434 +};
435 +
436 +// Efficient sigmoid approximation from TensorRT-LLM
437 +__device__ inline float sigmoid_accurate(float x) {
438 + return 0.5f * tanhf(0.5f * x) + 0.5f;
439 +}
440 +
441 template <typename T>
431 -__device__ void topk_with_k2(T* output, T const* input,
442 +__device__ inline T apply_sigmoid(T val) {
443 + float f = cuda_cast<float, T>(val);
444 + return cuda_cast<T, float>(sigmoid_accurate(f));
445 +}
446 +
447 +template <typename T>
448 +__device__ void topk_with_k2(T* output, T const* input, T const* bias,
449 cg::thread_block_tile<32> const& tile,
450 int32_t const lane_id,
434 - int const num_experts_per_group) {
451 + int const num_experts_per_group,
452 + int const scoring_func) {
453 // Get the top2 per thread
454 T largest = neg_inf<T>();
455 T second_largest = neg_inf<T>();
@@ -439,6 +457,12 @@ __device__ void topk_with_k2(T* output, T const* input,
457 if (num_experts_per_group > WARP_SIZE) {
458 for (int i = lane_id; i < num_experts_per_group; i += WARP_SIZE) {
459 T value = input[i];
460 + // Apply scoring function if needed
461 + if (scoring_func == SCORING_SIGMOID) {
462 + value = apply_sigmoid(value);
463 + }
464 + value = value + bias[i];
465 +
466 if (value > largest) {
467 second_largest = largest;
468 largest = value;
@@ -448,7 +472,13 @@ __device__ void topk_with_k2(T* output, T const* input,
472 }
473 } else {
474 for (int i = lane_id; i < num_experts_per_group; i += WARP_SIZE) {
451 - largest = input[i];
475 + T value = input[i];
476 + // Apply scoring function if needed
477 + if (scoring_func == SCORING_SIGMOID) {
478 + value = apply_sigmoid(value);
479 + }
480 + value = value + bias[i];
481 + largest = value;
482 }
483 }
484
@@ -472,17 +502,21 @@ __device__ void topk_with_k2(T* output, T const* input,
502 }
503
504 template <typename T>
475 -__global__ void topk_with_k2_kernel(T* output, T* input,
505 +__global__ void topk_with_k2_kernel(T* output, T* input, T const* bias,
506 int64_t const num_tokens,
507 int64_t const num_cases,
508 int64_t const n_group,
479 - int64_t const num_experts_per_group) {
509 + int64_t const num_experts_per_group,
510 + int const scoring_func) {
511 int32_t warp_id = threadIdx.x / WARP_SIZE;
512 int32_t lane_id = threadIdx.x % WARP_SIZE;
513
514 int32_t case_id = blockIdx.x * NUM_WARPS_PER_BLOCK + warp_id;
515 if (case_id < num_cases) {
516 input += case_id * num_experts_per_group;
517 + // bias is per expert group, offset to current group
518 + int32_t group_id = case_id % n_group;
519 + T const* group_bias = bias + group_id * num_experts_per_group;
520 output += case_id;
521
522 cg::thread_block block = cg::this_thread_block();
@@ -491,7 +525,8 @@ __global__ void topk_with_k2_kernel(T* output, T* input,
525 #if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900))
526 asm volatile("griddepcontrol.wait;");
527 #endif
494 - topk_with_k2(output, input, tile, lane_id, num_experts_per_group);
528 + topk_with_k2(output, input, group_bias, tile, lane_id,
529 + num_experts_per_group, scoring_func);
530 }
531 #if (defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900))
532 asm volatile("griddepcontrol.launch_dependents;");
@@ -500,16 +535,15 @@ __global__ void topk_with_k2_kernel(T* output, T* input,
535
536 template <typename T, typename IdxT>
537 __global__ void group_idx_and_topk_idx_kernel(
503 - T* scores, T const* group_scores, T* topk_values, IdxT* topk_indices,
504 - T* scores_with_bias, int64_t const num_tokens, int64_t const n_group,
538 + T* scores, T const* group_scores, float* topk_values, IdxT* topk_indices,
539 + T const* bias, int64_t const num_tokens, int64_t const n_group,
540 int64_t const topk_group, int64_t const topk, int64_t const num_experts,
541 int64_t const num_experts_per_group, bool renormalize,
507 - double routed_scaling_factor) {
542 + double routed_scaling_factor, int scoring_func) {
543 int32_t warp_id = threadIdx.x / WARP_SIZE;
544 int32_t lane_id = threadIdx.x % WARP_SIZE;
545 int32_t case_id =
546 blockIdx.x * NUM_WARPS_PER_BLOCK + warp_id; // one per token
512 - scores_with_bias += case_id * num_experts;
547 scores += case_id * num_experts;
548 group_scores += case_id * n_group;
549 topk_values += case_id * topk;
@@ -577,10 +611,16 @@ __global__ void group_idx_and_topk_idx_kernel(
611 int32_t offset = i_group * num_experts_per_group;
612 for (int32_t i = lane_id; i < align_num_experts_per_group;
613 i += WARP_SIZE) {
580 - T candidates = (i < num_experts_per_group) &&
581 - is_finite(scores_with_bias[offset + i])
582 - ? scores_with_bias[offset + i]
583 - : neg_inf<T>();
614 + T candidates = neg_inf<T>();
615 + if (i < num_experts_per_group) {
616 + // Apply scoring function (if any) and add bias
617 + T input = scores[offset + i];
618 + if (is_finite(input)) {
619 + T score = (scoring_func == SCORING_SIGMOID) ? apply_sigmoid(input)
620 + : input;
621 + candidates = score + bias[offset + i];
622 + }
623 + }
624 queue.add(candidates, offset + i);
625 }
626 if (group_scores[i_group] == topk_group_value) {
@@ -602,11 +642,12 @@ __global__ void group_idx_and_topk_idx_kernel(
642 for (int i = lane_id;
643 i < warp_topk::round_up_to_multiple_of<WARP_SIZE>(topk);
644 i += WARP_SIZE) {
605 - T value =
606 - i < topk
607 - ? scores[s_topk_idx[i]]
608 - : cuda_cast<T, float>(0.0f); // Load the valid value of expert
645 + T value = cuda_cast<T, float>(0.0f);
646 if (i < topk) {
647 + // Load the score value (without bias) for normalization
648 + T input = scores[s_topk_idx[i]];
649 + value =
650 + (scoring_func == SCORING_SIGMOID) ? apply_sigmoid(input) : input;
651 s_topk_value[i] = value;
652 }
653 topk_sum +=
@@ -627,12 +668,12 @@ __global__ void group_idx_and_topk_idx_kernel(
668 value = cuda_cast<float, T>(s_topk_value[i]) * routed_scaling_factor;
669 }
670 topk_indices[i] = s_topk_idx[i];
630 - topk_values[i] = cuda_cast<T, float>(value);
671 + topk_values[i] = value;
672 }
673 } else {
674 for (int i = lane_id; i < topk; i += WARP_SIZE) {
675 topk_indices[i] = i;
635 - topk_values[i] = cuda_cast<T, float>(1.0f / topk);
676 + topk_values[i] = 1.0f / topk;
677 }
678 }
679 // Note: when if_proceed_next_topk==false, choose the first 8 experts as the
@@ -644,12 +685,12 @@ __global__ void group_idx_and_topk_idx_kernel(
685 }
686
687 template <typename T, typename IdxT>
647 -void invokeNoAuxTc(T* scores, T* group_scores, T* topk_values,
648 - IdxT* topk_indices, T* scores_with_bias,
649 - int64_t const num_tokens, int64_t const num_experts,
650 - int64_t const n_group, int64_t const topk_group,
651 - int64_t const topk, bool const renormalize,
652 - double const routed_scaling_factor, bool enable_pdl = false,
688 +void invokeNoAuxTc(T* scores, T* group_scores, float* topk_values,
689 + IdxT* topk_indices, T const* bias, int64_t const num_tokens,
690 + int64_t const num_experts, int64_t const n_group,
691 + int64_t const topk_group, int64_t const topk,
692 + bool const renormalize, double const routed_scaling_factor,
693 + int const scoring_func, bool enable_pdl = false,
694 cudaStream_t const stream = 0) {
695 int64_t num_cases = num_tokens * n_group;
696 int64_t topk_with_k2_num_blocks = (num_cases - 1) / NUM_WARPS_PER_BLOCK + 1;
@@ -664,8 +705,9 @@ void invokeNoAuxTc(T* scores, T* group_scores, T* topk_values,
705 attrs[0].val.programmaticStreamSerializationAllowed = enable_pdl;
706 config.numAttrs = 1;
707 config.attrs = attrs;
667 - cudaLaunchKernelEx(&config, kernel_instance1, group_scores, scores_with_bias,
668 - num_tokens, num_cases, n_group, num_experts / n_group);
708 + cudaLaunchKernelEx(&config, kernel_instance1, group_scores, scores, bias,
709 + num_tokens, num_cases, n_group, num_experts / n_group,
710 + scoring_func);
711
712 int64_t topk_with_k_group_num_blocks =
713 (num_tokens - 1) / NUM_WARPS_PER_BLOCK + 1;
@@ -682,19 +724,18 @@ void invokeNoAuxTc(T* scores, T* group_scores, T* topk_values,
724 config.numAttrs = 1;
725 config.attrs = attrs;
726 cudaLaunchKernelEx(&config, kernel_instance2, scores, group_scores,
685 - topk_values, topk_indices, scores_with_bias, num_tokens,
686 - n_group, topk_group, topk, num_experts,
687 - num_experts / n_group, renormalize, routed_scaling_factor);
727 + topk_values, topk_indices, bias, num_tokens, n_group,
728 + topk_group, topk, num_experts, num_experts / n_group,
729 + renormalize, routed_scaling_factor, scoring_func);
730 }
731
732 #define INSTANTIATE_NOAUX_TC(T, IdxT) \
733 template void invokeNoAuxTc<T, IdxT>( \
692 - T * scores, T * group_scores, T * topk_values, IdxT * topk_indices, \
693 - T * scores_with_bias, int64_t const num_tokens, \
694 - int64_t const num_experts, int64_t const n_group, \
695 - int64_t const topk_group, int64_t const topk, bool const renormalize, \
696 - double const routed_scaling_factor, bool enable_pdl, \
697 - cudaStream_t const stream);
734 + T * scores, T * group_scores, float* topk_values, IdxT* topk_indices, \
735 + T const* bias, int64_t const num_tokens, int64_t const num_experts, \
736 + int64_t const n_group, int64_t const topk_group, int64_t const topk, \
737 + bool const renormalize, double const routed_scaling_factor, \
738 + int const scoring_func, bool enable_pdl, cudaStream_t const stream);
739
740 INSTANTIATE_NOAUX_TC(float, int32_t);
741 INSTANTIATE_NOAUX_TC(half, int32_t);
@@ -703,28 +744,32 @@ INSTANTIATE_NOAUX_TC(__nv_bfloat16, int32_t);
744 } // namespace vllm
745
746 std::tuple<torch::Tensor, torch::Tensor> grouped_topk(
706 - torch::Tensor const& scores, torch::Tensor const& scores_with_bias,
707 - int64_t n_group, int64_t topk_group, int64_t topk, bool renormalize,
708 - double routed_scaling_factor) {
709 - auto data_type = scores_with_bias.scalar_type();
710 - auto input_size = scores_with_bias.sizes();
747 + torch::Tensor const& scores, int64_t n_group, int64_t topk_group,
748 + int64_t topk, bool renormalize, double routed_scaling_factor,
749 + torch::Tensor const& bias, int64_t scoring_func = 0) {
750 + auto data_type = scores.scalar_type();
751 + auto input_size = scores.sizes();
752 int64_t num_tokens = input_size[0];
753 int64_t num_experts = input_size[1];
713 - TORCH_CHECK(input_size.size() == 2, "scores_with_bias must be a 2D Tensor");
754 + TORCH_CHECK(input_size.size() == 2, "scores must be a 2D Tensor");
755 TORCH_CHECK(num_experts % n_group == 0,
756 "num_experts should be divisible by n_group");
757 TORCH_CHECK(n_group <= 32,
758 "n_group should be smaller than or equal to 32 for now");
759 TORCH_CHECK(topk <= 32, "topk should be smaller than or equal to 32 for now");
760 + TORCH_CHECK(scoring_func == vllm::moe::SCORING_NONE ||
761 + scoring_func == vllm::moe::SCORING_SIGMOID,
762 + "scoring_func must be SCORING_NONE (0) or SCORING_SIGMOID (1)");
763
764 torch::Tensor group_scores = torch::empty(
765 {num_tokens, n_group}, torch::dtype(data_type).device(torch::kCUDA));
766 + // Always output float32 for topk_values (eliminates Python-side conversion)
767 torch::Tensor topk_values = torch::empty(
723 - {num_tokens, topk}, torch::dtype(data_type).device(torch::kCUDA));
768 + {num_tokens, topk}, torch::dtype(torch::kFloat32).device(torch::kCUDA));
769 torch::Tensor topk_indices = torch::empty(
770 {num_tokens, topk}, torch::dtype(torch::kInt32).device(torch::kCUDA));
771
727 - auto stream = c10::cuda::getCurrentCUDAStream(scores_with_bias.get_device());
772 + auto stream = c10::cuda::getCurrentCUDAStream(scores.get_device());
773
774 switch (data_type) {
775 case torch::kFloat16:
@@ -732,11 +777,11 @@ std::tuple<torch::Tensor, torch::Tensor> grouped_topk(
777 vllm::moe::invokeNoAuxTc<half, int32_t>(
778 reinterpret_cast<half*>(scores.mutable_data_ptr()),
779 reinterpret_cast<half*>(group_scores.mutable_data_ptr()),
735 - reinterpret_cast<half*>(topk_values.mutable_data_ptr()),
780 + reinterpret_cast<float*>(topk_values.mutable_data_ptr()),
781 reinterpret_cast<int32_t*>(topk_indices.mutable_data_ptr()),
737 - reinterpret_cast<half*>(scores_with_bias.data_ptr()), num_tokens,
782 + reinterpret_cast<half const*>(bias.data_ptr()), num_tokens,
783 num_experts, n_group, topk_group, topk, renormalize,
739 - routed_scaling_factor, false, stream);
784 + routed_scaling_factor, static_cast<int>(scoring_func), false, stream);
785 break;
786 case torch::kFloat32:
787 // Handle Float32
@@ -745,20 +790,20 @@ std::tuple<torch::Tensor, torch::Tensor> grouped_topk(
790 reinterpret_cast<float*>(group_scores.mutable_data_ptr()),
791 reinterpret_cast<float*>(topk_values.mutable_data_ptr()),
792 reinterpret_cast<int32_t*>(topk_indices.mutable_data_ptr()),
748 - reinterpret_cast<float*>(scores_with_bias.data_ptr()), num_tokens,
793 + reinterpret_cast<float const*>(bias.data_ptr()), num_tokens,
794 num_experts, n_group, topk_group, topk, renormalize,
750 - routed_scaling_factor, false, stream);
795 + routed_scaling_factor, static_cast<int>(scoring_func), false, stream);
796 break;
797 case torch::kBFloat16:
798 // Handle BFloat16
799 vllm::moe::invokeNoAuxTc<__nv_bfloat16, int32_t>(
800 reinterpret_cast<__nv_bfloat16*>(scores.mutable_data_ptr()),
801 reinterpret_cast<__nv_bfloat16*>(group_scores.mutable_data_ptr()),
757 - reinterpret_cast<__nv_bfloat16*>(topk_values.mutable_data_ptr()),
802 + reinterpret_cast<float*>(topk_values.mutable_data_ptr()),
803 reinterpret_cast<int32_t*>(topk_indices.mutable_data_ptr()),
759 - reinterpret_cast<__nv_bfloat16*>(scores_with_bias.data_ptr()),
760 - num_tokens, num_experts, n_group, topk_group, topk, renormalize,
761 - routed_scaling_factor, false, stream);
804 + reinterpret_cast<__nv_bfloat16 const*>(bias.data_ptr()), num_tokens,
805 + num_experts, n_group, topk_group, topk, renormalize,
806 + routed_scaling_factor, static_cast<int>(scoring_func), false, stream);
807 break;
808 default:
809 // Handle other data types
csrc/moe/moe_ops.h
+3 -3
@@ -39,9 +39,9 @@ torch::Tensor moe_wna16_gemm(torch::Tensor input, torch::Tensor output,
39 int64_t BLOCK_SIZE_K, int64_t bit);
40
41 std::tuple<torch::Tensor, torch::Tensor> grouped_topk(
42 - torch::Tensor const& scores, torch::Tensor const& scores_with_bias,
43 - int64_t n_group, int64_t topk_group, int64_t topk, bool renormalize,
44 - double routed_scaling_factor);
42 + torch::Tensor const& scores, int64_t n_group, int64_t topk_group,
43 + int64_t topk, bool renormalize, double routed_scaling_factor,
44 + torch::Tensor const& bias, int64_t scoring_func);
45 #endif
46
47 bool moe_permute_unpermute_supported();
csrc/moe/torch_bindings.cpp
+3 -2
@@ -107,9 +107,10 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, m) {
107
108 // Apply grouped topk routing to select experts.
109 m.def(
110 - "grouped_topk(Tensor scores, Tensor scores_with_bias, int n_group, int "
110 + "grouped_topk(Tensor scores, int n_group, int "
111 "topk_group, int topk, bool renormalize, float "
112 - "routed_scaling_factor) -> (Tensor, Tensor)");
112 + "routed_scaling_factor, Tensor bias, int scoring_func) -> (Tensor, "
113 + "Tensor)");
114 m.impl("grouped_topk", torch::kCUDA, &grouped_topk);
115 #endif
116 }
vllm/_custom_ops.py
+17 -2
@@ -1898,25 +1898,40 @@ def topk_softmax(
1898
1899 def grouped_topk(
1900 scores: torch.Tensor,
1901 - scores_with_bias: torch.Tensor,
1901 num_expert_group: int,
1902 topk_group: int,
1903 topk: int,
1904 renormalize: bool,
1905 routed_scaling_factor: float,
1906 + bias: torch.Tensor,
1907 + scoring_func: int = 0,
1908 ):
1909 + """
1910 + Perform grouped top-k routing for mixture of experts.
1911 +
1912 + Args:
1913 + scores: Raw inputs (logits if scoring_func=1, scores if scoring_func=0)
1914 + num_expert_group: Number of expert groups
1915 + topk_group: Number of groups to select
1916 + topk: Number of experts to select per token
1917 + renormalize: Whether to renormalize the output weights
1918 + routed_scaling_factor: Scaling factor for routing weights
1919 + bias: Bias tensor (e_score_correction_bias). Always fused in kernel.
1920 + scoring_func: 0=none (no activation), 1=sigmoid
1921 + """
1922 if not current_platform.is_cuda():
1923 raise NotImplementedError(
1924 "The fused grouped_topk kernel is only available on CUDA platforms"
1925 )
1926 return torch.ops._moe_C.grouped_topk(
1927 scores,
1914 - scores_with_bias,
1928 num_expert_group,
1929 topk_group,
1930 topk,
1931 renormalize,
1932 routed_scaling_factor,
1933 + bias,
1934 + scoring_func,
1935 )
1936
1937
vllm/model_executor/layers/fused_moe/fused_moe.py
+27 -14
@@ -1330,24 +1330,37 @@ def fused_grouped_topk(
1330 ) -> tuple[torch.Tensor, torch.Tensor]:
1331 assert hidden_states.size(0) == gating_output.size(0), "Number of tokens mismatch"
1332
1333 - if scoring_func == "softmax":
1333 + if scoring_func == "sigmoid":
1334 + # Fully fused kernel path for sigmoid
1335 + topk_values, topk_indices = ops.grouped_topk(
1336 + gating_output, # raw logits
1337 + num_expert_group,
1338 + topk_group,
1339 + topk,
1340 + renormalize,
1341 + routed_scaling_factor,
1342 + e_score_correction_bias.to(gating_output.dtype),
1343 + 1, # scoring_func=1 for sigmoid
1344 + )
1345 + elif scoring_func == "softmax":
1346 + # Apply softmax in Python, then use fused kernel
1347 + # TODO: Add support for softmax in kernel
1348 scores = torch.softmax(gating_output, dim=-1)
1335 - elif scoring_func == "sigmoid":
1336 - scores = gating_output.sigmoid()
1349 + topk_values, topk_indices = ops.grouped_topk(
1350 + scores, # pre-computed scores
1351 + num_expert_group,
1352 + topk_group,
1353 + topk,
1354 + renormalize,
1355 + routed_scaling_factor,
1356 + e_score_correction_bias.to(gating_output.dtype),
1357 + 0, # scoring_func=0 (no activation, scores already computed)
1358 + )
1359 else:
1360 raise ValueError(f"Unsupported scoring function: {scoring_func}")
1361
1340 - scores_with_bias = scores + e_score_correction_bias.unsqueeze(0)
1341 - topk_values, topk_indices = ops.grouped_topk(
1342 - scores,
1343 - scores_with_bias.to(scores.dtype),
1344 - num_expert_group,
1345 - topk_group,
1346 - topk,
1347 - renormalize,
1348 - routed_scaling_factor,
1349 - )
1350 - return topk_values.to(torch.float32), topk_indices.to(torch.int32)
1362 + # Fused kernel outputs float32 values and int32 indices directly
1363 + return topk_values, topk_indices
1364
1365
1366 def inplace_fused_experts(