chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,453 @@
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize, SymbolicDevice
#include <sgl_kernel/utils.h> // For RuntimeCheck, Panic, div_ceil
#include <sgl_kernel/utils.cuh> // For LaunchKernel
#include <tvm/ffi/container/tensor.h>
#include <cfloat>
#include <cstdint>
namespace {
// Kimi K2 MoE fused gate, supports NUM_EXPERTS in {256 (MiMo V2 Flash), 384 (Kimi K2)}.
// Routing (DeepSeek "noaux_tc" with num_expert_group = 1):
// 1. sigmoid(gate_logit)
// 2. add per-expert correction bias (ranking only)
// 3. pick top-k by biased score
// 4. weights = sigmoid (no bias)
// 5. optional renorm; routed_scaling_factor folded into renorm (no-op when not renormalizing)
__device__ __forceinline__ float sigmoid_accurate(float x) {
return 1.0f / (1.0f + expf(-x));
}
// Scalar widening: input/bias may arrive as fp32, bf16, or fp16; the kernel math
// always runs in fp32. Widening bf16/fp16 -> fp32 is exact, so results are
// bitwise identical to upcasting on the host first (the casts we are removing).
__device__ __forceinline__ float to_float(float x) {
return x;
}
__device__ __forceinline__ float to_float(__nv_bfloat16 x) {
return __bfloat162float(x);
}
__device__ __forceinline__ float to_float(__half x) {
return __half2float(x);
}
// Vectorized load of 4 consecutive elements of type T at vector index `vec_idx`,
// widened to a float4. fp32 reads a 16B float4; bf16/fp16 read an 8B float2 and
// expand. Used only by the large-token kernel's lane-strided loads.
template <typename T>
struct VecLoader;
template <>
struct VecLoader<float> {
__device__ __forceinline__ static float4 load(const float* base, int vec_idx) {
return reinterpret_cast<const float4*>(base)[vec_idx];
}
};
template <>
struct VecLoader<__nv_bfloat16> {
__device__ __forceinline__ static float4 load(const __nv_bfloat16* base, int vec_idx) {
float2 raw = reinterpret_cast<const float2*>(base)[vec_idx]; // 4 bf16 = 8 bytes
const __nv_bfloat162* packed = reinterpret_cast<const __nv_bfloat162*>(&raw);
float2 lo = __bfloat1622float2(packed[0]);
float2 hi = __bfloat1622float2(packed[1]);
return make_float4(lo.x, lo.y, hi.x, hi.y);
}
};
template <>
struct VecLoader<__half> {
__device__ __forceinline__ static float4 load(const __half* base, int vec_idx) {
float2 raw = reinterpret_cast<const float2*>(base)[vec_idx]; // 4 fp16 = 8 bytes
const __half2* packed = reinterpret_cast<const __half2*>(&raw);
float2 lo = __half22float2(packed[0]);
float2 hi = __half22float2(packed[1]);
return make_float4(lo.x, lo.y, hi.x, hi.y);
}
};
template <int N>
struct GateConfig {
static_assert(
N == 256 || N == 384,
"kimi_k2_moe_fused_gate currently only supports "
"NUM_EXPERTS == 256 or 384");
static constexpr int NUM_EXPERTS = N;
static constexpr int WARP_SIZE = 32;
static constexpr int WARPS_PER_CTA = 6; // only used by the large-token kernel
static constexpr int VPT = N / 32; // 8 (256) or 12 (384)
static constexpr int VEC_SIZE = 4;
static constexpr int VEC_PER_LANE = VPT / VEC_SIZE; // 2 or 3
static constexpr int WARPS_PER_TOKEN_SMALL = N / 32; // 8 or 12
static constexpr int THREADS_PER_BLOCK_SMALL = N; // 256 or 384
static constexpr int SMALL_TOKEN_THRESHOLD = 512;
static constexpr int MAX_TOPK = 8; // must match RuntimeCheck(topk <= 8) at the host launcher
static_assert(VPT % VEC_SIZE == 0, "VPT must be a multiple of VEC_SIZE for the float4 vec load");
};
// Small-token kernel: 1 block per token, NUM_EXPERTS threads (1 thread = 1 expert).
template <int N, typename InputT, typename BiasT>
__global__ void kimi_k2_moe_fused_gate_kernel_small_token(
const InputT* input,
const BiasT* bias,
float* output_ptr,
int32_t* indices_ptr,
int64_t num_rows,
int64_t topk,
bool renormalize,
double routed_scaling_factor,
bool apply_routed_scaling_factor_on_output) {
using Cfg = GateConfig<N>;
constexpr int NUM_EXPERTS = Cfg::NUM_EXPERTS;
constexpr int WARP_SIZE = Cfg::WARP_SIZE;
constexpr int WARPS_PER_TOKEN_SMALL = Cfg::WARPS_PER_TOKEN_SMALL;
constexpr int MAX_TOPK = Cfg::MAX_TOPK;
int64_t row_idx = blockIdx.x;
if (row_idx >= num_rows) return;
int tid = threadIdx.x;
int warp_id = tid / WARP_SIZE;
int lane_id = tid % WARP_SIZE;
// Sigmoid weights (no bias) for final lookup, indexed by expert id.
__shared__ float shared_original_scores[NUM_EXPERTS];
__shared__ float warp_maxs[WARPS_PER_TOKEN_SMALL];
__shared__ int warp_experts[WARPS_PER_TOKEN_SMALL];
__shared__ int selected_experts[MAX_TOPK];
// Keep biased_val in register; mask the winner in-place each iteration to
// avoid round-tripping through shared memory.
float input_val = to_float(input[row_idx * NUM_EXPERTS + tid]);
float bias_val = to_float(bias[tid]);
float sigmoid_val = sigmoid_accurate(input_val);
float biased_val = sigmoid_val + bias_val;
shared_original_scores[tid] = sigmoid_val;
__syncthreads();
// Lane 0 of warp 0 accumulates the renorm sum as it picks each winner,
// saving a second pass over selected_experts during writeback.
float sum_for_renorm = 0.0f;
for (int k = 0; k < topk; k++) {
// Stage 1: per-warp argmax.
float warp_max_val = biased_val;
int warp_max_expert = tid;
#pragma unroll
for (int offset = 16; offset > 0; offset /= 2) {
float other_val = __shfl_down_sync(0xFFFFFFFF, warp_max_val, offset);
int other_expert = __shfl_down_sync(0xFFFFFFFF, warp_max_expert, offset);
if (other_val > warp_max_val) {
warp_max_val = other_val;
warp_max_expert = other_expert;
}
}
if (lane_id == 0) {
warp_maxs[warp_id] = warp_max_val;
warp_experts[warp_id] = warp_max_expert;
}
__syncthreads();
// Stage 2: warp 0 merges warp-leaders into a single winner.
if (warp_id == 0) {
float final_max = (lane_id < WARPS_PER_TOKEN_SMALL) ? warp_maxs[lane_id] : -FLT_MAX;
int final_expert = (lane_id < WARPS_PER_TOKEN_SMALL) ? warp_experts[lane_id] : -1;
#pragma unroll
for (int offset = 16; offset > 0; offset /= 2) {
float other_val = __shfl_down_sync(0xFFFFFFFF, final_max, offset);
int other_expert = __shfl_down_sync(0xFFFFFFFF, final_expert, offset);
if (other_val > final_max) {
final_max = other_val;
final_expert = other_expert;
}
}
if (lane_id == 0) {
selected_experts[k] = final_expert;
if (renormalize && final_expert >= 0 && final_expert < NUM_EXPERTS) {
sum_for_renorm += shared_original_scores[final_expert];
}
}
}
__syncthreads();
int selected = selected_experts[k];
if (tid == selected) biased_val = -FLT_MAX;
}
// Lane 0 of warp 0 writes the output. sum_for_renorm was accumulated
// during the topk loop, so we just fold it into rcp.
if (warp_id == 0 && lane_id == 0) {
float rcp = 1.0f;
if (renormalize && sum_for_renorm > 0.0f) {
rcp = 1.0f / sum_for_renorm;
if (apply_routed_scaling_factor_on_output) {
rcp *= static_cast<float>(routed_scaling_factor);
}
}
for (int k = 0; k < topk; k++) {
int expert_id = selected_experts[k];
bool valid = (expert_id >= 0 && expert_id < NUM_EXPERTS);
output_ptr[row_idx * topk + k] = valid ? shared_original_scores[expert_id] * rcp : 0.0f;
indices_ptr[row_idx * topk + k] = valid ? expert_id : 0;
}
}
}
// Large-token kernel: 1 warp per token, WARPS_PER_CTA warps per block.
template <int N, typename InputT, typename BiasT>
__global__ void kimi_k2_moe_fused_gate_kernel(
const InputT* input,
const BiasT* bias,
float* output_ptr,
int32_t* indices_ptr,
int64_t num_rows,
int64_t topk,
bool renormalize,
double routed_scaling_factor,
bool apply_routed_scaling_factor_on_output) {
using Cfg = GateConfig<N>;
constexpr int NUM_EXPERTS = Cfg::NUM_EXPERTS;
constexpr int WARP_SIZE = Cfg::WARP_SIZE;
constexpr int WARPS_PER_CTA = Cfg::WARPS_PER_CTA;
constexpr int VEC_SIZE = Cfg::VEC_SIZE;
constexpr int VEC_PER_LANE = Cfg::VEC_PER_LANE;
constexpr int MAX_TOPK = Cfg::MAX_TOPK;
int64_t row_idx = blockIdx.x * WARPS_PER_CTA + threadIdx.y;
if (row_idx >= num_rows) return;
int lane_id = threadIdx.x;
int warp_id = threadIdx.y;
__shared__ float shared_scores[NUM_EXPERTS * WARPS_PER_CTA];
__shared__ float shared_original_scores[NUM_EXPERTS * WARPS_PER_CTA];
float* warp_scores = shared_scores + warp_id * NUM_EXPERTS;
float* warp_original_scores = shared_original_scores + warp_id * NUM_EXPERTS;
float4* warp_scores_v4 = reinterpret_cast<float4*>(warp_scores);
float4* warp_original_scores_v4 = reinterpret_cast<float4*>(warp_original_scores);
const InputT* input_row = input + row_idx * NUM_EXPERTS;
// Lane-strided vec_idx (each lane k stores at vec_idx k, k+32, k+64, ...) so each
// iteration's STS.128 is lane-contiguous, avoiding shared-mem bank conflicts.
#pragma unroll
for (int i = 0; i < VEC_PER_LANE; i++) {
int vec_idx = lane_id + i * WARP_SIZE;
float4 input_val = VecLoader<InputT>::load(input_row, vec_idx);
float4 bias_val = VecLoader<BiasT>::load(bias, vec_idx);
float4 sigmoid_v4;
float4 biased_v4;
#pragma unroll
for (int j = 0; j < VEC_SIZE; j++) {
float inp = ((float*)&input_val)[j];
float b = ((float*)&bias_val)[j];
float sigmoid_val = sigmoid_accurate(inp);
((float*)&sigmoid_v4)[j] = sigmoid_val;
((float*)&biased_v4)[j] = sigmoid_val + b;
}
warp_original_scores_v4[vec_idx] = sigmoid_v4;
warp_scores_v4[vec_idx] = biased_v4;
}
__syncwarp();
// Lane 0 records the picked expert ids and accumulates the renorm sum as
// it goes; the global write is a single pass after the loop.
int top_indices[MAX_TOPK];
float sum_for_renorm = 0.0f;
for (int k = 0; k < topk; k++) {
float max_val = -FLT_MAX;
int max_expert = -1;
for (int expert = lane_id; expert < NUM_EXPERTS; expert += WARP_SIZE) {
if (warp_scores[expert] > max_val) {
max_val = warp_scores[expert];
max_expert = expert;
}
}
// warp shfl reduce; tie-break by lower expert id
#pragma unroll
for (int offset = 16; offset > 0; offset /= 2) {
float other_val = __shfl_down_sync(0xFFFFFFFF, max_val, offset);
int other_expert = __shfl_down_sync(0xFFFFFFFF, max_expert, offset);
if (other_val > max_val || (other_val == max_val && other_expert < max_expert)) {
max_val = other_val;
max_expert = other_expert;
}
}
if (lane_id == 0) {
bool valid = (max_expert >= 0 && max_expert < NUM_EXPERTS);
top_indices[k] = valid ? max_expert : -1;
if (renormalize && valid) {
sum_for_renorm += warp_original_scores[max_expert];
}
if (valid) warp_scores[max_expert] = -FLT_MAX;
}
__syncwarp();
}
if (lane_id == 0) {
float rcp = 1.0f;
if (renormalize && sum_for_renorm > 0.0f) {
rcp = 1.0f / sum_for_renorm;
if (apply_routed_scaling_factor_on_output) {
rcp *= static_cast<float>(routed_scaling_factor);
}
}
for (int k = 0; k < topk; k++) {
int e = top_indices[k];
bool valid = (e >= 0);
output_ptr[row_idx * topk + k] = valid ? warp_original_scores[e] * rcp : 0.0f;
indices_ptr[row_idx * topk + k] = valid ? e : 0;
}
}
}
// Bundles the dtype-agnostic launch parameters so the templated dispatch below
// only has to thread the typed input/bias pointers.
struct GateLaunchArgs {
float* output;
int32_t* indices;
int64_t num_rows;
int64_t topk;
bool renormalize;
double routed_scaling_factor;
bool apply_routed_scaling_factor_on_output;
DLDevice device;
};
template <int N, typename InputT, typename BiasT>
void launch_for_n(const InputT* input, const BiasT* bias, const GateLaunchArgs& args) {
using namespace host;
using Cfg = GateConfig<N>;
bool use_small_token_kernel = args.num_rows <= Cfg::SMALL_TOKEN_THRESHOLD;
if (use_small_token_kernel) {
LaunchKernel(
static_cast<uint32_t>(args.num_rows), static_cast<uint32_t>(Cfg::THREADS_PER_BLOCK_SMALL), args.device)(
kimi_k2_moe_fused_gate_kernel_small_token<N, InputT, BiasT>,
input,
bias,
args.output,
args.indices,
args.num_rows,
args.topk,
args.renormalize,
args.routed_scaling_factor,
args.apply_routed_scaling_factor_on_output);
} else {
uint32_t num_blocks = div_ceil(args.num_rows, static_cast<int64_t>(Cfg::WARPS_PER_CTA));
dim3 block_dim(Cfg::WARP_SIZE, Cfg::WARPS_PER_CTA);
LaunchKernel(num_blocks, block_dim, args.device)(
kimi_k2_moe_fused_gate_kernel<N, InputT, BiasT>,
input,
bias,
args.output,
args.indices,
args.num_rows,
args.topk,
args.renormalize,
args.routed_scaling_factor,
args.apply_routed_scaling_factor_on_output);
}
}
// input/bias each independently arrive as fp32, bf16, or fp16; widen both to
// fp32 inside the kernel so the host no longer has to upcast. Dispatch is nested:
// num_experts -> input dtype -> bias dtype.
template <int N, typename InputT>
void dispatch_bias(
const InputT* input, const void* bias, const host::SymbolicDType& bias_dtype, const GateLaunchArgs& args) {
using namespace host;
if (bias_dtype.is_type<float>()) {
launch_for_n<N, InputT, float>(input, static_cast<const float*>(bias), args);
} else if (bias_dtype.is_type<bf16_t>()) {
launch_for_n<N, InputT, bf16_t>(input, static_cast<const bf16_t*>(bias), args);
} else {
launch_for_n<N, InputT, fp16_t>(input, static_cast<const fp16_t*>(bias), args);
}
}
template <int N>
void dispatch_input(
const void* input,
const host::SymbolicDType& input_dtype,
const void* bias,
const host::SymbolicDType& bias_dtype,
const GateLaunchArgs& args) {
using namespace host;
if (input_dtype.is_type<float>()) {
dispatch_bias<N, float>(static_cast<const float*>(input), bias, bias_dtype, args);
} else if (input_dtype.is_type<bf16_t>()) {
dispatch_bias<N, bf16_t>(static_cast<const bf16_t*>(input), bias, bias_dtype, args);
} else {
dispatch_bias<N, fp16_t>(static_cast<const fp16_t*>(input), bias, bias_dtype, args);
}
}
struct KimiK2MoEFusedGateKernel {
static void
run(const tvm::ffi::TensorView input,
const tvm::ffi::TensorView bias,
const tvm::ffi::TensorView output,
const tvm::ffi::TensorView indices,
int64_t topk,
bool renormalize,
double routed_scaling_factor,
bool apply_routed_scaling_factor_on_output) {
using namespace host;
auto N = SymbolicSize{"num_rows"};
auto E = SymbolicSize{"num_experts"};
auto K = SymbolicSize{"topk"};
auto input_dtype = SymbolicDType{};
auto bias_dtype = SymbolicDType{};
auto device = SymbolicDevice{};
K.set_value(topk);
device.set_options<kDLCUDA>();
TensorMatcher({N, E}).with_dtype<float, bf16_t, fp16_t>(input_dtype).with_device(device).verify(input);
TensorMatcher({E}).with_dtype<float, bf16_t, fp16_t>(bias_dtype).with_device(device).verify(bias);
TensorMatcher({N, K}).with_dtype<float>().with_device(device).verify(output);
TensorMatcher({N, K}).with_dtype<int32_t>().with_device(device).verify(indices);
const auto num_rows = static_cast<int64_t>(N.unwrap());
const auto num_experts = static_cast<int64_t>(E.unwrap());
RuntimeCheck(topk <= 8, "kimi_k2_moe_fused_gate only supports topk <= 8, got ", topk);
const GateLaunchArgs args{
.output = static_cast<float*>(output.data_ptr()),
.indices = static_cast<int32_t*>(indices.data_ptr()),
.num_rows = num_rows,
.topk = topk,
.renormalize = renormalize,
.routed_scaling_factor = routed_scaling_factor,
.apply_routed_scaling_factor_on_output = apply_routed_scaling_factor_on_output,
.device = device.unwrap()};
switch (num_experts) {
case 256:
dispatch_input<256>(input.data_ptr(), input_dtype, bias.data_ptr(), bias_dtype, args);
break;
case 384:
dispatch_input<384>(input.data_ptr(), input_dtype, bias.data_ptr(), bias_dtype, args);
break;
default:
Panic("kimi_k2_moe_fused_gate only supports num_experts in {256, 384}, got ", num_experts);
}
}
};
} // namespace
@@ -0,0 +1,589 @@
/* Copyright 2025 SGLang Team. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
// LoRA merged-virtual-expert routing align, fused with the virtual-expert id
// computation. Replaces the (_fused_virtual_topk_ids triton kernel + native
// moe_align_block_size) two-launch pair on the `--lora-use-virtual-experts`
// path: the align/scatter kernels read the RAW topk_ids + token_lora_mapping
// and compute the merged virtual id inline (mirrors _fused_virtual_topk_ids),
// so virtual_topk_ids is never materialized to global memory.
//
// Commit 1 scope: pure fusion (inline virtual id), NO EP skip. Output is
// bucket-for-bucket equivalent to the old path (dropped/-1 tokens still land in
// the sentinel bucket 0), so it can be asserted equal to the old kernels.
// Only the `64 < num_buckets <= 1024` branch is implemented here; other expert
// counts keep the old path (handled by the Python dispatcher).
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/utils.cuh>
#include <tvm/ffi/container/tensor.h>
#include <algorithm>
#ifndef WARP_SIZE
#define WARP_SIZE 32
#endif
#define CEILDIV(x, y) (((x) + (y) - 1) / (y))
#define VEC_SIZE 4
using Vec = int4;
inline uint32_t next_pow2(uint32_t x) noexcept {
--x;
x |= x >> 1;
x |= x >> 2;
x |= x >> 4;
x |= x >> 8;
x |= x >> 16;
return x + 1;
}
namespace moe_lora_merged {
__device__ __forceinline__ int warp_exclusive_scan(int v, unsigned mask = 0xffffffffu) {
int original = v;
#pragma unroll
for (int offset = 1; offset < WARP_SIZE; offset <<= 1) {
int n = __shfl_up_sync(mask, v, offset);
if ((threadIdx.x & (WARP_SIZE - 1)) >= offset) v += n;
}
return v - original;
}
// Inline mirror of _fused_virtual_topk_ids_kernel (virtual_experts.py). Returns
// the merged virtual expert id for flat slot `i` (range [-1, virtual_num_experts);
// -1 is the dropped/masked sentinel). The caller adds +1 to get the histogram
// bucket (sentinel -> bucket 0), matching the native +1 offset convention.
template <typename scalar_t>
__device__ __forceinline__ int compute_virtual_id(
const scalar_t* __restrict__ topk_ids,
const int32_t* __restrict__ token_lora_mapping,
size_t i,
int top_k,
int num_experts_for_weight,
int local_expert_offset,
int local_num_experts,
bool ep_local,
bool shared_outer,
bool compact) {
int m = static_cast<int>(i) / top_k;
int lora_id = token_lora_mapping[m];
bool mask_val = lora_id >= 0;
int safe_lora = lora_id > 0 ? lora_id : 0;
int base = shared_outer ? 0 : static_cast<int>(topk_ids[i]);
if (ep_local) {
bool owned = base >= local_expert_offset && base < local_expert_offset + local_num_experts;
base = owned ? base : -1;
}
if (!mask_val || base < 0) return -1;
// compact: dense LOCAL expert id in [0, local_num_experts) so the histogram
// spans only local_num_experts buckets instead of the full global virtual
// space (337/385 empty under EP). Assumes max_loras==1 (safe_lora shift is 0;
// the wrapper guards). expert_ids is converted back to global at write time.
if (compact) return base - local_expert_offset;
return base + safe_lora * num_experts_for_weight;
}
template <typename scalar_t>
__global__ void count_and_sort_expert_tokens_kernel(
const scalar_t* __restrict__ topk_ids,
const int32_t* __restrict__ token_lora_mapping,
int32_t* __restrict__ sorted_token_ids,
int32_t* __restrict__ cumsum_buffer,
size_t numel,
int top_k,
int num_experts_for_weight,
int local_expert_offset,
int local_num_experts,
bool ep_local,
bool shared_outer,
bool do_skip,
bool compact) {
const size_t tid = blockIdx.x * blockDim.x + threadIdx.x;
const size_t stride = blockDim.x * gridDim.x;
for (size_t i = tid; i < numel; i += stride) {
int vid = compute_virtual_id<scalar_t>(
topk_ids,
token_lora_mapping,
i,
top_k,
num_experts_for_weight,
local_expert_offset,
local_num_experts,
ep_local,
shared_outer,
compact);
// EP skip: dropped/masked slots (vid < 0) produce no delta on this rank, so
// they never need a slot in sorted_token_ids -> skip the global atomicAdd
// (kills the sentinel-bucket-0 contention). When do_skip is off they fall
// into bucket 0 (old behavior, kept for the bitwise-equivalence guardrail).
if (do_skip && vid < 0) continue;
int32_t expert_id = vid + 1;
int32_t rank_post_pad = atomicAdd(&cumsum_buffer[expert_id], 1);
sorted_token_ids[rank_post_pad] = i;
}
}
template <typename scalar_t>
__global__ void moe_align_block_size_kernel(
const scalar_t* __restrict__ topk_ids,
const int32_t* __restrict__ token_lora_mapping,
bool* __restrict__ token_lora_mask,
int32_t* __restrict__ sorted_token_ids,
int32_t* __restrict__ expert_ids,
int32_t* __restrict__ total_tokens_post_pad,
int32_t num_experts,
int32_t block_size,
size_t numel,
int32_t* __restrict__ cumsum,
bool pad_sorted_token_ids,
const int32_t scan_size,
int32_t max_num_tokens_padded,
int top_k,
int num_experts_for_weight,
int local_expert_offset,
int local_num_experts,
bool ep_local,
bool shared_outer,
bool do_skip,
bool compact) {
// Use a separate thread block to populate sorted_token_ids
if (blockIdx.x == 1) {
if (pad_sorted_token_ids) {
Vec fill_vec;
fill_vec.x = fill_vec.y = fill_vec.z = fill_vec.w = numel;
int32_t total_vecs = (max_num_tokens_padded + VEC_SIZE - 1) / VEC_SIZE;
Vec* out_ptr = reinterpret_cast<Vec*>(sorted_token_ids);
for (int32_t i = threadIdx.x; i < total_vecs; i += blockDim.x) {
out_ptr[i] = fill_vec;
}
}
return;
}
extern __shared__ int32_t smem[];
int32_t* shared_counts = smem; // [num_experts]
int32_t* prefix = shared_counts + num_experts; // [num_experts + 1]
int32_t* scan_buf = prefix + num_experts + 1; // [scan_size]
__shared__ int32_t s_total_tokens_post_pad;
const size_t tid = threadIdx.x;
const size_t stride = blockDim.x;
if (tid < num_experts) {
shared_counts[tid] = 0;
}
__syncthreads();
for (size_t i = tid; i < numel; i += stride) {
int vid = compute_virtual_id<scalar_t>(
topk_ids,
token_lora_mapping,
i,
top_k,
num_experts_for_weight,
local_expert_offset,
local_num_experts,
ep_local,
shared_outer,
compact);
// EP skip: dropped/masked slots don't increment any bucket (sentinel bucket
// 0 stays empty), so they never get a block and never reach count_and_sort.
if (!do_skip || vid >= 0) {
atomicAdd(&shared_counts[vid + 1], 1);
}
// token_lora_mask[m] = token_lora_mapping[m] >= 0, written once per row.
if (static_cast<int>(i) % top_k == 0) {
int m = static_cast<int>(i) / top_k;
token_lora_mask[m] = token_lora_mapping[m] >= 0;
}
}
__syncthreads();
int32_t padded_count = 0;
if (tid < num_experts) {
int32_t count = shared_counts[tid];
padded_count = (count + block_size - 1) / block_size * block_size;
scan_buf[tid] = padded_count;
}
// Intra warp prefix sum
int32_t* warp_sums = scan_buf + scan_size; // [<= 32]
const int warp_id = tid / WARP_SIZE;
const int lane_id = tid & (WARP_SIZE - 1);
const int num_warps_for_scan = (scan_size + WARP_SIZE - 1) / WARP_SIZE;
const int warp_sum = warp_exclusive_scan(padded_count) + padded_count;
if (lane_id == WARP_SIZE - 1) warp_sums[warp_id] = warp_sum;
__syncthreads();
// warp0 accumulate all the block's prefix sum
if (tid < WARP_SIZE) {
int val = (tid < num_warps_for_scan) ? warp_sums[tid] : 0;
int incl = warp_exclusive_scan(val) + val;
warp_sums[tid] = incl;
}
__syncthreads();
// Every thread obtains the whole block's sum
if (tid == 0) {
prefix[num_experts] = warp_sums[num_warps_for_scan - 1];
s_total_tokens_post_pad = prefix[num_experts];
*total_tokens_post_pad = s_total_tokens_post_pad;
}
__syncthreads();
// Fill 0 to scan_buf extended area (tid >= num_expert)
if (tid >= num_experts && tid < scan_size) scan_buf[tid] = 0;
__syncthreads();
// Perform 2 level exclusive-prefix-sum to scan_buf
int v = (tid < scan_size) ? scan_buf[tid] : 0;
int pre = warp_exclusive_scan(v);
if (lane_id == WARP_SIZE - 1) warp_sums[warp_id] = pre + v;
__syncthreads();
if (warp_id == 0) {
int val = (lane_id < num_warps_for_scan) ? warp_sums[lane_id] : 0;
warp_sums[lane_id] = warp_exclusive_scan(val);
}
__syncthreads();
int offset = warp_sums[warp_id];
if (tid < scan_size) scan_buf[tid] = pre + offset;
__syncthreads();
// Write prefix[0..num_experts - 1] and cumsum
if (tid < num_experts) prefix[tid] = scan_buf[tid];
if (tid <= num_experts) {
cumsum[tid] = prefix[tid];
}
// fill expert_ids
const int32_t num_blocks = s_total_tokens_post_pad / block_size;
for (int32_t i = tid; i < num_blocks; i += stride) {
int32_t block_start = i * block_size;
int left = 0, right = num_experts;
while (left < right) {
int mid = (left + right) >> 1;
if (prefix[mid] <= block_start) {
left = mid + 1;
} else {
right = mid;
}
}
// compact buckets hold LOCAL expert ids; restore the global id (+offset) so
// the downstream GEMM still indexes the global contiguous LoRA weight.
expert_ids[i] = left - 2 + (compact ? local_expert_offset : 0);
}
}
// Single-block fused variant: does fill + histogram + scan + expert_ids + scatter
// in ONE threadblock (one launch), eliminating the separate count_and_sort kernel
// AND its redundant re-computation of the virtual id (cached in shared `svids`).
// Only valid for small numel (the scatter is single-block); the wrapper routes
// large numel (prefill) to the 2-kernel path. do_skip is implied (this is the
// decode hot path); dropped slots are simply never scattered.
template <typename scalar_t>
__global__ void fused_align_scatter_kernel(
const scalar_t* __restrict__ topk_ids,
const int32_t* __restrict__ token_lora_mapping,
bool* __restrict__ token_lora_mask,
int32_t* __restrict__ sorted_token_ids,
int32_t* __restrict__ expert_ids,
int32_t* __restrict__ total_tokens_post_pad,
int32_t num_experts,
int32_t block_size,
size_t numel,
int32_t* __restrict__ cumsum,
const int32_t scan_size,
int32_t max_num_tokens_padded,
int top_k,
int num_experts_for_weight,
int local_expert_offset,
int local_num_experts,
bool ep_local,
bool shared_outer,
bool do_skip,
bool compact) {
extern __shared__ int32_t smem[];
int32_t* shared_counts = smem; // [num_experts]
int32_t* prefix = shared_counts + num_experts; // [num_experts + 1]
int32_t* scan_buf = prefix + num_experts + 1; // [scan_size]
int32_t* warp_sums = scan_buf + scan_size; // [WARP_SIZE]
int32_t* cursor = warp_sums + WARP_SIZE; // [num_experts] scatter cursor
int32_t* svids = cursor + num_experts; // [numel] cached virtual ids
__shared__ int32_t s_total_tokens_post_pad;
const size_t tid = threadIdx.x;
const size_t stride = blockDim.x;
const int warp_id = tid / WARP_SIZE;
const int lane_id = tid & (WARP_SIZE - 1);
const int num_warps_for_scan = (scan_size + WARP_SIZE - 1) / WARP_SIZE;
// Phase 1: fill sorted_token_ids with the `numel` padding sentinel.
{
Vec fill_vec;
fill_vec.x = fill_vec.y = fill_vec.z = fill_vec.w = numel;
int32_t total_vecs = (max_num_tokens_padded + VEC_SIZE - 1) / VEC_SIZE;
Vec* out_ptr = reinterpret_cast<Vec*>(sorted_token_ids);
for (int32_t i = threadIdx.x; i < total_vecs; i += blockDim.x) {
out_ptr[i] = fill_vec;
}
}
if (tid < num_experts) shared_counts[tid] = 0;
__syncthreads();
// Phase 2: histogram + cache the virtual id per slot + token_lora_mask.
for (size_t i = tid; i < numel; i += stride) {
int vid = compute_virtual_id<scalar_t>(
topk_ids,
token_lora_mapping,
i,
top_k,
num_experts_for_weight,
local_expert_offset,
local_num_experts,
ep_local,
shared_outer,
compact);
svids[i] = vid;
if (!do_skip || vid >= 0) {
atomicAdd(&shared_counts[vid + 1], 1);
}
if (static_cast<int>(i) % top_k == 0) {
int m = static_cast<int>(i) / top_k;
token_lora_mask[m] = token_lora_mapping[m] >= 0;
}
}
__syncthreads();
// Phase 3: padded counts + two-level warp exclusive prefix sum (verbatim).
int32_t padded_count = 0;
if (tid < num_experts) {
int32_t count = shared_counts[tid];
padded_count = (count + block_size - 1) / block_size * block_size;
scan_buf[tid] = padded_count;
}
const int warp_sum = warp_exclusive_scan(padded_count) + padded_count;
if (lane_id == WARP_SIZE - 1) warp_sums[warp_id] = warp_sum;
__syncthreads();
if (tid < WARP_SIZE) {
int val = (tid < num_warps_for_scan) ? warp_sums[tid] : 0;
int incl = warp_exclusive_scan(val) + val;
warp_sums[tid] = incl;
}
__syncthreads();
if (tid == 0) {
prefix[num_experts] = warp_sums[num_warps_for_scan - 1];
s_total_tokens_post_pad = prefix[num_experts];
*total_tokens_post_pad = s_total_tokens_post_pad;
}
__syncthreads();
if (tid >= num_experts && tid < scan_size) scan_buf[tid] = 0;
__syncthreads();
int v = (tid < scan_size) ? scan_buf[tid] : 0;
int pre = warp_exclusive_scan(v);
if (lane_id == WARP_SIZE - 1) warp_sums[warp_id] = pre + v;
__syncthreads();
if (warp_id == 0) {
int val = (lane_id < num_warps_for_scan) ? warp_sums[lane_id] : 0;
warp_sums[lane_id] = warp_exclusive_scan(val);
}
__syncthreads();
int off = warp_sums[warp_id];
if (tid < scan_size) scan_buf[tid] = pre + off;
__syncthreads();
if (tid < num_experts) prefix[tid] = scan_buf[tid];
if (tid <= num_experts) cumsum[tid] = prefix[tid];
__syncthreads();
// Phase 4: expert_ids (binary search per block) + init the scatter cursor.
const int32_t num_blocks = s_total_tokens_post_pad / block_size;
for (int32_t i = tid; i < num_blocks; i += stride) {
int32_t block_start = i * block_size;
int left = 0, right = num_experts;
while (left < right) {
int mid = (left + right) >> 1;
if (prefix[mid] <= block_start) {
left = mid + 1;
} else {
right = mid;
}
}
expert_ids[i] = left - 2 + (compact ? local_expert_offset : 0);
}
if (tid < num_experts) cursor[tid] = prefix[tid];
__syncthreads();
// Phase 5: scatter owned tokens using the cached virtual ids + shared cursor.
for (size_t i = tid; i < numel; i += stride) {
int vid = svids[i];
if (do_skip && vid < 0) continue;
int bucket = vid + 1;
int pos = atomicAdd(&cursor[bucket], 1);
sorted_token_ids[pos] = i;
}
}
} // namespace moe_lora_merged
namespace {
template <typename scalar_t>
struct MoeLoraMergedAlignKernel {
static void
run(tvm::ffi::TensorView topk_ids,
tvm::ffi::TensorView token_lora_mapping,
tvm::ffi::TensorView token_lora_mask,
int64_t num_experts,
int64_t block_size,
tvm::ffi::TensorView sorted_token_ids,
tvm::ffi::TensorView expert_ids,
tvm::ffi::TensorView num_tokens_post_pad,
tvm::ffi::TensorView cumsum_buffer,
bool pad_sorted_token_ids,
int64_t top_k,
int64_t num_experts_for_weight,
int64_t local_expert_offset,
int64_t local_num_experts,
bool ep_local,
bool shared_outer,
bool do_skip,
bool compact,
bool fuse_scatter) {
using namespace host;
auto device = topk_ids.device();
const cudaStream_t stream = LaunchKernel::resolve_device(device);
int threads = 1024;
threads = ((threads + WARP_SIZE - 1) / WARP_SIZE) * WARP_SIZE;
int64_t max_num_tokens_padded = sorted_token_ids.size(0);
// num_experts here is the bucket count. Non-compact: virtual_num_experts+1
// (typically 385). Compact: local_num_experts+1 (typically 49). Both use the
// same single-block align path (valid for any bucket count <= 1024 that fits
// shared memory); the v2 (>1024) regime keeps the old path via the wrapper.
RuntimeCheck(
num_experts <= 1024, "moe_lora_merged_align: num_experts (bucket count) must be <= 1024, got ", num_experts);
// compact buckets hold LOCAL ids and restore global expert ids as
// (left-2+offset). For the sentinel bucket 0 that yields (offset-1), NOT the
// -1 the GEMM expects to skip -- only safe when do_skip empties bucket 0.
RuntimeCheck(
!compact || do_skip, "moe_lora_merged_align: compact requires do_skip (sentinel bucket must be empty)");
const scalar_t* topk_ids_ptr = static_cast<const scalar_t*>(topk_ids.data_ptr());
const int32_t* tlm_ptr = static_cast<const int32_t*>(token_lora_mapping.data_ptr());
bool* token_lora_mask_ptr = static_cast<bool*>(token_lora_mask.data_ptr());
int32_t* sorted_token_ids_ptr = static_cast<int32_t*>(sorted_token_ids.data_ptr());
int32_t* expert_ids_ptr = static_cast<int32_t*>(expert_ids.data_ptr());
int32_t* num_tokens_post_pad_ptr = static_cast<int32_t*>(num_tokens_post_pad.data_ptr());
int32_t* cumsum_buffer_ptr = static_cast<int32_t*>(cumsum_buffer.data_ptr());
size_t numel = topk_ids.numel();
const size_t scan_size = next_pow2(num_experts);
if (fuse_scatter) {
// One block does fill + histogram + scan + expert_ids + scatter. Extra
// shared for the scatter cursor [num_experts] and cached virtual ids [numel].
const size_t shmem =
(num_experts + (num_experts + 1) + scan_size + WARP_SIZE + num_experts + numel) * sizeof(int32_t);
auto fused = moe_lora_merged::fused_align_scatter_kernel<scalar_t>;
LaunchKernel(dim3(1), dim3(threads), stream, shmem)(
fused,
topk_ids_ptr,
tlm_ptr,
token_lora_mask_ptr,
sorted_token_ids_ptr,
expert_ids_ptr,
num_tokens_post_pad_ptr,
(int32_t)num_experts,
(int32_t)block_size,
numel,
cumsum_buffer_ptr,
(int32_t)scan_size,
(int32_t)max_num_tokens_padded,
(int)top_k,
(int)num_experts_for_weight,
(int)local_expert_offset,
(int)local_num_experts,
ep_local,
shared_outer,
do_skip,
compact);
return;
}
const size_t shared_mem_size = (num_experts + (num_experts + 1) + scan_size + WARP_SIZE) * sizeof(int32_t);
auto align_kernel = moe_lora_merged::moe_align_block_size_kernel<scalar_t>;
LaunchKernel(dim3(2), dim3(threads), stream, shared_mem_size)(
align_kernel,
topk_ids_ptr,
tlm_ptr,
token_lora_mask_ptr,
sorted_token_ids_ptr,
expert_ids_ptr,
num_tokens_post_pad_ptr,
(int32_t)num_experts,
(int32_t)block_size,
numel,
cumsum_buffer_ptr,
pad_sorted_token_ids,
(int32_t)scan_size,
(int32_t)max_num_tokens_padded,
(int)top_k,
(int)num_experts_for_weight,
(int)local_expert_offset,
(int)local_num_experts,
ep_local,
shared_outer,
do_skip,
compact);
const int block_threads = std::min(256, threads);
const int num_blocks = (numel + block_threads - 1) / block_threads;
const int max_blocks = 65535;
const int actual_blocks = std::min(num_blocks, max_blocks);
auto sort_kernel = moe_lora_merged::count_and_sort_expert_tokens_kernel<scalar_t>;
LaunchKernel(dim3(actual_blocks), dim3(block_threads), stream)(
sort_kernel,
topk_ids_ptr,
tlm_ptr,
sorted_token_ids_ptr,
cumsum_buffer_ptr,
numel,
(int)top_k,
(int)num_experts_for_weight,
(int)local_expert_offset,
(int)local_num_experts,
ep_local,
shared_outer,
do_skip,
compact);
}
};
} // namespace
@@ -0,0 +1,412 @@
/*
* Fused top-k gating softmax WITH routed-pack output.
*
* JIT port of the power-of-2 fast path of sgl-kernel's
* csrc/moe/moe_topk_softmax_kernels.cu (`topkGatingSoftmax`, itself adapted from
* vLLM v0.7.3 / TensorRT-LLM v0.7.1, Apache-2.0), extended with a third output:
* the FlashInfer routed-MoE packed format
*
* packed[idx] = (topk_id << 16) | bf16_bits(topk_weight)
*
* computed in the kernel epilogue AFTER renormalization — bit-identical to the
* standalone `fused_pack_topk` triton kernel applied to the (post-processed)
* topk_ids/topk_weights, including the padded-region mask: rows at or beyond
* `num_token_non_padded` pack id = -1 (the `_mask_topk_ids_padded_region`
* sentinel), matching what the separate pack would produce after the mask.
* This removes the per-MoE-layer `_pack_topk_kernel` launch from the decode
* critical path entirely (fusion instead of stream overlap).
*
* Scope intentionally narrowed vs the AOT kernel (callers fall back to the AOT
* topk_softmax + separate pack otherwise):
* - power-of-2 num_experts in [1, 512] only (no cub workspace fallback)
* - no softcapping / correction bias (the Qwen3-MoE softmax path uses neither)
*/
#include <sgl_kernel/tensor.h> // TensorMatcher, SymbolicSize, SymbolicDevice
#include <sgl_kernel/utils.h> // RuntimeCheck
#include <sgl_kernel/utils.cuh> // LaunchKernel, fp32_t/fp16_t/bf16_t, is_type
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <cfloat>
#include <cstdint>
namespace {
static constexpr int WARP_SIZE = 32;
#define TSP_MAX(a, b) ((a) > (b) ? (a) : (b))
#define TSP_MIN(a, b) ((a) < (b) ? (a) : (b))
/// Aligned array type (mirrors the AOT kernel's CUTLASS-free aligned array)
template <typename T, int N, int Alignment = sizeof(T) * N>
class alignas(Alignment) AlignedArray {
T data[N];
};
template <typename T>
__device__ float convert_to_float(T x) {
if constexpr (std::is_same_v<T, __half>) {
return __half2float(x);
} else if constexpr (std::is_same_v<T, __nv_bfloat16>) {
return __bfloat162float(x);
} else if constexpr (std::is_same_v<T, float>) {
return x;
} else {
return static_cast<float>(x);
}
}
// Reference pack (bit-identical to jit_kernel/flashinfer_trtllm_moe/topk_pack.py):
// low 16 bits = bf16(weight) bits (round-to-nearest-even, same as torch/triton
// `.to(bfloat16)`), high 16 bits = int16 expert id.
__device__ __forceinline__ int32_t pack_routed(int32_t id, float w) {
const uint32_t wbits = static_cast<uint32_t>(__bfloat16_as_ushort(__float2bfloat16(w)));
return static_cast<int32_t>((static_cast<uint32_t>(id) << 16) | wbits);
}
template <typename T, int VPT, int NUM_EXPERTS, int WARPS_PER_CTA, int BYTES_PER_LDG>
__launch_bounds__(WARPS_PER_CTA* WARP_SIZE) __global__ void topkGatingSoftmaxPack(
const T* input,
float* output,
const int num_rows,
int* indices,
int* packed_output,
const int32_t* num_token_non_padded,
const int k,
const bool renormalize) {
static_assert(VPT == (VPT & -VPT), "VPT must be power of 2");
static_assert(NUM_EXPERTS == (NUM_EXPERTS & -NUM_EXPERTS), "NUM_EXPERTS must be power of 2");
static_assert(BYTES_PER_LDG == (BYTES_PER_LDG & -BYTES_PER_LDG), "BYTES_PER_LDG must be power of 2");
static_assert(BYTES_PER_LDG <= 16, "BYTES_PER_LDG must be leq 16");
static constexpr int ELTS_PER_LDG = BYTES_PER_LDG / sizeof(T);
static constexpr int ELTS_PER_ROW = NUM_EXPERTS;
static constexpr int THREADS_PER_ROW = ELTS_PER_ROW / VPT;
static constexpr int LDG_PER_THREAD = VPT / ELTS_PER_LDG;
static_assert(VPT % ELTS_PER_LDG == 0, "The elements per thread must be a multiple of the elements per ldg");
static_assert(WARP_SIZE % THREADS_PER_ROW == 0, "The threads per row must cleanly divide the threads per warp");
static_assert(THREADS_PER_ROW == (THREADS_PER_ROW & -THREADS_PER_ROW), "THREADS_PER_ROW must be power of 2");
static_assert(THREADS_PER_ROW <= WARP_SIZE, "THREADS_PER_ROW can be at most warp size");
static constexpr int ELTS_PER_WARP = WARP_SIZE * VPT;
static constexpr int ROWS_PER_WARP = ELTS_PER_WARP / ELTS_PER_ROW;
static constexpr int ROWS_PER_CTA = WARPS_PER_CTA * ROWS_PER_WARP;
static_assert(ELTS_PER_WARP % ELTS_PER_ROW == 0, "The elts per row must cleanly divide the total elt per warp");
const int cta_base_row = blockIdx.x * ROWS_PER_CTA;
const int warp_base_row = cta_base_row + threadIdx.y * ROWS_PER_WARP;
const int thread_row_in_warp = threadIdx.x / THREADS_PER_ROW;
const int thread_row = warp_base_row + thread_row_in_warp;
if (thread_row >= num_rows) {
return;
}
const T* thread_row_ptr = input + thread_row * ELTS_PER_ROW;
const int thread_group_idx = threadIdx.x % THREADS_PER_ROW;
const int first_elt_read_by_thread = thread_group_idx * ELTS_PER_LDG;
const T* thread_read_ptr = thread_row_ptr + first_elt_read_by_thread;
using AccessType = AlignedArray<T, ELTS_PER_LDG>;
T row_chunk_temp[VPT];
AccessType* row_chunk_vec_ptr = reinterpret_cast<AccessType*>(&row_chunk_temp);
const AccessType* vec_thread_read_ptr = reinterpret_cast<const AccessType*>(thread_read_ptr);
#pragma unroll
for (int ii = 0; ii < LDG_PER_THREAD; ++ii) {
row_chunk_vec_ptr[ii] = vec_thread_read_ptr[ii * THREADS_PER_ROW];
}
float row_chunk[VPT];
#pragma unroll
for (int ii = 0; ii < VPT; ++ii) {
row_chunk[ii] = convert_to_float<T>(row_chunk_temp[ii]);
}
float thread_max = row_chunk[0];
#pragma unroll
for (int ii = 1; ii < VPT; ++ii) {
thread_max = max(thread_max, row_chunk[ii]);
}
#pragma unroll
for (int mask = THREADS_PER_ROW / 2; mask > 0; mask /= 2) {
thread_max = max(thread_max, __shfl_xor_sync(0xffffffffu, thread_max, mask, THREADS_PER_ROW));
}
float row_sum = 0;
#pragma unroll
for (int ii = 0; ii < VPT; ++ii) {
row_chunk[ii] = expf(row_chunk[ii] - thread_max);
row_sum += row_chunk[ii];
}
#pragma unroll
for (int mask = THREADS_PER_ROW / 2; mask > 0; mask /= 2) {
row_sum += __shfl_xor_sync(0xffffffffu, row_sum, mask, THREADS_PER_ROW);
}
const float reciprocal_row_sum = 1.f / row_sum;
#pragma unroll
for (int ii = 0; ii < VPT; ++ii) {
row_chunk[ii] = row_chunk[ii] * reciprocal_row_sum;
}
int start_col = first_elt_read_by_thread;
static constexpr int COLS_PER_GROUP_LDG = ELTS_PER_LDG * THREADS_PER_ROW;
float row_sum_for_renormalize = 0;
for (int k_idx = 0; k_idx < k; ++k_idx) {
float max_val = row_chunk[0];
int expert = start_col;
#pragma unroll
for (int ldg = 0, col = start_col; ldg < LDG_PER_THREAD; ++ldg, col += COLS_PER_GROUP_LDG) {
#pragma unroll
for (int ii = 0; ii < ELTS_PER_LDG; ++ii) {
float val = row_chunk[ldg * ELTS_PER_LDG + ii];
if (val > max_val) {
max_val = val;
expert = col + ii;
}
}
}
#pragma unroll
for (int mask = THREADS_PER_ROW / 2; mask > 0; mask /= 2) {
float other_max = __shfl_xor_sync(0xffffffffu, max_val, mask, THREADS_PER_ROW);
int other_expert = __shfl_xor_sync(0xffffffffu, expert, mask, THREADS_PER_ROW);
if (other_max > max_val || (other_max == max_val && other_expert < expert)) {
max_val = other_max;
expert = other_expert;
}
}
if (thread_group_idx == 0) {
const int idx = k * thread_row + k_idx;
output[idx] = max_val;
indices[idx] = expert;
row_sum_for_renormalize += max_val;
}
if (k_idx + 1 < k) {
const int ldg_group_for_expert = expert / COLS_PER_GROUP_LDG;
const int thread_to_clear_in_group = (expert / ELTS_PER_LDG) % THREADS_PER_ROW;
if (thread_group_idx == thread_to_clear_in_group) {
const int offset_for_expert = expert % ELTS_PER_LDG;
row_chunk[ldg_group_for_expert * ELTS_PER_LDG + offset_for_expert] = -10000.f;
}
}
}
if (thread_group_idx == 0) {
// Fused renormalization (same as the AOT kernel).
if (renormalize) {
float row_sum_for_renormalize_inv = 1.f / row_sum_for_renormalize;
#pragma unroll
for (int k_idx = 0; k_idx < k; ++k_idx) {
const int idx = k * thread_row + k_idx;
output[idx] = output[idx] * row_sum_for_renormalize_inv;
}
}
// Fused routed pack: pack the FINAL (post-renorm) weights. Padded rows
// (>= *num_token_non_padded) pack id = -1, mirroring the in-place
// `_mask_topk_ids_padded_region` sentinel that the separate pack kernel
// would otherwise observe. The plain `indices` output is left unmasked
// here exactly like the AOT kernel — the existing python post-process
// masks it afterwards; only the packed tensor needs the mask baked in
// because it is produced BEFORE that post-process runs.
const bool row_padded = (num_token_non_padded != nullptr) && (thread_row >= *num_token_non_padded);
#pragma unroll
for (int k_idx = 0; k_idx < k; ++k_idx) {
const int idx = k * thread_row + k_idx;
const int32_t id = row_padded ? -1 : indices[idx];
packed_output[idx] = pack_routed(id, output[idx]);
}
}
}
namespace detail {
template <typename T, int EXPERTS, int BYTES_PER_LDG>
struct TopkConstants {
static constexpr int ELTS_PER_LDG = BYTES_PER_LDG / sizeof(T);
static_assert(EXPERTS / (ELTS_PER_LDG * WARP_SIZE) == 0 || EXPERTS % (ELTS_PER_LDG * WARP_SIZE) == 0, "");
static constexpr int VECs_PER_THREAD = TSP_MAX(1, EXPERTS / (ELTS_PER_LDG * WARP_SIZE));
static constexpr int VPT = VECs_PER_THREAD * ELTS_PER_LDG;
static constexpr int THREADS_PER_ROW = EXPERTS / VPT;
static constexpr int ROWS_PER_WARP = WARP_SIZE / THREADS_PER_ROW;
};
} // namespace detail
template <typename T, int EXPERTS, int WARPS_PER_TB>
void launchTopkGatingSoftmaxPack(
const T* input,
float* output,
int* indices,
int* packed_output,
const int32_t* num_token_non_padded,
const int num_rows,
const int k,
const bool renormalize,
DLDevice device) {
static constexpr std::size_t MAX_BYTES_PER_LDG = 16;
static constexpr int BYTES_PER_LDG = TSP_MIN(MAX_BYTES_PER_LDG, sizeof(T) * EXPERTS);
using Constants = detail::TopkConstants<T, EXPERTS, BYTES_PER_LDG>;
static constexpr int VPT = Constants::VPT;
static constexpr int ROWS_PER_WARP = Constants::ROWS_PER_WARP;
const int num_warps = (num_rows + ROWS_PER_WARP - 1) / ROWS_PER_WARP;
const int num_blocks = (num_warps + WARPS_PER_TB - 1) / WARPS_PER_TB;
dim3 block_dim(WARP_SIZE, WARPS_PER_TB);
host::LaunchKernel(dim3(num_blocks), block_dim, device)(
topkGatingSoftmaxPack<T, VPT, EXPERTS, WARPS_PER_TB, BYTES_PER_LDG>,
input,
output,
num_rows,
indices,
packed_output,
num_token_non_padded,
k,
renormalize);
}
template <typename T>
void dispatchExperts(
const T* input,
float* output,
int* indices,
int* packed_output,
const int32_t* num_token_non_padded,
const int num_rows,
const int num_experts,
const int k,
const bool renormalize,
DLDevice device) {
static constexpr int WARPS_PER_TB = 4;
#define TSP_LAUNCH(E) \
launchTopkGatingSoftmaxPack<T, E, WARPS_PER_TB>( \
input, output, indices, packed_output, num_token_non_padded, num_rows, k, renormalize, device)
switch (num_experts) {
case 1:
TSP_LAUNCH(1);
break;
case 2:
TSP_LAUNCH(2);
break;
case 4:
TSP_LAUNCH(4);
break;
case 8:
TSP_LAUNCH(8);
break;
case 16:
TSP_LAUNCH(16);
break;
case 32:
TSP_LAUNCH(32);
break;
case 64:
TSP_LAUNCH(64);
break;
case 128:
TSP_LAUNCH(128);
break;
case 256:
TSP_LAUNCH(256);
break;
case 512:
TSP_LAUNCH(512);
break;
default:
host::RuntimeCheck(false, "topk_softmax_pack: num_experts must be a power of 2 in [1, 512], got ", num_experts);
}
#undef TSP_LAUNCH
}
// ─────────────────────────────────────────────────────────────────────────────
// Launcher
// ─────────────────────────────────────────────────────────────────────────────
void topk_softmax_pack(
tvm::ffi::TensorView topk_weights,
tvm::ffi::TensorView topk_indices,
tvm::ffi::TensorView packed,
tvm::ffi::TensorView gating_output,
tvm::ffi::Optional<tvm::ffi::TensorView> num_token_non_padded,
bool renormalize) {
using namespace host;
SymbolicSize N{"num_tokens"};
SymbolicSize E{"num_experts"};
SymbolicSize K{"topk"};
SymbolicDevice device_;
device_.set_options<kDLCUDA>();
TensorMatcher({N, E}).with_dtype<fp32_t, fp16_t, bf16_t>().with_device<kDLCUDA>(device_).verify(gating_output);
TensorMatcher({N, K}).with_dtype<fp32_t>().with_device<kDLCUDA>(device_).verify(topk_weights);
TensorMatcher({N, K}).with_dtype<int32_t>().with_device<kDLCUDA>(device_).verify(topk_indices);
TensorMatcher({N, K}).with_dtype<int32_t>().with_device<kDLCUDA>(device_).verify(packed);
const int32_t* ntnp_ptr = nullptr;
if (num_token_non_padded.has_value()) {
SymbolicSize One{"ntnp_numel"};
TensorMatcher({One}).with_dtype<int32_t>().with_device<kDLCUDA>(device_).verify(num_token_non_padded.value());
RuntimeCheck(One.unwrap() == 1, "num_token_non_padded must be a 1-element tensor");
ntnp_ptr = static_cast<const int32_t*>(num_token_non_padded.value().data_ptr());
}
const int num_tokens = static_cast<int>(N.unwrap());
const int num_experts = static_cast<int>(E.unwrap());
const int topk = static_cast<int>(K.unwrap());
DLDevice device = device_.unwrap();
RuntimeCheck(topk <= num_experts, "topk must be <= num_experts");
if (num_tokens == 0) return;
auto* weights_ptr = static_cast<float*>(topk_weights.data_ptr());
auto* indices_ptr = static_cast<int*>(topk_indices.data_ptr());
auto* packed_ptr = static_cast<int*>(packed.data_ptr());
if (is_type<fp32_t>(gating_output.dtype())) {
dispatchExperts<float>(
static_cast<const float*>(gating_output.data_ptr()),
weights_ptr,
indices_ptr,
packed_ptr,
ntnp_ptr,
num_tokens,
num_experts,
topk,
renormalize,
device);
} else if (is_type<fp16_t>(gating_output.dtype())) {
dispatchExperts<__half>(
static_cast<const __half*>(gating_output.data_ptr()),
weights_ptr,
indices_ptr,
packed_ptr,
ntnp_ptr,
num_tokens,
num_experts,
topk,
renormalize,
device);
} else {
dispatchExperts<__nv_bfloat16>(
static_cast<const __nv_bfloat16*>(gating_output.data_ptr()),
weights_ptr,
indices_ptr,
packed_ptr,
ntnp_ptr,
num_tokens,
num_experts,
topk,
renormalize,
device);
}
}
} // namespace