[MoE] Generalize masked activation for padded layouts (#51217)

Signed-off-by: mgoin <mgoin64@gmail.com> Co-authored-by: OpenAI Codex <codex@openai.com>

Misha Goin committed Sep 1, 2026 at 11:34 UTC 82b7d49a6e30cf6d1d53838667ee20f09ac1f08b
15 files changed +1217 -196
csrc/libtorch_stable/activation_kernels.cu
+297 -48
@@ -500,10 +500,8 @@ template <typename scalar_t>
500 __global__ void situ_and_mul_kernel(
501 scalar_t* __restrict__ out, // [..., d]
502 const scalar_t* __restrict__ input, // [..., 2, d]
503 - const int d, const float beta, const float linear_beta,
504 - const int64_t* __restrict__ valid_rows_ptr) {
503 + const int d, const float beta, const float linear_beta) {
504 const int64_t row = blockIdx.x;
506 - if (valid_rows_ptr != nullptr && row >= *valid_rows_ptr) return;
505 const scalar_t* gate_ptr = input + row * 2 * d;
506 const scalar_t* up_ptr = gate_ptr + d;
507 scalar_t* out_ptr = out + row * d;
@@ -558,15 +556,16 @@ template <typename scalar_t, typename fp8_type, int THREADS, int NUM_STAGES,
556 __global__ void situ_and_mul_quant_group_pipelined_kernel(
557 fp8_type* __restrict__ out, float* __restrict__ scale_out,
558 const scalar_t* __restrict__ input, const int64_t num_rows,
561 - const int32_t* __restrict__ valid_rows_ptr, const int64_t topk) {
559 + const int32_t* __restrict__ num_valid_tokens_ptr, const int64_t topk) {
560 const int tid = threadIdx.x;
561 const int warp_id = tid / WARP_SIZE;
562 const int lane_id = tid % WARP_SIZE;
563 static constexpr int NUM_WARPS = THREADS / WARP_SIZE;
564 static constexpr int NUM_GROUPS = D / GROUP_SIZE;
565 const int64_t row_bound =
568 - valid_rows_ptr != nullptr
569 - ? max((int64_t)0, min((int64_t)(*valid_rows_ptr) * topk, num_rows))
566 + num_valid_tokens_ptr != nullptr
567 + ? max((int64_t)0,
568 + min((int64_t)(*num_valid_tokens_ptr) * topk, num_rows))
569 : num_rows;
570
571 static_assert(NUM_GROUPS % 4 == 0,
@@ -710,15 +709,16 @@ __global__ void situ_and_mul_quant_group_scalar_kernel(
709 const scalar_t* __restrict__ input, // [num_tokens, 2, d]
710 const int d, const int num_groups, const float beta,
711 const float linear_beta, const int64_t num_rows,
713 - const int32_t* __restrict__ valid_rows_ptr, const int64_t topk) {
712 + const int32_t* __restrict__ num_valid_tokens_ptr, const int64_t topk) {
713 static constexpr int ELTS_PER_LANE = GROUP_SIZE / WARP_SIZE;
714 const int tid = threadIdx.x;
715 const int warp_id = tid / WARP_SIZE;
716 const int lane_id = tid % WARP_SIZE;
717 const int num_warps = blockDim.x / WARP_SIZE;
718 const int64_t row_bound =
720 - valid_rows_ptr != nullptr
721 - ? max((int64_t)0, min((int64_t)(*valid_rows_ptr) * topk, num_rows))
719 + num_valid_tokens_ptr != nullptr
720 + ? max((int64_t)0,
721 + min((int64_t)(*num_valid_tokens_ptr) * topk, num_rows))
722 : num_rows;
723
724 const bool clamp_up = linear_beta > 0.0f;
@@ -768,24 +768,61 @@ __global__ void situ_and_mul_quant_group_scalar_kernel(
768 }
769 }
770
771 -template <typename scalar_t>
771 +constexpr int kMaxMaskedTokenBlocks = 32;
772 +
773 +template <bool BATCHED_EXPERTS>
774 +__device__ __forceinline__ bool get_masked_row_range(
775 + const int* __restrict__ valid_token_counts, const int max_num_tokens,
776 + int64_t& first_row, int64_t& end_row, int64_t& row_stride) {
777 + if constexpr (BATCHED_EXPERTS) {
778 + // [E, T, *]: z lanes grid-stride over one expert's valid token prefix.
779 + const int expert = blockIdx.y;
780 + const int num_tokens =
781 + max(0, min(valid_token_counts[expert], max_num_tokens));
782 + const int token_block = blockIdx.z;
783 + if (token_block >= num_tokens) {
784 + return false;
785 + }
786 + const int64_t expert_first_row =
787 + static_cast<int64_t>(expert) * max_num_tokens;
788 + first_row = expert_first_row + token_block;
789 + end_row = expert_first_row + num_tokens;
790 + row_stride = gridDim.z;
791 + } else {
792 + // [T, *]: y lanes grid-stride over the single valid token prefix.
793 + const int num_tokens = max(0, min(valid_token_counts[0], max_num_tokens));
794 + const int token_block = blockIdx.y;
795 + if (token_block >= num_tokens) {
796 + return false;
797 + }
798 + first_row = token_block;
799 + end_row = num_tokens;
800 + row_stride = gridDim.y;
801 + }
802 + return first_row < end_row;
803 +}
804 +
805 +__device__ __forceinline__ int get_masked_feature_index() {
806 + return blockIdx.x * blockDim.x + threadIdx.x;
807 +}
808 +
809 +template <typename scalar_t, bool BATCHED_EXPERTS>
810 __global__ void masked_situ_and_mul_kernel(
811 scalar_t* __restrict__ out, const scalar_t* __restrict__ input,
774 - const int* __restrict__ expert_num_tokens, const int max_num_tokens,
812 + const int* __restrict__ valid_token_counts, const int max_num_tokens,
813 const int d, const float beta, const float linear_beta) {
776 - const int expert = blockIdx.y;
777 - const int num_tokens = expert_num_tokens[expert];
778 - const int idx = blockIdx.x * blockDim.x + threadIdx.x;
779 - if (idx >= d || num_tokens == 0) {
814 + int64_t first_row, end_row, row_stride;
815 + const int idx = get_masked_feature_index();
816 + if (idx >= d ||
817 + !get_masked_row_range<BATCHED_EXPERTS>(valid_token_counts, max_num_tokens,
818 + first_row, end_row, row_stride)) {
819 return;
820 }
821
822 const bool clamp_up = linear_beta > 0.0f;
823 const float inv_beta = 1.0f / beta;
824 const float inv_linear_beta = clamp_up ? 1.0f / linear_beta : 0.0f;
786 - const int64_t expert_row = static_cast<int64_t>(expert) * max_num_tokens;
787 - for (int token = 0; token < num_tokens; ++token) {
788 - const int64_t row = expert_row + token;
825 + for (int64_t row = first_row; row < end_row; row += row_stride) {
826 const scalar_t* gate_ptr = input + row * 2 * d;
827 const scalar_t* up_ptr = gate_ptr + d;
828 scalar_t* out_ptr = out + row * d;
@@ -796,6 +833,119 @@ __global__ void masked_situ_and_mul_kernel(
833 }
834 }
835
836 +template <typename scalar_t, scalar_t (*ACT_FN)(const scalar_t&, const float),
837 + bool act_first, bool HAS_CLAMP, bool BATCHED_EXPERTS>
838 +__global__ void masked_act_and_mul_kernel(
839 + scalar_t* __restrict__ out, const scalar_t* __restrict__ input,
840 + const int* __restrict__ valid_token_counts, const int max_num_tokens,
841 + const int d, const float limit, const float alpha, const float beta) {
842 + int64_t first_row, end_row, row_stride;
843 + const int idx = get_masked_feature_index();
844 + if (idx >= d ||
845 + !get_masked_row_range<BATCHED_EXPERTS>(valid_token_counts, max_num_tokens,
846 + first_row, end_row, row_stride)) {
847 + return;
848 + }
849 +
850 + for (int64_t row = first_row; row < end_row; row += row_stride) {
851 + const scalar_t* x_ptr = input + row * 2 * d;
852 + const scalar_t* y_ptr = x_ptr + d;
853 + scalar_t* out_ptr = out + row * d;
854 + const scalar_t x = VLLM_LDG(&x_ptr[idx]);
855 + const scalar_t y = VLLM_LDG(&y_ptr[idx]);
856 + out_ptr[idx] = compute<scalar_t, ACT_FN, act_first, HAS_CLAMP>(x, y, limit,
857 + alpha, beta);
858 + }
859 +}
860 +
861 +template <typename scalar_t, bool BATCHED_EXPERTS>
862 +__global__ void masked_swigluoai_and_mul_kernel(
863 + scalar_t* __restrict__ out, const scalar_t* __restrict__ input,
864 + const int* __restrict__ valid_token_counts, const int max_num_tokens,
865 + const int d, const float alpha, const float limit) {
866 + int64_t first_row, end_row, row_stride;
867 + const int idx = get_masked_feature_index();
868 + if (idx >= d ||
869 + !get_masked_row_range<BATCHED_EXPERTS>(valid_token_counts, max_num_tokens,
870 + first_row, end_row, row_stride)) {
871 + return;
872 + }
873 +
874 + for (int64_t row = first_row; row < end_row; row += row_stride) {
875 + const scalar_t* in_ptr = input + row * 2 * d;
876 + scalar_t* out_ptr = out + row * d;
877 + out_ptr[idx] =
878 + swigluoai_and_mul(in_ptr[2 * idx], in_ptr[2 * idx + 1], alpha, limit);
879 + }
880 +}
881 +
882 +template <typename scalar_t, bool BATCHED_EXPERTS>
883 +__global__ void masked_swiglustep_and_mul_kernel(
884 + scalar_t* __restrict__ out, const scalar_t* __restrict__ input,
885 + const int* __restrict__ valid_token_counts, const int max_num_tokens,
886 + const int d, const float limit) {
887 + int64_t first_row, end_row, row_stride;
888 + const int idx = get_masked_feature_index();
889 + if (idx >= d ||
890 + !get_masked_row_range<BATCHED_EXPERTS>(valid_token_counts, max_num_tokens,
891 + first_row, end_row, row_stride)) {
892 + return;
893 + }
894 +
895 + for (int64_t row = first_row; row < end_row; row += row_stride) {
896 + const scalar_t* gate_ptr = input + row * 2 * d;
897 + const scalar_t* up_ptr = gate_ptr + d;
898 + scalar_t* out_ptr = out + row * d;
899 + const float gate = (float)VLLM_LDG(&gate_ptr[idx]);
900 + const float up = (float)VLLM_LDG(&up_ptr[idx]);
901 + const float gate_silu = gate / (1.0f + expf(-gate));
902 + const float gate_clamped = fminf(gate_silu, limit);
903 + const float up_clamped = fmaxf(fminf(up, limit), -limit);
904 + out_ptr[idx] = (scalar_t)(gate_clamped * up_clamped);
905 + }
906 +}
907 +
908 +template <typename T>
909 +__device__ __forceinline__ T silu_no_mul_kernel(const T& x) {
910 + return silu_kernel(x, 1.0f);
911 +}
912 +
913 +template <typename T>
914 +__device__ __forceinline__ T gelu_no_mul_kernel(const T& x) {
915 + return gelu_kernel(x, 1.0f);
916 +}
917 +
918 +template <typename T>
919 +__device__ __forceinline__ T gelu_tanh_no_mul_kernel(const T& x) {
920 + return gelu_tanh_kernel(x, 1.0f);
921 +}
922 +
923 +template <typename T>
924 +__device__ __forceinline__ T relu2_no_mul_kernel(const T& x) {
925 + const float value = fmaxf((float)x, 0.0f);
926 + return (T)(value * value);
927 +}
928 +
929 +template <typename scalar_t, scalar_t (*ACT_FN)(const scalar_t&),
930 + bool BATCHED_EXPERTS>
931 +__global__ void masked_activation_kernel(
932 + scalar_t* __restrict__ out, const scalar_t* __restrict__ input,
933 + const int* __restrict__ valid_token_counts, const int max_num_tokens,
934 + const int d) {
935 + int64_t first_row, end_row, row_stride;
936 + const int idx = get_masked_feature_index();
937 + if (idx >= d ||
938 + !get_masked_row_range<BATCHED_EXPERTS>(valid_token_counts, max_num_tokens,
939 + first_row, end_row, row_stride)) {
940 + return;
941 + }
942 +
943 + for (int64_t row = first_row; row < end_row; row += row_stride) {
944 + const int64_t offset = row * d + idx;
945 + out[offset] = ACT_FN(VLLM_LDG(&input[offset]));
946 + }
947 +}
948 +
949 } // namespace vllm
950
951 #define LAUNCH_ACTIVATION_GATE_KERNEL_WITH_PARAM(KERNEL, PACKED_KERNEL, PARAM) \
@@ -890,8 +1040,7 @@ void swigluoai_and_mul(torch::stable::Tensor& out, // [..., d]
1040 // through), matching SituAndMul(linear_beta=None) on the Python side.
1041 void situ_and_mul(torch::stable::Tensor& out, // [..., d]
1042 torch::stable::Tensor& input, // [..., 2 * d]
893 - double beta, double linear_beta,
894 - std::optional<torch::stable::Tensor> valid_rows) {
1043 + double beta, double linear_beta) {
1044 int d = input.size(-1) / 2;
1045 int64_t num_tokens = input.numel() / input.size(-1);
1046 if (num_tokens == 0) {
@@ -899,14 +1048,6 @@ void situ_and_mul(torch::stable::Tensor& out, // [..., d]
1048 }
1049 dim3 grid(num_tokens);
1050 dim3 block(std::min(d, 1024));
902 - const int64_t* valid_rows_ptr = nullptr;
903 - if (valid_rows.has_value()) {
904 - STD_TORCH_CHECK(valid_rows->is_cuda() && valid_rows->get_device_index() ==
905 - input.get_device_index(),
906 - "situ_and_mul: valid_rows must be a CUDA tensor on the "
907 - "same device as input");
908 - valid_rows_ptr = valid_rows->const_data_ptr<int64_t>();
909 - }
1051 const torch::stable::accelerator::DeviceGuard device_guard(
1052 input.get_device_index());
1053 const cudaStream_t stream = get_current_cuda_stream();
@@ -914,7 +1055,7 @@ void situ_and_mul(torch::stable::Tensor& out, // [..., d]
1055 input.scalar_type(), "situ_and_mul_kernel", [&] {
1056 vllm::situ_and_mul_kernel<scalar_t><<<grid, block, 0, stream>>>(
1057 out.mutable_data_ptr<scalar_t>(), input.const_data_ptr<scalar_t>(),
917 - d, (float)beta, (float)linear_beta, valid_rows_ptr);
1058 + d, (float)beta, (float)linear_beta);
1059 });
1060 }
1061
@@ -925,7 +1066,7 @@ void masked_situ_and_mul(torch::stable::Tensor& out, // [E, T, d]
1066 int num_experts = input.size(0);
1067 int max_num_tokens = input.size(1);
1068 int d = input.size(2) / 2;
928 - if (num_experts == 0 || max_num_tokens == 0) {
1069 + if (num_experts == 0 || max_num_tokens == 0 || d == 0) {
1070 return;
1071 }
1072 constexpr int block_size = 256;
@@ -936,21 +1077,128 @@ void masked_situ_and_mul(torch::stable::Tensor& out, // [E, T, d]
1077 const cudaStream_t stream = get_current_cuda_stream();
1078 VLLM_STABLE_DISPATCH_FLOATING_TYPES(
1079 input.scalar_type(), "masked_situ_and_mul_kernel", [&] {
939 - vllm::masked_situ_and_mul_kernel<scalar_t><<<grid, block, 0, stream>>>(
940 - out.mutable_data_ptr<scalar_t>(), input.const_data_ptr<scalar_t>(),
941 - expert_num_tokens.const_data_ptr<int>(), max_num_tokens, d,
942 - (float)beta, (float)linear_beta);
1080 + vllm::masked_situ_and_mul_kernel<scalar_t, true>
1081 + <<<grid, block, 0, stream>>>(
1082 + out.mutable_data_ptr<scalar_t>(),
1083 + input.const_data_ptr<scalar_t>(),
1084 + expert_num_tokens.const_data_ptr<int>(), max_num_tokens, d,
1085 + (float)beta, (float)linear_beta);
1086 });
1087 }
1088
1089 +#define LAUNCH_MASKED_ACT_AND_MUL(KERNEL, ACT_FIRST, HAS_CLAMP, LIMIT, ALPHA, \
1090 + BETA) \
1091 + vllm::masked_act_and_mul_kernel<scalar_t, KERNEL<scalar_t>, ACT_FIRST, \
1092 + HAS_CLAMP, BATCHED_EXPERTS> \
1093 + <<<grid, block, 0, stream>>>(out.mutable_data_ptr<scalar_t>(), \
1094 + input.const_data_ptr<scalar_t>(), \
1095 + valid_token_counts.const_data_ptr<int>(), \
1096 + max_num_tokens, d, LIMIT, ALPHA, BETA)
1097 +
1098 +#define LAUNCH_MASKED_ACTIVATION(KERNEL) \
1099 + vllm::masked_activation_kernel<scalar_t, KERNEL<scalar_t>, BATCHED_EXPERTS> \
1100 + <<<grid, block, 0, stream>>>( \
1101 + out.mutable_data_ptr<scalar_t>(), input.const_data_ptr<scalar_t>(), \
1102 + valid_token_counts.const_data_ptr<int>(), max_num_tokens, d)
1103 +
1104 +void masked_moe_activation(
1105 + torch::stable::Tensor& out, // [T, d] or [E, T, d]
1106 + torch::stable::Tensor& input, // [T, d] or [E, T, d], gated has 2 * d
1107 + const torch::stable::Tensor& valid_token_counts,
1108 + const std::string& activation, double clamp_limit, double alpha,
1109 + double beta, double situ_beta, double situ_linear_beta) {
1110 + const bool batched_experts = input.dim() == 3;
1111 + const int num_experts = batched_experts ? input.size(0) : 1;
1112 + const int max_num_tokens = batched_experts ? input.size(1) : input.size(0);
1113 + const int d = out.size(-1);
1114 + if (num_experts == 0 || max_num_tokens == 0 || d == 0) {
1115 + return;
1116 + }
1117 +
1118 + constexpr int block_size = 256;
1119 + const int feature_blocks = (d + block_size - 1) / block_size;
1120 + const int token_blocks =
1121 + std::min(max_num_tokens, vllm::kMaxMaskedTokenBlocks);
1122 + // Batched grid: (feature tile, expert, token lane); flat grid: (feature
1123 + // tile, token lane). Token lanes grid-stride each valid prefix.
1124 + dim3 grid = batched_experts ? dim3(feature_blocks, num_experts, token_blocks)
1125 + : dim3(feature_blocks, token_blocks);
1126 + dim3 block(block_size);
1127 + const torch::stable::accelerator::DeviceGuard device_guard(
1128 + input.get_device_index());
1129 + const cudaStream_t stream = get_current_cuda_stream();
1130 + auto launch = [&]<bool BATCHED_EXPERTS>() {
1131 + VLLM_STABLE_DISPATCH_FLOATING_TYPES(
1132 + input.scalar_type(), "masked_moe_activation", [&] {
1133 + if (activation == "silu") {
1134 + LAUNCH_MASKED_ACT_AND_MUL(vllm::silu_kernel, true, false, 0.0f,
1135 + 1.0f, 0.0f);
1136 + } else if (activation == "silu_with_clamp") {
1137 + LAUNCH_MASKED_ACT_AND_MUL(vllm::silu_kernel, true, true,
1138 + (float)clamp_limit, (float)alpha,
1139 + (float)beta);
1140 + } else if (activation == "gelu") {
1141 + LAUNCH_MASKED_ACT_AND_MUL(vllm::gelu_kernel, true, false, 0.0f,
1142 + 1.0f, 0.0f);
1143 + } else if (activation == "gelu_tanh") {
1144 + LAUNCH_MASKED_ACT_AND_MUL(vllm::gelu_tanh_kernel, true, false, 0.0f,
1145 + 1.0f, 0.0f);
1146 + } else if (activation == "situ") {
1147 + vllm::masked_situ_and_mul_kernel<scalar_t, BATCHED_EXPERTS>
1148 + <<<grid, block, 0, stream>>>(
1149 + out.mutable_data_ptr<scalar_t>(),
1150 + input.const_data_ptr<scalar_t>(),
1151 + valid_token_counts.const_data_ptr<int>(), max_num_tokens, d,
1152 + (float)situ_beta, (float)situ_linear_beta);
1153 + } else if (activation == "swigluoai") {
1154 + vllm::masked_swigluoai_and_mul_kernel<scalar_t, BATCHED_EXPERTS>
1155 + <<<grid, block, 0, stream>>>(
1156 + out.mutable_data_ptr<scalar_t>(),
1157 + input.const_data_ptr<scalar_t>(),
1158 + valid_token_counts.const_data_ptr<int>(), max_num_tokens, d,
1159 + (float)alpha, (float)clamp_limit);
1160 + } else if (activation == "swigluoai_uninterleave") {
1161 + LAUNCH_MASKED_ACT_AND_MUL(vllm::silu_kernel, true, true,
1162 + (float)clamp_limit, (float)alpha,
1163 + (float)beta);
1164 + } else if (activation == "swiglustep") {
1165 + vllm::masked_swiglustep_and_mul_kernel<scalar_t, BATCHED_EXPERTS>
1166 + <<<grid, block, 0, stream>>>(
1167 + out.mutable_data_ptr<scalar_t>(),
1168 + input.const_data_ptr<scalar_t>(),
1169 + valid_token_counts.const_data_ptr<int>(), max_num_tokens, d,
1170 + (float)clamp_limit);
1171 + } else if (activation == "silu_no_mul") {
1172 + LAUNCH_MASKED_ACTIVATION(vllm::silu_no_mul_kernel);
1173 + } else if (activation == "gelu_no_mul") {
1174 + LAUNCH_MASKED_ACTIVATION(vllm::gelu_no_mul_kernel);
1175 + } else if (activation == "gelu_tanh_no_mul") {
1176 + LAUNCH_MASKED_ACTIVATION(vllm::gelu_tanh_no_mul_kernel);
1177 + } else if (activation == "relu2_no_mul") {
1178 + LAUNCH_MASKED_ACTIVATION(vllm::relu2_no_mul_kernel);
1179 + } else {
1180 + STD_TORCH_CHECK(false,
1181 + "Unsupported masked MoE activation: ", activation);
1182 + }
1183 + });
1184 + };
1185 + if (batched_experts) {
1186 + launch.template operator()<true>();
1187 + } else {
1188 + launch.template operator()<false>();
1189 + }
1190 +}
1191 +
1192 +#undef LAUNCH_MASKED_ACT_AND_MUL
1193 +#undef LAUNCH_MASKED_ACTIVATION
1194 // Fused SITU activation and block-FP8 quantization for Humming w2.
947 -// `valid_rows` is the int32 DeepEP token count (psum[-1]); rows = it * `topk`,
948 -// so padding is excluded on-device and skipped rows receive scale 1.
1195 +// `num_valid_tokens` is the int32 DeepEP token count (psum[-1]); rows = it *
1196 +// `topk`, so padding is excluded on-device and skipped rows receive scale 1.
1197 void situ_and_mul_quant(torch::stable::Tensor& out, // [..., d] (fp8)
1198 torch::stable::Tensor& scale, // [..., 1 or d/128]
1199 torch::stable::Tensor& input, // [..., 2 * d]
1200 double beta, double linear_beta, int64_t group_size,
953 - std::optional<torch::stable::Tensor> valid_rows,
1201 + std::optional<torch::stable::Tensor> num_valid_tokens,
1202 int64_t topk) {
1203 STD_TORCH_CHECK(
1204 out.scalar_type() == torch::headeronly::ScalarType::Float8_e4m3fn ||
@@ -974,16 +1222,17 @@ void situ_and_mul_quant(torch::stable::Tensor& out, // [..., d] (fp8)
1222 }
1223 STD_TORCH_CHECK(out.size(-1) == d && out.numel() == num_tokens * (int64_t)d,
1224 "situ_and_mul_quant: out shape must be [num_tokens, d]");
977 - const int32_t* valid_rows_ptr = nullptr;
978 - if (valid_rows.has_value()) {
979 - STD_TORCH_CHECK(valid_rows->is_cuda() && valid_rows->get_device_index() ==
980 - input.get_device_index(),
981 - "situ_and_mul_quant: valid_rows must be a CUDA tensor on "
982 - "the same device as input");
1225 + const int32_t* num_valid_tokens_ptr = nullptr;
1226 + if (num_valid_tokens.has_value()) {
1227 + STD_TORCH_CHECK(
1228 + num_valid_tokens->is_cuda() &&
1229 + num_valid_tokens->get_device_index() == input.get_device_index(),
1230 + "situ_and_mul_quant: num_valid_tokens must be a CUDA tensor on the "
1231 + "same device as input");
1232 STD_TORCH_CHECK(
984 - valid_rows->scalar_type() == torch::headeronly::ScalarType::Int,
985 - "situ_and_mul_quant: valid_rows must be int32 (psum count)");
986 - valid_rows_ptr = valid_rows->const_data_ptr<int32_t>();
1233 + num_valid_tokens->scalar_type() == torch::headeronly::ScalarType::Int,
1234 + "situ_and_mul_quant: num_valid_tokens must be int32 (psum count)");
1235 + num_valid_tokens_ptr = num_valid_tokens->const_data_ptr<int32_t>();
1236 }
1237 const torch::stable::accelerator::DeviceGuard device_guard(
1238 input.get_device_index());
@@ -1031,7 +1280,7 @@ void situ_and_mul_quant(torch::stable::Tensor& out, // [..., d] (fp8)
1280 out.mutable_data_ptr<fp8_t>(),
1281 scale.mutable_data_ptr<float>(),
1282 input.const_data_ptr<scalar_t>(), num_tokens,
1034 - valid_rows_ptr, topk);
1283 + num_valid_tokens_ptr, topk);
1284 return;
1285 }
1286 }
@@ -1045,7 +1294,7 @@ void situ_and_mul_quant(torch::stable::Tensor& out, // [..., d] (fp8)
1294 scale.mutable_data_ptr<float>(),
1295 input.const_data_ptr<scalar_t>(), d, num_groups,
1296 (float)beta, (float)linear_beta, num_tokens,
1048 - valid_rows_ptr, topk);
1297 + num_valid_tokens_ptr, topk);
1298 });
1299 });
1300 }
csrc/libtorch_stable/moe/permute_unpermute_kernels/moe_permute_unpermute_kernel.cu
+8 -7
@@ -146,16 +146,17 @@ __global__ void preprocessTopkIdKernel(int* topk_id_ptr, int size,
146 }
147 __syncthreads();
148
149 - // query global expert id in expert map.
150 - // if global expert id = -1 in exert map, plus n_expert
151 - // else set global expert id = exert map[global expert id]
149 if (offset + tidx < bound) {
150 auto topk_id = topk_id_ptr[offset + tidx];
154 - auto local_expert_idx = smem_expert_map[topk_id];
155 - if (local_expert_idx == -1) {
156 - topk_id += num_experts;
151 + if (topk_id < 0 || topk_id >= num_experts) {
152 + topk_id = num_experts;
153 } else {
158 - topk_id = local_expert_idx;
154 + auto local_expert_idx = smem_expert_map[topk_id];
155 + if (local_expert_idx == -1) {
156 + topk_id += num_experts;
157 + } else {
158 + topk_id = local_expert_idx;
159 + }
160 }
161 __syncwarp();
162 topk_id_ptr[offset + tidx] = topk_id;
csrc/libtorch_stable/ops.h
+10 -5
@@ -530,20 +530,25 @@ void fatrelu_and_mul(torch::stable::Tensor& out, torch::stable::Tensor& input,
530 double threshold);
531 void swigluoai_and_mul(torch::stable::Tensor& out, torch::stable::Tensor& input,
532 double alpha = 1.702, double limit = 7.0);
533 -void situ_and_mul(
534 - torch::stable::Tensor& out, torch::stable::Tensor& input, double beta = 1.0,
535 - double linear_beta = -1.0,
536 - std::optional<torch::stable::Tensor> valid_rows = std::nullopt);
533 +void situ_and_mul(torch::stable::Tensor& out, torch::stable::Tensor& input,
534 + double beta = 1.0, double linear_beta = -1.0);
535 void situ_and_mul_quant(
536 torch::stable::Tensor& out, torch::stable::Tensor& scale,
537 torch::stable::Tensor& input, double beta = 1.0, double linear_beta = -1.0,
538 int64_t group_size = 0,
541 - std::optional<torch::stable::Tensor> valid_rows = std::nullopt,
539 + std::optional<torch::stable::Tensor> num_valid_tokens = std::nullopt,
540 int64_t topk = 1);
541 void masked_situ_and_mul(torch::stable::Tensor& out,
542 torch::stable::Tensor& input,
543 const torch::stable::Tensor& expert_num_tokens,
544 double beta = 1.0, double linear_beta = -1.0);
545 +void masked_moe_activation(torch::stable::Tensor& out,
546 + torch::stable::Tensor& input,
547 + const torch::stable::Tensor& valid_token_counts,
548 + const std::string& activation,
549 + double clamp_limit = 0.0, double alpha = 1.0,
550 + double beta = 0.0, double situ_beta = 1.0,
551 + double situ_linear_beta = -1.0);
552 void gelu_new(torch::stable::Tensor& out, torch::stable::Tensor& input);
553 void gelu_fast(torch::stable::Tensor& out, torch::stable::Tensor& input);
554 void gelu_quick(torch::stable::Tensor& out, torch::stable::Tensor& input);
csrc/libtorch_stable/torch_bindings.cpp
+8 -2
@@ -614,7 +614,7 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
614 // SituGLU implementation used in Kimi models.
615 ops.def(
616 "situ_and_mul(Tensor! out, Tensor input, float beta=1.0, float "
617 - "linear_beta=-1.0, Tensor? valid_rows=None) -> ()");
617 + "linear_beta=-1.0) -> ()");
618 // Fused SituGLU activation + dynamic FP8 quantization for the Humming w2 path
619 // (writes the fp8 down input and its float32 scale). group_size=0 ->
620 // per-token scale [.., 1]; group_size=128 -> k-major block-FP8 scale [..,
@@ -622,10 +622,15 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
622 ops.def(
623 "situ_and_mul_quant(Tensor! out, Tensor! scale, Tensor input, "
624 "float beta=1.0, float linear_beta=-1.0, int group_size=0, "
625 - "Tensor? valid_rows=None, int topk=1) -> ()");
625 + "Tensor? num_valid_tokens=None, int topk=1) -> ()");
626 ops.def(
627 "masked_situ_and_mul(Tensor! out, Tensor input, Tensor "
628 "expert_num_tokens, float beta=1.0, float linear_beta=-1.0) -> ()");
629 + ops.def(
630 + "masked_moe_activation(Tensor! out, Tensor input, Tensor "
631 + "valid_token_counts, str activation, float clamp_limit=0.0, float "
632 + "alpha=1.0, float beta=0.0, float situ_beta=1.0, float "
633 + "situ_linear_beta=-1.0) -> ()");
634
635 // GELU implementation used in GPT-2.
636 ops.def("gelu_new(Tensor! out, Tensor input) -> ()");
@@ -852,6 +857,7 @@ STABLE_TORCH_LIBRARY_IMPL(_C, CUDA, ops) {
857 ops.impl("situ_and_mul", TORCH_BOX(&situ_and_mul));
858 ops.impl("situ_and_mul_quant", TORCH_BOX(&situ_and_mul_quant));
859 ops.impl("masked_situ_and_mul", TORCH_BOX(&masked_situ_and_mul));
860 + ops.impl("masked_moe_activation", TORCH_BOX(&masked_moe_activation));
861 ops.impl("gelu_new", TORCH_BOX(&gelu_new));
862 ops.impl("gelu_fast", TORCH_BOX(&gelu_fast));
863 ops.impl("gelu_quick", TORCH_BOX(&gelu_quick));
tests/kernels/core/test_activation.py
+208
@@ -22,7 +22,13 @@ from vllm.model_executor.layers.activation import (
22 SwigluStepAndMul,
23 swiglustep_and_mul_triton,
24 )
25 +from vllm.model_executor.layers.fused_moe.activation import (
26 + ApplyMoEActivationConfig,
27 + MoEActivation,
28 + apply_moe_activation,
29 +)
30 from vllm.model_executor.layers.fused_moe.utils import swiglu_limit_func
31 +from vllm.platforms import current_platform
32 from vllm.utils.torch_utils import set_random_seed
33
34 DTYPES = [torch.half, torch.bfloat16, torch.float]
@@ -34,6 +40,100 @@ CUDA_DEVICES = [
40 ]
41
42
43 +def test_masked_moe_activation_rejects_unsupported_activation() -> None:
44 + input = torch.empty(1, 1, 2)
45 + output = torch.empty(1, 1, 1)
46 + valid_token_counts = torch.ones(1, dtype=torch.int32)
47 +
48 + with pytest.raises(NotImplementedError, match="relu2"):
49 + apply_moe_activation(
50 + MoEActivation.RELU2,
51 + output,
52 + input,
53 + valid_token_counts=valid_token_counts,
54 + )
55 +
56 +
57 +def test_moe_silu_clamp_uses_native_xpu_fallback(
58 + default_vllm_config, monkeypatch
59 +) -> None:
60 + monkeypatch.setattr(current_platform, "is_xpu", lambda: True)
61 + clamp_limit = 3.0
62 + input = torch.tensor([[12.0, -12.0, 8.0, -8.0], [-2.0, 2.0, -4.0, 4.0]])
63 + output = torch.empty(2, 2)
64 +
65 + apply_moe_activation(
66 + MoEActivation.SILU,
67 + output,
68 + input,
69 + activation_config=ApplyMoEActivationConfig(clamp_limit=clamp_limit),
70 + )
71 +
72 + expected = SiluAndMulWithClamp(clamp_limit, compile_native=False).forward_native(
73 + input
74 + )
75 + torch.testing.assert_close(output, expected)
76 +
77 +
78 +def _assert_masked_moe_activation(
79 + activation: MoEActivation,
80 + activation_config: ApplyMoEActivationConfig,
81 + *,
82 + dtype: torch.dtype,
83 + mask_layout: str,
84 + d: int,
85 + max_num_tokens: int,
86 +) -> None:
87 + device = CUDA_DEVICES[0]
88 + num_experts = 4
89 + input_dim = 2 * d if activation.is_gated else d
90 + if mask_layout == "flat":
91 + input = torch.randn(max_num_tokens, input_dim, dtype=dtype, device=device)
92 + valid_token_counts = torch.tensor(
93 + [max_num_tokens // 2], dtype=torch.int32, device=device
94 + )
95 + output = torch.full((max_num_tokens, d), 42.0, dtype=dtype, device=device)
96 + else:
97 + input = torch.randn(
98 + num_experts, max_num_tokens, input_dim, dtype=dtype, device=device
99 + )
100 + valid_token_counts = torch.tensor(
101 + [0, 1, max_num_tokens // 2, max_num_tokens],
102 + dtype=torch.int32,
103 + device=device,
104 + )
105 + output = torch.full(
106 + (num_experts, max_num_tokens, d), 42.0, dtype=dtype, device=device
107 + )
108 +
109 + apply_moe_activation(
110 + activation,
111 + output,
112 + input,
113 + activation_config=activation_config,
114 + valid_token_counts=valid_token_counts,
115 + )
116 +
117 + batched_input = input.view(-1, max_num_tokens, input_dim)
118 + batched_output = output.view(-1, max_num_tokens, d)
119 + for expert, num_tokens in enumerate(valid_token_counts.cpu().tolist()):
120 + if num_tokens:
121 + expected = torch.empty((num_tokens, d), dtype=dtype, device=device)
122 + apply_moe_activation(
123 + activation,
124 + expected,
125 + batched_input[expert, :num_tokens].clone(),
126 + activation_config=activation_config,
127 + )
128 + torch.testing.assert_close(
129 + batched_output[expert, :num_tokens],
130 + expected,
131 + atol=get_default_atol(output),
132 + rtol=get_default_rtol(output),
133 + )
134 + assert torch.all(batched_output[expert, num_tokens:] == 42.0)
135 +
136 +
137 @pytest.mark.parametrize(
138 "activation",
139 [
@@ -252,6 +352,114 @@ def test_masked_situ_and_mul(
352 )
353
354
355 +@pytest.mark.parametrize(
356 + ("activation", "activation_config"),
357 + [
358 + (MoEActivation.SILU, ApplyMoEActivationConfig()),
359 + (
360 + MoEActivation.SILU,
361 + ApplyMoEActivationConfig(clamp_limit=3.0),
362 + ),
363 + (MoEActivation.GELU, ApplyMoEActivationConfig()),
364 + (MoEActivation.GELU_TANH, ApplyMoEActivationConfig()),
365 + (
366 + MoEActivation.SITU,
367 + ApplyMoEActivationConfig(
368 + activation_situ_beta=1.5,
369 + activation_situ_linear_beta=2.0,
370 + ),
371 + ),
372 + (MoEActivation.SWIGLUOAI, ApplyMoEActivationConfig()),
373 + (
374 + MoEActivation.SWIGLUOAI_UNINTERLEAVE,
375 + ApplyMoEActivationConfig(clamp_limit=3.0, alpha=1.3, beta=0.5),
376 + ),
377 + (MoEActivation.SWIGLUSTEP, ApplyMoEActivationConfig()),
378 + (MoEActivation.SILU_NO_MUL, ApplyMoEActivationConfig()),
379 + (MoEActivation.GELU_NO_MUL, ApplyMoEActivationConfig()),
380 + (MoEActivation.GELU_TANH_NO_MUL, ApplyMoEActivationConfig()),
381 + (MoEActivation.RELU2_NO_MUL, ApplyMoEActivationConfig()),
382 + ],
383 + ids=[
384 + "silu",
385 + "silu_clamp",
386 + "gelu",
387 + "gelu_tanh",
388 + "situ",
389 + "swigluoai",
390 + "swigluoai_uninterleave",
391 + "swiglustep",
392 + "silu_no_mul",
393 + "gelu_no_mul",
394 + "gelu_tanh_no_mul",
395 + "relu2_no_mul",
396 + ],
397 +)
398 +@torch.inference_mode()
399 +def test_masked_moe_activation_dispatch(
400 + default_vllm_config,
401 + activation: MoEActivation,
402 + activation_config: ApplyMoEActivationConfig,
403 +) -> None:
404 + _assert_masked_moe_activation(
405 + activation,
406 + activation_config,
407 + dtype=torch.bfloat16,
408 + mask_layout="batched_experts",
409 + d=513,
410 + max_num_tokens=7,
411 + )
412 +
413 +
414 +@pytest.mark.parametrize(
415 + ("activation", "activation_config", "mask_layout"),
416 + [
417 + pytest.param(
418 + MoEActivation.SILU,
419 + ApplyMoEActivationConfig(),
420 + "flat",
421 + id="flat",
422 + ),
423 + pytest.param(
424 + MoEActivation.SITU,
425 + ApplyMoEActivationConfig(
426 + activation_situ_beta=1.5,
427 + activation_situ_linear_beta=2.0,
428 + ),
429 + "batched_experts",
430 + id="batched-experts",
431 + ),
432 + ],
433 +)
434 +@torch.inference_mode()
435 +def test_masked_moe_activation_grid_stride(
436 + default_vllm_config,
437 + activation: MoEActivation,
438 + activation_config: ApplyMoEActivationConfig,
439 + mask_layout: str,
440 +) -> None:
441 + _assert_masked_moe_activation(
442 + activation,
443 + activation_config,
444 + dtype=torch.half,
445 + mask_layout=mask_layout,
446 + d=513,
447 + max_num_tokens=67,
448 + )
449 +
450 +
451 +@torch.inference_mode()
452 +def test_masked_moe_activation_opcheck(default_vllm_config) -> None:
453 + device = CUDA_DEVICES[0]
454 + input = torch.randn(2, 3, 64, dtype=torch.half, device=device)
455 + output = torch.empty(2, 3, 32, dtype=torch.half, device=device)
456 + valid_token_counts = torch.tensor([1, 3], dtype=torch.int32, device=device)
457 + opcheck(
458 + torch.ops._C.masked_moe_activation,
459 + (output, input, valid_token_counts, "silu", 0.0, 1.0, 0.0, 1.0, -1.0),
460 + )
461 +
462 +
463 @pytest.mark.parametrize(
464 "activation",
465 [
tests/kernels/moe/test_batched_moe.py
+145 -3
@@ -8,6 +8,7 @@ import torch
8
9 from tests.kernels.moe.utils import (
10 batched_moe,
11 + make_dummy_moe_config,
12 make_quantized_test_activations,
13 make_test_weights,
14 naive_batched_moe,
@@ -16,7 +17,13 @@ from tests.kernels.quant_utils import native_batched_masked_quant_matmul
17 from tests.kernels.utils import torch_experts
18 from vllm.config import VllmConfig, set_current_vllm_config
19 from vllm.model_executor.layers.fused_moe import fused_topk
20 +from vllm.model_executor.layers.fused_moe.activation import (
21 + ApplyMoEActivationConfig,
22 + MoEActivation,
23 + apply_moe_activation,
24 +)
25 from vllm.model_executor.layers.fused_moe.experts.fused_batched_moe import (
26 + BatchedTritonExperts,
27 invoke_moe_batched_triton_kernel,
28 )
29 from vllm.platforms import current_platform
@@ -496,11 +503,146 @@ def test_batched_mm_td_zero_expert_tokens():
503
504 # BatchedTritonExperts device enablement (XPU)
505 def test_batched_triton_experts_supports_current_device():
499 - from vllm.model_executor.layers.fused_moe.experts.fused_batched_moe import (
500 - BatchedTritonExperts,
506 + assert BatchedTritonExperts._supports_current_device()
507 +
508 +
509 +@pytest.mark.parametrize(
510 + "activation",
511 + [
512 + MoEActivation.SITU,
513 + MoEActivation.SWIGLUOAI_UNINTERLEAVE,
514 + MoEActivation.SWIGLUSTEP,
515 + ],
516 +)
517 +def test_batched_triton_supports_masked_activations(
518 + monkeypatch, activation: MoEActivation
519 +):
520 + monkeypatch.setattr(current_platform, "is_xpu", lambda: False)
521 + assert BatchedTritonExperts._supports_activation(activation)
522 +
523 + monkeypatch.setattr(current_platform, "is_xpu", lambda: True)
524 + assert not BatchedTritonExperts._supports_activation(activation)
525 +
526 +
527 +def _torch_experts_with_shared_activation(
528 + a: torch.Tensor,
529 + w1: torch.Tensor,
530 + w2: torch.Tensor,
531 + topk_weights: torch.Tensor,
532 + topk_ids: torch.Tensor,
533 + activation: MoEActivation,
534 + activation_config: ApplyMoEActivationConfig,
535 +) -> torch.Tensor:
536 + num_tokens, hidden_dim = a.shape
537 + topk = topk_ids.shape[1]
538 + routed_input = a[:, None, :].expand(-1, topk, -1).reshape(-1, hidden_dim)
539 + routed_experts = topk_ids.reshape(-1)
540 + routed_output = torch.zeros_like(routed_input)
541 +
542 + for expert in range(w1.shape[0]):
543 + expert_mask = routed_experts == expert
544 + if not expert_mask.any():
545 + continue
546 + projected = routed_input[expert_mask] @ w1[expert].T
547 + activated = torch.empty(
548 + projected.shape[0], w2.shape[2], dtype=a.dtype, device=a.device
549 + )
550 + apply_moe_activation(
551 + activation,
552 + activated,
553 + projected,
554 + activation_config=activation_config,
555 + )
556 + routed_output[expert_mask] = activated @ w2[expert].T
557 +
558 + return (
559 + (
560 + routed_output.view(num_tokens, topk, hidden_dim).float()
561 + * topk_weights[..., None]
562 + )
563 + .sum(dim=1)
564 + .to(a.dtype)
565 )
566
503 - assert BatchedTritonExperts._supports_current_device()
567 +
568 +_MASKED_BATCHED_ACTIVATION_CASES = [
569 + pytest.param(
570 + MoEActivation.SITU,
571 + ApplyMoEActivationConfig(
572 + activation_situ_beta=1.5,
573 + activation_situ_linear_beta=2.0,
574 + ),
575 + id="situ",
576 + ),
577 + pytest.param(
578 + MoEActivation.SWIGLUOAI_UNINTERLEAVE,
579 + ApplyMoEActivationConfig(clamp_limit=3.0, alpha=1.3, beta=0.5),
580 + id="swigluoai-uninterleave",
581 + ),
582 + pytest.param(
583 + MoEActivation.SWIGLUSTEP,
584 + ApplyMoEActivationConfig(),
585 + id="swiglustep",
586 + ),
587 +]
588 +
589 +
590 +@pytest.mark.parametrize(
591 + ("activation", "activation_config"), _MASKED_BATCHED_ACTIVATION_CASES
592 +)
593 +@torch.inference_mode()
594 +def test_batched_experts_masked_activation_end_to_end(
595 + activation: MoEActivation,
596 + activation_config: ApplyMoEActivationConfig,
597 +):
598 + if current_platform.is_xpu():
599 + pytest.skip("Masked activations are not enabled on XPU")
600 +
601 + from vllm.v1.worker.workspace import init_workspace_manager
602 +
603 + set_random_seed(7)
604 + device = current_platform.device_type
605 + init_workspace_manager(torch.device(f"{device}:0"))
606 + m, n, k, e, topk = 16, 128, 128, 8, 2
607 +
608 + a = torch.randn((m, k), device=device, dtype=torch.bfloat16) / 10
609 + score = torch.randn((m, e), device=device, dtype=torch.bfloat16)
610 + w1 = torch.randn((e, 2 * n, k), device=device, dtype=torch.bfloat16) / 15
611 + w2 = torch.randn((e, k, n), device=device, dtype=torch.bfloat16) / 15
612 + moe_config = make_dummy_moe_config(
613 + num_experts=e,
614 + experts_per_token=topk,
615 + hidden_dim=k,
616 + intermediate_size=n,
617 + in_dtype=a.dtype,
618 + activation=activation,
619 + )
620 + moe_config.swiglu_limit = activation_config.clamp_limit
621 + moe_config.swiglu_alpha = activation_config.alpha
622 + moe_config.swiglu_beta = activation_config.beta
623 + moe_config.activation_situ_beta = activation_config.activation_situ_beta
624 + moe_config.activation_situ_linear_beta = (
625 + activation_config.activation_situ_linear_beta
626 + )
627 +
628 + routing_weights = torch.softmax(score.float(), dim=-1)
629 + topk_weights, topk_ids = torch.topk(routing_weights, topk, dim=-1)
630 + topk_ids = topk_ids.to(torch.int32)
631 + with set_current_vllm_config(vllm_config):
632 + baseline_output = _torch_experts_with_shared_activation(
633 + a,
634 + w1,
635 + w2,
636 + topk_weights,
637 + topk_ids,
638 + activation,
639 + activation_config,
640 + )
641 + triton_output = batched_moe(
642 + a, w1, w2, topk_weights, topk_ids, moe_config=moe_config
643 + )
644 +
645 + torch.testing.assert_close(triton_output, baseline_output, atol=3e-2, rtol=2e-2)
646
647
648 @pytest.mark.parametrize("m,n,k,e,topk", [(32, 512, 512, 8, 2), (45, 1024, 128, 8, 1)])
tests/kernels/moe/test_moe.py
+260 -1
@@ -1377,6 +1377,102 @@ def test_humming_activation_metadata_tracks_shared_apply(activation: MoEActivati
1377 ) == apply_moe_activation_supported(activation)
1378
1379
1380 +@pytest.mark.parametrize(
1381 + ("env_override", "use_ep", "expected"),
1382 + [
1383 + (None, False, "indexed"),
1384 + (None, True, "grouped_contiguous"),
1385 + ("auto", True, "grouped_contiguous"),
1386 + ("indexed", True, "indexed"),
1387 + ("grouped", False, "grouped_contiguous"),
1388 + ],
1389 +)
1390 +def test_humming_selects_gemm_from_parallelism_and_override(
1391 + monkeypatch: pytest.MonkeyPatch,
1392 + env_override: str | None,
1393 + use_ep: bool,
1394 + expected: str,
1395 +):
1396 + from vllm.model_executor.layers.fused_moe.experts.fused_humming_moe import (
1397 + get_humming_moe_gemm_type,
1398 + )
1399 +
1400 + if env_override is None:
1401 + monkeypatch.delenv("VLLM_HUMMING_MOE_GEMM_TYPE", raising=False)
1402 + else:
1403 + monkeypatch.setenv("VLLM_HUMMING_MOE_GEMM_TYPE", env_override)
1404 + moe_config = make_dummy_moe_config()
1405 + moe_config.moe_parallel_config.use_ep = use_ep
1406 +
1407 + assert get_humming_moe_gemm_type(moe_config) == expected
1408 +
1409 +
1410 +@pytest.mark.parametrize(
1411 + ("dp_token_counts", "topk_shape", "expected"),
1412 + [
1413 + pytest.param(None, (7, 3), 21, id="runtime-layout"),
1414 + pytest.param([3, 5], (48, 1), 48, id="expanded-dp-layout"),
1415 + ],
1416 +)
1417 +def test_humming_global_valid_shape_m(
1418 + monkeypatch: pytest.MonkeyPatch,
1419 + dp_token_counts: list[int] | None,
1420 + topk_shape: tuple[int, int],
1421 + expected: int,
1422 +):
1423 + from types import SimpleNamespace
1424 +
1425 + import vllm.model_executor.layers.fused_moe.experts.fused_humming_moe as humming
1426 +
1427 + dp_metadata = (
1428 + None
1429 + if dp_token_counts is None
1430 + else SimpleNamespace(
1431 + num_tokens_across_dp_cpu=torch.tensor(dp_token_counts, dtype=torch.int32)
1432 + )
1433 + )
1434 + monkeypatch.setattr(
1435 + humming,
1436 + "get_forward_context",
1437 + lambda: SimpleNamespace(dp_metadata=dp_metadata),
1438 + )
1439 + experts = SimpleNamespace(moe_config=make_dummy_moe_config(experts_per_token=6))
1440 + topk_ids = torch.empty(topk_shape, dtype=torch.int64)
1441 +
1442 + result = humming.HummingExpertsBase.get_global_valid_shape_m(experts, topk_ids)
1443 +
1444 + assert result == expected
1445 +
1446 +
1447 +def test_humming_permute_scratch_is_keyed_by_runtime_topk(
1448 + monkeypatch: pytest.MonkeyPatch,
1449 +):
1450 + from types import SimpleNamespace
1451 + from unittest.mock import Mock
1452 +
1453 + import vllm.model_executor.layers.fused_moe.experts.fused_humming_moe as humming
1454 +
1455 + scratch_topk6 = Mock()
1456 + scratch_topk1 = Mock()
1457 + scratch_type = Mock(side_effect=[scratch_topk6, scratch_topk1])
1458 + monkeypatch.setattr(humming, "moe_permute_unpermute_supported", lambda: True)
1459 + monkeypatch.setattr(humming, "MoEPermuteScratch", scratch_type)
1460 + moe_config = make_dummy_moe_config(max_num_tokens=512, experts_per_token=6)
1461 + moe_config.moe_parallel_config.dp_size = 2
1462 + experts = SimpleNamespace(_permute_scratch={}, moe_config=moe_config)
1463 +
1464 + assert humming.HummingExpertsBase._get_permute_scratch(experts, 6) is scratch_topk6
1465 + assert humming.HummingExpertsBase._get_permute_scratch(experts, 1) is scratch_topk1
1466 + assert humming.HummingExpertsBase._get_permute_scratch(experts, 6) is scratch_topk6
1467 +
1468 + assert scratch_type.call_count == 2
1469 + first_call, second_call = scratch_type.call_args_list
1470 + assert first_call.kwargs["max_num_tokens"] == 1024
1471 + assert first_call.kwargs["topk"] == 6
1472 + assert second_call.kwargs["max_num_tokens"] == 6144
1473 + assert second_call.kwargs["topk"] == 1
1474 +
1475 +
1476 def test_humming_delegates_to_instance_activation():
1477 from types import SimpleNamespace
1478 from unittest.mock import Mock
@@ -1408,10 +1504,173 @@ def test_humming_delegates_to_instance_activation():
1504 activation=MoEActivation.SWIGLUOAI_UNINTERLEAVE,
1505 input=input,
1506 output=output,
1411 - valid_rows=None,
1507 )
1508
1509
1510 +def test_humming_grouped_apply_forwards_valid_prefix(
1511 + monkeypatch: pytest.MonkeyPatch,
1512 +):
1513 + from types import MethodType, SimpleNamespace
1514 + from unittest.mock import Mock
1515 +
1516 + import vllm.model_executor.layers.fused_moe.experts.fused_humming_moe as humming
1517 + from vllm.model_executor.layers.fused_moe.experts.fused_humming_moe import (
1518 + HummingExpertsBase,
1519 + HummingGroupedExperts,
1520 + )
1521 +
1522 + activation_func = Mock()
1523 + buffers = {
1524 + "gate_up_output": torch.empty(6, 4),
1525 + "activation_output": torch.empty(6, 2),
1526 + "down_output": torch.empty(6, 4),
1527 + }
1528 + experts = SimpleNamespace(
1529 + activation=activation_func,
1530 + num_experts=2,
1531 + estimate_local_valid_shape_m=lambda _: 6,
1532 + prepare_buffers=lambda *_: buffers,
1533 + _get_permute_scratch=lambda _: None,
1534 + quantize_input=lambda _, *, inputs, input_scale=None, quanted_input: (
1535 + inputs,
1536 + input_scale,
1537 + ),
1538 + humming_forward=Mock(),
1539 + compute_config_str="",
1540 + w13_tuning_config_str="",
1541 + w2_tuning_config_str="",
1542 + )
1543 + experts.apply_activation = MethodType(HummingExpertsBase.apply_activation, experts)
1544 +
1545 + hidden_states = torch.empty(3, 4)
1546 + output = torch.empty_like(hidden_states)
1547 + topk_ids = torch.zeros((3, 2), dtype=torch.int64)
1548 + topk_weights = torch.ones_like(topk_ids, dtype=torch.float32)
1549 + expected_counts = torch.tensor([4], dtype=torch.int32)
1550 + expert_offsets = torch.tensor([0, 2, 4], dtype=torch.int64)
1551 + monkeypatch.setattr(
1552 + humming,
1553 + "moe_permute",
1554 + lambda **_: (
1555 + torch.empty(6, 4),
1556 + None,
1557 + expert_offsets,
1558 + torch.arange(6),
1559 + None,
1560 + ),
1561 + )
1562 + monkeypatch.setattr(humming, "moe_unpermute", lambda **_: None)
1563 +
1564 + HummingGroupedExperts.apply(
1565 + experts,
1566 + output=output,
1567 + hidden_states=hidden_states,
1568 + w1=torch.empty(0),
1569 + w2=torch.empty(0),
1570 + topk_weights=topk_weights,
1571 + topk_ids=topk_ids,
1572 + activation=MoEActivation.SITU,
1573 + global_num_experts=2,
1574 + expert_map=None,
1575 + a1q_scale=None,
1576 + a2_scale=None,
1577 + workspace13=torch.empty(0),
1578 + workspace2=torch.empty(0),
1579 + expert_tokens_meta=None,
1580 + apply_router_weight_on_input=False,
1581 + )
1582 +
1583 + activation_func.assert_called_once()
1584 + call_kwargs = activation_func.call_args.kwargs
1585 + assert call_kwargs["activation"] == MoEActivation.SITU
1586 + assert call_kwargs["input"].shape == (6, 4)
1587 + assert call_kwargs["output"].shape == (6, 2)
1588 + torch.testing.assert_close(call_kwargs["valid_token_counts"], expected_counts)
1589 +
1590 +
1591 +def test_batched_marlin_activation_uses_expert_token_counts(
1592 + monkeypatch: pytest.MonkeyPatch,
1593 +):
1594 + from types import SimpleNamespace
1595 + from unittest.mock import Mock
1596 +
1597 + import vllm.model_executor.layers.fused_moe.experts.marlin_moe as marlin
1598 + from vllm.model_executor.layers.fused_moe.experts.marlin_moe import (
1599 + BatchedMarlinExperts,
1600 + )
1601 + from vllm.model_executor.layers.fused_moe.modular_kernel import (
1602 + ExpertTokensMetadata,
1603 + )
1604 +
1605 + activation_func = Mock()
1606 + experts = SimpleNamespace(
1607 + activation=activation_func,
1608 + w1_bias=None,
1609 + w2_bias=None,
1610 + w1_scale=None,
1611 + w2_scale=None,
1612 + quant_type_id=0,
1613 + a1_gscale=None,
1614 + a2_gscale=None,
1615 + w13_g_idx=None,
1616 + w2_g_idx=None,
1617 + w13_g_idx_sort_indices=None,
1618 + w2_g_idx_sort_indices=None,
1619 + w1_zp=None,
1620 + w2_zp=None,
1621 + input_dtype=None,
1622 + is_k_full=True,
1623 + activation_config=ApplyMoEActivationConfig(
1624 + clamp_limit=3.0, alpha=1.3, beta=0.5
1625 + ),
1626 + )
1627 + hidden_states = torch.empty(2, 3, 4)
1628 + expert_num_tokens = torch.tensor([1, 2], dtype=torch.int32)
1629 + topk_ids = torch.zeros((2, 3), dtype=torch.int64)
1630 + expert_map = torch.arange(2, dtype=torch.int32)
1631 +
1632 + def fake_batched_fused_marlin_moe(**kwargs):
1633 + kwargs["activation_func"](
1634 + kwargs["activation"],
1635 + torch.empty(6, 4),
1636 + torch.empty(6, 8),
1637 + topk_ids=topk_ids,
1638 + expert_map=expert_map,
1639 + )
1640 + return kwargs["output"]
1641 +
1642 + monkeypatch.setattr(
1643 + marlin, "batched_fused_marlin_moe", fake_batched_fused_marlin_moe
1644 + )
1645 + output = torch.empty_like(hidden_states)
1646 +
1647 + BatchedMarlinExperts.apply(
1648 + experts,
1649 + output=output,
1650 + hidden_states=hidden_states,
1651 + w1=torch.empty(0),
1652 + w2=torch.empty(0),
1653 + topk_weights=torch.ones_like(topk_ids, dtype=torch.float32),
1654 + topk_ids=topk_ids,
1655 + activation=MoEActivation.SWIGLUOAI_UNINTERLEAVE,
1656 + global_num_experts=2,
1657 + expert_map=expert_map,
1658 + a1q_scale=None,
1659 + a2_scale=None,
1660 + workspace13=torch.empty(0),
1661 + workspace2=torch.empty(0),
1662 + expert_tokens_meta=ExpertTokensMetadata(expert_num_tokens, None),
1663 + apply_router_weight_on_input=False,
1664 + )
1665 +
1666 + activation_func.assert_called_once()
1667 + call = activation_func.call_args
1668 + assert call.args[0] == MoEActivation.SWIGLUOAI_UNINTERLEAVE
1669 + assert call.args[1].shape == (2, 3, 4)
1670 + assert call.args[2].shape == (2, 3, 8)
1671 + torch.testing.assert_close(call.kwargs["valid_token_counts"], expert_num_tokens)
1672 +
1673 +
1674 @pytest.mark.parametrize(
1675 "activation",
1676 [
tests/kernels/moe/test_moe_permute_unpermute.py
+36
@@ -286,3 +286,39 @@ def test_moe_permute_reuses_scratch_buffers(dtype: torch.dtype):
286 permuted_idx_1.untyped_storage().data_ptr()
287 == scratch.permuted_idx.untyped_storage().data_ptr()
288 )
289 +
290 +
291 +def test_moe_permute_ignores_invalid_expert_ids_with_scratch() -> None:
292 + if not moe_permute_unpermute_supported():
293 + pytest.skip("moe_permute_unpermute is not supported on this platform.")
294 +
295 + hidden_states = torch.arange(5 * 16, dtype=torch.bfloat16, device="cuda").view(
296 + 5, 16
297 + )
298 + topk_ids = torch.tensor([[0], [-1], [1], [4], [2]], device="cuda")
299 + expert_map = torch.tensor([0, 1, -1, -1], dtype=torch.int32, device="cuda")
300 + scratch = MoEPermuteScratch(
301 + max_num_tokens=5,
302 + topk=1,
303 + num_experts=4,
304 + num_local_experts=2,
305 + device=hidden_states.device,
306 + hidden_size=16,
307 + hidden_dtype=hidden_states.dtype,
308 + )
309 +
310 + permuted, _, expert_offsets, _, _ = moe_permute(
311 + hidden_states=hidden_states,
312 + a1q_scale=None,
313 + topk_ids=topk_ids,
314 + n_expert=4,
315 + n_local_expert=2,
316 + expert_map=expert_map,
317 + scratch=scratch,
318 + )
319 +
320 + torch.testing.assert_close(
321 + expert_offsets,
322 + torch.tensor([0, 1, 2], dtype=torch.int64, device="cuda"),
323 + )
324 + torch.testing.assert_close(permuted[:2], hidden_states[[0, 2]])
tests/kernels/moe/test_situ_mul_fp8_quant.py
+7 -7
@@ -5,7 +5,7 @@ quant kernel (``situ_and_mul_quant``).
5
6 Mirrors ``test_silu_mul_fp8_quant_deep_gemm.py`` in structure: build a reference
7 activation + block-FP8 quant, drive random per-row scale magnitudes, exercise
8 -the masked (``valid_rows``) contiguous layout, and compare per valid row.
8 +the masked contiguous layout, and compare per valid row.
9
10 The kernel computes SITU in fp32 and converts to fp8 with hardware rounding,
11 which torch cannot reproduce bit-exactly. It is therefore validated against the
@@ -40,7 +40,7 @@ SITU_LINEAR_BETA = 25.0
40 SITU_D = 3072
41 GROUP_SIZE = 128
42
43 -# (num_tokens, valid_rows). valid_rows=None => all rows valid. 2048/4096 rows
43 +# (num_tokens, num_valid_tokens). None means all rows are valid. 2048/4096 rows
44 # span multiple grid-stride waves of the persistent grid (GRID_DIM = 132*8).
45 CASES = [
46 (1, None),
@@ -111,7 +111,7 @@ def test_situ_and_mul_quant_pipelined(
111 (num_tokens, d // GROUP_SIZE), -1.0, dtype=torch.float32, device=DEVICE
112 )
113 out_sentinel = out.clone()
114 - valid_rows = (
114 + num_valid_tokens = (
115 None if valid is None else torch.tensor(valid, dtype=torch.int32, device=DEVICE)
116 )
117
@@ -122,7 +122,7 @@ def test_situ_and_mul_quant_pipelined(
122 beta=SITU_BETA,
123 linear_beta=SITU_LINEAR_BETA,
124 group_size=GROUP_SIZE,
125 - valid_rows=valid_rows,
125 + num_valid_tokens=num_valid_tokens,
126 )
127
128 # Padding contract: skipped rows keep their bytes; their scale becomes 1.0.
@@ -157,7 +157,7 @@ def test_situ_and_mul_quant_pipelined(
157 @pytest.mark.parametrize("tokens,topk", [(300, 8), (1, 4), (256, 6)])
158 @torch.inference_mode()
159 def test_situ_and_mul_quant_topk_row_bound(tokens: int, topk: int) -> None:
160 - """valid_rows is a token count; the kernel bounds rows at tokens*topk."""
160 + """The kernel expands num_valid_tokens by topk to bound rows."""
161 set_random_seed(7)
162 d = SITU_D
163 num_tokens = tokens * topk + 37 # padding tail past the valid rows
@@ -168,7 +168,7 @@ def test_situ_and_mul_quant_topk_row_bound(tokens: int, topk: int) -> None:
168 (num_tokens, d // GROUP_SIZE), -1.0, dtype=torch.float32, device=DEVICE
169 )
170 out_sentinel = out.clone()
171 - valid_rows = torch.tensor(tokens, dtype=torch.int32, device=DEVICE)
171 + num_valid_tokens = torch.tensor(tokens, dtype=torch.int32, device=DEVICE)
172
173 situ_and_mul_quant(
174 out,
@@ -177,7 +177,7 @@ def test_situ_and_mul_quant_topk_row_bound(tokens: int, topk: int) -> None:
177 beta=SITU_BETA,
178 linear_beta=SITU_LINEAR_BETA,
179 group_size=GROUP_SIZE,
180 - valid_rows=valid_rows,
180 + num_valid_tokens=num_valid_tokens,
181 topk=topk,
182 )
183
tests/kernels/moe/utils.py
+3 -1
@@ -128,6 +128,7 @@ def batched_moe(
128 quant_dtype: torch.dtype | None = None,
129 per_act_token_quant: bool = False,
130 block_shape: list[int] | None = None,
131 + moe_config: FusedMoEConfig | None = None,
132 ) -> torch.Tensor:
133 max_num_tokens = round_up(a.shape[0], 64)
134
@@ -141,7 +142,8 @@ def batched_moe(
142 a2_scale=a2_scale,
143 )
144
144 - moe_config = make_dummy_moe_config()
145 + if moe_config is None:
146 + moe_config = make_dummy_moe_config()
147
148 fused_experts = FusedMoEKernel(
149 BatchedPrepareAndFinalize(
vllm/model_executor/layers/fused_moe/activation.py
+139 -22
@@ -9,6 +9,8 @@ from typing import TYPE_CHECKING
9 import torch
10 import torch.nn.functional as F
11
12 +from vllm.platforms import current_platform
13 +
14 if TYPE_CHECKING:
15 from vllm.model_executor.layers.fused_moe.config import (
16 FusedMoEConfig,
@@ -134,11 +136,31 @@ _APPLY_MOE_ACTIVATIONS = frozenset(
136 )
137
138
139 +_MASKED_MOE_ACTIVATION_NAMES: dict[MoEActivation, str] = {
140 + MoEActivation.SILU: "silu",
141 + MoEActivation.GELU: "gelu",
142 + MoEActivation.GELU_TANH: "gelu_tanh",
143 + MoEActivation.SITU: "situ",
144 + MoEActivation.SWIGLUOAI: "swigluoai",
145 + MoEActivation.SWIGLUOAI_UNINTERLEAVE: "swigluoai_uninterleave",
146 + MoEActivation.SWIGLUSTEP: "swiglustep",
147 + MoEActivation.SILU_NO_MUL: "silu_no_mul",
148 + MoEActivation.GELU_NO_MUL: "gelu_no_mul",
149 + MoEActivation.GELU_TANH_NO_MUL: "gelu_tanh_no_mul",
150 + MoEActivation.RELU2_NO_MUL: "relu2_no_mul",
151 +}
152 +
153 +
154 def apply_moe_activation_supported(activation: MoEActivation) -> bool:
155 """Whether ``apply_moe_activation`` supports an activation."""
156 return activation in _APPLY_MOE_ACTIVATIONS
157
158
159 +def apply_moe_activation_masked_supported(activation: MoEActivation) -> bool:
160 + """Whether the masked ``apply_moe_activation`` path supports an activation."""
161 + return activation in _MASKED_MOE_ACTIVATION_NAMES
162 +
163 +
164 @dataclass(frozen=True)
165 class ApplyMoEActivationConfig:
166 """Configuration forwarded to ``apply_moe_activation``."""
@@ -177,6 +199,96 @@ class ApplyMoEActivationConfig:
199 _DEFAULT_APPLY_MOE_ACTIVATION_CONFIG = ApplyMoEActivationConfig()
200
201
202 +def _validate_moe_activation_shapes(
203 + activation: MoEActivation,
204 + output: torch.Tensor,
205 + input: torch.Tensor,
206 + expected_dim: int,
207 +) -> None:
208 + assert input.dim() == expected_dim, f"Input must be {expected_dim}D"
209 + assert output.dim() == expected_dim, f"Output must be {expected_dim}D"
210 + assert input.shape[:-1] == output.shape[:-1], (
211 + f"Input/output leading shapes must match: {input.shape} vs {output.shape}"
212 + )
213 + if activation.is_gated:
214 + assert output.size(-1) * 2 == input.size(-1), (
215 + f"{activation.value} expects 2x ratio: "
216 + f"{output.size(-1) * 2} vs {input.size(-1)}"
217 + )
218 + else:
219 + assert output.size(-1) == input.size(-1), (
220 + f"{activation.value} expects equal sizes: "
221 + f"{output.size(-1)} vs {input.size(-1)}"
222 + )
223 +
224 +
225 +def _apply_moe_activation_masked(
226 + activation: MoEActivation,
227 + output: torch.Tensor,
228 + input: torch.Tensor,
229 + valid_token_counts: torch.Tensor,
230 + config: ApplyMoEActivationConfig,
231 +) -> torch.Tensor:
232 + masked_activation = _MASKED_MOE_ACTIVATION_NAMES.get(activation)
233 + if masked_activation is None:
234 + raise NotImplementedError(
235 + f"Masked MoE activation is not implemented for {activation.value}"
236 + )
237 +
238 + assert input.dim() in (2, 3), "Masked input must be 2D or 3D"
239 + _validate_moe_activation_shapes(activation, output, input, expected_dim=input.dim())
240 + assert input.dtype == output.dtype, "Input and output dtypes must match"
241 + assert input.device == output.device, "Input and output devices must match"
242 + assert input.is_contiguous(), "Input must be contiguous"
243 + assert output.is_contiguous(), "Output must be contiguous"
244 + assert valid_token_counts.dtype == torch.int32, (
245 + "valid_token_counts must use torch.int32"
246 + )
247 + assert valid_token_counts.dim() == 1, "valid_token_counts must be 1D"
248 + assert valid_token_counts.device == input.device, (
249 + "valid_token_counts must be on the input device"
250 + )
251 + assert valid_token_counts.is_contiguous(), "valid_token_counts must be contiguous"
252 + expected_counts = input.size(0) if input.dim() == 3 else 1
253 + assert valid_token_counts.size(0) == expected_counts, (
254 + f"valid_token_counts must have {expected_counts} element(s) for "
255 + f"{input.dim()}D input"
256 + )
257 +
258 + masked_clamp_limit = 0.0 if config.clamp_limit is None else config.clamp_limit
259 + masked_alpha = config.alpha
260 + if activation == MoEActivation.SILU and config.clamp_limit is not None:
261 + masked_activation = "silu_with_clamp"
262 + elif activation == MoEActivation.SITU:
263 + assert config.activation_situ_beta is not None, (
264 + "SITU requires activation_situ_beta from FusedMoEConfig"
265 + )
266 + elif activation == MoEActivation.SWIGLUOAI:
267 + masked_clamp_limit = 7.0
268 + masked_alpha = 1.702
269 + elif activation == MoEActivation.SWIGLUOAI_UNINTERLEAVE:
270 + assert config.clamp_limit is not None, (
271 + "SWIGLUOAI_UNINTERLEAVE requires clamp_limit"
272 + )
273 + elif activation == MoEActivation.SWIGLUSTEP:
274 + masked_clamp_limit = 7.0
275 +
276 + torch.ops._C.masked_moe_activation(
277 + output,
278 + input,
279 + valid_token_counts,
280 + masked_activation,
281 + masked_clamp_limit,
282 + masked_alpha,
283 + config.beta,
284 + 1.0 if config.activation_situ_beta is None else config.activation_situ_beta,
285 + -1.0
286 + if config.activation_situ_linear_beta is None
287 + else config.activation_situ_linear_beta,
288 + )
289 + return output
290 +
291 +
292 def silu_and_mul_with_clamp(
293 output: torch.Tensor,
294 input: torch.Tensor,
@@ -184,7 +296,12 @@ def silu_and_mul_with_clamp(
296 topk_ids: torch.Tensor | None = None,
297 expert_map: torch.Tensor | None = None,
298 ) -> None:
187 - if topk_ids is not None and expert_map is not None:
299 + if current_platform.is_xpu():
300 + d = input.shape[-1] // 2
301 + gate = torch.clamp(input[..., :d], max=clamp_limit)
302 + up = torch.clamp(input[..., d:], min=-clamp_limit, max=clamp_limit)
303 + output.copy_(gate * torch.sigmoid(gate) * up)
304 + elif topk_ids is not None and expert_map is not None:
305 from vllm.model_executor.layers.fused_moe.utils import swiglu_limit_func
306
307 swiglu_limit_func(output, input, clamp_limit, topk_ids, expert_map)
@@ -201,12 +318,15 @@ def apply_moe_activation(
318 activation_config: ApplyMoEActivationConfig | None = None,
319 topk_ids: torch.Tensor | None = None,
320 expert_map: torch.Tensor | None = None,
204 - valid_rows: torch.Tensor | None = None,
321 + valid_token_counts: torch.Tensor | None = None,
322 ) -> torch.Tensor:
323 """Apply MoE activation function.
324
325 The configuration drives specialized activation behavior. Routing tensors
209 - remain per-call inputs because they depend on the current token assignment.
326 + and valid token counts remain per-call inputs because they depend on the
327 + current token assignment. A single token count masks a flat ``[T, D]``
328 + buffer; one count per expert masks each prefix in a padded ``[E, T, D]``
329 + buffer.
330 """
331 config = (
332 _DEFAULT_APPLY_MOE_ACTIVATION_CONFIG
@@ -214,19 +334,17 @@ def apply_moe_activation(
334 else activation_config
335 )
336
217 - assert input.dim() == 2, "Input must be 2D"
218 - assert output.dim() == 2, "Output must be 2D"
219 - if activation.is_gated:
220 - assert output.size(-1) * 2 == input.size(-1), (
221 - f"{activation.value} expects 2x ratio: "
222 - f"{output.size(-1) * 2} vs {input.size(-1)}"
223 - )
224 - else:
225 - assert output.size(-1) == input.size(-1), (
226 - f"{activation.value} expects equal sizes: "
227 - f"{output.size(-1)} vs {input.size(-1)}"
337 + if valid_token_counts is not None:
338 + return _apply_moe_activation_masked(
339 + activation,
340 + output,
341 + input,
342 + valid_token_counts,
343 + config,
344 )
345
346 + _validate_moe_activation_shapes(activation, output, input, expected_dim=2)
347 +
348 # Activations with gated multiplication (gate × activation(up))
349 if activation == MoEActivation.SILU:
350 if config.clamp_limit is not None:
@@ -257,7 +375,6 @@ def apply_moe_activation(
375 -1.0
376 if config.activation_situ_linear_beta is None
377 else config.activation_situ_linear_beta,
260 - valid_rows,
378 )
379 elif activation == MoEActivation.SWIGLUOAI:
380 torch.ops._C.swigluoai_and_mul(output, input)
@@ -298,7 +415,7 @@ def situ_and_mul_quant(
415 beta: float,
416 linear_beta: float | None,
417 group_size: int = 0,
301 - valid_rows: torch.Tensor | None = None,
418 + num_valid_tokens: torch.Tensor | None = None,
419 topk: int = 1,
420 ) -> None:
421 """Fused Kimi SITU activation + dynamic FP8 quantization.
@@ -314,11 +431,11 @@ def situ_and_mul_quant(
431 [M, d // 128], matching humming ``quant_input(group_size=128, float32)``.
432
433 ``linear_beta`` <= 0 (or None) means "unset" (up passed through), matching
317 - ``SituAndMul(linear_beta=None)``. ``valid_rows`` (int32 scalar tensor) is the
318 - DeepEP v2 contiguous-layout valid *token* count (e.g. ``psum[-1:]``); the
319 - kernel multiplies it by ``topk`` to bound rows, so no host-side cast/multiply
320 - is needed. Padding rows are skipped and receive a benign scale of 1.0. Kept
321 - in this module so that every ``torch.ops._C.situ*`` call lives in one file.
434 + ``SituAndMul(linear_beta=None)``. ``num_valid_tokens`` (int32 scalar tensor)
435 + is the DeepEP v2 contiguous-layout token count (e.g. ``psum[-1:]``); the
436 + kernel multiplies it by ``topk`` to bound rows. Padding rows are skipped and
437 + receive a benign scale of 1.0. Kept in this module so that every
438 + ``torch.ops._C.situ*`` call lives in one file.
439 """
440 torch.ops._C.situ_and_mul_quant(
441 output,
@@ -327,6 +444,6 @@ def situ_and_mul_quant(
444 beta,
445 -1.0 if linear_beta is None else linear_beta,
446 group_size,
330 - valid_rows,
447 + num_valid_tokens,
448 topk,
449 )
vllm/model_executor/layers/fused_moe/experts/fused_batched_moe.py
+29 -31
@@ -5,7 +5,10 @@
5 import torch
6
7 import vllm.model_executor.layers.fused_moe.modular_kernel as mk
8 -from vllm.model_executor.layers.fused_moe.activation import MoEActivation
8 +from vllm.model_executor.layers.fused_moe.activation import (
9 + MoEActivation,
10 + apply_moe_activation_masked_supported,
11 +)
12 from vllm.model_executor.layers.fused_moe.config import (
13 FusedMoEConfig,
14 FusedMoEParallelConfig,
@@ -19,7 +22,6 @@ from vllm.model_executor.layers.fused_moe.utils import (
22 _resize_cache,
23 moe_kernel_quantize_input,
24 normalize_batched_scales_shape,
22 - swiglu_limit_func,
25 )
26 from vllm.model_executor.layers.quantization.utils.quant_utils import (
27 QuantKey,
@@ -839,16 +841,18 @@ class BatchedTritonExperts(mk.FusedMoEExpertsModular):
841
842 @staticmethod
843 def _supports_activation(activation: MoEActivation) -> bool:
842 - return activation in [
843 - MoEActivation.SILU,
844 - MoEActivation.GELU,
845 - MoEActivation.GELU_TANH,
846 - MoEActivation.SWIGLUOAI,
847 - MoEActivation.SILU_NO_MUL,
848 - MoEActivation.GELU_NO_MUL,
849 - MoEActivation.GELU_TANH_NO_MUL,
850 - MoEActivation.RELU2_NO_MUL,
851 - ]
844 + if current_platform.is_xpu():
845 + return activation in [
846 + MoEActivation.SILU,
847 + MoEActivation.GELU,
848 + MoEActivation.GELU_TANH,
849 + MoEActivation.SWIGLUOAI,
850 + MoEActivation.SILU_NO_MUL,
851 + MoEActivation.GELU_NO_MUL,
852 + MoEActivation.GELU_TANH_NO_MUL,
853 + MoEActivation.RELU2_NO_MUL,
854 + ]
855 + return apply_moe_activation_masked_supported(activation)
856
857 @staticmethod
858 def _supports_parallel_config(moe_parallel_config: FusedMoEParallelConfig) -> bool:
@@ -858,20 +862,6 @@ class BatchedTritonExperts(mk.FusedMoEExpertsModular):
862 # Let PrepareAndFinalize::finalize() decide the impl.
863 return TopKWeightAndReduceDelegate()
864
861 - def activation(
862 - self,
863 - activation: MoEActivation,
864 - output: torch.Tensor,
865 - input: torch.Tensor,
866 - **kwargs,
867 - ) -> None:
868 - gemm1_clamp_limit = self.quant_config.gemm1_clamp_limit
869 - if activation == MoEActivation.SILU and gemm1_clamp_limit is not None:
870 - swiglu_limit_func(output, input, float(gemm1_clamp_limit))
871 - return
872 -
873 - super().activation(activation, output, input)
874 -
865 def workspace_shapes(
866 self,
867 M: int,
@@ -998,11 +988,19 @@ class BatchedTritonExperts(mk.FusedMoEExpertsModular):
988 intermediate_cache2.fill_(0)
989
990 # TODO (bnell): use triton utility from batched deep gemm.
1001 - self.activation(
1002 - activation,
1003 - intermediate_cache2.view(-1, activation_out_dim),
1004 - intermediate_cache1.view(-1, N),
1005 - )
991 + if current_platform.is_xpu():
992 + self.activation(
993 + activation,
994 + intermediate_cache2.view(-1, activation_out_dim),
995 + intermediate_cache1.view(-1, N),
996 + )
997 + else:
998 + self.activation(
999 + activation,
1000 + intermediate_cache2,
1001 + intermediate_cache1,
1002 + valid_token_counts=expert_num_tokens,
1003 + )
1004
1005 qintermediate_cache2, a2q_scale = batched_moe_kernel_quantize_input(
1006 intermediate_cache2,
vllm/model_executor/layers/fused_moe/experts/fused_humming_moe.py
+52 -51
@@ -35,10 +35,7 @@ from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
35 TopKWeightAndReduceDelegate,
36 TopKWeightAndReduceNoOP,
37 )
38 -from vllm.model_executor.layers.fused_moe.utils import (
39 - _resize_cache,
40 - swiglu_limit_func,
41 -)
38 +from vllm.model_executor.layers.fused_moe.utils import _resize_cache
39 from vllm.model_executor.layers.quantization.utils.quant_utils import (
40 INT4_DTYPE,
41 INT8_DTYPE,
@@ -103,17 +100,18 @@ def _is_supported_wna16_weight_key(weight_key: QuantKey | None) -> bool:
100 )
101
102
106 -def get_humming_moe_gemm_type() -> str:
103 +def get_humming_moe_gemm_type(moe_config: FusedMoEConfig | None = None) -> str:
104 env_gemm_type: str | None = envs.VLLM_HUMMING_MOE_GEMM_TYPE
108 - gemm_type = "indexed"
109 - if env_gemm_type is not None:
105 + if env_gemm_type is not None and env_gemm_type.lower() != "auto":
106 env_gemm_type = env_gemm_type.lower()
111 - if env_gemm_type == "indexed":
112 - gemm_type = env_gemm_type
113 - elif env_gemm_type in ["grouped_contiguous", "grouped"]:
107 + if env_gemm_type == "grouped":
108 gemm_type = "grouped_contiguous"
109 else:
116 - gemm_type = "indexed"
110 + gemm_type = env_gemm_type
111 + elif moe_config is not None and moe_config.moe_parallel_config.use_ep:
112 + gemm_type = "grouped_contiguous"
113 + else:
114 + gemm_type = "indexed"
115
116 logger.info_once(f"Using {gemm_type} gemm for humming moe") # noqa
117 return gemm_type
@@ -178,7 +176,7 @@ class HummingExpertsBase(mk.FusedMoEExpertsModular):
176 max_num_tokens=max_num_tokens,
177 num_dispatchers=num_dispatchers,
178 )
181 - self._permute_scratch: MoEPermuteScratch | None = None
179 + self._permute_scratch: dict[int, MoEPermuteScratch] = {}
180
181 def init_humming_moe(self):
182 from vllm.utils.humming import get_heuristics_config
@@ -257,26 +255,36 @@ class HummingExpertsBase(mk.FusedMoEExpertsModular):
255 **kwargs,
256 )
257
260 - def _get_permute_scratch(self) -> MoEPermuteScratch | None:
261 - if self._permute_scratch is None and moe_permute_unpermute_supported():
262 - self._permute_scratch = MoEPermuteScratch(
263 - max_num_tokens=self.moe_config.max_num_tokens,
264 - topk=self.moe_config.experts_per_token,
258 + def _get_permute_scratch(self, topk: int) -> MoEPermuteScratch | None:
259 + if not moe_permute_unpermute_supported():
260 + return None
261 +
262 + scratch = self._permute_scratch.get(topk)
263 + if scratch is None:
264 + max_expanded_rows = (
265 + self.moe_config.max_num_tokens
266 + * self.moe_config.dp_size
267 + * self.moe_config.experts_per_token
268 + )
269 + scratch = MoEPermuteScratch(
270 + max_num_tokens=math.ceil(max_expanded_rows / topk),
271 + topk=topk,
272 num_experts=self.moe_config.num_experts,
273 num_local_experts=self.moe_config.num_local_experts,
274 device=torch.device(self.moe_config.device),
275 hidden_size=self.moe_config.hidden_dim,
276 hidden_dtype=self.moe_config.in_dtype,
277 )
271 - return self._permute_scratch
278 + self._permute_scratch[topk] = scratch
279 + return scratch
280
281 def get_global_valid_shape_m(self, topk_ids: torch.Tensor):
274 - num_tokens = topk_ids.size(0)
282 ctx = get_forward_context()
283 if ctx.dp_metadata is not None:
284 num_tokens = ctx.dp_metadata.num_tokens_across_dp_cpu.sum().item()
285 + return num_tokens * self.moe_config.experts_per_token
286
279 - return num_tokens * topk_ids.size(1)
287 + return topk_ids.size(0) * topk_ids.size(1)
288
289 def estimate_local_valid_shape_m(self, topk_ids: torch.Tensor):
290 # estimate shape_m for kernel tuning
@@ -645,7 +653,7 @@ class HummingExpertsBase(mk.FusedMoEExpertsModular):
653 if supported:
654 assert hasattr(cls, "humming_gemm_type")
655 gemm_type = cls.humming_gemm_type().value.lower()
648 - preferred_gemm_type = get_humming_moe_gemm_type()
656 + preferred_gemm_type = get_humming_moe_gemm_type(moe_config)
657 supported = preferred_gemm_type.lower() == gemm_type
658 if not supported:
659 reason = (
@@ -660,25 +668,16 @@ class HummingExpertsBase(mk.FusedMoEExpertsModular):
668 activation: MoEActivation,
669 output: torch.Tensor,
670 input: torch.Tensor,
663 - valid_rows: torch.Tensor | None = None,
671 + valid_token_counts: torch.Tensor | None = None,
672 ) -> None:
665 - activation_config = self.activation_config
666 - if (
667 - activation == MoEActivation.SILU
668 - and activation_config.clamp_limit is not None
669 - ):
670 - swiglu_limit_func(
671 - output=output,
672 - input=input,
673 - swiglu_limit=activation_config.clamp_limit,
674 - )
675 - else:
676 - self.activation(
677 - activation=activation,
678 - input=input,
679 - output=output,
680 - valid_rows=valid_rows,
681 - )
673 + activation_kwargs: dict[str, Any] = dict(
674 + activation=activation,
675 + input=input,
676 + output=output,
677 + )
678 + if valid_token_counts is not None:
679 + activation_kwargs["valid_token_counts"] = valid_token_counts
680 + self.activation(**activation_kwargs)
681
682 def fused_situ_quant_enabled(self, activation: MoEActivation) -> bool:
683 """Whether the SITU activation + w2 quant can be fused into one kernel.
@@ -718,7 +717,7 @@ class HummingExpertsBase(mk.FusedMoEExpertsModular):
717 self,
718 gate_up_output: torch.Tensor,
719 quanted_down_input: torch.Tensor,
721 - valid_rows: torch.Tensor | None,
720 + num_valid_tokens: torch.Tensor | None,
721 topk: int = 1,
722 ) -> tuple[torch.Tensor, torch.Tensor]:
723 """Run the fused SITU activation + FP8 quant for the w2 input.
@@ -750,7 +749,7 @@ class HummingExpertsBase(mk.FusedMoEExpertsModular):
749 beta=cfg.activation_situ_beta,
750 linear_beta=cfg.activation_situ_linear_beta,
751 group_size=group_size,
753 - valid_rows=valid_rows,
752 + num_valid_tokens=num_valid_tokens,
753 topk=topk,
754 )
755 return quanted_down_input, input_scale
@@ -878,8 +877,8 @@ class HummingIndexedExperts(HummingExpertsBase):
877 )
878
879 # psum[-1:] is the DeepEP valid *token* count as a zero-cost int32 view.
881 - # situ (rows = tokens*topk) multiplies by topk on-device; mul_sum bounds
882 - # on tokens, so both read this pointer directly -- no host cast/multiply.
880 + # The fused path and mul_sum consume tokens directly. The unfused
881 + # activation consumes the expanded row count (tokens * topk).
882 valid_tokens = None
883 topk = topk_ids.size(1)
884 if (
@@ -894,21 +893,18 @@ class HummingIndexedExperts(HummingExpertsBase):
893 inputs, input_scale = self.fused_situ_quant(
894 gate_up_output=buffers["gate_up_output"],
895 quanted_down_input=buffers["quanted_down_input"],
897 - valid_rows=valid_tokens,
896 + num_valid_tokens=valid_tokens,
897 topk=topk,
898 )
899 else:
901 - # Fallback situ_and_mul takes int64 row counts (tokens*topk).
902 - valid_rows = (
903 - valid_tokens.to(torch.int64) * topk
904 - if valid_tokens is not None
905 - else None
900 + valid_token_counts = (
901 + valid_tokens * topk if valid_tokens is not None else None
902 )
903 self.apply_activation(
904 activation=activation,
905 input=buffers["gate_up_output"],
906 output=buffers["activation_output"],
911 - valid_rows=valid_rows,
907 + valid_token_counts=valid_token_counts,
908 )
909
910 inputs, input_scale = self.quantize_input(
@@ -1002,7 +998,7 @@ class HummingGroupedExperts(HummingExpertsBase):
998 n_expert=global_num_experts,
999 n_local_expert=self.num_experts,
1000 expert_map=expert_map,
1005 - scratch=self._get_permute_scratch(),
1001 + scratch=self._get_permute_scratch(topk_ids.size(1)),
1002 )
1003
1004 inputs, input_scale = self.quantize_input(
@@ -1028,6 +1024,11 @@ class HummingGroupedExperts(HummingExpertsBase):
1024 activation=activation,
1025 input=buffers["gate_up_output"],
1026 output=buffers["activation_output"],
1027 + valid_token_counts=(
1028 + expert_first_token_offset[-1:].to(torch.int32)
1029 + if expert_tokens_meta is None
1030 + else None
1031 + ),
1032 )
1033
1034 inputs, input_scale = self.quantize_input(
vllm/model_executor/layers/fused_moe/experts/marlin_moe.py
+13 -16
@@ -13,6 +13,7 @@ from vllm.model_executor.layers.fused_moe.activation import (
13 ApplyMoEActivationConfig,
14 MoEActivation,
15 apply_moe_activation,
16 + apply_moe_activation_masked_supported,
17 apply_moe_activation_supported,
18 )
19 from vllm.model_executor.layers.fused_moe.config import (
@@ -955,6 +956,10 @@ class BatchedMarlinExperts(MarlinExpertsBase):
956 def activation_format() -> mk.FusedMoEActivationFormat:
957 return mk.FusedMoEActivationFormat.BatchedExperts
958
959 + @staticmethod
960 + def _supports_activation(activation: MoEActivation) -> bool:
961 + return apply_moe_activation_masked_supported(activation)
962 +
963 def workspace_shapes(
964 self,
965 M: int,
@@ -1000,26 +1005,18 @@ class BatchedMarlinExperts(MarlinExpertsBase):
1005 act: MoEActivation,
1006 act_output: torch.Tensor,
1007 act_input: torch.Tensor,
1003 - **kwargs,
1008 + *,
1009 + topk_ids: torch.Tensor | None = None,
1010 + expert_map: torch.Tensor | None = None,
1011 ) -> None:
1005 - if act != MoEActivation.SITU:
1006 - self.activation(
1007 - act,
1008 - act_output,
1009 - act_input,
1010 - **kwargs,
1011 - )
1012 - return
1013 -
1012 num_experts, max_num_tokens = hidden_states.shape[:2]
1015 - beta = self.activation_config.activation_situ_beta
1016 - linear_beta = self.activation_config.activation_situ_linear_beta
1017 - torch.ops._C.masked_situ_and_mul(
1013 + self.activation(
1014 + act,
1015 act_output.view(num_experts, max_num_tokens, -1),
1016 act_input.view(num_experts, max_num_tokens, -1),
1020 - expert_tokens_meta.expert_num_tokens,
1021 - 1.0 if beta is None else beta,
1022 - -1.0 if linear_beta is None else linear_beta,
1017 + topk_ids=topk_ids,
1018 + expert_map=expert_map,
1019 + valid_token_counts=expert_tokens_meta.expert_num_tokens,
1020 )
1021
1022 return batched_fused_marlin_moe(
vllm/model_executor/layers/fused_moe/modular_kernel.py
+2 -2
@@ -902,7 +902,7 @@ class FusedMoEExpertsModular(FusedMoEExperts):
902 *,
903 topk_ids: torch.Tensor | None = None,
904 expert_map: torch.Tensor | None = None,
905 - valid_rows: torch.Tensor | None = None,
905 + valid_token_counts: torch.Tensor | None = None,
906 ) -> None:
907 apply_moe_activation(
908 activation,
@@ -911,7 +911,7 @@ class FusedMoEExpertsModular(FusedMoEExperts):
911 activation_config=self.activation_config,
912 topk_ids=topk_ids,
913 expert_map=expert_map,
914 - valid_rows=valid_rows,
914 + valid_token_counts=valid_token_counts,
915 )
916
917 @abstractmethod