Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 54 additions & 21 deletions csrc/moe/topk_softmax_kernels.cu
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,17 @@ enum ScoringFunc {
// in the softmax kernel when we extend this module to support expert-choice routing.
template <int TPB, typename InputType>
__launch_bounds__(TPB) __global__
void moeSoftmax(const InputType* input, const bool* finished, float* output, const int num_cols)
void moeSoftmax(const InputType* input, const bool* finished, float* output, const int num_cols,
const int64_t input_row_stride)
{
using BlockReduce = cub::BlockReduce<float, TPB>;
__shared__ typename BlockReduce::TempStorage tmpStorage;

__shared__ float normalizing_factor;
__shared__ float float_max;

const int thread_row_offset = blockIdx.x * num_cols;
const int64_t input_row_offset = blockIdx.x * input_row_stride;
const int output_row_offset = blockIdx.x * num_cols;

float threadData(-FLT_MAX);

Expand All @@ -98,8 +100,7 @@ __launch_bounds__(TPB) __global__

for (int ii = threadIdx.x; ii < num_cols; ii += TPB)
{
const int idx = thread_row_offset + ii;
const float val = toFloat(input[idx]);
const float val = toFloat(input[input_row_offset + ii]);
threadData = max(val, threadData);
}

Expand All @@ -114,8 +115,7 @@ __launch_bounds__(TPB) __global__

for (int ii = threadIdx.x; ii < num_cols; ii += TPB)
{
const int idx = thread_row_offset + ii;
const float val = toFloat(input[idx]);
const float val = toFloat(input[input_row_offset + ii]);
threadData += expf(val - float_max);
}

Expand All @@ -129,20 +129,21 @@ __launch_bounds__(TPB) __global__

for (int ii = threadIdx.x; ii < num_cols; ii += TPB)
{
const int idx = thread_row_offset + ii;
const float val = toFloat(input[idx]);
const float val = toFloat(input[input_row_offset + ii]);
float softmax_val = expf(val - float_max) * normalizing_factor;
// Clamp NaN/Inf to 0 to prevent duplicate expert IDs downstream.
if (isnan(softmax_val) || isinf(softmax_val)) softmax_val = 0.f;
output[idx] = softmax_val;
output[output_row_offset + ii] = softmax_val;
}
}

template <int TPB, typename InputType>
__launch_bounds__(TPB) __global__
void moeSigmoid(const InputType* input, const bool* finished, float* output, const int num_cols)
void moeSigmoid(const InputType* input, const bool* finished, float* output, const int num_cols,
const int64_t input_row_stride)
{
const int thread_row_offset = blockIdx.x * num_cols;
const int64_t input_row_offset = blockIdx.x * input_row_stride;
const int output_row_offset = blockIdx.x * num_cols;

// Don't touch finished rows.
if ((finished != nullptr) && finished[blockIdx.x])
Expand All @@ -152,12 +153,11 @@ __launch_bounds__(TPB) __global__

for (int ii = threadIdx.x; ii < num_cols; ii += TPB)
{
const int idx = thread_row_offset + ii;
const float val = toFloat(input[idx]);
const float val = toFloat(input[input_row_offset + ii]);
float sigmoid_val = 1.0f / (1.0f + __expf(-val));
// Clamp NaN/Inf to 0 to prevent duplicate expert IDs downstream.
if (isnan(sigmoid_val) || isinf(sigmoid_val)) sigmoid_val = 0.f;
output[idx] = sigmoid_val;
output[output_row_offset + ii] = sigmoid_val;
}
}

Expand Down Expand Up @@ -688,6 +688,7 @@ void topkGatingKernelLauncher(
float* workspace,
const int num_tokens,
const int num_experts,
const int64_t input_row_stride,
const int topk,
const bool renormalize,
const float* bias,
Expand All @@ -703,6 +704,28 @@ void topkGatingKernelLauncher(
static constexpr int BYTES_PER_LDG_MULTIPLE_64 =
(std::is_same_v<InputType, __nv_bfloat16> || std::is_same_v<InputType, __half>) ? 4 : 8;
#endif
// Inductor can pad GEMM outputs (for example, a [M, 60] view with a row
// stride of 64). The fused kernels assume packed rows, so use the
// stride-aware unfused path for row-padded inputs.
if (input_row_stride != num_experts) {
STD_TORCH_CHECK(workspace != nullptr,
"workspace must be provided for row-padded gating output.");
static constexpr int TPB = 256;
if constexpr (SF == SCORING_SOFTMAX) {
moeSoftmax<TPB, InputType><<<num_tokens, TPB, 0, stream>>>(
gating_output, nullptr, workspace, num_experts, input_row_stride);
} else if constexpr (SF == SCORING_SIGMOID) {
moeSigmoid<TPB, InputType><<<num_tokens, TPB, 0, stream>>>(
gating_output, nullptr, workspace, num_experts, input_row_stride);
} else {
STD_TORCH_CHECK(false, "Unsupported scoring func");
}
moeTopK<TPB><<<num_tokens, TPB, 0, stream>>>(
workspace, nullptr, topk_weights, topk_indices, token_expert_indices,
num_experts, topk, 0, num_experts, renormalize, bias,
routed_scaling_factor, is_padding);
return;
}
switch (num_experts) {
case 1:
LAUNCH_TOPK(1, WARPS_PER_TB, BYTES_PER_LDG_POWER_OF_2);
Expand Down Expand Up @@ -771,10 +794,10 @@ void topkGatingKernelLauncher(
static constexpr int TPB = 256;
if constexpr (SF == SCORING_SOFTMAX) {
moeSoftmax<TPB, InputType><<<num_tokens, TPB, 0, stream>>>(
gating_output, nullptr, workspace, num_experts);
gating_output, nullptr, workspace, num_experts, input_row_stride);
} else if constexpr (SF == SCORING_SIGMOID) {
moeSigmoid<TPB, InputType><<<num_tokens, TPB, 0, stream>>>(
gating_output, nullptr, workspace, num_experts);
gating_output, nullptr, workspace, num_experts, input_row_stride);
} else {
STD_TORCH_CHECK(false, "Unsupported scoring func");
}
Expand Down Expand Up @@ -831,7 +854,7 @@ void dispatch_topk_launch(
topk_indices.mutable_data_ptr<int>(),
token_expert_indices.mutable_data_ptr<int>(),
softmax_workspace.mutable_data_ptr<float>(),
num_tokens, num_experts, topk, renormalize,
num_tokens, num_experts, gating_output.stride(0), topk, renormalize,
bias_ptr, routed_scaling_factor, stream, is_padding_ptr);
} else if (topk_indices.scalar_type() == torch::headeronly::ScalarType::UInt32) {
vllm::moe::topkGatingKernelLauncher<uint32_t, ComputeType, SF>(
Expand All @@ -840,7 +863,7 @@ void dispatch_topk_launch(
topk_indices.mutable_data_ptr<uint32_t>(),
token_expert_indices.mutable_data_ptr<int>(),
softmax_workspace.mutable_data_ptr<float>(),
num_tokens, num_experts, topk, renormalize,
num_tokens, num_experts, gating_output.stride(0), topk, renormalize,
bias_ptr, routed_scaling_factor, stream, is_padding_ptr);
} else {
STD_TORCH_CHECK(topk_indices.scalar_type() == torch::headeronly::ScalarType::Long);
Expand All @@ -850,7 +873,7 @@ void dispatch_topk_launch(
topk_indices.mutable_data_ptr<int64_t>(),
token_expert_indices.mutable_data_ptr<int>(),
softmax_workspace.mutable_data_ptr<float>(),
num_tokens, num_experts, topk, renormalize,
num_tokens, num_experts, gating_output.stride(0), topk, renormalize,
bias_ptr, routed_scaling_factor, stream, is_padding_ptr);
}
}
Expand All @@ -864,12 +887,17 @@ void topk_softmax(
std::optional<torch::stable::Tensor> bias,
std::optional<torch::stable::Tensor> is_padding)
{
STD_TORCH_CHECK(gating_output.dim() == 2,
"gating_output must be a 2D tensor");
const int num_experts = gating_output.size(-1);
STD_TORCH_CHECK(num_experts == 1 || gating_output.stride(1) == 1,
"gating_output's last dimension must be contiguous");
const auto num_tokens = gating_output.numel() / num_experts;
const int topk = topk_weights.size(-1);

const bool is_pow_2 = (num_experts != 0) && ((num_experts & (num_experts - 1)) == 0);
const bool needs_workspace = !is_pow_2 || num_experts > 256;
const bool needs_workspace = !is_pow_2 || num_experts > 256 ||
gating_output.stride(0) != num_experts;
const int64_t workspace_size = needs_workspace ? num_tokens * num_experts : 0;

torch::stable::accelerator::DeviceGuard guard(gating_output.get_device_index());
Expand Down Expand Up @@ -905,12 +933,17 @@ void topk_sigmoid(
double routed_scaling_factor,
std::optional<torch::stable::Tensor> is_padding)
{
STD_TORCH_CHECK(gating_output.dim() == 2,
"gating_output must be a 2D tensor");
const int num_experts = gating_output.size(-1);
STD_TORCH_CHECK(num_experts == 1 || gating_output.stride(1) == 1,
"gating_output's last dimension must be contiguous");
const auto num_tokens = gating_output.numel() / num_experts;
const int topk = topk_weights.size(-1);

const bool is_pow_2 = (num_experts != 0) && ((num_experts & (num_experts - 1)) == 0);
const bool needs_workspace = !is_pow_2 || num_experts > 256;
const bool needs_workspace = !is_pow_2 || num_experts > 256 ||
gating_output.stride(0) != num_experts;
const int64_t workspace_size = needs_workspace ? num_tokens * num_experts : 0;

torch::stable::accelerator::DeviceGuard guard(gating_output.get_device_index());
Expand Down
54 changes: 54 additions & 0 deletions tests/kernels/moe/test_fused_topk.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,60 @@ def test_fused_topk(
torch.testing.assert_close(topk_ids_ref.to(torch.int32), topk_ids, atol=0, rtol=0)


@pytest.mark.skipif(
not current_platform.is_cuda_alike(),
reason="This test is skipped on non-CUDA platform.",
)
@pytest.mark.parametrize("num_experts,padded_num_experts", [(60, 64), (64, 72)])
@pytest.mark.parametrize("scoring_func", ["softmax", "sigmoid"])
def test_fused_topk_padded_row_stride(
num_experts: int,
padded_num_experts: int,
scoring_func: str,
):
"""Regression test for row-padded router logits produced by Inductor."""
torch.manual_seed(0)
num_tokens = 33
hidden_size = 128
topk = 4
dtype = torch.bfloat16

hidden_states = torch.randn((num_tokens, hidden_size), dtype=dtype, device="cuda")
storage = torch.full(
(num_tokens, padded_num_experts),
1e4,
dtype=dtype,
device="cuda",
)
gating_output = storage[:, :num_experts]
gating_output.copy_(
torch.linspace(-4, 4, num_experts, dtype=dtype, device="cuda").expand(
num_tokens, -1
)
)
assert gating_output.stride() == (padded_num_experts, 1)
assert not gating_output.is_contiguous()

topk_weights_ref, topk_ids_ref = torch_topk(
gating_output=gating_output,
topk=topk,
renormalize=True,
scoring_func=scoring_func,
)
topk_weights, topk_ids, _ = fused_topk(
hidden_states=hidden_states,
gating_output=gating_output,
topk=topk,
renormalize=True,
scoring_func=scoring_func,
)

torch.testing.assert_close(
topk_weights_ref.to(torch.float32), topk_weights, atol=1e-2, rtol=1e-2
)
torch.testing.assert_close(topk_ids_ref.to(torch.int32), topk_ids, atol=0, rtol=0)


@pytest.mark.skipif(
not current_platform.is_cuda_alike(),
reason="This test is skipped on non-CUDA platform.",
Expand Down