[minimax-m3] Split 1/4: sparse attention ops + JIT kernels + config foundation (#28712)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from types import SimpleNamespace
|
||||
from typing import Dict, List, TypedDict
|
||||
|
||||
import torch
|
||||
@@ -7,7 +8,7 @@ from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe import get_config_d
|
||||
from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe_triton_config import (
|
||||
get_config_file_name,
|
||||
)
|
||||
from sglang.srt.utils import is_hip
|
||||
from sglang.srt.utils import is_gfx95_supported, is_hip
|
||||
from sglang.srt.utils.hf_transformers_utils import get_config
|
||||
|
||||
|
||||
@@ -59,7 +60,13 @@ def get_model_config(
|
||||
assert len(block_shape) == 2
|
||||
# Replace config with text_config for encoder-decoder models after getting block_shape and architecture
|
||||
if hasattr(config, "text_config"):
|
||||
config = config.get_text_config()
|
||||
text_config = config.get_text_config()
|
||||
# Some models (e.g. MiniMax-M3) carry text_config as a plain dict; wrap
|
||||
# it so downstream attribute access works uniformly.
|
||||
if isinstance(text_config, dict):
|
||||
config = SimpleNamespace(**text_config)
|
||||
else:
|
||||
config = text_config
|
||||
|
||||
hidden_size = config.hidden_size
|
||||
if architecture == "DbrxForCausalLM":
|
||||
@@ -148,6 +155,17 @@ def get_model_config(
|
||||
E = config.num_experts // ep_size
|
||||
topk = config.num_experts_per_tok
|
||||
intermediate_size = config.moe_intermediate_size
|
||||
elif architecture == "MiniMaxM3SparseForConditionalGeneration":
|
||||
# Serving fuses the shared expert into the routed-expert tensor by
|
||||
# default (E = num_local_experts + 1), so tune for that shape unless
|
||||
# fusion is explicitly disabled.
|
||||
E = config.num_local_experts // ep_size + (
|
||||
0 if disable_shared_experts_fusion else 1
|
||||
)
|
||||
topk = config.num_experts_per_tok + (
|
||||
0 if disable_shared_experts_fusion or topk_ids_dir is None else 1
|
||||
)
|
||||
intermediate_size = config.intermediate_size
|
||||
else:
|
||||
# Default: Mixtral
|
||||
E = config.num_local_experts // ep_size
|
||||
@@ -158,12 +176,25 @@ def get_model_config(
|
||||
intermediate_size, tp_size, ep_size
|
||||
)
|
||||
|
||||
# gfx942 (MI300X) remaps MXFP8 [1,32]->[128,128] at load; tune must match.
|
||||
# sm100/gfx95 run native [1,32] and must not remap (mirrors fp8.py).
|
||||
if (
|
||||
architecture == "MiniMaxM3SparseForConditionalGeneration"
|
||||
and is_hip()
|
||||
and not is_gfx95_supported()
|
||||
and block_shape == [1, 32]
|
||||
):
|
||||
block_shape = [128, 128]
|
||||
|
||||
# text_config may not carry torch_dtype; fall back to bf16.
|
||||
torch_dtype = getattr(config, "torch_dtype", None) or torch.bfloat16
|
||||
|
||||
return {
|
||||
"num_experts": E,
|
||||
"topk": topk,
|
||||
"hidden_size": hidden_size,
|
||||
"shard_intermediate_size": shard_intermediate_size,
|
||||
"dtype": config.torch_dtype,
|
||||
"dtype": torch_dtype,
|
||||
"block_shape": block_shape,
|
||||
"architecture": architecture,
|
||||
}
|
||||
|
||||
@@ -248,8 +248,10 @@ class BenchmarkWorker:
|
||||
torch.get_device_module().manual_seed_all(0)
|
||||
self.seed = seed
|
||||
# Get the device ID to allocate tensors and kernels
|
||||
# on the respective GPU.
|
||||
self.device_id = int(ray.get_gpu_ids()[0])
|
||||
# on the respective GPU. Ray isolates each worker to a single visible
|
||||
# GPU via CUDA_VISIBLE_DEVICES, so the local ordinal is always 0. On
|
||||
# ROCm using the global ray gpu id here raises "invalid device ordinal".
|
||||
self.device_id = 0 if is_hip() else int(ray.get_gpu_ids()[0])
|
||||
set_global_server_args_for_scheduler(server_args)
|
||||
|
||||
def benchmark(
|
||||
|
||||
@@ -569,7 +569,8 @@ RUN if [ "$BUILD_TRITON" = "1" ]; then \
|
||||
&& cd triton-custom \
|
||||
&& git checkout ${TRITON_COMMIT} \
|
||||
&& pip install -r python/requirements.txt \
|
||||
&& pip install -e .; \
|
||||
&& pip install -e . \
|
||||
&& if [ -d python/triton_kernels ]; then pip install -e python/triton_kernels --no-deps; fi; \
|
||||
fi
|
||||
|
||||
# -----------------------
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
#include <sgl_kernel/tensor.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/type.cuh>
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/vec.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace {
|
||||
|
||||
// Up to this many norm "groups" are fused into one launch. A group is a
|
||||
// contiguous run of heads (within the per-token row) that share one norm
|
||||
// weight and all receive RoPE. Heads not covered by any group (e.g. the V /
|
||||
// index-V regions) are simply never assigned a job and left untouched.
|
||||
//
|
||||
// For MiniMax-M3 the groups are, in order: Q (main), K (main), index-Q,
|
||||
// index-K -- which is why 4 slots are enough. This mirrors the multi-branch
|
||||
// single-launch design of fused_store_kv_index.cuh (one kernel handles main
|
||||
// K/V + index K/V), here applied to GemmaRMSNorm + partial RoPE.
|
||||
constexpr int kMaxGroups = 4;
|
||||
|
||||
struct FusedGemmaQKNormParams {
|
||||
bf16_t* __restrict__ qkv;
|
||||
const bf16_t* __restrict__ weight[kMaxGroups]; // per-group norm weight [head_dim]
|
||||
uint32_t group_offset[kMaxGroups]; // head offset of the group in the row
|
||||
uint32_t group_count[kMaxGroups]; // number of heads in the group
|
||||
uint32_t num_groups;
|
||||
uint32_t total_heads; // sum of group_count[0..num_groups)
|
||||
const float* __restrict__ cos_sin_cache;
|
||||
const void* __restrict__ positions; // dtype depends on PosT template
|
||||
uint32_t num_tokens;
|
||||
int64_t token_stride;
|
||||
float eps;
|
||||
};
|
||||
|
||||
template <typename PosT, int64_t kHeadDim, int64_t kRopeDim, bool kUsePDL>
|
||||
struct FusedTrait {
|
||||
static_assert(kHeadDim == 128 && kRopeDim == 64, "kernel specialized for HEAD_DIM=128, ROTARY_DIM=64");
|
||||
static constexpr uint32_t kWorkerSize = device::kWarpThreads; // full warp per head
|
||||
SGL_DEVICE static void forward(const FusedGemmaQKNormParams& params) {
|
||||
using namespace device;
|
||||
const auto tx = threadIdx.x;
|
||||
const auto bx = blockIdx.x;
|
||||
const auto lane_id = tx % kWorkerSize;
|
||||
const auto work_id = (bx * blockDim.x + tx) / kWorkerSize;
|
||||
const auto total_heads = params.total_heads;
|
||||
if (work_id >= params.num_tokens * total_heads) return;
|
||||
const auto token_id = work_id / total_heads;
|
||||
const auto grouped_head = work_id % total_heads;
|
||||
|
||||
// Resolve which group this job belongs to (num_groups <= kMaxGroups, so a
|
||||
// short scan is cheaper than any precomputed table and warp-uniform).
|
||||
uint32_t g = 0, base = 0;
|
||||
for (uint32_t i = 0; i < params.num_groups; ++i) {
|
||||
const auto cnt = params.group_count[i];
|
||||
if (grouped_head < base + cnt) {
|
||||
g = i;
|
||||
break;
|
||||
}
|
||||
base += cnt;
|
||||
}
|
||||
const auto local_head = grouped_head - base;
|
||||
const auto head_id = params.group_offset[g] + local_head;
|
||||
const auto weight = params.weight[g];
|
||||
const auto input = params.qkv + token_id * params.token_stride + head_id * kHeadDim;
|
||||
|
||||
// prefetch weight and rope index
|
||||
const auto idx_0 = lane_id + 0; // rope first half [0,32)
|
||||
const auto idx_1 = lane_id + 32; // rope second half [32, 64)
|
||||
const auto idx_2 = lane_id + 64; // pass
|
||||
const auto idx_3 = lane_id + 96; // pass
|
||||
const auto w0_bf16 = weight[idx_0];
|
||||
const auto w1_bf16 = weight[idx_1];
|
||||
const auto w2_bf16 = weight[idx_2];
|
||||
const auto w3_bf16 = weight[idx_3];
|
||||
const auto rope_idx = static_cast<const PosT*>(params.positions)[token_id];
|
||||
PDLWaitPrimary<kUsePDL>();
|
||||
|
||||
// load input and compute RMS, fp32 accumulation for stability
|
||||
const auto i0_bf16 = input[idx_0];
|
||||
const auto i1_bf16 = input[idx_1];
|
||||
const auto i2_bf16 = input[idx_2];
|
||||
const auto i3_bf16 = input[idx_3];
|
||||
const auto [i0, i1] = cast<fp32x2_t>(bf16x2_t{i0_bf16, i1_bf16});
|
||||
const auto [i2, i3] = cast<fp32x2_t>(bf16x2_t{i2_bf16, i3_bf16});
|
||||
const auto ss = warp::reduce_sum(i0 * i0 + i1 * i1 + i2 * i2 + i3 * i3);
|
||||
const auto inv_rms = rsqrtf(ss / static_cast<float>(kHeadDim) + params.eps);
|
||||
|
||||
// apply norm
|
||||
const auto [w0, w1] = cast<fp32x2_t>(bf16x2_t{w0_bf16, w1_bf16});
|
||||
const auto [w2, w3] = cast<fp32x2_t>(bf16x2_t{w2_bf16, w3_bf16});
|
||||
const auto n0 = i0 * inv_rms * (1.0f + w0);
|
||||
const auto n1 = i1 * inv_rms * (1.0f + w1);
|
||||
const auto n2 = i2 * inv_rms * (1.0f + w2);
|
||||
const auto n3 = i3 * inv_rms * (1.0f + w3);
|
||||
|
||||
const auto cs = params.cos_sin_cache + rope_idx * kRopeDim;
|
||||
const auto cos = cs[lane_id];
|
||||
const auto sin = cs[lane_id + kRopeDim / 2];
|
||||
|
||||
// apply rope to the first kRopeDim dims, and write back
|
||||
device::PDLTriggerSecondary<kUsePDL>();
|
||||
const auto o0 = n0 * cos - n1 * sin;
|
||||
const auto o1 = n1 * cos + n0 * sin;
|
||||
const auto [o0_bf16, o1_bf16] = cast<bf16x2_t>(fp32x2_t{o0, o1});
|
||||
const auto [o2_bf16, o3_bf16] = cast<bf16x2_t>(fp32x2_t{n2, n3});
|
||||
input[idx_0] = o0_bf16;
|
||||
input[idx_1] = o1_bf16;
|
||||
input[idx_2] = o2_bf16;
|
||||
input[idx_3] = o3_bf16;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Trait>
|
||||
__global__ void fused_gemma_qknorm_rope_kernel(const __grid_constant__ FusedGemmaQKNormParams params) {
|
||||
return Trait::forward(params);
|
||||
}
|
||||
|
||||
// Multi-group fused GemmaRMSNorm + partial NeoX RoPE, in place over `qkv`.
|
||||
//
|
||||
// Up to kMaxGroups norm groups are passed as (weight, head offset, head count)
|
||||
// triples. `w0..w3` are the per-group norm weights ([head_dim] bf16 each); the
|
||||
// `offN` / `cntN` scalars give each group's head offset (within the per-token
|
||||
// row) and head count. These offsets/counts are host-known constants (passed as
|
||||
// scalars, never device tensors) so the launch stays CUDA-graph capturable.
|
||||
// Weight slots beyond `num_groups` may be dummies (e.g. == w0); the kernel
|
||||
// never reads them because `num_groups` bounds the group scan. The main Q/K and
|
||||
// index-Q/index-K heads are all normed and rotated in one launch; the V /
|
||||
// index-V heads, lying outside every group, are left untouched.
|
||||
template <typename PosT, int64_t HEAD_DIM, int64_t ROTARY_DIM, bool kUsePDL>
|
||||
void fused_gemma_qknorm_rope(
|
||||
tvm::ffi::TensorView qkv,
|
||||
tvm::ffi::TensorView w0,
|
||||
tvm::ffi::TensorView w1,
|
||||
tvm::ffi::TensorView w2,
|
||||
tvm::ffi::TensorView w3,
|
||||
tvm::ffi::TensorView cos_sin_cache,
|
||||
tvm::ffi::TensorView positions,
|
||||
int64_t off0,
|
||||
int64_t cnt0,
|
||||
int64_t off1,
|
||||
int64_t cnt1,
|
||||
int64_t off2,
|
||||
int64_t cnt2,
|
||||
int64_t off3,
|
||||
int64_t cnt3,
|
||||
int64_t num_groups,
|
||||
double eps) {
|
||||
using namespace host;
|
||||
auto N = SymbolicSize{"num_tokens"};
|
||||
auto device = SymbolicDevice{};
|
||||
constexpr auto D = HEAD_DIM;
|
||||
constexpr auto R = ROTARY_DIM;
|
||||
device.set_options<kDLCUDA>();
|
||||
TensorMatcher({N, -1}).with_dtype<bf16_t>().with_device(device).verify(qkv);
|
||||
TensorMatcher({D}).with_dtype<bf16_t>().with_device(device).verify(w0).verify(w1).verify(w2).verify(w3);
|
||||
TensorMatcher({-1, R}).with_dtype<fp32_t>().with_device(device).verify(cos_sin_cache);
|
||||
TensorMatcher({N}).with_dtype<PosT>().with_device(device).verify(positions);
|
||||
|
||||
RuntimeCheck(num_groups >= 1 && num_groups <= kMaxGroups);
|
||||
|
||||
const tvm::ffi::TensorView weights[kMaxGroups] = {w0, w1, w2, w3};
|
||||
const int64_t offsets[kMaxGroups] = {off0, off1, off2, off3};
|
||||
const int64_t counts[kMaxGroups] = {cnt0, cnt1, cnt2, cnt3};
|
||||
|
||||
auto params = FusedGemmaQKNormParams{};
|
||||
params.qkv = static_cast<bf16_t*>(qkv.data_ptr());
|
||||
params.cos_sin_cache = static_cast<const float*>(cos_sin_cache.data_ptr());
|
||||
params.positions = positions.data_ptr();
|
||||
params.num_tokens = static_cast<uint32_t>(N.unwrap());
|
||||
params.num_groups = static_cast<uint32_t>(num_groups);
|
||||
params.token_stride = static_cast<int64_t>(qkv.stride(0));
|
||||
params.eps = static_cast<float>(eps);
|
||||
|
||||
uint32_t total_heads = 0;
|
||||
for (int64_t i = 0; i < kMaxGroups; ++i) {
|
||||
const auto cnt = (i < num_groups) ? counts[i] : 0;
|
||||
params.weight[i] = static_cast<const bf16_t*>(weights[i].data_ptr());
|
||||
params.group_offset[i] = static_cast<uint32_t>(i < num_groups ? offsets[i] : 0);
|
||||
params.group_count[i] = static_cast<uint32_t>(cnt);
|
||||
total_heads += static_cast<uint32_t>(cnt);
|
||||
}
|
||||
params.total_heads = total_heads;
|
||||
|
||||
using Trait = FusedTrait<PosT, HEAD_DIM, ROTARY_DIM, kUsePDL>;
|
||||
const auto needed_threads = static_cast<int64_t>(params.num_tokens) * total_heads * device::kWarpThreads;
|
||||
RuntimeCheck(needed_threads < std::numeric_limits<uint32_t>::max());
|
||||
if (needed_threads == 0) return;
|
||||
const uint32_t block_size = 256u;
|
||||
const uint32_t num_blocks = div_ceil(static_cast<uint32_t>(needed_threads), block_size);
|
||||
LaunchKernel(num_blocks, block_size, device.unwrap()) //
|
||||
.enable_pdl(kUsePDL)(fused_gemma_qknorm_rope_kernel<Trait>, params);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,171 @@
|
||||
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize/DType/Device
|
||||
#include <sgl_kernel/utils.h> // For RuntimeCheck, div_ceil, pointer::offset
|
||||
|
||||
#include <sgl_kernel/utils.cuh> // For LaunchKernel, SGL_DEVICE, PDL helpers
|
||||
#include <sgl_kernel/vec.cuh> // For AlignedVector
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace {
|
||||
|
||||
struct StoreKVIndexParams {
|
||||
const void* __restrict__ input_ptrs[4];
|
||||
void* __restrict__ cache_ptrs[4];
|
||||
const void* __restrict__ indices;
|
||||
int64_t input_stride_bytes[4]; // k stride, v stride, idx_k stride, idx_v stride
|
||||
int64_t cache_stride_bytes[4]; // k stride, v stride, idx_k stride, idx_v stride
|
||||
uint32_t num_k_heads;
|
||||
uint32_t num_total_heads; // 2 * num_k_heads + 1 + has_index_v
|
||||
uint32_t total_jobs; // batch_size * heads_per_token
|
||||
};
|
||||
|
||||
template <int64_t kHeadBytes, bool kUsePDL>
|
||||
struct StoreTrait {
|
||||
static_assert(kHeadBytes % 16 == 0, "head bytes must be a multiple of 16 (128-bit vector)");
|
||||
using vec_t = device::AlignedVector<uint32_t, 4>; // 16 bytes / thread
|
||||
static constexpr uint32_t kWorkerSize = kHeadBytes / 16; // threads per head
|
||||
|
||||
template <typename T>
|
||||
SGL_DEVICE static void forward(const StoreKVIndexParams& params) {
|
||||
using namespace device;
|
||||
const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
const uint32_t work_id = tid / kWorkerSize; // which (token, head) job
|
||||
const uint32_t lane = tid % kWorkerSize; // which 16-byte chunk of the head
|
||||
if (work_id >= params.total_jobs) return;
|
||||
|
||||
const uint32_t num_total_heads = params.num_total_heads;
|
||||
const uint32_t token_id = work_id / num_total_heads;
|
||||
const uint32_t slot_id = work_id % num_total_heads;
|
||||
const uint32_t num_k_heads = params.num_k_heads;
|
||||
|
||||
const auto loc = static_cast<const T*>(params.indices)[token_id];
|
||||
uint32_t head_id, which;
|
||||
if (slot_id < num_k_heads) {
|
||||
head_id = slot_id;
|
||||
which = 0; // K
|
||||
} else if (slot_id < 2 * num_k_heads) {
|
||||
head_id = slot_id - num_k_heads;
|
||||
which = 1; // V
|
||||
} else if (slot_id == 2 * num_k_heads) {
|
||||
head_id = 0;
|
||||
which = 2; // idx K
|
||||
} else {
|
||||
head_id = 0;
|
||||
which = 3; // idx V
|
||||
}
|
||||
const auto cache_ptr = static_cast<char*>(params.cache_ptrs[which]);
|
||||
const auto input_ptr = static_cast<const char*>(params.input_ptrs[which]);
|
||||
const auto cache_stride = params.cache_stride_bytes[which];
|
||||
const auto input_stride = params.input_stride_bytes[which];
|
||||
const auto src = pointer::offset(input_ptr, token_id * input_stride, head_id * kHeadBytes);
|
||||
const auto dst = pointer::offset(cache_ptr, loc * cache_stride, head_id * kHeadBytes);
|
||||
PDLWaitPrimary<kUsePDL>();
|
||||
vec_t chunk;
|
||||
chunk.load(src, lane);
|
||||
chunk.store(dst, lane);
|
||||
PDLTriggerSecondary<kUsePDL>();
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Trait, typename T>
|
||||
__global__ void store_kv_index_kernel(const __grid_constant__ StoreKVIndexParams params) {
|
||||
Trait::template forward<T>(params);
|
||||
}
|
||||
|
||||
// idx_v / idx_v_cache may be dummies (== idx_k / idx_k_cache) when the layer
|
||||
// has no index value; the caller signals "no V" via heads_per_token == 2*hkv+1
|
||||
// so the index-V branch is never taken.
|
||||
template <int64_t kHeadBytes, bool kUsePDL>
|
||||
void store_kv_index(
|
||||
tvm::ffi::TensorView k,
|
||||
tvm::ffi::TensorView v,
|
||||
tvm::ffi::TensorView k_cache,
|
||||
tvm::ffi::TensorView v_cache,
|
||||
tvm::ffi::TensorView idx_k,
|
||||
tvm::ffi::TensorView idx_k_cache,
|
||||
tvm::ffi::TensorView idx_v,
|
||||
tvm::ffi::TensorView idx_v_cache,
|
||||
tvm::ffi::TensorView indices,
|
||||
int64_t num_kv_heads,
|
||||
int64_t heads_per_token) {
|
||||
using namespace host;
|
||||
auto B = SymbolicSize{"batch"};
|
||||
auto Mrow = SymbolicSize{"main_row"}; // num_kv_heads * head_dim
|
||||
auto Drow = SymbolicSize{"idx_row"}; // head_dim
|
||||
auto dtype = SymbolicDType{};
|
||||
auto indice_dtype = SymbolicDType{};
|
||||
auto device = SymbolicDevice{};
|
||||
device.set_options<kDLCUDA>();
|
||||
|
||||
// All cache/source tensors share the same store dtype (fast-path precondition).
|
||||
TensorMatcher({B, Mrow}) //
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.verify(k)
|
||||
.verify(v);
|
||||
TensorMatcher({-1, Mrow}) //
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.verify(k_cache)
|
||||
.verify(v_cache);
|
||||
TensorMatcher({B, Drow}) //
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.verify(idx_k)
|
||||
.verify(idx_v);
|
||||
TensorMatcher({-1, Drow}) //
|
||||
.with_strides({-1, 1})
|
||||
.with_dtype(dtype)
|
||||
.with_device(device)
|
||||
.verify(idx_k_cache)
|
||||
.verify(idx_v_cache);
|
||||
TensorMatcher({B}) //
|
||||
.with_dtype<int32_t, int64_t>(indice_dtype)
|
||||
.with_device(device)
|
||||
.verify(indices);
|
||||
|
||||
const int64_t dsize = dtype_bytes(dtype.unwrap());
|
||||
RuntimeCheck(kHeadBytes == Drow.unwrap() * dsize);
|
||||
RuntimeCheck(Mrow.unwrap() == num_kv_heads * Drow.unwrap());
|
||||
|
||||
const auto params = StoreKVIndexParams{
|
||||
.input_ptrs = {k.data_ptr(), v.data_ptr(), idx_k.data_ptr(), idx_v.data_ptr()},
|
||||
.cache_ptrs = {k_cache.data_ptr(), v_cache.data_ptr(), idx_k_cache.data_ptr(), idx_v_cache.data_ptr()},
|
||||
.indices = indices.data_ptr(),
|
||||
.input_stride_bytes =
|
||||
{
|
||||
k.stride(0) * dsize,
|
||||
v.stride(0) * dsize,
|
||||
idx_k.stride(0) * dsize,
|
||||
idx_v.stride(0) * dsize,
|
||||
},
|
||||
.cache_stride_bytes =
|
||||
{
|
||||
k_cache.stride(0) * dsize,
|
||||
v_cache.stride(0) * dsize,
|
||||
idx_k_cache.stride(0) * dsize,
|
||||
idx_v_cache.stride(0) * dsize,
|
||||
},
|
||||
.num_k_heads = static_cast<uint32_t>(num_kv_heads),
|
||||
.num_total_heads = static_cast<uint32_t>(heads_per_token),
|
||||
.total_jobs = static_cast<uint32_t>(B.unwrap() * heads_per_token),
|
||||
};
|
||||
if (params.total_jobs == 0) return;
|
||||
|
||||
using Trait = StoreTrait<kHeadBytes, kUsePDL>;
|
||||
constexpr uint32_t kBlockSize = 256u;
|
||||
const uint32_t num_blocks = div_ceil(params.total_jobs * Trait::kWorkerSize, kBlockSize);
|
||||
const auto kernel = indice_dtype.is_type<int32_t>() //
|
||||
? store_kv_index_kernel<Trait, int32_t>
|
||||
: store_kv_index_kernel<Trait, int64_t>;
|
||||
LaunchKernel(num_blocks, kBlockSize, device.unwrap()) //
|
||||
.enable_pdl(kUsePDL)(kernel, params);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,535 @@
|
||||
#include <sgl_kernel/tensor.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
|
||||
#include <cfloat>
|
||||
#include <cstdint>
|
||||
#if defined(__HIP_PLATFORM_AMD__)
|
||||
static constexpr unsigned long long kWarpSyncMask = 0xFFFFFFFFFFFFFFFFull;
|
||||
#else
|
||||
#include <math_constants.h>
|
||||
static constexpr unsigned int kWarpSyncMask = 0xFFFFFFFFu;
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
// Block top-k selection over a per-(head, batch) row of block scores, run by one
|
||||
// CTA of TopKTrait::kCTASize threads. Picks the `topk` highest-scoring block ids
|
||||
// (k_eff = min(topk, num_blocks)). Three size regimes, chosen by num_blocks:
|
||||
// * <= kSmallThreshold : O(n^2) rank-by-compare (no radix).
|
||||
// * <= kCTASize : 4-pass 8-bit radix, one element per thread in a reg.
|
||||
// * <= kMaxNumBlocks : 4-pass 8-bit radix, kIters elements per thread cached
|
||||
// in registers (row read from global exactly once);
|
||||
// liveness is a uint32_t bitmask, selection is an
|
||||
// in-loop scatter -- nothing is cached in shared memory.
|
||||
// The trivial case num_blocks <= topk (every block selected) is handled by the
|
||||
// kernels below, outside the Trait.
|
||||
struct TopKTrait {
|
||||
static constexpr uint32_t kMaxTopK = 32;
|
||||
static constexpr uint32_t kCTASize = 512;
|
||||
static constexpr uint32_t kNumWarps = kCTASize / device::kWarpThreads;
|
||||
static constexpr uint32_t kMaxNumBlocks = 4096; // block topk
|
||||
static constexpr uint32_t kSmallThreshold = 8 * kNumWarps;
|
||||
static constexpr uint32_t kRadixBits = 8;
|
||||
static constexpr uint32_t kRadixSize = 1 << kRadixBits;
|
||||
static constexpr float kNegInf = -std::numeric_limits<float>::infinity();
|
||||
|
||||
struct Smem {
|
||||
uint32_t warp_sum[kNumWarps];
|
||||
alignas(128) uint32_t counter;
|
||||
alignas(128) uint32_t counter_final;
|
||||
alignas(128) uint32_t threshold_bin;
|
||||
uint32_t equal_count;
|
||||
uint32_t above_count;
|
||||
uint32_t histogram[2][kRadixSize]; // 8 bit radix
|
||||
float small_scores[kSmallThreshold]; // small (O(n^2)) path only
|
||||
};
|
||||
|
||||
SGL_DEVICE static void forward(
|
||||
const float* __restrict__ scores,
|
||||
const uint32_t num_blocks,
|
||||
int32_t* __restrict__ topk_out,
|
||||
const uint32_t topk,
|
||||
Smem* smem) {
|
||||
using namespace device;
|
||||
const auto tx = threadIdx.x;
|
||||
__builtin_assume(tx < kCTASize);
|
||||
const auto warp_id = tx / kWarpThreads;
|
||||
const auto lane_id = tx % kWarpThreads;
|
||||
|
||||
constexpr auto is_greater = [](float x, float y, int32_t delta) {
|
||||
return (x > y) || ((x == y) && delta < 0); // lower block id wins
|
||||
};
|
||||
constexpr auto warp_inclusive_sum = [](uint32_t lane_id, uint32_t val) {
|
||||
#pragma unroll
|
||||
for (uint32_t offset = 1; offset < 32; offset *= 2) {
|
||||
uint32_t n = __shfl_up_sync(kWarpSyncMask, val, offset, 32);
|
||||
if (lane_id >= offset) val += n;
|
||||
}
|
||||
return val;
|
||||
};
|
||||
constexpr auto clip_nan = [](float x) { return x != x ? kNegInf : x; };
|
||||
constexpr auto score_to_key = [](float x) {
|
||||
uint32_t b = __float_as_uint(x);
|
||||
return (b & 0x80000000u) ? ~b : (b | 0x80000000u);
|
||||
};
|
||||
// Find the radix bin holding the topk_remain-th largest of `total_active`
|
||||
// elements currently counted in `histogram`. Writes threshold_bin (the bin),
|
||||
// above_count (elements strictly above it), equal_count (elements in it).
|
||||
const auto find_threshold = [&](uint32_t* histogram, uint32_t total_active, uint32_t topk_remain) {
|
||||
using namespace device;
|
||||
uint32_t hist_val = 0;
|
||||
uint32_t warp_inc = 0;
|
||||
if (tx < kRadixSize) {
|
||||
hist_val = histogram[tx];
|
||||
warp_inc = warp_inclusive_sum(lane_id, hist_val);
|
||||
if (lane_id == kWarpThreads - 1) smem->warp_sum[warp_id] = warp_inc;
|
||||
}
|
||||
__syncthreads();
|
||||
if (tx < kRadixSize) {
|
||||
const auto inter = warp::reduce_sum(lane_id < warp_id ? smem->warp_sum[lane_id] : 0);
|
||||
const auto prefix = inter + warp_inc; // count in bins [0, tx]
|
||||
const auto above = total_active - prefix; // count in bins ABOVE tx
|
||||
if (above < topk_remain && above + hist_val >= topk_remain) {
|
||||
smem->threshold_bin = tx;
|
||||
smem->above_count = above;
|
||||
smem->equal_count = hist_val;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
};
|
||||
|
||||
if (num_blocks <= kSmallThreshold) {
|
||||
// O(n^2) compare: each block's rank = #blocks that outrank it; the ones
|
||||
// with rank < topk are selected (rank is its position in topk_out).
|
||||
static_assert(kSmallThreshold <= kCTASize);
|
||||
if (tx < num_blocks) smem->small_scores[tx] = clip_nan(scores[tx]);
|
||||
__syncthreads();
|
||||
constexpr uint32_t kNumCandidates = kSmallThreshold / kNumWarps;
|
||||
constexpr uint32_t kNumTargets = kSmallThreshold / kWarpThreads;
|
||||
float candidates[kNumCandidates];
|
||||
float target[kNumTargets];
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kNumTargets; ++i) {
|
||||
const auto idx = lane_id + i * kWarpThreads;
|
||||
target[i] = (idx < num_blocks) ? smem->small_scores[idx] : kNegInf;
|
||||
}
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kNumCandidates; ++i) {
|
||||
const auto idx = warp_id + i * kNumWarps;
|
||||
candidates[i] = (idx < num_blocks) ? smem->small_scores[idx] : kNegInf;
|
||||
}
|
||||
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kNumCandidates; ++i) {
|
||||
const int32_t idx = warp_id + i * kNumWarps;
|
||||
if (idx >= static_cast<int32_t>(num_blocks)) break;
|
||||
uint32_t rank = 0;
|
||||
#pragma unroll
|
||||
for (uint32_t j = 0; j < kNumTargets; ++j) {
|
||||
const int32_t delta = lane_id + j * kWarpThreads - idx;
|
||||
// partial rank = how many of this lane's targets outrank the candidate
|
||||
rank += is_greater(target[j], candidates[i], delta);
|
||||
}
|
||||
// full rank = sum of the per-lane partial ranks across the warp
|
||||
rank = warp::reduce_sum(rank);
|
||||
if (rank < topk) topk_out[rank] = idx;
|
||||
}
|
||||
} else if (num_blocks <= kCTASize) {
|
||||
// 4-pass 8-bit radix select, one element per thread held in a register.
|
||||
bool active = tx < num_blocks;
|
||||
const auto value = active ? clip_nan(scores[tx]) : kNegInf;
|
||||
const auto key = score_to_key(value);
|
||||
uint32_t topk_remain = topk;
|
||||
uint32_t write_pos = topk; // sentinel: not selected
|
||||
if (tx < kRadixSize) smem->histogram[0][tx] = 0;
|
||||
if (tx == kRadixSize) smem->counter = smem->counter_final = 0;
|
||||
__syncthreads();
|
||||
uint32_t total_active = num_blocks;
|
||||
|
||||
#pragma unroll
|
||||
for (int round = 0; round < 4; round++) {
|
||||
const uint32_t shift = 24 - round * 8;
|
||||
const uint32_t bin = (key >> shift) & 0xFFu;
|
||||
const auto hist_idx = round % 2;
|
||||
const auto histogram = smem->histogram[hist_idx];
|
||||
|
||||
if (active) atomicAdd(&histogram[bin], 1);
|
||||
if (round < 3 && tx < kRadixSize) smem->histogram[hist_idx ^ 1][tx] = 0;
|
||||
__syncthreads();
|
||||
|
||||
find_threshold(histogram, total_active, topk_remain);
|
||||
|
||||
const auto threshold_bin = smem->threshold_bin;
|
||||
const auto above_count = smem->above_count;
|
||||
const auto equal_count = smem->equal_count;
|
||||
|
||||
if (round < 3) total_active = equal_count;
|
||||
topk_remain -= above_count;
|
||||
|
||||
// scatter: above -> selected now; equal at the last pass -> keep the rest
|
||||
if (active) {
|
||||
if (bin > threshold_bin) {
|
||||
write_pos = atomicAdd(&smem->counter, 1);
|
||||
active = false;
|
||||
} else if (bin < threshold_bin) {
|
||||
active = false;
|
||||
} else if (round == 3) {
|
||||
write_pos = topk - topk_remain + atomicAdd(&smem->counter_final, 1);
|
||||
}
|
||||
// bin == threshold && round < 3: stay active for the next pass
|
||||
}
|
||||
|
||||
if (round == 3 || topk_remain == 0) break;
|
||||
}
|
||||
|
||||
if (write_pos < topk) topk_out[write_pos] = tx;
|
||||
} else {
|
||||
// num_blocks in (kCTASize, kMaxNumBlocks]: each thread caches its (up to
|
||||
// kIters) slice of the row in registers -- read from global exactly ONCE --
|
||||
// then runs the same 4-pass radix select as the single-element path looped
|
||||
// over those slots. Liveness is a uint32_t bitmask (bit i = slot i still in
|
||||
// the running set), so there is no per-element flag array; selection is an
|
||||
// in-loop scatter, so there is no per-element position array. Nothing is
|
||||
// cached in shared memory beyond the histogram.
|
||||
constexpr uint32_t kIters = kMaxNumBlocks / kCTASize;
|
||||
static_assert(kIters <= 32, "active liveness is packed into a uint32_t");
|
||||
uint32_t key[kIters];
|
||||
uint32_t active = 0;
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kIters; ++i) {
|
||||
const uint32_t idx = i * kCTASize + tx;
|
||||
if (idx < num_blocks) {
|
||||
key[i] = score_to_key(clip_nan(scores[idx]));
|
||||
active |= 1u << i;
|
||||
}
|
||||
}
|
||||
if (tx < kRadixSize) smem->histogram[0][tx] = 0;
|
||||
if (tx == kRadixSize) smem->counter = smem->counter_final = 0;
|
||||
__syncthreads();
|
||||
|
||||
uint32_t topk_remain = topk;
|
||||
uint32_t total_active = num_blocks;
|
||||
|
||||
#pragma unroll
|
||||
for (int round = 0; round < 4; ++round) {
|
||||
const uint32_t shift = 24 - round * 8;
|
||||
const auto hb = round & 1;
|
||||
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kIters; ++i)
|
||||
if (active & (1u << i)) atomicAdd(&smem->histogram[hb][(key[i] >> shift) & 0xFFu], 1);
|
||||
if (round < 3 && tx < kRadixSize) smem->histogram[hb ^ 1][tx] = 0;
|
||||
__syncthreads();
|
||||
|
||||
find_threshold(smem->histogram[hb], total_active, topk_remain);
|
||||
const auto threshold_bin = smem->threshold_bin;
|
||||
const auto above_count = smem->above_count;
|
||||
const auto equal_count = smem->equal_count;
|
||||
|
||||
if (round < 3) total_active = equal_count;
|
||||
topk_remain -= above_count;
|
||||
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kIters; ++i) {
|
||||
if (active & (1u << i)) {
|
||||
const uint32_t bin = (key[i] >> shift) & 0xFFu;
|
||||
if (bin > threshold_bin) {
|
||||
topk_out[atomicAdd(&smem->counter, 1)] = i * kCTASize + tx;
|
||||
active &= ~(1u << i);
|
||||
} else if (bin < threshold_bin) {
|
||||
active &= ~(1u << i);
|
||||
} else if (round == 3) {
|
||||
const auto pos = topk - topk_remain + atomicAdd(&smem->counter_final, 1);
|
||||
if (pos < topk) topk_out[pos] = i * kCTASize + tx;
|
||||
}
|
||||
// bin == threshold && round < 3: slot stays live for the next pass
|
||||
}
|
||||
}
|
||||
|
||||
if (round == 3 || topk_remain == 0) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Kernels: one CTA (kCTASize threads) per (head, batch) row. The trivial case
|
||||
// num_blocks <= topk (every block selected) is special-judged here, outside the
|
||||
// Trait; otherwise the Trait selects the top-k block ids.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// Block-id output: topk_idx[h, b, 0:k_eff) = selected block ids (front-packed,
|
||||
// unordered), [k_eff:topk) = -1.
|
||||
template <typename SeqLenT, bool kUsePDL>
|
||||
__global__ void minimax_decode_topk_block_kernel(
|
||||
const float* __restrict__ score,
|
||||
const SeqLenT* __restrict__ seq_lens,
|
||||
int32_t* __restrict__ topk_idx,
|
||||
int batch,
|
||||
int num_heads,
|
||||
int max_seqblock,
|
||||
int block_size,
|
||||
int topk) {
|
||||
const int b = blockIdx.x; // grid.x = batch
|
||||
const int h = blockIdx.y; // grid.y = num_heads
|
||||
const int tx = threadIdx.x;
|
||||
|
||||
// seq_lens is from an earlier kernel; prefetch it (and the cheap setup) before
|
||||
// waiting on the score producer so the prologue overlaps its tail (PDL).
|
||||
const int64_t seq_len = static_cast<int64_t>(seq_lens[b]);
|
||||
const int num_blocks_raw = static_cast<int>((seq_len + block_size - 1) / block_size);
|
||||
// Never scan past the materialized score columns.
|
||||
const int num_blocks = num_blocks_raw < max_seqblock ? num_blocks_raw : max_seqblock;
|
||||
int32_t* __restrict__ out = topk_idx + (static_cast<int64_t>(h) * batch + b) * topk;
|
||||
device::PDLWaitPrimary<kUsePDL>();
|
||||
|
||||
if (num_blocks <= topk) { // trivial: identity, -1 padded
|
||||
for (int i = tx; i < topk; i += TopKTrait::kCTASize)
|
||||
out[i] = (i < num_blocks) ? i : -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const float* __restrict__ row = score + (static_cast<int64_t>(h) * batch + b) * max_seqblock;
|
||||
__shared__ TopKTrait::Smem smem;
|
||||
TopKTrait::forward(row, static_cast<uint32_t>(num_blocks), out, static_cast<uint32_t>(topk), &smem);
|
||||
}
|
||||
|
||||
// Page-table output: for each (batch b, kv-head h) pseudo-request emit the
|
||||
// trtllm/fa3 page table -- selected blocks sorted ascending (so the final partial
|
||||
// block's pages land last), each expanded to its ppb = block_size/page_size pages
|
||||
// via req_to_token -- plus the effective KV length seq_lens_out.
|
||||
//
|
||||
// DP attention (num_kv_heads > 1): each kv head selects its OWN blocks, so the
|
||||
// per-request page table can't be shared across heads. We flatten (b, h) into
|
||||
// num_heads*batch pseudo-requests laid out batch-major (row = b*num_heads + h,
|
||||
// matching q.view(bs, nkv, gqa, d).reshape(bs*nkv, gqa, d)). seq_lens / slot_ids /
|
||||
// req_to_token are per-batch (head-independent: a token's cache slot is the same
|
||||
// for every head). The page index is head-encoded (head-minor) as
|
||||
// base_page*num_heads + h, which is exactly the page index into an HND cache
|
||||
// [num_pages, nkv, page_size, D] reshaped to [num_pages*nkv, 1, page_size, D] (a
|
||||
// free view when the cache is contiguous HND). num_heads == 1 (h == 0) reproduces
|
||||
// the single-kv-head TP>=4 behavior (page index == base_page).
|
||||
template <typename SeqLenT, bool kUsePDL>
|
||||
__global__ void minimax_decode_topk_page_table_kernel(
|
||||
const float* __restrict__ score,
|
||||
const SeqLenT* __restrict__ seq_lens,
|
||||
const int32_t* __restrict__ req_to_token,
|
||||
const int64_t* __restrict__ slot_ids,
|
||||
int32_t* __restrict__ page_table,
|
||||
int32_t* __restrict__ seq_lens_out,
|
||||
int batch,
|
||||
int num_heads,
|
||||
int max_seqblock,
|
||||
int block_size,
|
||||
int topk,
|
||||
int page_size,
|
||||
int r2t_stride,
|
||||
int max_kv_len,
|
||||
int max_sparse_pages) {
|
||||
const int b = blockIdx.x; // grid.x = batch
|
||||
const int h = blockIdx.y; // grid.y = num_heads (kv head)
|
||||
const int tx = threadIdx.x;
|
||||
|
||||
// Prefetch seq_lens / slot_ids (from earlier kernels) and the cheap setup
|
||||
// before waiting on the score producer, so the prologue overlaps its tail (PDL).
|
||||
const int64_t seq_len = static_cast<int64_t>(seq_lens[b]);
|
||||
const int num_blocks_raw = static_cast<int>((seq_len + block_size - 1) / block_size);
|
||||
const int num_blocks = num_blocks_raw < max_seqblock ? num_blocks_raw : max_seqblock;
|
||||
const int ppb = block_size / page_size;
|
||||
const int64_t out_row = static_cast<int64_t>(b) * num_heads + h; // flattened pseudo-request
|
||||
int32_t* __restrict__ pt_row = page_table + out_row * max_sparse_pages;
|
||||
const int64_t r2t_base = static_cast<int64_t>(slot_ids[b]) * r2t_stride;
|
||||
device::PDLWaitPrimary<kUsePDL>();
|
||||
|
||||
if (num_blocks <= topk) { // trivial: every block selected, all tokens valid
|
||||
if (tx == 0) seq_lens_out[out_row] = static_cast<int>(seq_len);
|
||||
// block id == ascending slot, so the partial final block's pages land last
|
||||
const int total = num_blocks * ppb;
|
||||
for (int e = tx; e < total; e += TopKTrait::kCTASize) {
|
||||
const int slot = e / ppb;
|
||||
const int pp = e % ppb;
|
||||
int tok = slot * block_size + pp * page_size;
|
||||
if (tok >= max_kv_len) tok = max_kv_len - 1;
|
||||
pt_row[e] = req_to_token[r2t_base + tok] / page_size * num_heads + h;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const int k_eff = topk; // num_blocks > topk
|
||||
const float* __restrict__ row = score + (static_cast<int64_t>(h) * batch + b) * max_seqblock; // head-major score
|
||||
__shared__ TopKTrait::Smem smem;
|
||||
__shared__ int32_t s_topk[TopKTrait::kMaxTopK];
|
||||
TopKTrait::forward(row, static_cast<uint32_t>(num_blocks), s_topk, static_cast<uint32_t>(topk), &smem);
|
||||
__syncthreads(); // s_topk fully written before the transform reads it
|
||||
|
||||
// Sort the selected block ids ascending (k_eff <= kMaxTopK is tiny) so the
|
||||
// partial final block lands last, accumulating the effective KV length in the
|
||||
// same pass: each selected block contributes min(block_size, seq_len - c*block)
|
||||
// valid tokens (only the final block can be partial).
|
||||
__shared__ int32_t s_sorted[TopKTrait::kMaxTopK];
|
||||
__shared__ int s_eff_kv;
|
||||
if (tx == 0) s_eff_kv = 0;
|
||||
__syncthreads();
|
||||
for (int slot = tx; slot < k_eff; slot += TopKTrait::kCTASize) {
|
||||
const int32_t v = s_topk[slot];
|
||||
int rank = 0;
|
||||
for (int j = 0; j < k_eff; ++j)
|
||||
rank += (s_topk[j] < v);
|
||||
s_sorted[rank] = v;
|
||||
const int rem = static_cast<int>(seq_len - static_cast<int64_t>(v) * block_size);
|
||||
atomicAdd(&s_eff_kv, rem < block_size ? rem : block_size);
|
||||
}
|
||||
__syncthreads();
|
||||
if (tx == 0) seq_lens_out[out_row] = s_eff_kv;
|
||||
|
||||
// Parallel page emit: one thread per output page.
|
||||
const int total = k_eff * ppb;
|
||||
for (int e = tx; e < total; e += TopKTrait::kCTASize) {
|
||||
const int slot = e / ppb;
|
||||
const int pp = e % ppb;
|
||||
int tok = s_sorted[slot] * block_size + pp * page_size;
|
||||
if (tok >= max_kv_len) tok = max_kv_len - 1;
|
||||
pt_row[e] = req_to_token[r2t_base + tok] / page_size * num_heads + h;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Launchers
|
||||
// -------------------------------------------------------------------------
|
||||
template <typename SeqLenT, bool kUsePDL>
|
||||
void minimax_decode_topk(
|
||||
tvm::ffi::TensorView score, // [H, B, S] fp32
|
||||
tvm::ffi::TensorView seq_lens, // [B] int32/int64
|
||||
tvm::ffi::TensorView topk_idx, // [H, B, T] int32
|
||||
int64_t block_size,
|
||||
int64_t topk) {
|
||||
using namespace host;
|
||||
|
||||
SymbolicSize H = {"num_heads"};
|
||||
SymbolicSize B = {"batch"};
|
||||
SymbolicSize S = {"max_seqblock"};
|
||||
SymbolicSize T = {"topk"};
|
||||
SymbolicDevice device_;
|
||||
device_.set_options<kDLCUDA>();
|
||||
|
||||
TensorMatcher({H, B, S}).with_dtype<fp32_t>().with_device(device_).verify(score);
|
||||
TensorMatcher({B}).with_dtype<SeqLenT>().with_device(device_).verify(seq_lens);
|
||||
TensorMatcher({H, B, T}).with_dtype<int32_t>().with_device(device_).verify(topk_idx);
|
||||
|
||||
const int num_heads = static_cast<int>(H.unwrap());
|
||||
const int batch = static_cast<int>(B.unwrap());
|
||||
const int max_seqblock = static_cast<int>(S.unwrap());
|
||||
const int topk_i = static_cast<int>(T.unwrap());
|
||||
const DLDevice device = device_.unwrap();
|
||||
|
||||
RuntimeCheck(
|
||||
static_cast<int64_t>(topk_i) == topk,
|
||||
"minimax_decode_topk: topk arg (",
|
||||
topk,
|
||||
") must match topk_idx last dim (",
|
||||
topk_i,
|
||||
")");
|
||||
RuntimeCheck(block_size > 0, "block_size must be > 0, got ", block_size);
|
||||
if (batch == 0 || num_heads == 0) return;
|
||||
|
||||
const dim3 grid(static_cast<unsigned>(batch), static_cast<unsigned>(num_heads));
|
||||
LaunchKernel(grid, TopKTrait::kCTASize, device, 0)
|
||||
.enable_pdl(kUsePDL)(
|
||||
minimax_decode_topk_block_kernel<SeqLenT, kUsePDL>,
|
||||
static_cast<const float*>(score.data_ptr()),
|
||||
static_cast<const SeqLenT*>(seq_lens.data_ptr()),
|
||||
static_cast<int32_t*>(topk_idx.data_ptr()),
|
||||
batch,
|
||||
num_heads,
|
||||
max_seqblock,
|
||||
static_cast<int>(block_size),
|
||||
topk_i);
|
||||
}
|
||||
|
||||
// Page-table variant: emit the per-(batch, kv-head) paged page table consumed by
|
||||
// the dense backend (trtllm_mha / fa3) plus the effective KV length, instead of
|
||||
// block ids. For DP attention (num_kv_heads > 1) each kv head selects its own
|
||||
// blocks, so (b, h) pseudo-requests are flattened batch-major into the output
|
||||
// (B*num_heads rows); num_heads == 1 is the TP>=4 single-kv-head case. The page
|
||||
// index is head-encoded (head-minor) as base_page*num_heads + h -- the index into
|
||||
// an HND cache [num_pages, nkv, ps, D] reshaped to [num_pages*nkv, 1, ps, D].
|
||||
// page_table and seq_lens_out are allocated by the caller.
|
||||
template <typename SeqLenT, bool kUsePDL>
|
||||
void minimax_decode_topk_page_table(
|
||||
tvm::ffi::TensorView score, // [H, B, S] fp32 (H = num_kv_heads)
|
||||
tvm::ffi::TensorView seq_lens, // [B] int32/int64
|
||||
tvm::ffi::TensorView req_to_token, // [max_reqs, max_kv_len] int32
|
||||
tvm::ffi::TensorView slot_ids, // [B] int64 (req_pool_indices)
|
||||
tvm::ffi::TensorView page_table, // [B*H, max_sparse_pages] int32 (out)
|
||||
tvm::ffi::TensorView seq_lens_out, // [B*H] int32 (effective KV length, out)
|
||||
int64_t block_size,
|
||||
int64_t topk,
|
||||
int64_t page_size) {
|
||||
using namespace host;
|
||||
|
||||
SymbolicSize H = {"num_heads"};
|
||||
SymbolicSize B = {"batch"};
|
||||
SymbolicSize S = {"max_seqblock"};
|
||||
SymbolicSize R = {"max_reqs"};
|
||||
SymbolicSize KV = {"max_kv_len"};
|
||||
SymbolicSize BH = {"batch_heads"};
|
||||
SymbolicSize P = {"max_sparse_pages"};
|
||||
SymbolicDevice device_;
|
||||
device_.set_options<kDLCUDA>();
|
||||
|
||||
TensorMatcher({H, B, S}).with_dtype<fp32_t>().with_device(device_).verify(score);
|
||||
TensorMatcher({B}).with_dtype<SeqLenT>().with_device(device_).verify(seq_lens);
|
||||
TensorMatcher({R, KV}).with_dtype<int32_t>().with_device(device_).verify(req_to_token);
|
||||
TensorMatcher({B}).with_dtype<int64_t>().with_device(device_).verify(slot_ids);
|
||||
TensorMatcher({BH, P}).with_dtype<int32_t>().with_device(device_).verify(page_table);
|
||||
TensorMatcher({BH}).with_dtype<int32_t>().with_device(device_).verify(seq_lens_out);
|
||||
|
||||
const int num_heads = static_cast<int>(H.unwrap());
|
||||
const int batch = static_cast<int>(B.unwrap());
|
||||
const int max_seqblock = static_cast<int>(S.unwrap());
|
||||
const int max_kv_len = static_cast<int>(KV.unwrap());
|
||||
const int max_sparse_pages = static_cast<int>(P.unwrap());
|
||||
const int r2t_stride = static_cast<int>(req_to_token.stride(0));
|
||||
const DLDevice device = device_.unwrap();
|
||||
|
||||
RuntimeCheck(
|
||||
BH.unwrap() == static_cast<int64_t>(batch) * num_heads,
|
||||
"page_table rows (",
|
||||
BH.unwrap(),
|
||||
") must equal batch*num_heads (",
|
||||
static_cast<int64_t>(batch) * num_heads,
|
||||
")");
|
||||
RuntimeCheck(
|
||||
block_size > 0 && page_size > 0 && block_size % page_size == 0,
|
||||
"block_size must be a positive multiple of page_size");
|
||||
RuntimeCheck(topk <= static_cast<int64_t>(TopKTrait::kMaxTopK), "topk exceeds kMaxTopK for page-table mode");
|
||||
if (batch == 0 || num_heads == 0) return;
|
||||
|
||||
const dim3 grid(static_cast<unsigned>(batch), static_cast<unsigned>(num_heads));
|
||||
LaunchKernel(grid, TopKTrait::kCTASize, device, 0)
|
||||
.enable_pdl(kUsePDL)(
|
||||
minimax_decode_topk_page_table_kernel<SeqLenT, kUsePDL>,
|
||||
static_cast<const float*>(score.data_ptr()),
|
||||
static_cast<const SeqLenT*>(seq_lens.data_ptr()),
|
||||
static_cast<const int32_t*>(req_to_token.data_ptr()),
|
||||
static_cast<const int64_t*>(slot_ids.data_ptr()),
|
||||
static_cast<int32_t*>(page_table.data_ptr()),
|
||||
static_cast<int32_t*>(seq_lens_out.data_ptr()),
|
||||
batch,
|
||||
num_heads,
|
||||
max_seqblock,
|
||||
static_cast<int>(block_size),
|
||||
static_cast<int>(topk),
|
||||
static_cast<int>(page_size),
|
||||
r2t_stride,
|
||||
max_kv_len,
|
||||
max_sparse_pages);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,280 @@
|
||||
#include <sgl_kernel/tensor.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/math.cuh>
|
||||
#include <sgl_kernel/type.cuh>
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/vec.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include <sgl_kernel/deepseek_v4/fp8_utils.cuh>
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace {
|
||||
|
||||
using deepseek_v4::fp8::cast_to_ue8m0;
|
||||
using deepseek_v4::fp8::pack_fp8;
|
||||
|
||||
// Per-token group quant to FP8-e4m3 with a fused UE8M0 scale. Each group of
|
||||
// kGroupSize columns gets one UE8M0 exponent byte written contiguously in
|
||||
// row-major order into ``x_sf`` (int32 [num_tokens, num_groups/4], 4 group
|
||||
// bytes per int32). This is the deep_gemm "transform_sf" pack done inline in
|
||||
// the quant, reusing the dsv4 ``cast_to_ue8m0``/``pack_fp8`` primitives -- it
|
||||
// is byte-identical to ``per_token_group_quant_fp8(scale_ue8m0=True)`` followed
|
||||
// by ``transform_sf_into_required_layout`` (both round via ceil(log2(absmax/
|
||||
// FP8_MAX))), but emits no separate transpose/pack kernel.
|
||||
struct PerTokenQuantUe8m0Params {
|
||||
const bf16_t* __restrict__ x; // [num_tokens, hidden]
|
||||
fp8_e4m3_t* __restrict__ x_q; // [num_tokens, hidden]
|
||||
int32_t* __restrict__ x_sf; // [num_tokens, num_groups/4]; written as bytes
|
||||
uint32_t num_tokens;
|
||||
uint32_t hidden;
|
||||
uint32_t num_groups; // hidden / kGroupSize
|
||||
};
|
||||
|
||||
template <uint32_t kGroupSize, bool kUsePDL>
|
||||
__global__ __launch_bounds__(1024, 2) void //
|
||||
per_token_quant_ue8m0_kernel(const PerTokenQuantUe8m0Params __grid_constant__ params) {
|
||||
using namespace device;
|
||||
constexpr uint32_t kVecElems = 8; // 8 bf16 = 16B load per thread
|
||||
static_assert(kGroupSize % kVecElems == 0, "group_size must be a multiple of 8");
|
||||
constexpr uint32_t kThreadsPerGroup = kGroupSize / kVecElems;
|
||||
using InputVec = AlignedVector<bf16x2_t, kVecElems / 2>;
|
||||
using OutputVec = AlignedVector<fp8x2_e4m3_t, kVecElems / 2>;
|
||||
|
||||
const uint32_t token_id = blockIdx.x;
|
||||
const uint32_t tid = threadIdx.x;
|
||||
PDLWaitPrimary<kUsePDL>();
|
||||
|
||||
const auto token_in = params.x + static_cast<uint64_t>(token_id) * params.hidden;
|
||||
const auto token_out = params.x_q + static_cast<uint64_t>(token_id) * params.hidden;
|
||||
|
||||
InputVec in_vec;
|
||||
in_vec.load(token_in, tid);
|
||||
float local_max = 0.0f;
|
||||
float vals[kVecElems];
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kVecElems / 2; ++i) {
|
||||
const auto [v0, v1] = cast<fp32x2_t>(in_vec[i]);
|
||||
vals[2 * i + 0] = v0;
|
||||
vals[2 * i + 1] = v1;
|
||||
local_max = fmaxf(local_max, fmaxf(fabsf(v0), fabsf(v1)));
|
||||
}
|
||||
// Absmax across the kThreadsPerGroup threads that cover one group.
|
||||
local_max = warp::reduce_max<kThreadsPerGroup>(local_max);
|
||||
const float absmax = fmaxf(local_max, 1e-10f);
|
||||
const float raw_scale = absmax / math::FP8_E4M3_MAX;
|
||||
const uint32_t ue8m0_exp = cast_to_ue8m0(raw_scale);
|
||||
const float inv_scale = __uint_as_float((127u + 127u - ue8m0_exp) << 23);
|
||||
|
||||
OutputVec out_vec;
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kVecElems / 2; ++i) {
|
||||
out_vec[i] = pack_fp8(vals[2 * i + 0] * inv_scale, vals[2 * i + 1] * inv_scale);
|
||||
}
|
||||
out_vec.store(token_out, tid);
|
||||
|
||||
const uint32_t group_id = tid / kThreadsPerGroup;
|
||||
const uint32_t within_group_id = tid % kThreadsPerGroup;
|
||||
if (within_group_id == 0 && group_id < params.num_groups) {
|
||||
const uint32_t byte_off = token_id * params.num_groups + group_id;
|
||||
reinterpret_cast<uint8_t*>(params.x_sf)[byte_off] = static_cast<uint8_t>(ue8m0_exp);
|
||||
}
|
||||
PDLTriggerSecondary<kUsePDL>();
|
||||
}
|
||||
|
||||
// Fused quant + scatter: like per_token_quant_ue8m0_kernel, but instead of
|
||||
// writing the fp8/scale for the single source token, it scatters them straight
|
||||
// into the permuted grouped-GEMM input -- replicating each token to its ``topk``
|
||||
// destination rows -- so the separate fill_gateup_input_triton_kernel launch (and
|
||||
// the intermediate x_q/x_sf buffers) are eliminated. The fp8 value + UE8M0 scale
|
||||
// are computed exactly once per token (identical to the non-fused kernel); only
|
||||
// the stores differ.
|
||||
struct PerTokenQuantUe8m0ScatterParams {
|
||||
const bf16_t* __restrict__ x; // [num_tokens, hidden]
|
||||
fp8_e4m3_t* __restrict__ gateup_input; // [E, m_max, hidden]
|
||||
int32_t* __restrict__ gateup_input_scale; // [E, num_groups/4, m_max] int32 (MN-major), written as bytes
|
||||
const int32_t* __restrict__ src2dst; // [num_tokens, topk] -> dst row = expert*m_max + slot
|
||||
const int32_t* __restrict__ topk_ids; // [num_tokens, topk]; <0 = skip
|
||||
uint32_t num_tokens;
|
||||
uint32_t hidden;
|
||||
uint32_t num_groups; // hidden / kGroupSize
|
||||
uint32_t topk;
|
||||
uint32_t m_max;
|
||||
};
|
||||
|
||||
template <uint32_t kGroupSize, uint32_t kTopK, bool kUsePDL>
|
||||
__global__ __launch_bounds__(1024, 2) void //
|
||||
per_token_quant_ue8m0_scatter_kernel(const PerTokenQuantUe8m0ScatterParams __grid_constant__ params) {
|
||||
using namespace device;
|
||||
constexpr uint32_t kVecElems = 8; // 8 bf16 = 16B load per thread
|
||||
static_assert(kGroupSize % kVecElems == 0, "group_size must be a multiple of 8");
|
||||
static_assert(kTopK <= kWarpThreads, "kTopK must fit in a warp for the lane-parallel scale write");
|
||||
constexpr uint32_t kThreadsPerGroup = kGroupSize / kVecElems;
|
||||
using InputVec = AlignedVector<bf16x2_t, kVecElems / 2>;
|
||||
using OutputVec = AlignedVector<fp8x2_e4m3_t, kVecElems / 2>;
|
||||
|
||||
const uint32_t token_id = blockIdx.x;
|
||||
const uint32_t tid = threadIdx.x;
|
||||
PDLWaitPrimary<kUsePDL>();
|
||||
|
||||
const auto token_in = params.x + static_cast<uint64_t>(token_id) * params.hidden;
|
||||
|
||||
InputVec in_vec;
|
||||
in_vec.load(token_in, tid);
|
||||
float local_max = 0.0f;
|
||||
float vals[kVecElems];
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kVecElems / 2; ++i) {
|
||||
const auto [v0, v1] = cast<fp32x2_t>(in_vec[i]);
|
||||
vals[2 * i + 0] = v0;
|
||||
vals[2 * i + 1] = v1;
|
||||
local_max = fmaxf(local_max, fmaxf(fabsf(v0), fabsf(v1)));
|
||||
}
|
||||
// Butterfly reduce: every thread of the group ends up with the group absmax
|
||||
// (so all of them can write scale bytes below, no broadcast needed).
|
||||
local_max = warp::reduce_max<kThreadsPerGroup>(local_max);
|
||||
const float absmax = fmaxf(local_max, 1e-10f);
|
||||
const float raw_scale = absmax / math::FP8_E4M3_MAX;
|
||||
const uint32_t ue8m0_exp = cast_to_ue8m0(raw_scale);
|
||||
const float inv_scale = __uint_as_float((127u + 127u - ue8m0_exp) << 23);
|
||||
|
||||
OutputVec out_vec;
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kVecElems / 2; ++i) {
|
||||
out_vec[i] = pack_fp8(vals[2 * i + 0] * inv_scale, vals[2 * i + 1] * inv_scale);
|
||||
}
|
||||
|
||||
// Read this token's kTopK destinations once (fully unrolled).
|
||||
const auto* src2dst_row = params.src2dst + static_cast<uint64_t>(token_id) * kTopK;
|
||||
const auto* topk_ids_row = params.topk_ids + static_cast<uint64_t>(token_id) * kTopK;
|
||||
int32_t dst_rows[kTopK];
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kTopK; ++i) {
|
||||
dst_rows[i] = (topk_ids_row[i] >= 0) ? src2dst_row[i] : -1;
|
||||
}
|
||||
|
||||
const uint32_t group_id = tid / kThreadsPerGroup;
|
||||
const uint32_t within_group = tid % kThreadsPerGroup;
|
||||
const uint32_t c = group_id / 4u; // packed int32 index along the group axis
|
||||
const uint32_t b = group_id % 4u; // byte within that int32
|
||||
const uint64_t scale_g4 = params.num_groups / 4u;
|
||||
auto* scale_bytes = reinterpret_cast<uint8_t*>(params.gateup_input_scale);
|
||||
|
||||
// 1) Output fp8: every thread replicates its 16B chunk to all kTopK dst rows.
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kTopK; ++i) {
|
||||
const int32_t dst = dst_rows[i];
|
||||
if (dst < 0) continue;
|
||||
out_vec.store(params.gateup_input + static_cast<uint64_t>(dst) * params.hidden, tid);
|
||||
}
|
||||
|
||||
// 2) Scale bytes: distribute the kTopK experts across the group's threads
|
||||
// (each already holds the group exponent) so they write in parallel instead
|
||||
// of one leader looping. lane `within_group` handles experts {within_group,
|
||||
// within_group + kThreadsPerGroup, ...}; for kTopK <= kThreadsPerGroup that is
|
||||
// exactly one expert per lane (no loop).
|
||||
#pragma unroll
|
||||
for (uint32_t i = within_group; i < kTopK; i += kThreadsPerGroup) {
|
||||
const int32_t dst = dst_rows[i];
|
||||
if (dst < 0) continue;
|
||||
const uint32_t expert = static_cast<uint32_t>(dst) / params.m_max;
|
||||
const uint32_t m = static_cast<uint32_t>(dst) % params.m_max;
|
||||
// int32 element [expert, c, m] of [E, G/4, m_max], byte b within it.
|
||||
const uint64_t int32_index =
|
||||
static_cast<uint64_t>(expert) * scale_g4 * params.m_max + static_cast<uint64_t>(c) * params.m_max + m;
|
||||
scale_bytes[int32_index * 4u + b] = static_cast<uint8_t>(ue8m0_exp);
|
||||
}
|
||||
|
||||
PDLTriggerSecondary<kUsePDL>();
|
||||
}
|
||||
|
||||
template <int64_t kGroupSize, int64_t kTopK, bool kUsePDL>
|
||||
void per_token_quant_ue8m0_scatter(
|
||||
tvm::ffi::TensorView x,
|
||||
tvm::ffi::TensorView gateup_input,
|
||||
tvm::ffi::TensorView gateup_input_scale,
|
||||
tvm::ffi::TensorView src2dst,
|
||||
tvm::ffi::TensorView topk_ids,
|
||||
int64_t topk,
|
||||
int64_t m_max) {
|
||||
using namespace host;
|
||||
auto device = SymbolicDevice{};
|
||||
auto M = SymbolicSize{"num_tokens"};
|
||||
auto H = SymbolicSize{"hidden"};
|
||||
auto E = SymbolicSize{"num_experts"};
|
||||
auto MM = SymbolicSize{"m_max"};
|
||||
auto G4 = SymbolicSize{"num_groups_div_4"};
|
||||
device.set_options<kDLCUDA>();
|
||||
TensorMatcher({M, H}).with_dtype<bf16_t>().with_device(device).verify(x);
|
||||
TensorMatcher({E, MM, H}).with_dtype<fp8_e4m3_t>().with_device(device).verify(gateup_input);
|
||||
TensorMatcher({E, G4, MM}).with_dtype<int32_t>().with_device(device).verify(gateup_input_scale);
|
||||
|
||||
const uint32_t num_tokens = static_cast<uint32_t>(M.unwrap());
|
||||
const uint32_t hidden = static_cast<uint32_t>(H.unwrap());
|
||||
RuntimeCheck(hidden % kGroupSize == 0, "hidden ", hidden, " not divisible by group_size ", kGroupSize);
|
||||
const uint32_t num_groups = hidden / static_cast<uint32_t>(kGroupSize);
|
||||
RuntimeCheck(num_groups % 4 == 0, "num_groups must be a multiple of 4 for int32 packing");
|
||||
RuntimeCheck(static_cast<uint32_t>(G4.unwrap()) * 4 == num_groups, "scale G/4 mismatch");
|
||||
RuntimeCheck(static_cast<uint32_t>(MM.unwrap()) == static_cast<uint32_t>(m_max), "m_max mismatch");
|
||||
const uint32_t threads = hidden / 8; // kVecElems
|
||||
RuntimeCheck(threads <= 1024, "hidden/8 must be <= 1024, got ", threads);
|
||||
RuntimeCheck(topk == kTopK, "topk does not match compiled template");
|
||||
|
||||
const auto params = PerTokenQuantUe8m0ScatterParams{
|
||||
.x = static_cast<const bf16_t*>(x.data_ptr()),
|
||||
.gateup_input = static_cast<fp8_e4m3_t*>(gateup_input.data_ptr()),
|
||||
.gateup_input_scale = static_cast<int32_t*>(gateup_input_scale.data_ptr()),
|
||||
.src2dst = static_cast<const int32_t*>(src2dst.data_ptr()),
|
||||
.topk_ids = static_cast<const int32_t*>(topk_ids.data_ptr()),
|
||||
.num_tokens = num_tokens,
|
||||
.hidden = hidden,
|
||||
.num_groups = num_groups,
|
||||
.topk = static_cast<uint32_t>(kTopK),
|
||||
.m_max = static_cast<uint32_t>(m_max),
|
||||
};
|
||||
if (num_tokens == 0) return;
|
||||
constexpr auto kernel = per_token_quant_ue8m0_scatter_kernel<kGroupSize, kTopK, kUsePDL>;
|
||||
LaunchKernel(num_tokens, threads, device.unwrap()) //
|
||||
.enable_pdl(kUsePDL)(kernel, params);
|
||||
}
|
||||
|
||||
template <int64_t kGroupSize, bool kUsePDL>
|
||||
void per_token_quant_ue8m0(tvm::ffi::TensorView x, tvm::ffi::TensorView x_q, tvm::ffi::TensorView x_sf) {
|
||||
using namespace host;
|
||||
auto device = SymbolicDevice{};
|
||||
auto M = SymbolicSize{"num_tokens"};
|
||||
auto H = SymbolicSize{"hidden"};
|
||||
auto G4 = SymbolicSize{"num_groups_div_4"};
|
||||
device.set_options<kDLCUDA>();
|
||||
TensorMatcher({M, H}).with_dtype<bf16_t>().with_device(device).verify(x);
|
||||
TensorMatcher({M, H}).with_dtype<fp8_e4m3_t>().with_device(device).verify(x_q);
|
||||
TensorMatcher({M, G4}).with_dtype<int32_t>().with_device(device).verify(x_sf);
|
||||
|
||||
const uint32_t num_tokens = static_cast<uint32_t>(M.unwrap());
|
||||
const uint32_t hidden = static_cast<uint32_t>(H.unwrap());
|
||||
RuntimeCheck(hidden % kGroupSize == 0, "hidden ", hidden, " not divisible by group_size ", kGroupSize);
|
||||
const uint32_t num_groups = hidden / static_cast<uint32_t>(kGroupSize);
|
||||
RuntimeCheck(static_cast<uint32_t>(G4.unwrap()) * 4 == num_groups);
|
||||
const uint32_t threads = hidden / 8; // kVecElems
|
||||
RuntimeCheck(threads <= 1024, "hidden/8 must be <= 1024, got ", threads);
|
||||
|
||||
const auto params = PerTokenQuantUe8m0Params{
|
||||
.x = static_cast<const bf16_t*>(x.data_ptr()),
|
||||
.x_q = static_cast<fp8_e4m3_t*>(x_q.data_ptr()),
|
||||
.x_sf = static_cast<int32_t*>(x_sf.data_ptr()),
|
||||
.num_tokens = num_tokens,
|
||||
.hidden = hidden,
|
||||
.num_groups = num_groups,
|
||||
};
|
||||
if (num_tokens == 0) return;
|
||||
constexpr auto kernel = per_token_quant_ue8m0_kernel<kGroupSize, kUsePDL>;
|
||||
LaunchKernel(num_tokens, threads, device.unwrap()) //
|
||||
.enable_pdl(kUsePDL)(kernel, params);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,544 @@
|
||||
|
||||
// Adapt from https://github.com/vllm-project/vllm/blob/v0.7.3/csrc/moe/topk_softmax_kernels.cu
|
||||
// which is originally adapted from
|
||||
// https://github.com/NVIDIA/TensorRT-LLM/blob/v0.7.1/cpp/tensorrt_llm/kernels/mixtureOfExperts/moe_kernels.cu
|
||||
#pragma once
|
||||
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
|
||||
#include <cub/cub.cuh>
|
||||
#include <cub/util_type.cuh>
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
#include <tvm/ffi/optional.h>
|
||||
|
||||
// CUDA 12.9+ deprecated cub::Max/Min in favour of cuda::maximum/minimum
|
||||
#if CUDA_VERSION >= 12090
|
||||
#include <cuda/functional>
|
||||
using MaxReduceOp = cuda::maximum<>;
|
||||
using MinReduceOp = cuda::minimum<>;
|
||||
#else
|
||||
using MaxReduceOp = cub::Max;
|
||||
using MinReduceOp = cub::Min;
|
||||
#endif
|
||||
|
||||
#include <cfloat>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
using tvm::ffi::TensorView;
|
||||
|
||||
#ifndef MOE_TOPK_SIGMOID_WARP_SIZE
|
||||
#define MOE_TOPK_SIGMOID_WARP_SIZE 32
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
static constexpr int WARP_SIZE = MOE_TOPK_SIGMOID_WARP_SIZE;
|
||||
|
||||
#define MAX(a, b) ((a) > (b) ? (a) : (b))
|
||||
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Aligned array — avoids CUTLASS dependency; identical semantics.
|
||||
// ---------------------------------------------------------------------------
|
||||
template <typename T, int N, int Alignment = static_cast<int>(sizeof(T) * N)>
|
||||
class alignas(Alignment) AlignedArray {
|
||||
T data[N];
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type conversion helper
|
||||
// ---------------------------------------------------------------------------
|
||||
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 {
|
||||
return static_cast<float>(x);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// moeSigmoid — fallback sigmoid kernel (used for non-power-of-2 experts)
|
||||
// ---------------------------------------------------------------------------
|
||||
template <typename T, int TPB>
|
||||
__launch_bounds__(TPB) __global__
|
||||
void moeSigmoid(const T* input, const bool* finished, float* output, const int num_cols) {
|
||||
const int thread_row_offset = blockIdx.x * num_cols;
|
||||
|
||||
if ((finished != nullptr) && finished[blockIdx.x]) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int ii = threadIdx.x; ii < num_cols; ii += TPB) {
|
||||
const int idx = thread_row_offset + ii;
|
||||
float val = convert_to_float<T>(input[idx]);
|
||||
val = 1.0f / (1.0f + expf(-val));
|
||||
output[idx] = val;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// moeTopK — fallback top-k kernel (used for non-power-of-2 experts)
|
||||
// ---------------------------------------------------------------------------
|
||||
template <int TPB>
|
||||
__launch_bounds__(TPB) __global__ void moeTopK(
|
||||
const float* inputs_after_sigmoid,
|
||||
const bool* finished,
|
||||
float* output,
|
||||
int* indices,
|
||||
const int num_experts,
|
||||
const int k,
|
||||
const int start_expert,
|
||||
const int end_expert,
|
||||
const bool renormalize,
|
||||
const float* correction_bias,
|
||||
double routed_scaling_factor,
|
||||
int num_fused_shared_experts) {
|
||||
using cub_kvp = cub::KeyValuePair<int, float>;
|
||||
using BlockReduce = cub::BlockReduce<cub_kvp, TPB>;
|
||||
__shared__ typename BlockReduce::TempStorage tmpStorage;
|
||||
|
||||
cub_kvp thread_kvp;
|
||||
cub::ArgMax arg_max;
|
||||
|
||||
const int block_row = blockIdx.x;
|
||||
const int topk = k + num_fused_shared_experts;
|
||||
|
||||
const bool row_is_active = finished ? !finished[block_row] : true;
|
||||
const int thread_read_offset = blockIdx.x * num_experts;
|
||||
float row_sum_for_renormalize = 0;
|
||||
|
||||
for (int k_idx = 0; k_idx < k; ++k_idx) {
|
||||
thread_kvp.key = 0;
|
||||
thread_kvp.value = -1.f;
|
||||
|
||||
cub_kvp inp_kvp;
|
||||
for (int expert = threadIdx.x; expert < num_experts; expert += TPB) {
|
||||
const int idx = thread_read_offset + expert;
|
||||
inp_kvp.key = expert;
|
||||
inp_kvp.value = inputs_after_sigmoid[idx];
|
||||
if (correction_bias != nullptr) {
|
||||
inp_kvp.value += correction_bias[expert];
|
||||
}
|
||||
|
||||
for (int prior_k = 0; prior_k < k_idx; ++prior_k) {
|
||||
const int prior_winning_expert = indices[topk * block_row + prior_k];
|
||||
if (prior_winning_expert == expert) {
|
||||
inp_kvp = thread_kvp;
|
||||
}
|
||||
}
|
||||
|
||||
thread_kvp = arg_max(inp_kvp, thread_kvp);
|
||||
}
|
||||
|
||||
const cub_kvp result_kvp = BlockReduce(tmpStorage).Reduce(thread_kvp, arg_max);
|
||||
if (threadIdx.x == 0) {
|
||||
const int expert = result_kvp.key;
|
||||
const bool node_uses_expert = expert >= start_expert && expert < end_expert;
|
||||
const bool should_process_row = row_is_active && node_uses_expert;
|
||||
|
||||
const int idx = topk * block_row + k_idx;
|
||||
float val;
|
||||
if (correction_bias != nullptr) {
|
||||
val = inputs_after_sigmoid[thread_read_offset + expert];
|
||||
} else {
|
||||
val = result_kvp.value;
|
||||
}
|
||||
output[idx] = val;
|
||||
indices[idx] = should_process_row ? (expert - start_expert) : num_experts;
|
||||
assert(indices[idx] >= 0);
|
||||
row_sum_for_renormalize += val;
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (num_fused_shared_experts > 0 && threadIdx.x == 0) {
|
||||
const int last_idx = topk * block_row + k;
|
||||
if (renormalize) {
|
||||
output[last_idx] = 1.0f;
|
||||
} else {
|
||||
output[last_idx] = row_sum_for_renormalize / routed_scaling_factor;
|
||||
}
|
||||
indices[last_idx] = num_experts;
|
||||
}
|
||||
if (renormalize && threadIdx.x == 0) {
|
||||
float row_sum_for_renormalize_inv = routed_scaling_factor / (row_sum_for_renormalize + 1e-20f);
|
||||
for (int k_idx = 0; k_idx < k; ++k_idx) {
|
||||
const int idx = topk * block_row + k_idx;
|
||||
output[idx] = output[idx] * row_sum_for_renormalize_inv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// topkGatingSigmoid — optimised kernel for power-of-2 expert counts
|
||||
// ---------------------------------------------------------------------------
|
||||
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 topkGatingSigmoid(
|
||||
const T* input,
|
||||
const bool* finished,
|
||||
float* output,
|
||||
const int num_rows,
|
||||
int* indices,
|
||||
const int k,
|
||||
const int start_expert,
|
||||
const int end_expert,
|
||||
const bool renormalize,
|
||||
const float* correction_bias,
|
||||
double routed_scaling_factor,
|
||||
int num_fused_shared_experts) {
|
||||
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, "");
|
||||
static_assert(WARP_SIZE % THREADS_PER_ROW == 0, "");
|
||||
static_assert(THREADS_PER_ROW == (THREADS_PER_ROW & -THREADS_PER_ROW), "");
|
||||
static_assert(THREADS_PER_ROW <= 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, "");
|
||||
|
||||
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;
|
||||
const int topk = k + num_fused_shared_experts;
|
||||
|
||||
if (thread_row >= num_rows) {
|
||||
return;
|
||||
}
|
||||
const bool row_is_active = finished ? !finished[thread_row] : true;
|
||||
|
||||
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) {
|
||||
float val = convert_to_float<T>(row_chunk_temp[ii]);
|
||||
val = 1.0f / (1.0f + expf(-val));
|
||||
if (correction_bias != nullptr) {
|
||||
const int group_id = ii / ELTS_PER_LDG;
|
||||
const int local_id = ii % ELTS_PER_LDG;
|
||||
const int expert_idx = first_elt_read_by_thread + group_id * THREADS_PER_ROW * ELTS_PER_LDG + local_id;
|
||||
val = val + correction_bias[expert_idx];
|
||||
}
|
||||
row_chunk[ii] = val;
|
||||
}
|
||||
|
||||
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(0xffffffff, max_val, mask, THREADS_PER_ROW);
|
||||
int other_expert = __shfl_xor_sync(0xffffffff, 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 bool node_uses_expert = expert >= start_expert && expert < end_expert;
|
||||
const bool should_process_row = row_is_active && node_uses_expert;
|
||||
|
||||
const int idx = topk * thread_row + k_idx;
|
||||
float out_val;
|
||||
if (correction_bias != nullptr) {
|
||||
out_val = convert_to_float<T>(thread_row_ptr[expert]);
|
||||
out_val = 1.0f / (1.0f + expf(-out_val));
|
||||
} else {
|
||||
out_val = max_val;
|
||||
}
|
||||
output[idx] = out_val;
|
||||
indices[idx] = should_process_row ? (expert - start_expert) : NUM_EXPERTS;
|
||||
row_sum_for_renormalize += out_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 (num_fused_shared_experts > 0 && thread_group_idx == 0) {
|
||||
const int last_idx = topk * thread_row + k;
|
||||
if (renormalize) {
|
||||
output[last_idx] = 1.0f;
|
||||
} else {
|
||||
output[last_idx] = row_sum_for_renormalize / routed_scaling_factor;
|
||||
}
|
||||
indices[last_idx] = NUM_EXPERTS;
|
||||
}
|
||||
if (renormalize && thread_group_idx == 0) {
|
||||
float row_sum_for_renormalize_inv = routed_scaling_factor / (row_sum_for_renormalize + 1e-20f);
|
||||
#pragma unroll
|
||||
for (int k_idx = 0; k_idx < k; ++k_idx) {
|
||||
const int idx = topk * thread_row + k_idx;
|
||||
output[idx] = output[idx] * row_sum_for_renormalize_inv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compile-time constants helper
|
||||
// ---------------------------------------------------------------------------
|
||||
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 = 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
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Per-expert-count launcher helper
|
||||
// ---------------------------------------------------------------------------
|
||||
template <typename T, int EXPERTS, int WARPS_PER_TB>
|
||||
void topkGatingSigmoidLauncherHelper(
|
||||
const T* input,
|
||||
const bool* finished,
|
||||
float* output,
|
||||
int* indices,
|
||||
const int num_rows,
|
||||
const int k,
|
||||
const int start_expert,
|
||||
const int end_expert,
|
||||
const bool renormalize,
|
||||
const float* correction_bias,
|
||||
double routed_scaling_factor,
|
||||
int num_fused_shared_experts,
|
||||
cudaStream_t stream) {
|
||||
static constexpr std::size_t MAX_BYTES_PER_LDG = 16;
|
||||
static constexpr int BYTES_PER_LDG = 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);
|
||||
topkGatingSigmoid<T, VPT, EXPERTS, WARPS_PER_TB, BYTES_PER_LDG><<<num_blocks, block_dim, 0, stream>>>(
|
||||
input,
|
||||
finished,
|
||||
output,
|
||||
num_rows,
|
||||
indices,
|
||||
k,
|
||||
start_expert,
|
||||
end_expert,
|
||||
renormalize,
|
||||
correction_bias,
|
||||
routed_scaling_factor,
|
||||
num_fused_shared_experts);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dispatch macro — used inside topkGatingSigmoidKernelLauncher
|
||||
// ---------------------------------------------------------------------------
|
||||
#define LAUNCH_SIGMOID(TYPE, NUM_EXPERTS, WARPS_PER_TB) \
|
||||
topkGatingSigmoidLauncherHelper<TYPE, NUM_EXPERTS, WARPS_PER_TB>( \
|
||||
gating_output, \
|
||||
nullptr, \
|
||||
topk_weights, \
|
||||
topk_indices, \
|
||||
num_tokens, \
|
||||
topk - num_fused_shared_experts, \
|
||||
0, \
|
||||
num_experts, \
|
||||
renormalize, \
|
||||
correction_bias, \
|
||||
routed_scaling_factor, \
|
||||
num_fused_shared_experts, \
|
||||
stream)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main launcher: dispatches on num_experts
|
||||
// ---------------------------------------------------------------------------
|
||||
template <typename T>
|
||||
void topkGatingSigmoidKernelLauncher(
|
||||
const T* gating_output,
|
||||
float* topk_weights,
|
||||
int* topk_indices,
|
||||
float* sigmoid_workspace,
|
||||
const int num_tokens,
|
||||
const int num_experts,
|
||||
const int topk,
|
||||
const bool renormalize,
|
||||
const float* correction_bias,
|
||||
double routed_scaling_factor,
|
||||
int num_fused_shared_experts,
|
||||
cudaStream_t stream) {
|
||||
static constexpr int WARPS_PER_TB = 4;
|
||||
switch (num_experts) {
|
||||
case 1:
|
||||
LAUNCH_SIGMOID(T, 1, WARPS_PER_TB);
|
||||
break;
|
||||
case 2:
|
||||
LAUNCH_SIGMOID(T, 2, WARPS_PER_TB);
|
||||
break;
|
||||
case 4:
|
||||
LAUNCH_SIGMOID(T, 4, WARPS_PER_TB);
|
||||
break;
|
||||
case 8:
|
||||
LAUNCH_SIGMOID(T, 8, WARPS_PER_TB);
|
||||
break;
|
||||
case 16:
|
||||
LAUNCH_SIGMOID(T, 16, WARPS_PER_TB);
|
||||
break;
|
||||
case 32:
|
||||
LAUNCH_SIGMOID(T, 32, WARPS_PER_TB);
|
||||
break;
|
||||
case 64:
|
||||
LAUNCH_SIGMOID(T, 64, WARPS_PER_TB);
|
||||
break;
|
||||
case 128:
|
||||
LAUNCH_SIGMOID(T, 128, WARPS_PER_TB);
|
||||
break;
|
||||
case 256:
|
||||
LAUNCH_SIGMOID(T, 256, WARPS_PER_TB);
|
||||
break;
|
||||
default: {
|
||||
// Fallback: non-power-of-2 or >256 experts
|
||||
using namespace host;
|
||||
RuntimeCheck(
|
||||
sigmoid_workspace != nullptr, "sigmoid_workspace must be provided for num_experts that are not a power of 2");
|
||||
static constexpr int TPB = 256;
|
||||
moeSigmoid<T, TPB><<<num_tokens, TPB, 0, stream>>>(gating_output, nullptr, sigmoid_workspace, num_experts);
|
||||
moeTopK<TPB><<<num_tokens, TPB, 0, stream>>>(
|
||||
sigmoid_workspace,
|
||||
nullptr,
|
||||
topk_weights,
|
||||
topk_indices,
|
||||
num_experts,
|
||||
topk - num_fused_shared_experts,
|
||||
0,
|
||||
num_experts,
|
||||
renormalize,
|
||||
correction_bias,
|
||||
routed_scaling_factor,
|
||||
num_fused_shared_experts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#undef LAUNCH_SIGMOID
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Host launcher (tvm-ffi interface)
|
||||
// ---------------------------------------------------------------------------
|
||||
template <typename T>
|
||||
void topk_sigmoid(
|
||||
TensorView gating_output,
|
||||
TensorView topk_weights,
|
||||
TensorView topk_ids,
|
||||
TensorView workspace,
|
||||
bool renormalize,
|
||||
tvm::ffi::Optional<TensorView> correction_bias,
|
||||
double routed_scaling_factor,
|
||||
int num_fused_shared_experts) {
|
||||
using namespace host;
|
||||
|
||||
// --- Input validation ---
|
||||
RuntimeCheck(gating_output.dim() == 2, "gating_output must be 2-D");
|
||||
RuntimeCheck(topk_weights.dim() == 2, "topk_weights must be 2-D");
|
||||
RuntimeCheck(topk_ids.dim() == 2, "topk_ids must be 2-D");
|
||||
|
||||
const int64_t num_tokens = gating_output.shape()[0];
|
||||
const int64_t num_experts = gating_output.shape()[1];
|
||||
const int64_t topk = topk_weights.shape()[1];
|
||||
|
||||
RuntimeCheck(
|
||||
topk_weights.shape()[0] == num_tokens && topk_ids.shape()[0] == num_tokens,
|
||||
"topk_weights and topk_ids must have num_tokens rows");
|
||||
RuntimeCheck(topk_ids.shape()[1] == topk, "topk_ids second dim must match topk_weights");
|
||||
RuntimeCheck(topk <= num_experts, "topk must be <= num_experts");
|
||||
RuntimeCheck(num_fused_shared_experts <= 1, "num_fused_shared_experts must be <= 1");
|
||||
|
||||
// correction_bias validation
|
||||
if (correction_bias.has_value()) {
|
||||
const auto& bias = correction_bias.value();
|
||||
RuntimeCheck(bias.dim() == 1, "correction_bias must be 1-D");
|
||||
RuntimeCheck(bias.shape()[0] == num_experts, "correction_bias size must equal num_experts");
|
||||
RuntimeCheck(
|
||||
bias.dtype().code == DLDataTypeCode::kDLFloat && bias.dtype().bits == 32, "correction_bias must be float32");
|
||||
}
|
||||
|
||||
const T* gating_ptr = static_cast<const T*>(gating_output.data_ptr());
|
||||
float* weights_ptr = static_cast<float*>(topk_weights.data_ptr());
|
||||
int* indices_ptr = static_cast<int*>(topk_ids.data_ptr());
|
||||
float* workspace_ptr = static_cast<float*>(workspace.data_ptr());
|
||||
const float* bias_ptr =
|
||||
correction_bias.has_value() ? static_cast<const float*>(correction_bias.value().data_ptr()) : nullptr;
|
||||
|
||||
cudaStream_t stream = LaunchKernel::resolve_device(gating_output.device());
|
||||
|
||||
topkGatingSigmoidKernelLauncher<T>(
|
||||
gating_ptr,
|
||||
weights_ptr,
|
||||
indices_ptr,
|
||||
workspace_ptr,
|
||||
static_cast<int>(num_tokens),
|
||||
static_cast<int>(num_experts),
|
||||
static_cast<int>(topk),
|
||||
renormalize,
|
||||
bias_ptr,
|
||||
routed_scaling_factor,
|
||||
num_fused_shared_experts,
|
||||
stream);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
"""Block top-k over per-row block scores for the MiniMax-M3 sparse decode indexer.
|
||||
|
||||
Drop-in replacement for the 2-stage split-K Triton topk
|
||||
(``_topk_index_partial_kernel`` + ``_topk_index_merge_kernel``): given the
|
||||
decode score tensor ``[num_heads, batch, max_seqblock]`` it produces
|
||||
``topk_idx`` ``[num_heads, batch, topk]`` (0-indexed block ids, front-packed,
|
||||
``-1`` padded), matching the consumer ``_gqa_share_sparse_decode_kernel``.
|
||||
|
||||
``minimax_decode_topk_page_table`` additionally fuses the page-table transform
|
||||
for the dense paged backend (trtllm_mha / fa3) and returns the page table plus
|
||||
the per-query effective KV length.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.utils import cache_once, load_jit, make_cpp_args
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tvm_ffi.module import Module
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_module(seq_dtype: torch.dtype) -> Module:
|
||||
args = make_cpp_args(seq_dtype, True) # SeqLenT, kUsePDL
|
||||
return load_jit(
|
||||
"minimax_decode_topk",
|
||||
*args,
|
||||
cuda_files=["minimax/minimax_decode_topk.cuh"],
|
||||
cuda_wrappers=[
|
||||
("minimax_decode_topk", f"minimax_decode_topk<{args}>"),
|
||||
(
|
||||
"minimax_decode_topk_page_table",
|
||||
f"minimax_decode_topk_page_table<{args}>",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def minimax_decode_topk(
|
||||
score: torch.Tensor, # [num_heads, batch, max_seqblock] fp32
|
||||
seq_lens: torch.Tensor, # [batch] int32/int64
|
||||
block_size: int,
|
||||
topk: int,
|
||||
out: torch.Tensor | None = None, # [num_heads, batch, topk] int32
|
||||
) -> torch.Tensor:
|
||||
assert score.is_cuda and score.dtype == torch.float32 and score.dim() == 3
|
||||
assert seq_lens.is_cuda and seq_lens.dim() == 1
|
||||
assert seq_lens.dtype in (torch.int32, torch.int64)
|
||||
num_heads, batch, max_seqblock = score.shape
|
||||
assert seq_lens.shape[0] == batch
|
||||
|
||||
if not score.is_contiguous():
|
||||
score = score.contiguous()
|
||||
if not seq_lens.is_contiguous():
|
||||
seq_lens = seq_lens.contiguous()
|
||||
|
||||
if out is None:
|
||||
out = torch.empty(
|
||||
(num_heads, batch, topk), dtype=torch.int32, device=score.device
|
||||
)
|
||||
else:
|
||||
assert out.shape == (num_heads, batch, topk)
|
||||
assert out.dtype == torch.int32 and out.is_cuda
|
||||
assert out.is_contiguous()
|
||||
|
||||
module = _jit_module(seq_lens.dtype)
|
||||
module.minimax_decode_topk(score, seq_lens, out, int(block_size), int(topk))
|
||||
return out
|
||||
|
||||
|
||||
def minimax_decode_topk_page_table(
|
||||
score: torch.Tensor, # [num_kv_heads, batch, max_seqblock] fp32
|
||||
seq_lens: torch.Tensor, # [batch] int32/int64
|
||||
req_to_token: torch.Tensor, # [max_reqs, max_kv_len] int32
|
||||
slot_ids: torch.Tensor, # [batch] int64 (req_pool_indices)
|
||||
block_size: int,
|
||||
topk: int,
|
||||
page_size: int,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Fused top-k + page-table transform: select the top-k blocks and emit the
|
||||
per-(batch, kv-head) paged page table consumed by the dense backend
|
||||
(trtllm_mha / fa3), instead of block ids, plus the per-pseudo-request effective
|
||||
KV length (cache_seqlens, from the actual selection). Both are allocated here
|
||||
and returned.
|
||||
|
||||
For DP attention (num_kv_heads > 1) each kv head selects its own blocks, so
|
||||
(batch, head) pseudo-requests are flattened batch-major into the outputs
|
||||
``[batch*num_kv_heads, topk*block_size/page_size]`` / ``[batch*num_kv_heads]``
|
||||
(matching ``q.view(bs, nkv, gqa, d).reshape(bs*nkv, gqa, d)``). The page index
|
||||
is head-encoded (head-minor) as ``base_page*num_kv_heads + head`` -- the index
|
||||
into an HND cache ``[num_pages, nkv, ps, D]`` reshaped to
|
||||
``[num_pages*nkv, 1, ps, D]``; num_kv_heads==1 reproduces the single-kv-head
|
||||
TP>=4 behavior (page index == base_page)."""
|
||||
assert score.is_cuda and score.dtype == torch.float32 and score.dim() == 3
|
||||
num_heads, batch, max_seqblock = score.shape
|
||||
assert block_size % page_size == 0
|
||||
assert req_to_token.dtype == torch.int32 and slot_ids.dtype == torch.int64
|
||||
if not score.is_contiguous():
|
||||
score = score.contiguous()
|
||||
if not seq_lens.is_contiguous():
|
||||
seq_lens = seq_lens.contiguous()
|
||||
if not slot_ids.is_contiguous():
|
||||
slot_ids = slot_ids.contiguous()
|
||||
|
||||
max_sparse_pages = topk * (block_size // page_size)
|
||||
page_table = torch.empty(
|
||||
(batch * num_heads, max_sparse_pages), dtype=torch.int32, device=score.device
|
||||
)
|
||||
real_seq_lens = torch.empty(
|
||||
(batch * num_heads,), dtype=torch.int32, device=score.device
|
||||
)
|
||||
|
||||
module = _jit_module(seq_lens.dtype)
|
||||
module.minimax_decode_topk_page_table(
|
||||
score,
|
||||
seq_lens,
|
||||
req_to_token,
|
||||
slot_ids,
|
||||
page_table,
|
||||
real_seq_lens,
|
||||
int(block_size),
|
||||
int(topk),
|
||||
int(page_size),
|
||||
)
|
||||
return page_table, real_seq_lens
|
||||
@@ -0,0 +1,25 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Fused Triton kernels for MiniMax-M3 on AMD ROCm (gfx94x / gfx95x).
|
||||
|
||||
Model-scoped JIT kernels (mirrors ``jit_kernel/dsv4``), split by op type:
|
||||
* ``rmsnorm`` -- fused fp32 Gemma RMSNorm (plain + fused-add-residual)
|
||||
* ``swiglu`` -- fused fp32 SwiGLU-OAI (split layout)
|
||||
"""
|
||||
|
||||
from sglang.jit_kernel.minimax_m3.rmsnorm import (
|
||||
_num_warps,
|
||||
gemma_fused_add_rmsnorm,
|
||||
gemma_rmsnorm,
|
||||
)
|
||||
from sglang.jit_kernel.minimax_m3.swiglu import (
|
||||
swiglu_oai_mxfp8_quant,
|
||||
swiglu_oai_split,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"gemma_rmsnorm",
|
||||
"gemma_fused_add_rmsnorm",
|
||||
"swiglu_oai_split",
|
||||
"swiglu_oai_mxfp8_quant",
|
||||
"_num_warps",
|
||||
]
|
||||
@@ -0,0 +1,659 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Fused MiniMax-M3 per-head Gemma Q/K RMSNorm + partial RoPE for ROCm."""
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _qk_gemma_rmsnorm_rope_kernel(
|
||||
q_ptr,
|
||||
k_ptr,
|
||||
q_out_ptr,
|
||||
k_out_ptr,
|
||||
q_weight_ptr,
|
||||
k_weight_ptr,
|
||||
positions_ptr,
|
||||
cos_sin_cache_ptr,
|
||||
q_stride_m,
|
||||
q_stride_d,
|
||||
k_stride_m,
|
||||
k_stride_d,
|
||||
q_heads: tl.constexpr,
|
||||
k_heads: tl.constexpr,
|
||||
head_dim: tl.constexpr,
|
||||
rotary_dim: tl.constexpr,
|
||||
eps: tl.constexpr,
|
||||
is_neox_style: tl.constexpr,
|
||||
BLOCK_HD: tl.constexpr,
|
||||
):
|
||||
token_id = tl.program_id(0)
|
||||
head_program = tl.program_id(1)
|
||||
cols = tl.arange(0, BLOCK_HD)
|
||||
mask = cols < head_dim
|
||||
half_rotary: tl.constexpr = rotary_dim // 2
|
||||
|
||||
is_q = head_program < q_heads
|
||||
head_id = tl.where(is_q, head_program, head_program - q_heads)
|
||||
in_ptr = tl.where(is_q, q_ptr, k_ptr)
|
||||
out_ptr = tl.where(is_q, q_out_ptr, k_out_ptr)
|
||||
weight_ptr = tl.where(is_q, q_weight_ptr, k_weight_ptr)
|
||||
stride_m = tl.where(is_q, q_stride_m, k_stride_m)
|
||||
stride_d = tl.where(is_q, q_stride_d, k_stride_d)
|
||||
n_heads = tl.where(is_q, q_heads, k_heads)
|
||||
|
||||
base_in = in_ptr + token_id * stride_m + head_id * head_dim * stride_d
|
||||
x = tl.load(base_in + cols * stride_d, mask=mask, other=0.0).to(tl.float32)
|
||||
w = tl.load(weight_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
var = tl.sum(x * x, axis=0) / head_dim
|
||||
rstd = tl.rsqrt(var + eps)
|
||||
normed = x * rstd * (1.0 + w)
|
||||
# Match the unfused path: GemmaRMSNorm writes bf16/fp16, then RoPE reads
|
||||
# that rounded value in the following kernel.
|
||||
normed = normed.to(q_out_ptr.dtype.element_ty).to(tl.float32)
|
||||
|
||||
rotary_mask = cols < rotary_dim
|
||||
if is_neox_style:
|
||||
partner_cols = tl.where(
|
||||
cols < half_rotary, cols + half_rotary, cols - half_rotary
|
||||
)
|
||||
cos_cols = tl.where(cols < half_rotary, cols, cols - half_rotary)
|
||||
sign = tl.where(cols < half_rotary, -1.0, 1.0)
|
||||
else:
|
||||
partner_cols = tl.where((cols % 2) == 0, cols + 1, cols - 1)
|
||||
cos_cols = cols // 2
|
||||
sign = tl.where((cols % 2) == 0, -1.0, 1.0)
|
||||
|
||||
partner_mask = partner_cols < head_dim
|
||||
x_partner = tl.load(
|
||||
base_in + partner_cols * stride_d,
|
||||
mask=partner_mask,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
w_partner = tl.load(
|
||||
weight_ptr + partner_cols,
|
||||
mask=partner_mask,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
partner_normed = x_partner * rstd * (1.0 + w_partner)
|
||||
partner_normed = partner_normed.to(q_out_ptr.dtype.element_ty).to(tl.float32)
|
||||
|
||||
pos = tl.load(positions_ptr + token_id).to(tl.int64)
|
||||
cos_sin_base = cos_sin_cache_ptr + pos * rotary_dim
|
||||
cos = tl.load(cos_sin_base + cos_cols, mask=rotary_mask, other=1.0).to(tl.float32)
|
||||
sin = tl.load(
|
||||
cos_sin_base + half_rotary + cos_cols,
|
||||
mask=rotary_mask,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
rotated = normed * cos + sign * partner_normed * sin
|
||||
out = tl.where(rotary_mask, rotated, normed)
|
||||
|
||||
base_out = out_ptr + token_id * n_heads * head_dim + head_id * head_dim
|
||||
tl.store(base_out + cols, out.to(out_ptr.dtype.element_ty), mask=mask)
|
||||
|
||||
|
||||
def qk_gemma_rmsnorm_rope(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
eps: float,
|
||||
head_dim: int,
|
||||
rotary_dim: int,
|
||||
is_neox_style: bool,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Return normalized+rotated Q/K tensors with the same shapes as ``q``/``k``."""
|
||||
assert q.dim() == 2 and k.dim() == 2
|
||||
assert positions.dim() == 1
|
||||
assert q.shape[0] == k.shape[0] == positions.shape[0]
|
||||
assert q.shape[1] % head_dim == 0
|
||||
assert k.shape[1] % head_dim == 0
|
||||
assert rotary_dim <= head_dim and rotary_dim % 2 == 0
|
||||
|
||||
q_heads = q.shape[1] // head_dim
|
||||
k_heads = k.shape[1] // head_dim
|
||||
q_out = torch.empty(q.shape, dtype=q.dtype, device=q.device)
|
||||
k_out = torch.empty(k.shape, dtype=k.dtype, device=k.device)
|
||||
block_hd = triton.next_power_of_2(head_dim)
|
||||
|
||||
_qk_gemma_rmsnorm_rope_kernel[(q.shape[0], q_heads + k_heads)](
|
||||
q,
|
||||
k,
|
||||
q_out,
|
||||
k_out,
|
||||
q_weight,
|
||||
k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
q.stride(0),
|
||||
q.stride(1),
|
||||
k.stride(0),
|
||||
k.stride(1),
|
||||
q_heads,
|
||||
k_heads,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
eps,
|
||||
is_neox_style,
|
||||
BLOCK_HD=block_hd,
|
||||
num_warps=4,
|
||||
)
|
||||
return q_out, k_out
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _sparse_qk_index_gemma_rmsnorm_rope_kernel(
|
||||
q_ptr,
|
||||
k_ptr,
|
||||
idx_q_ptr,
|
||||
idx_k_ptr,
|
||||
q_out_ptr,
|
||||
k_out_ptr,
|
||||
idx_q_out_ptr,
|
||||
idx_k_out_ptr,
|
||||
q_weight_ptr,
|
||||
k_weight_ptr,
|
||||
idx_q_weight_ptr,
|
||||
idx_k_weight_ptr,
|
||||
positions_ptr,
|
||||
cos_sin_cache_ptr,
|
||||
q_stride_m,
|
||||
q_stride_d,
|
||||
k_stride_m,
|
||||
k_stride_d,
|
||||
idx_q_stride_m,
|
||||
idx_q_stride_d,
|
||||
idx_k_stride_m,
|
||||
idx_k_stride_d,
|
||||
q_heads: tl.constexpr,
|
||||
k_heads: tl.constexpr,
|
||||
idx_q_heads: tl.constexpr,
|
||||
head_dim: tl.constexpr,
|
||||
rotary_dim: tl.constexpr,
|
||||
eps: tl.constexpr,
|
||||
is_neox_style: tl.constexpr,
|
||||
BLOCK_HD: tl.constexpr,
|
||||
):
|
||||
token_id = tl.program_id(0)
|
||||
head_program = tl.program_id(1)
|
||||
cols = tl.arange(0, BLOCK_HD)
|
||||
mask = cols < head_dim
|
||||
half_rotary: tl.constexpr = rotary_dim // 2
|
||||
|
||||
main_heads: tl.constexpr = q_heads + k_heads
|
||||
idx_k_program: tl.constexpr = q_heads + k_heads + idx_q_heads
|
||||
|
||||
is_q = head_program < q_heads
|
||||
is_k = (head_program >= q_heads) & (head_program < main_heads)
|
||||
is_idx_q = (head_program >= main_heads) & (head_program < idx_k_program)
|
||||
|
||||
head_id = tl.where(
|
||||
is_q,
|
||||
head_program,
|
||||
tl.where(
|
||||
is_k,
|
||||
head_program - q_heads,
|
||||
tl.where(is_idx_q, head_program - main_heads, 0),
|
||||
),
|
||||
)
|
||||
|
||||
in_ptr = tl.where(
|
||||
is_q,
|
||||
q_ptr,
|
||||
tl.where(is_k, k_ptr, tl.where(is_idx_q, idx_q_ptr, idx_k_ptr)),
|
||||
)
|
||||
out_ptr = tl.where(
|
||||
is_q,
|
||||
q_out_ptr,
|
||||
tl.where(is_k, k_out_ptr, tl.where(is_idx_q, idx_q_out_ptr, idx_k_out_ptr)),
|
||||
)
|
||||
weight_ptr = tl.where(
|
||||
is_q,
|
||||
q_weight_ptr,
|
||||
tl.where(
|
||||
is_k,
|
||||
k_weight_ptr,
|
||||
tl.where(is_idx_q, idx_q_weight_ptr, idx_k_weight_ptr),
|
||||
),
|
||||
)
|
||||
stride_m = tl.where(
|
||||
is_q,
|
||||
q_stride_m,
|
||||
tl.where(
|
||||
is_k,
|
||||
k_stride_m,
|
||||
tl.where(is_idx_q, idx_q_stride_m, idx_k_stride_m),
|
||||
),
|
||||
)
|
||||
stride_d = tl.where(
|
||||
is_q,
|
||||
q_stride_d,
|
||||
tl.where(
|
||||
is_k,
|
||||
k_stride_d,
|
||||
tl.where(is_idx_q, idx_q_stride_d, idx_k_stride_d),
|
||||
),
|
||||
)
|
||||
out_heads = tl.where(
|
||||
is_q,
|
||||
q_heads,
|
||||
tl.where(is_k, k_heads, tl.where(is_idx_q, idx_q_heads, 1)),
|
||||
)
|
||||
|
||||
base_in = in_ptr + token_id * stride_m + head_id * head_dim * stride_d
|
||||
x = tl.load(base_in + cols * stride_d, mask=mask, other=0.0).to(tl.float32)
|
||||
w = tl.load(weight_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
var = tl.sum(x * x, axis=0) / head_dim
|
||||
rstd = tl.rsqrt(var + eps)
|
||||
normed = x * rstd * (1.0 + w)
|
||||
normed = normed.to(q_out_ptr.dtype.element_ty).to(tl.float32)
|
||||
|
||||
rotary_mask = cols < rotary_dim
|
||||
if is_neox_style:
|
||||
partner_cols = tl.where(
|
||||
cols < half_rotary, cols + half_rotary, cols - half_rotary
|
||||
)
|
||||
cos_cols = tl.where(cols < half_rotary, cols, cols - half_rotary)
|
||||
sign = tl.where(cols < half_rotary, -1.0, 1.0)
|
||||
else:
|
||||
partner_cols = tl.where((cols % 2) == 0, cols + 1, cols - 1)
|
||||
cos_cols = cols // 2
|
||||
sign = tl.where((cols % 2) == 0, -1.0, 1.0)
|
||||
|
||||
partner_mask = partner_cols < head_dim
|
||||
x_partner = tl.load(
|
||||
base_in + partner_cols * stride_d,
|
||||
mask=partner_mask,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
w_partner = tl.load(
|
||||
weight_ptr + partner_cols,
|
||||
mask=partner_mask,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
partner_normed = x_partner * rstd * (1.0 + w_partner)
|
||||
partner_normed = partner_normed.to(q_out_ptr.dtype.element_ty).to(tl.float32)
|
||||
|
||||
pos = tl.load(positions_ptr + token_id).to(tl.int64)
|
||||
cos_sin_base = cos_sin_cache_ptr + pos * rotary_dim
|
||||
cos = tl.load(cos_sin_base + cos_cols, mask=rotary_mask, other=1.0).to(tl.float32)
|
||||
sin = tl.load(
|
||||
cos_sin_base + half_rotary + cos_cols,
|
||||
mask=rotary_mask,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
rotated = normed * cos + sign * partner_normed * sin
|
||||
out = tl.where(rotary_mask, rotated, normed)
|
||||
|
||||
base_out = out_ptr + token_id * out_heads * head_dim + head_id * head_dim
|
||||
tl.store(base_out + cols, out.to(q_out_ptr.dtype.element_ty), mask=mask)
|
||||
|
||||
|
||||
def sparse_qk_index_gemma_rmsnorm_rope(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
idx_q: torch.Tensor,
|
||||
idx_k: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
idx_q_weight: torch.Tensor,
|
||||
idx_k_weight: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
eps: float,
|
||||
head_dim: int,
|
||||
rotary_dim: int,
|
||||
is_neox_style: bool,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Fuse main and sparse-index Gemma Q/K RMSNorm + RoPE into one launch."""
|
||||
assert q.dim() == k.dim() == idx_q.dim() == idx_k.dim() == 2
|
||||
assert positions.dim() == 1
|
||||
assert q.shape[0] == k.shape[0] == idx_q.shape[0] == idx_k.shape[0]
|
||||
assert q.shape[0] == positions.shape[0]
|
||||
assert q.shape[1] % head_dim == 0
|
||||
assert k.shape[1] % head_dim == 0
|
||||
assert idx_q.shape[1] % head_dim == 0
|
||||
assert idx_k.shape[1] == head_dim
|
||||
assert rotary_dim <= head_dim and rotary_dim % 2 == 0
|
||||
|
||||
q_heads = q.shape[1] // head_dim
|
||||
k_heads = k.shape[1] // head_dim
|
||||
idx_q_heads = idx_q.shape[1] // head_dim
|
||||
q_out = torch.empty(q.shape, dtype=q.dtype, device=q.device)
|
||||
k_out = torch.empty(k.shape, dtype=k.dtype, device=k.device)
|
||||
idx_q_out = torch.empty(idx_q.shape, dtype=idx_q.dtype, device=idx_q.device)
|
||||
idx_k_out = torch.empty(idx_k.shape, dtype=idx_k.dtype, device=idx_k.device)
|
||||
block_hd = triton.next_power_of_2(head_dim)
|
||||
|
||||
_sparse_qk_index_gemma_rmsnorm_rope_kernel[
|
||||
(q.shape[0], q_heads + k_heads + idx_q_heads + 1)
|
||||
](
|
||||
q,
|
||||
k,
|
||||
idx_q,
|
||||
idx_k,
|
||||
q_out,
|
||||
k_out,
|
||||
idx_q_out,
|
||||
idx_k_out,
|
||||
q_weight,
|
||||
k_weight,
|
||||
idx_q_weight,
|
||||
idx_k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
q.stride(0),
|
||||
q.stride(1),
|
||||
k.stride(0),
|
||||
k.stride(1),
|
||||
idx_q.stride(0),
|
||||
idx_q.stride(1),
|
||||
idx_k.stride(0),
|
||||
idx_k.stride(1),
|
||||
q_heads,
|
||||
k_heads,
|
||||
idx_q_heads,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
eps,
|
||||
is_neox_style,
|
||||
BLOCK_HD=block_hd,
|
||||
num_warps=4,
|
||||
)
|
||||
return q_out, k_out, idx_q_out, idx_k_out
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _sparse_qk_index_gemma_rmsnorm_rope_cache_kernel(
|
||||
q_ptr,
|
||||
k_ptr,
|
||||
v_ptr,
|
||||
idx_q_ptr,
|
||||
idx_k_ptr,
|
||||
q_out_ptr,
|
||||
k_out_ptr,
|
||||
idx_q_out_ptr,
|
||||
idx_k_out_ptr,
|
||||
k_cache_ptr,
|
||||
v_cache_ptr,
|
||||
idx_k_cache_ptr,
|
||||
loc_ptr,
|
||||
q_weight_ptr,
|
||||
k_weight_ptr,
|
||||
idx_q_weight_ptr,
|
||||
idx_k_weight_ptr,
|
||||
positions_ptr,
|
||||
cos_sin_cache_ptr,
|
||||
q_stride_m,
|
||||
q_stride_d,
|
||||
k_stride_m,
|
||||
k_stride_d,
|
||||
v_stride_m,
|
||||
v_stride_d,
|
||||
idx_q_stride_m,
|
||||
idx_q_stride_d,
|
||||
idx_k_stride_m,
|
||||
idx_k_stride_d,
|
||||
k_cache_stride_s,
|
||||
k_cache_stride_h,
|
||||
k_cache_stride_d,
|
||||
v_cache_stride_s,
|
||||
v_cache_stride_h,
|
||||
v_cache_stride_d,
|
||||
idx_k_cache_stride_s,
|
||||
idx_k_cache_stride_h,
|
||||
idx_k_cache_stride_d,
|
||||
q_heads: tl.constexpr,
|
||||
k_heads: tl.constexpr,
|
||||
idx_q_heads: tl.constexpr,
|
||||
head_dim: tl.constexpr,
|
||||
rotary_dim: tl.constexpr,
|
||||
eps: tl.constexpr,
|
||||
is_neox_style: tl.constexpr,
|
||||
BLOCK_HD: tl.constexpr,
|
||||
):
|
||||
token_id = tl.program_id(0)
|
||||
head_program = tl.program_id(1)
|
||||
cols = tl.arange(0, BLOCK_HD)
|
||||
mask = cols < head_dim
|
||||
half_rotary: tl.constexpr = rotary_dim // 2
|
||||
|
||||
main_heads: tl.constexpr = q_heads + k_heads
|
||||
idx_k_program: tl.constexpr = q_heads + k_heads + idx_q_heads
|
||||
|
||||
is_q = head_program < q_heads
|
||||
is_k = (head_program >= q_heads) & (head_program < main_heads)
|
||||
is_idx_q = (head_program >= main_heads) & (head_program < idx_k_program)
|
||||
|
||||
head_id = tl.where(
|
||||
is_q,
|
||||
head_program,
|
||||
tl.where(
|
||||
is_k,
|
||||
head_program - q_heads,
|
||||
tl.where(is_idx_q, head_program - main_heads, 0),
|
||||
),
|
||||
)
|
||||
|
||||
in_ptr = tl.where(
|
||||
is_q,
|
||||
q_ptr,
|
||||
tl.where(is_k, k_ptr, tl.where(is_idx_q, idx_q_ptr, idx_k_ptr)),
|
||||
)
|
||||
out_ptr = tl.where(
|
||||
is_q,
|
||||
q_out_ptr,
|
||||
tl.where(is_k, k_out_ptr, tl.where(is_idx_q, idx_q_out_ptr, idx_k_out_ptr)),
|
||||
)
|
||||
weight_ptr = tl.where(
|
||||
is_q,
|
||||
q_weight_ptr,
|
||||
tl.where(
|
||||
is_k,
|
||||
k_weight_ptr,
|
||||
tl.where(is_idx_q, idx_q_weight_ptr, idx_k_weight_ptr),
|
||||
),
|
||||
)
|
||||
stride_m = tl.where(
|
||||
is_q,
|
||||
q_stride_m,
|
||||
tl.where(
|
||||
is_k,
|
||||
k_stride_m,
|
||||
tl.where(is_idx_q, idx_q_stride_m, idx_k_stride_m),
|
||||
),
|
||||
)
|
||||
stride_d = tl.where(
|
||||
is_q,
|
||||
q_stride_d,
|
||||
tl.where(
|
||||
is_k,
|
||||
k_stride_d,
|
||||
tl.where(is_idx_q, idx_q_stride_d, idx_k_stride_d),
|
||||
),
|
||||
)
|
||||
out_heads = tl.where(
|
||||
is_q,
|
||||
q_heads,
|
||||
tl.where(is_k, k_heads, tl.where(is_idx_q, idx_q_heads, 1)),
|
||||
)
|
||||
|
||||
base_in = in_ptr + token_id * stride_m + head_id * head_dim * stride_d
|
||||
x = tl.load(base_in + cols * stride_d, mask=mask, other=0.0).to(tl.float32)
|
||||
w = tl.load(weight_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
var = tl.sum(x * x, axis=0) / head_dim
|
||||
rstd = tl.rsqrt(var + eps)
|
||||
normed = x * rstd * (1.0 + w)
|
||||
normed = normed.to(q_out_ptr.dtype.element_ty).to(tl.float32)
|
||||
|
||||
rotary_mask = cols < rotary_dim
|
||||
if is_neox_style:
|
||||
partner_cols = tl.where(
|
||||
cols < half_rotary, cols + half_rotary, cols - half_rotary
|
||||
)
|
||||
cos_cols = tl.where(cols < half_rotary, cols, cols - half_rotary)
|
||||
sign = tl.where(cols < half_rotary, -1.0, 1.0)
|
||||
else:
|
||||
partner_cols = tl.where((cols % 2) == 0, cols + 1, cols - 1)
|
||||
cos_cols = cols // 2
|
||||
sign = tl.where((cols % 2) == 0, -1.0, 1.0)
|
||||
|
||||
partner_mask = partner_cols < head_dim
|
||||
x_partner = tl.load(
|
||||
base_in + partner_cols * stride_d,
|
||||
mask=partner_mask,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
w_partner = tl.load(
|
||||
weight_ptr + partner_cols,
|
||||
mask=partner_mask,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
partner_normed = x_partner * rstd * (1.0 + w_partner)
|
||||
partner_normed = partner_normed.to(q_out_ptr.dtype.element_ty).to(tl.float32)
|
||||
|
||||
pos = tl.load(positions_ptr + token_id).to(tl.int64)
|
||||
cos_sin_base = cos_sin_cache_ptr + pos * rotary_dim
|
||||
cos = tl.load(cos_sin_base + cos_cols, mask=rotary_mask, other=1.0).to(tl.float32)
|
||||
sin = tl.load(
|
||||
cos_sin_base + half_rotary + cos_cols,
|
||||
mask=rotary_mask,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
rotated = normed * cos + sign * partner_normed * sin
|
||||
out = tl.where(rotary_mask, rotated, normed)
|
||||
out_typed = out.to(q_out_ptr.dtype.element_ty)
|
||||
|
||||
base_out = out_ptr + token_id * out_heads * head_dim + head_id * head_dim
|
||||
tl.store(base_out + cols, out_typed, mask=mask)
|
||||
|
||||
loc = tl.load(loc_ptr + token_id)
|
||||
cache_k_base = (
|
||||
k_cache_ptr
|
||||
+ loc * k_cache_stride_s
|
||||
+ head_id * k_cache_stride_h
|
||||
+ cols * k_cache_stride_d
|
||||
)
|
||||
tl.store(cache_k_base, out_typed, mask=mask & is_k)
|
||||
|
||||
v_base = v_ptr + token_id * v_stride_m + head_id * head_dim * v_stride_d
|
||||
v_val = tl.load(v_base + cols * v_stride_d, mask=mask & is_k, other=0.0)
|
||||
cache_v_base = (
|
||||
v_cache_ptr
|
||||
+ loc * v_cache_stride_s
|
||||
+ head_id * v_cache_stride_h
|
||||
+ cols * v_cache_stride_d
|
||||
)
|
||||
tl.store(cache_v_base, v_val, mask=mask & is_k)
|
||||
|
||||
is_idx_k = head_program == idx_k_program
|
||||
idx_cache_base = (
|
||||
idx_k_cache_ptr + loc * idx_k_cache_stride_s + cols * idx_k_cache_stride_d
|
||||
)
|
||||
tl.store(idx_cache_base, out_typed, mask=mask & is_idx_k)
|
||||
|
||||
|
||||
def sparse_qk_index_gemma_rmsnorm_rope_cache(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
idx_q: torch.Tensor,
|
||||
idx_k: torch.Tensor,
|
||||
k_cache: torch.Tensor,
|
||||
v_cache: torch.Tensor,
|
||||
idx_k_cache: torch.Tensor,
|
||||
out_cache_loc: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
idx_q_weight: torch.Tensor,
|
||||
idx_k_weight: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
eps: float,
|
||||
head_dim: int,
|
||||
rotary_dim: int,
|
||||
is_neox_style: bool,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Fuse sparse Q/K/index norm+RoPE with main KV and index-K cache stores."""
|
||||
assert q.dim() == k.dim() == v.dim() == idx_q.dim() == idx_k.dim() == 2
|
||||
assert k_cache.dim() == v_cache.dim() == idx_k_cache.dim() == 3
|
||||
assert out_cache_loc.dim() == positions.dim() == 1
|
||||
assert q.shape[0] == k.shape[0] == v.shape[0] == idx_q.shape[0] == idx_k.shape[0]
|
||||
assert q.shape[0] == positions.shape[0] == out_cache_loc.shape[0]
|
||||
assert q.shape[1] % head_dim == 0
|
||||
assert k.shape[1] % head_dim == 0
|
||||
assert v.shape[1] == k.shape[1]
|
||||
assert idx_q.shape[1] % head_dim == 0
|
||||
assert idx_k.shape[1] == head_dim
|
||||
assert rotary_dim <= head_dim and rotary_dim % 2 == 0
|
||||
|
||||
q_heads = q.shape[1] // head_dim
|
||||
k_heads = k.shape[1] // head_dim
|
||||
idx_q_heads = idx_q.shape[1] // head_dim
|
||||
assert k_cache.shape[1] == v_cache.shape[1] == k_heads
|
||||
assert idx_k_cache.shape[1] == 1
|
||||
|
||||
q_out = torch.empty(q.shape, dtype=q.dtype, device=q.device)
|
||||
k_out = torch.empty(k.shape, dtype=k.dtype, device=k.device)
|
||||
idx_q_out = torch.empty(idx_q.shape, dtype=idx_q.dtype, device=idx_q.device)
|
||||
idx_k_out = torch.empty(idx_k.shape, dtype=idx_k.dtype, device=idx_k.device)
|
||||
block_hd = triton.next_power_of_2(head_dim)
|
||||
|
||||
_sparse_qk_index_gemma_rmsnorm_rope_cache_kernel[
|
||||
(q.shape[0], q_heads + k_heads + idx_q_heads + 1)
|
||||
](
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
idx_q,
|
||||
idx_k,
|
||||
q_out,
|
||||
k_out,
|
||||
idx_q_out,
|
||||
idx_k_out,
|
||||
k_cache,
|
||||
v_cache,
|
||||
idx_k_cache,
|
||||
out_cache_loc,
|
||||
q_weight,
|
||||
k_weight,
|
||||
idx_q_weight,
|
||||
idx_k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
q.stride(0),
|
||||
q.stride(1),
|
||||
k.stride(0),
|
||||
k.stride(1),
|
||||
v.stride(0),
|
||||
v.stride(1),
|
||||
idx_q.stride(0),
|
||||
idx_q.stride(1),
|
||||
idx_k.stride(0),
|
||||
idx_k.stride(1),
|
||||
k_cache.stride(0),
|
||||
k_cache.stride(1),
|
||||
k_cache.stride(2),
|
||||
v_cache.stride(0),
|
||||
v_cache.stride(1),
|
||||
v_cache.stride(2),
|
||||
idx_k_cache.stride(0),
|
||||
idx_k_cache.stride(1),
|
||||
idx_k_cache.stride(2),
|
||||
q_heads,
|
||||
k_heads,
|
||||
idx_q_heads,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
eps,
|
||||
is_neox_style,
|
||||
BLOCK_HD=block_hd,
|
||||
num_warps=4,
|
||||
)
|
||||
return q_out, k_out, idx_q_out, idx_k_out
|
||||
@@ -0,0 +1,148 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Fused Gemma RMSNorm Triton kernels for MiniMax-M3 on AMD ROCm.
|
||||
|
||||
Gemma RMSNorm = ``normalize(x) * (1 + weight)``, computed in a single fp32 pass.
|
||||
On ROCm with AITER, ``GemmaRMSNorm.forward_hip`` otherwise falls back to a
|
||||
~8-op PyTorch sequence: ``sgl_kernel``'s Gemma kernels are CUDA-only, and
|
||||
AITER's ``rmsnorm2d_fwd`` requires weight.dtype == activation.dtype (fp32
|
||||
weight + bf16 activation silently corrupts on gfx950). These kernels read
|
||||
strided inputs, so they serve both the full-hidden norms and the per-head
|
||||
q/k/index norms (non-contiguous ``qkv.split`` views).
|
||||
"""
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _gemma_rmsnorm_kernel(
|
||||
x_ptr,
|
||||
w_ptr,
|
||||
out_ptr,
|
||||
n_cols,
|
||||
stride_row,
|
||||
stride_col,
|
||||
eps,
|
||||
BLOCK_N: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0)
|
||||
cols = tl.arange(0, BLOCK_N)
|
||||
mask = cols < n_cols
|
||||
x = tl.load(x_ptr + row * stride_row + cols * stride_col, mask=mask, other=0.0).to(
|
||||
tl.float32
|
||||
)
|
||||
var = tl.sum(x * x, axis=0) / n_cols
|
||||
rstd = 1.0 / tl.sqrt(var + eps)
|
||||
w = tl.load(w_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
out = x * rstd * (1.0 + w)
|
||||
tl.store(
|
||||
out_ptr + row * n_cols + cols,
|
||||
out.to(out_ptr.dtype.element_ty),
|
||||
mask=mask,
|
||||
)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _gemma_fused_add_rmsnorm_kernel(
|
||||
x_ptr,
|
||||
res_ptr,
|
||||
w_ptr,
|
||||
out_ptr,
|
||||
res_out_ptr,
|
||||
n_cols,
|
||||
stride_xrow,
|
||||
stride_xcol,
|
||||
stride_rrow,
|
||||
stride_rcol,
|
||||
eps,
|
||||
BLOCK_N: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0)
|
||||
cols = tl.arange(0, BLOCK_N)
|
||||
mask = cols < n_cols
|
||||
x = tl.load(
|
||||
x_ptr + row * stride_xrow + cols * stride_xcol, mask=mask, other=0.0
|
||||
).to(tl.float32)
|
||||
r = tl.load(
|
||||
res_ptr + row * stride_rrow + cols * stride_rcol, mask=mask, other=0.0
|
||||
).to(tl.float32)
|
||||
s = x + r
|
||||
# residual_out is the pre-norm sum (consumed by the next layer's add).
|
||||
tl.store(
|
||||
res_out_ptr + row * n_cols + cols,
|
||||
s.to(res_out_ptr.dtype.element_ty),
|
||||
mask=mask,
|
||||
)
|
||||
var = tl.sum(s * s, axis=0) / n_cols
|
||||
rstd = 1.0 / tl.sqrt(var + eps)
|
||||
w = tl.load(w_ptr + cols, mask=mask, other=0.0).to(tl.float32)
|
||||
out = s * rstd * (1.0 + w)
|
||||
tl.store(
|
||||
out_ptr + row * n_cols + cols,
|
||||
out.to(out_ptr.dtype.element_ty),
|
||||
mask=mask,
|
||||
)
|
||||
|
||||
|
||||
def _num_warps(block_n: int) -> int:
|
||||
if block_n >= 4096:
|
||||
return 16
|
||||
if block_n >= 1024:
|
||||
return 8
|
||||
return 4
|
||||
|
||||
|
||||
def gemma_rmsnorm(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor:
|
||||
"""Gemma RMSNorm = normalize(x) * (1 + weight), fp32 math, single pass."""
|
||||
orig_shape = x.shape
|
||||
n = orig_shape[-1]
|
||||
x2 = x.reshape(-1, n)
|
||||
m = x2.shape[0]
|
||||
out = torch.empty((m, n), dtype=x.dtype, device=x.device)
|
||||
block_n = triton.next_power_of_2(n)
|
||||
_gemma_rmsnorm_kernel[(m,)](
|
||||
x2,
|
||||
weight,
|
||||
out,
|
||||
n,
|
||||
x2.stride(0),
|
||||
x2.stride(1),
|
||||
eps,
|
||||
BLOCK_N=block_n,
|
||||
num_warps=_num_warps(block_n),
|
||||
)
|
||||
return out.reshape(orig_shape)
|
||||
|
||||
|
||||
def gemma_fused_add_rmsnorm(
|
||||
x: torch.Tensor,
|
||||
residual: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
eps: float,
|
||||
):
|
||||
"""Fused (x + residual) then Gemma RMSNorm; returns (normed, pre-norm sum)."""
|
||||
orig_shape = x.shape
|
||||
n = orig_shape[-1]
|
||||
x2 = x.reshape(-1, n)
|
||||
r2 = residual.reshape(-1, n)
|
||||
m = x2.shape[0]
|
||||
out = torch.empty((m, n), dtype=x.dtype, device=x.device)
|
||||
res_out = torch.empty((m, n), dtype=x.dtype, device=x.device)
|
||||
block_n = triton.next_power_of_2(n)
|
||||
_gemma_fused_add_rmsnorm_kernel[(m,)](
|
||||
x2,
|
||||
r2,
|
||||
weight,
|
||||
out,
|
||||
res_out,
|
||||
n,
|
||||
x2.stride(0),
|
||||
x2.stride(1),
|
||||
r2.stride(0),
|
||||
r2.stride(1),
|
||||
eps,
|
||||
BLOCK_N=block_n,
|
||||
num_warps=_num_warps(block_n),
|
||||
)
|
||||
return out.reshape(orig_shape), res_out.reshape(orig_shape)
|
||||
@@ -0,0 +1,201 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Fused SwiGLU-OAI (split layout) Triton kernel for MiniMax-M3 on AMD ROCm.
|
||||
|
||||
SwiGLU-OAI on a ``[*, 2I]`` split-layout tensor (gate = first half, up = second
|
||||
half): ``gate * sigmoid(alpha * gate) * (up + beta)`` with optional clamp,
|
||||
computed in fp32. Used by the dense MLP / shared experts ``swigluoai``
|
||||
activation on ROCm in place of the ``@torch.compile`` bf16 elementwise variant.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _swiglu_oai_kernel(
|
||||
g_ptr,
|
||||
out_ptr,
|
||||
n_inter,
|
||||
stride_gm,
|
||||
stride_gn,
|
||||
stride_om,
|
||||
stride_on,
|
||||
alpha,
|
||||
beta,
|
||||
limit,
|
||||
HAS_LIMIT: tl.constexpr,
|
||||
BLOCK_I: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0)
|
||||
pid_i = tl.program_id(1)
|
||||
cols = pid_i * BLOCK_I + tl.arange(0, BLOCK_I)
|
||||
mask = cols < n_inter
|
||||
gate = tl.load(g_ptr + row * stride_gm + cols * stride_gn, mask=mask, other=0.0).to(
|
||||
tl.float32
|
||||
)
|
||||
up = tl.load(
|
||||
g_ptr + row * stride_gm + (n_inter + cols) * stride_gn,
|
||||
mask=mask,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
if HAS_LIMIT:
|
||||
gate = tl.minimum(gate, limit)
|
||||
up = tl.minimum(tl.maximum(up, -limit), limit)
|
||||
out = gate * tl.sigmoid(alpha * gate) * (up + beta)
|
||||
tl.store(
|
||||
out_ptr + row * stride_om + cols * stride_on,
|
||||
out.to(out_ptr.dtype.element_ty),
|
||||
mask=mask,
|
||||
)
|
||||
|
||||
|
||||
def swiglu_oai_split(
|
||||
gate_up: torch.Tensor,
|
||||
alpha: float,
|
||||
beta: float,
|
||||
limit: Optional[float],
|
||||
out_dtype: Optional[torch.dtype] = None,
|
||||
) -> torch.Tensor:
|
||||
"""SwiGLU-OAI on a split-layout ``[*, 2I]`` tensor -> ``[*, I]`` (fp32 math)."""
|
||||
orig_shape = gate_up.shape
|
||||
two_i = orig_shape[-1]
|
||||
n_inter = two_i // 2
|
||||
x2 = gate_up.reshape(-1, two_i)
|
||||
m = x2.shape[0]
|
||||
dt = out_dtype if out_dtype is not None else gate_up.dtype
|
||||
out = torch.empty((m, n_inter), dtype=dt, device=gate_up.device)
|
||||
# Adaptive tile (tuned on gfx950). A 512-wide tile only helps
|
||||
# once the (TP-sharded) per-rank slice is large enough to be bandwidth-bound
|
||||
# (~1.25-1.35x faster than 256 at TP=1 prefill for the dense MLP I=12288).
|
||||
# For small sharded slices (high TP) / decode the kernel is launch-bound, so
|
||||
# fall back to 256. num_warps is pinned to 4 (8 underfills this tile).
|
||||
block_i = 512 if n_inter >= 2048 else 256
|
||||
grid = (m, triton.cdiv(n_inter, block_i))
|
||||
_swiglu_oai_kernel[grid](
|
||||
x2,
|
||||
out,
|
||||
n_inter,
|
||||
x2.stride(0),
|
||||
x2.stride(1),
|
||||
out.stride(0),
|
||||
out.stride(1),
|
||||
float(alpha),
|
||||
float(beta),
|
||||
0.0 if limit is None else float(limit),
|
||||
HAS_LIMIT=limit is not None,
|
||||
BLOCK_I=block_i,
|
||||
num_warps=4,
|
||||
)
|
||||
return out.reshape(*orig_shape[:-1], n_inter)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _swiglu_oai_mxfp8_quant_kernel(
|
||||
g_ptr,
|
||||
q_ptr,
|
||||
scale_ptr,
|
||||
n_inter,
|
||||
stride_gm,
|
||||
stride_gn,
|
||||
stride_qm,
|
||||
stride_qn,
|
||||
stride_sm,
|
||||
stride_sn,
|
||||
alpha,
|
||||
beta,
|
||||
limit,
|
||||
HAS_LIMIT: tl.constexpr,
|
||||
BLOCK_I: tl.constexpr,
|
||||
):
|
||||
row = tl.program_id(0)
|
||||
pid_i = tl.program_id(1)
|
||||
cols = pid_i * BLOCK_I + tl.arange(0, BLOCK_I)
|
||||
mask = cols < n_inter
|
||||
|
||||
gate = tl.load(g_ptr + row * stride_gm + cols * stride_gn, mask=mask, other=0.0)
|
||||
up = tl.load(
|
||||
g_ptr + row * stride_gm + (n_inter + cols) * stride_gn,
|
||||
mask=mask,
|
||||
other=0.0,
|
||||
)
|
||||
gate = gate.to(tl.float32)
|
||||
up = up.to(tl.float32)
|
||||
if HAS_LIMIT:
|
||||
gate = tl.minimum(gate, limit)
|
||||
up = tl.minimum(tl.maximum(up, -limit), limit)
|
||||
|
||||
# Keep the activation in fp32 all the way to the E8M0 scale selection (no
|
||||
# bf16 round-trip to HBM). Matches the vLLM/ame fused swiglu+quant kernel:
|
||||
# marginally more accurate than the unfused bf16 two-kernel chain.
|
||||
activated = gate * tl.sigmoid(alpha * gate) * (up + beta)
|
||||
|
||||
groups: tl.constexpr = BLOCK_I // 32
|
||||
activated_2d = tl.reshape(activated, (groups, 32))
|
||||
valid_groups = pid_i * groups + tl.arange(0, groups) < (n_inter // 32)
|
||||
|
||||
amax = tl.maximum(tl.max(tl.abs(activated_2d), axis=1), 1e-30)
|
||||
# Round the E8M0 exponent up (ceil(log2(amax / e4m3_max))) so the block amax
|
||||
# stays inside the e4m3 range and the full dynamic range is used.
|
||||
scale_biased = tl.ceil(tl.log2(amax / 448.0)) + 127.0
|
||||
scale_biased = tl.minimum(tl.maximum(scale_biased, 0.0), 254.0)
|
||||
descale = tl.reshape(tl.exp2(scale_biased - 127.0), (groups, 1))
|
||||
|
||||
q_2d = tl.clamp(activated_2d / descale, -448.0, 448.0)
|
||||
q = tl.reshape(q_2d, (BLOCK_I,)).to(q_ptr.dtype.element_ty)
|
||||
|
||||
tl.store(q_ptr + row * stride_qm + cols * stride_qn, q, mask=mask)
|
||||
tl.store(
|
||||
scale_ptr
|
||||
+ row * stride_sm
|
||||
+ (pid_i * groups + tl.arange(0, groups)) * stride_sn,
|
||||
scale_biased.to(tl.uint8),
|
||||
mask=valid_groups,
|
||||
)
|
||||
|
||||
|
||||
def swiglu_oai_mxfp8_quant(
|
||||
gate_up: torch.Tensor,
|
||||
alpha: float,
|
||||
beta: float,
|
||||
limit: Optional[float],
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""SwiGLU-OAI on split layout, then MiniMax MXFP8 quant, in one launch.
|
||||
|
||||
The activation stays in fp32 through the E8M0 scale selection (no bf16
|
||||
round-trip), matching the vLLM/ame fused swiglu+quant kernel.
|
||||
"""
|
||||
orig_shape = gate_up.shape
|
||||
two_i = orig_shape[-1]
|
||||
n_inter = two_i // 2
|
||||
assert n_inter % 32 == 0, "MiniMax MXFP8 quant requires I divisible by 32."
|
||||
|
||||
x2 = gate_up.reshape(-1, two_i)
|
||||
m = x2.shape[0]
|
||||
q = torch.empty((m, n_inter), dtype=torch.float8_e4m3fn, device=gate_up.device)
|
||||
scales = torch.empty((m, n_inter // 32), dtype=torch.uint8, device=gate_up.device)
|
||||
block_i = 512 if n_inter >= 2048 else 256
|
||||
grid = (m, triton.cdiv(n_inter, block_i))
|
||||
_swiglu_oai_mxfp8_quant_kernel[grid](
|
||||
x2,
|
||||
q,
|
||||
scales,
|
||||
n_inter,
|
||||
x2.stride(0),
|
||||
x2.stride(1),
|
||||
q.stride(0),
|
||||
q.stride(1),
|
||||
scales.stride(0),
|
||||
scales.stride(1),
|
||||
float(alpha),
|
||||
float(beta),
|
||||
0.0 if limit is None else float(limit),
|
||||
HAS_LIMIT=limit is not None,
|
||||
BLOCK_I=block_i,
|
||||
num_warps=4,
|
||||
)
|
||||
return q.reshape(*orig_shape[:-1], n_inter), scales.reshape(
|
||||
*orig_shape[:-1], n_inter // 32
|
||||
)
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Fused per-head Gemma-RMSNorm + partial NeoX RoPE for MiniMax-M3 attention.
|
||||
|
||||
In-place over a fused QKV tensor: normalizes + rotates one or more groups of
|
||||
heads (each group = a contiguous head run sharing one norm weight, all getting
|
||||
RoPE), leaving every other head (V, index-V) untouched. Consumes the model's
|
||||
own ``cos_sin_cache`` (fp32) so the rotation matches sglang's RotaryEmbedding
|
||||
exactly.
|
||||
|
||||
Two entry points:
|
||||
|
||||
* :func:`minimax_qknorm_rope` -- the original main-attention call
|
||||
``(q | k | v ...)``: Q heads then K heads, both normed + roped.
|
||||
* :func:`minimax_qknorm_rope_grouped` -- a multi-group launch (up to 4 groups),
|
||||
used to fold the main Q/K *and* the sparse-index Q/K of one fused
|
||||
qkv+index-qkv GEMM output into a single kernel launch (mirroring the
|
||||
multi-branch single-launch design of ``fused_store_kv_index.cuh``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, List, Sequence, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.utils import (
|
||||
cache_once,
|
||||
is_arch_support_pdl,
|
||||
load_jit,
|
||||
make_cpp_args,
|
||||
)
|
||||
from sglang.srt.utils.custom_op import register_custom_op
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tvm_ffi.module import Module
|
||||
|
||||
_MAX_GROUPS = 4
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_module(pos_dtype, head_dim, rope_dim) -> Module:
|
||||
args = make_cpp_args(pos_dtype, head_dim, rope_dim, is_arch_support_pdl())
|
||||
return load_jit(
|
||||
"fused_gemma_qknorm_rope",
|
||||
*args,
|
||||
cuda_files=["minimax/fused_gemma_qknorm_rope.cuh"],
|
||||
cuda_wrappers=[("fused_gemma_qknorm_rope", f"fused_gemma_qknorm_rope<{args}>")],
|
||||
)
|
||||
|
||||
|
||||
@register_custom_op(mutates_args=["qkv"])
|
||||
def _fused_gemma_qknorm_rope(
|
||||
qkv: torch.Tensor,
|
||||
w0: torch.Tensor,
|
||||
w1: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
w3: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
off0: int,
|
||||
cnt0: int,
|
||||
off1: int,
|
||||
cnt1: int,
|
||||
off2: int,
|
||||
cnt2: int,
|
||||
off3: int,
|
||||
cnt3: int,
|
||||
num_groups: int,
|
||||
eps: float,
|
||||
) -> None:
|
||||
# Wrap the tvm-ffi kernel as a custom op so torch.compile / piecewise CUDA
|
||||
# graph can trace past the otherwise-opaque FFI call. The launch is
|
||||
# graph-capturable (host-side constant offsets/counts), so it stays inside
|
||||
# the captured region.
|
||||
module = _jit_module(positions.dtype, 128, 64)
|
||||
module.fused_gemma_qknorm_rope(
|
||||
qkv,
|
||||
w0,
|
||||
w1,
|
||||
w2,
|
||||
w3,
|
||||
cos_sin_cache,
|
||||
positions,
|
||||
off0,
|
||||
cnt0,
|
||||
off1,
|
||||
cnt1,
|
||||
off2,
|
||||
cnt2,
|
||||
off3,
|
||||
cnt3,
|
||||
num_groups,
|
||||
eps,
|
||||
)
|
||||
|
||||
|
||||
def minimax_qknorm_rope_grouped(
|
||||
qkv: torch.Tensor,
|
||||
groups: Sequence[Tuple[torch.Tensor, int, int]],
|
||||
cos_sin_cache: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
"""Fused GemmaRMSNorm + partial NeoX RoPE over ``groups``, in place on ``qkv``.
|
||||
|
||||
``qkv`` is ``[T, total_heads * head_dim]`` (head_dim == 128). Each group is
|
||||
``(weight, head_offset, head_count)``: ``head_count`` consecutive heads
|
||||
starting at ``head_offset`` (in head units) are normed with ``weight``
|
||||
(a ``[head_dim]`` bf16 tensor, the *raw* Gemma weight -- the kernel applies
|
||||
``1 + weight``) and rotated. Heads outside every group are untouched.
|
||||
|
||||
Up to 4 groups are supported in one launch. The offsets/counts are
|
||||
host-side constants, so the launch is CUDA-graph capturable.
|
||||
"""
|
||||
groups = [(w, off, cnt) for (w, off, cnt) in groups if cnt > 0]
|
||||
num_groups = len(groups)
|
||||
assert (
|
||||
1 <= num_groups <= _MAX_GROUPS
|
||||
), f"need 1..{_MAX_GROUPS} groups, got {num_groups}"
|
||||
|
||||
weights: List[torch.Tensor] = [g[0] for g in groups]
|
||||
offsets: List[int] = [int(g[1]) for g in groups]
|
||||
counts: List[int] = [int(g[2]) for g in groups]
|
||||
# Pad weight slots up to 4 with a dummy (group 0's weight); the kernel never
|
||||
# reads padded slots because num_groups bounds the in-kernel group scan.
|
||||
while len(weights) < _MAX_GROUPS:
|
||||
weights.append(weights[0])
|
||||
offsets.append(0)
|
||||
counts.append(0)
|
||||
|
||||
_fused_gemma_qknorm_rope(
|
||||
qkv,
|
||||
weights[0],
|
||||
weights[1],
|
||||
weights[2],
|
||||
weights[3],
|
||||
cos_sin_cache,
|
||||
positions,
|
||||
offsets[0],
|
||||
counts[0],
|
||||
offsets[1],
|
||||
counts[1],
|
||||
offsets[2],
|
||||
counts[2],
|
||||
offsets[3],
|
||||
counts[3],
|
||||
num_groups,
|
||||
eps,
|
||||
)
|
||||
return qkv
|
||||
|
||||
|
||||
def minimax_qknorm_rope(
|
||||
qkv: torch.Tensor,
|
||||
q_weight: torch.Tensor,
|
||||
k_weight: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
nq: int,
|
||||
nk: int,
|
||||
nv: int, # deprecated / ignored: V heads are simply left untouched
|
||||
eps: float,
|
||||
) -> torch.Tensor:
|
||||
"""Main-attention layout ``[q (nq) | k (nk) | v ...]``: norm + rope Q then K."""
|
||||
return minimax_qknorm_rope_grouped(
|
||||
qkv,
|
||||
[(q_weight, 0, nq), (k_weight, nq, nk)],
|
||||
cos_sin_cache,
|
||||
positions,
|
||||
eps,
|
||||
)
|
||||
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.utils import (
|
||||
cache_once,
|
||||
is_arch_support_pdl,
|
||||
load_jit,
|
||||
make_cpp_args,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tvm_ffi.module import Module
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_module(group_size: int) -> Module:
|
||||
args = make_cpp_args(group_size, is_arch_support_pdl())
|
||||
return load_jit(
|
||||
"minimax_per_token_quant_ue8m0",
|
||||
*args,
|
||||
cuda_files=["minimax/per_token_quant_ue8m0.cuh"],
|
||||
cuda_wrappers=[
|
||||
("per_token_quant_ue8m0", f"per_token_quant_ue8m0<{args}>"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_scatter_module(group_size: int, topk: int) -> Module:
|
||||
# topk is a template arg so the dst-row load/store loops fully unroll.
|
||||
args = make_cpp_args(group_size, topk, is_arch_support_pdl())
|
||||
return load_jit(
|
||||
"minimax_per_token_quant_ue8m0_scatter",
|
||||
*args,
|
||||
cuda_files=["minimax/per_token_quant_ue8m0.cuh"],
|
||||
cuda_wrappers=[
|
||||
(
|
||||
"per_token_quant_ue8m0_scatter",
|
||||
f"per_token_quant_ue8m0_scatter<{args}>",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def per_token_quant_fp8_ue8m0(
|
||||
x: torch.Tensor, group_size: int = 128
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Per-token group quant to FP8-e4m3 with a fused UE8M0 (int32-packed) scale.
|
||||
|
||||
Returns ``(x_q, x_sf)`` where ``x_q`` is fp8_e4m3 ``[num_tokens, hidden]`` and
|
||||
``x_sf`` is the int32-packed UE8M0 scale ``[num_tokens, hidden//group_size//4]``
|
||||
(row-major). Byte-identical to ``per_token_group_quant_fp8(scale_ue8m0=True)``
|
||||
followed by ``transform_sf_into_required_layout`` (both ceil-round the scale),
|
||||
but does it in a single kernel -- no separate transpose/pack launch.
|
||||
"""
|
||||
assert x.is_cuda and x.dtype == torch.bfloat16 and x.dim() == 2
|
||||
assert x.is_contiguous()
|
||||
num_tokens, hidden = x.shape
|
||||
assert hidden % group_size == 0
|
||||
num_groups = hidden // group_size
|
||||
assert num_groups % 4 == 0, "num_groups must be a multiple of 4 for int32 packing"
|
||||
|
||||
x_q = torch.empty_like(x, dtype=torch.float8_e4m3fn)
|
||||
x_sf = torch.empty(
|
||||
(num_tokens, num_groups // 4), dtype=torch.int32, device=x.device
|
||||
)
|
||||
_jit_module(group_size).per_token_quant_ue8m0(x, x_q, x_sf)
|
||||
return x_q, x_sf
|
||||
|
||||
|
||||
def per_token_quant_fp8_ue8m0_scatter(
|
||||
x: torch.Tensor,
|
||||
gateup_input: torch.Tensor,
|
||||
gateup_input_scale: torch.Tensor,
|
||||
src2dst: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
topk: int,
|
||||
m_max: int,
|
||||
group_size: int = 128,
|
||||
) -> None:
|
||||
"""Fused per-token FP8/UE8M0 quant **and** scatter into the permuted grouped-GEMM
|
||||
input -- a single kernel replacing ``per_token_quant_fp8_ue8m0`` +
|
||||
``fill_gateup_input_triton_kernel``.
|
||||
|
||||
For each source token it computes the fp8 row + int32-packed UE8M0 scale once,
|
||||
then writes them to each of the token's ``topk`` destination rows:
|
||||
``gateup_input`` fp8 ``[E, m_max, hidden]`` (row ``src2dst[token, i]``)
|
||||
``gateup_input_scale`` int32 ``[E, hidden//group//4, m_max]`` (MN-major; byte-scattered)
|
||||
Slots with ``topk_ids[token, i] < 0`` are skipped. Byte-identical to the
|
||||
two-kernel path on every written row.
|
||||
"""
|
||||
assert x.is_cuda and x.dtype == torch.bfloat16 and x.dim() == 2
|
||||
assert x.is_contiguous()
|
||||
assert gateup_input.dtype == torch.float8_e4m3fn and gateup_input.dim() == 3
|
||||
assert gateup_input_scale.dtype == torch.int32 and gateup_input_scale.dim() == 3
|
||||
num_tokens, hidden = x.shape
|
||||
assert hidden % group_size == 0
|
||||
num_groups = hidden // group_size
|
||||
assert num_groups % 4 == 0, "num_groups must be a multiple of 4 for int32 packing"
|
||||
_jit_scatter_module(group_size, int(topk)).per_token_quant_ue8m0_scatter(
|
||||
x, gateup_input, gateup_input_scale, src2dst, topk_ids, int(topk), int(m_max)
|
||||
)
|
||||
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.utils import (
|
||||
cache_once,
|
||||
is_arch_support_pdl,
|
||||
load_jit,
|
||||
make_cpp_args,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tvm_ffi.module import Module
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_module(head_bytes: int) -> Module:
|
||||
# Build marker is (head_bytes, kUsePDL); the index dtype (int32/int64) is a
|
||||
# runtime dispatch inside the C++ launcher.
|
||||
args = make_cpp_args(head_bytes, is_arch_support_pdl())
|
||||
return load_jit(
|
||||
"minimax_store_kv_index",
|
||||
*args,
|
||||
cuda_files=["minimax/fused_store_kv_index.cuh"],
|
||||
cuda_wrappers=[("store_kv_index", f"store_kv_index<{args}>")],
|
||||
)
|
||||
|
||||
|
||||
def store_kv_index(
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
k_cache: torch.Tensor,
|
||||
v_cache: torch.Tensor,
|
||||
idx_k: torch.Tensor,
|
||||
idx_k_cache: torch.Tensor,
|
||||
idx_v: Optional[torch.Tensor],
|
||||
idx_v_cache: Optional[torch.Tensor],
|
||||
indices: torch.Tensor,
|
||||
*,
|
||||
num_kv_heads: int,
|
||||
head_bytes: int,
|
||||
) -> None:
|
||||
"""Fused store of the MiniMax-M3 sparse caches in one launch.
|
||||
|
||||
Writes the main ``k``/``v`` (``num_kv_heads`` heads each), the single index
|
||||
``idx_k`` head, and optionally the single ``idx_v`` head into their caches
|
||||
at the per-token rows given by ``indices`` (out_cache_loc). In-place on the
|
||||
four cache tensors.
|
||||
|
||||
All tensors must share the same (store) dtype and a head_dim whose byte size
|
||||
equals ``head_bytes`` (a multiple of 16). ``k``/``idx_k`` are 2D rows
|
||||
``[T, num_kv_heads*head_dim]`` / ``[T, head_dim]``; caches are the matching
|
||||
``[num_pages, ...]`` buffers. When ``idx_v`` is None there is no index value
|
||||
head (the layer is a pure block selector).
|
||||
"""
|
||||
has_v = idx_v is not None
|
||||
if not has_v:
|
||||
# Pass idx_k as a dummy for the unused index-V slot; heads_per_token is
|
||||
# set so the kernel never reaches the index-V branch.
|
||||
idx_v = idx_k
|
||||
idx_v_cache = idx_k_cache
|
||||
heads_per_token = 2 * num_kv_heads + 1 + (1 if has_v else 0)
|
||||
|
||||
module = _jit_module(head_bytes)
|
||||
module.store_kv_index(
|
||||
k,
|
||||
v,
|
||||
k_cache,
|
||||
v_cache,
|
||||
idx_k,
|
||||
idx_k_cache,
|
||||
idx_v,
|
||||
idx_v_cache,
|
||||
indices,
|
||||
num_kv_heads,
|
||||
heads_per_token,
|
||||
)
|
||||
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.utils import cache_once, load_jit, make_cpp_args
|
||||
from sglang.srt.utils.custom_op import register_custom_op
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tvm_ffi.module import Module
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_moe_topk_sigmoid_module(dtype: torch.dtype) -> Module:
|
||||
args = make_cpp_args(dtype)
|
||||
return load_jit(
|
||||
"moe_topk_sigmoid",
|
||||
*args,
|
||||
cuda_files=["moe/moe_topk_sigmoid.cuh"],
|
||||
cuda_wrappers=[("topk_sigmoid", f"topk_sigmoid<{args}>")],
|
||||
extra_cuda_cflags=["--use_fast_math"],
|
||||
)
|
||||
|
||||
|
||||
@register_custom_op(
|
||||
op_name="moe_topk_sigmoid_out",
|
||||
mutates_args=["topk_weights", "topk_ids"],
|
||||
)
|
||||
def moe_topk_sigmoid_out(
|
||||
gating_output: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
workspace: torch.Tensor,
|
||||
renormalize: bool,
|
||||
correction_bias: Optional[torch.Tensor],
|
||||
routed_scaling_factor: float,
|
||||
num_fused_shared_experts: int,
|
||||
) -> None:
|
||||
"""
|
||||
Fused sigmoid top-k MoE gate (destination-passing style).
|
||||
|
||||
Args:
|
||||
gating_output: [num_tokens, num_experts], fp32/fp16/bf16
|
||||
topk_weights: [num_tokens, topk], float32, pre-allocated output
|
||||
topk_ids: [num_tokens, topk], int32, pre-allocated output
|
||||
workspace: [num_tokens * num_experts] float32 scratch (may be size 1
|
||||
when num_experts is a supported power-of-2 ≤ 256)
|
||||
renormalize: whether to renormalize weights to sum to 1 per row
|
||||
correction_bias: [num_experts] float32 per-expert bias, or None
|
||||
routed_scaling_factor: [num_tokens, num_experts] float32, or None
|
||||
"""
|
||||
module = _jit_moe_topk_sigmoid_module(gating_output.dtype)
|
||||
module.topk_sigmoid(
|
||||
gating_output,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
workspace,
|
||||
renormalize,
|
||||
correction_bias,
|
||||
routed_scaling_factor,
|
||||
num_fused_shared_experts,
|
||||
)
|
||||
|
||||
|
||||
def topk_sigmoid(
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
gating_output: torch.Tensor,
|
||||
renormalize: bool = False,
|
||||
correction_bias: Optional[torch.Tensor] = None,
|
||||
routed_scaling_factor: float = 1.0,
|
||||
num_fused_shared_experts: int = 0,
|
||||
) -> None:
|
||||
"""
|
||||
Fused sigmoid top-k MoE gate with the same call signature as
|
||||
``sgl_kernel.topk_sigmoid`` (destination-passing, in-place).
|
||||
|
||||
Args:
|
||||
topk_weights: [num_tokens, topk] float32, written in-place
|
||||
topk_ids: [num_tokens, topk] int32, written in-place
|
||||
gating_output: [num_tokens, num_experts] fp32/fp16/bf16
|
||||
renormalize: whether to renormalize weights to sum to 1 per row
|
||||
correction_bias: [num_experts] float32 per-expert bias, or None
|
||||
"""
|
||||
num_tokens = gating_output.shape[0]
|
||||
num_experts = gating_output.shape[1]
|
||||
|
||||
is_pow2 = num_experts != 0 and (num_experts & (num_experts - 1)) == 0
|
||||
needs_workspace = not is_pow2 or num_experts > 256
|
||||
workspace_size = num_tokens * num_experts if needs_workspace else 1
|
||||
workspace = torch.empty(
|
||||
workspace_size, dtype=torch.float32, device=gating_output.device
|
||||
)
|
||||
|
||||
moe_topk_sigmoid_out(
|
||||
gating_output,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
workspace,
|
||||
renormalize,
|
||||
correction_bias,
|
||||
routed_scaling_factor,
|
||||
num_fused_shared_experts,
|
||||
)
|
||||
@@ -0,0 +1,265 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Reference-vs-fused unit tests for the MiniMax-M3 ROCm native MXFP8 ops.
|
||||
|
||||
Each fused kernel has a slow PyTorch / dequant-to-bf16 reference; these assert
|
||||
the two agree within tolerance:
|
||||
|
||||
* Fused MXFP8 activation quant (Triton) -> torch reference
|
||||
* Native MXFP8 linear (tl.dot_scaled) -> dequant-to-bf16 @ matmul
|
||||
* Native MXFP8 MoE (dot_scaled grouped GEMM) -> dequant-to-bf16 MoE math
|
||||
|
||||
ROCm-only. The pure quant test runs on any ROCm arch; the native MXFP8
|
||||
``dot_scaled`` linear/MoE tests are gated to CDNA4 gfx95x (the hardware
|
||||
microscaling matrix cores) -- gfx942 has no native ``dot_scaled`` MX path.
|
||||
|
||||
Run: pytest python/sglang/jit_kernel/tests/test_minimax_m3_mxfp8.py -v
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
if not is_hip():
|
||||
pytest.skip(
|
||||
"MiniMax-M3 native MXFP8 ops are the ROCm path.", allow_module_level=True
|
||||
)
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("Requires a GPU.", allow_module_level=True)
|
||||
|
||||
from sglang.srt.layers.quantization.mxfp8_amd_gfx95 import ( # noqa: E402
|
||||
_mxfp8_dot_scaled_linear,
|
||||
_mxfp8_e4m3_quantize_torch,
|
||||
_mxfp8_e4m3_quantize_triton,
|
||||
dequant_mxfp8_to_bf16,
|
||||
)
|
||||
|
||||
DEVICE = "cuda"
|
||||
|
||||
|
||||
def _gcn_arch() -> str:
|
||||
try:
|
||||
return torch.cuda.get_device_properties(0).gcnArchName
|
||||
except Exception: # pragma: no cover - no device / non-AMD
|
||||
return ""
|
||||
|
||||
|
||||
requires_gfx950 = pytest.mark.skipif(
|
||||
"gfx95" not in _gcn_arch(),
|
||||
reason="native MXFP8 dot_scaled is a CDNA4 (gfx95x) feature; "
|
||||
"gfx942 has no native dot_scaled MX path.",
|
||||
)
|
||||
|
||||
|
||||
def _relerr(a: torch.Tensor, b: torch.Tensor) -> float:
|
||||
a = a.float()
|
||||
b = b.float()
|
||||
return ((a - b).norm() / (b.norm() + 1e-8)).item()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Fused MXFP8 activation quant (Triton vs torch reference)
|
||||
# --------------------------------------------------------------------------- #
|
||||
@pytest.mark.parametrize("shape", [(64, 4096), (1, 6144), (333, 2048)])
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@torch.inference_mode()
|
||||
def test_mxfp8_quant_triton_matches_torch(shape, dtype):
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn(*shape, device=DEVICE, dtype=dtype)
|
||||
xq_t, s_t = _mxfp8_e4m3_quantize_torch(x)
|
||||
xq_k, s_k = _mxfp8_e4m3_quantize_triton(x)
|
||||
assert s_k.shape == s_t.shape == (shape[0], shape[1] // 32)
|
||||
# E8M0 block exponents share the round-up ceil(log2(amax/e4m3_max))+127
|
||||
# algorithm; allow at most a 1-step difference at exact powers of two.
|
||||
assert (s_k.int() - s_t.int()).abs().max().item() <= 1
|
||||
# Dequantized values agree to fp8 granularity.
|
||||
deq_t = dequant_mxfp8_to_bf16(xq_t, s_t)
|
||||
deq_k = dequant_mxfp8_to_bf16(xq_k, s_k)
|
||||
assert _relerr(deq_k, deq_t) < 1e-2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("m,inter", [(8, 512), (65, 2048)])
|
||||
@torch.inference_mode()
|
||||
def test_minimax_swiglu_mxfp8_quant_matches_unfused_fp32(m, inter):
|
||||
# The fused swiglu+quant kernel keeps the activation in fp32 through the
|
||||
# E8M0 scale selection (no bf16 round-trip; matches the vLLM/ame kernel), so
|
||||
# the reference is the unfused fp32 swiglu followed by MXFP8 quant. Not
|
||||
# bit-identical because the reference quant runs in torch vs the fused triton
|
||||
# path, but numerically equivalent (tight relerr, scales agree within 1 ulp).
|
||||
from sglang.jit_kernel.minimax_m3 import (
|
||||
swiglu_oai_mxfp8_quant,
|
||||
swiglu_oai_split,
|
||||
)
|
||||
from sglang.srt.layers.quantization.mxfp8_amd_gfx95 import mxfp8_e4m3_quantize
|
||||
|
||||
torch.manual_seed(0)
|
||||
alpha, beta, limit = 1.702, 1.0, 7.0
|
||||
gate_up = torch.randn(m, 2 * inter, device=DEVICE, dtype=torch.bfloat16) * 0.5
|
||||
|
||||
act = swiglu_oai_split(
|
||||
gate_up, alpha=alpha, beta=beta, limit=limit, out_dtype=torch.float32
|
||||
)
|
||||
q_ref, s_ref = mxfp8_e4m3_quantize(act)
|
||||
q, s = swiglu_oai_mxfp8_quant(gate_up, alpha=alpha, beta=beta, limit=limit)
|
||||
|
||||
assert q.shape == q_ref.shape
|
||||
assert s.shape == s_ref.shape
|
||||
# E8M0 block scales agree within one exponent step (last-bit amax differences).
|
||||
assert (s.int() - s_ref.int()).abs().max().item() <= 1
|
||||
assert (
|
||||
_relerr(dequant_mxfp8_to_bf16(q, s), dequant_mxfp8_to_bf16(q_ref, s_ref)) < 1e-2
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Native MXFP8 linear (dot_scaled) vs dequant-to-bf16 matmul
|
||||
# --------------------------------------------------------------------------- #
|
||||
@requires_gfx950
|
||||
@pytest.mark.parametrize("m,n,k", [(64, 256, 128), (37, 512, 256), (1, 6144, 4096)])
|
||||
@torch.inference_mode()
|
||||
def test_mxfp8_native_linear(m, n, k):
|
||||
torch.manual_seed(0)
|
||||
w_bf16 = torch.randn(n, k, device=DEVICE, dtype=torch.bfloat16) * 0.1
|
||||
w_fp8, w_scale = _mxfp8_e4m3_quantize_torch(w_bf16)
|
||||
x = torch.randn(m, k, device=DEVICE, dtype=torch.bfloat16) * 0.5
|
||||
|
||||
got = _mxfp8_dot_scaled_linear(x, w_fp8, w_scale)
|
||||
# Reference consumes the SAME quantized weights (isolates activation-quant
|
||||
# noise) -> dequant to bf16, plain matmul.
|
||||
w_deq = dequant_mxfp8_to_bf16(w_fp8, w_scale)
|
||||
ref = torch.nn.functional.linear(x, w_deq).to(x.dtype)
|
||||
assert got.shape == (m, n)
|
||||
assert _relerr(got, ref) < 5e-2
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Native MXFP8 MoE (dot_scaled grouped GEMM) vs dequant-to-bf16 MoE math
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _ref_moe(x, w13, w2, topk_weights, topk_ids, alpha, beta, limit):
|
||||
T, H = x.shape
|
||||
inter = w2.shape[-1]
|
||||
top_k = topk_ids.shape[1]
|
||||
out = torch.zeros(T, H, device=x.device, dtype=torch.float32)
|
||||
for t in range(T):
|
||||
for j in range(top_k):
|
||||
e = int(topk_ids[t, j].item())
|
||||
if e < 0 or e >= w13.shape[0]:
|
||||
continue
|
||||
g1 = x[t].float() @ w13[e].float().T # [2I]
|
||||
gate = g1[:inter]
|
||||
up = g1[inter:]
|
||||
if limit is not None:
|
||||
gate = gate.clamp(max=limit)
|
||||
up = up.clamp(min=-limit, max=limit)
|
||||
act = gate * torch.sigmoid(alpha * gate) * (up + beta)
|
||||
g2 = act @ w2[e].float().T # [H]
|
||||
out[t] += topk_weights[t, j].float() * g2
|
||||
return out.to(x.dtype)
|
||||
|
||||
|
||||
@requires_gfx950
|
||||
@pytest.mark.parametrize(
|
||||
"T,H,inter,E,top_k", [(8, 256, 512, 8, 2), (1, 512, 256, 16, 4)]
|
||||
)
|
||||
@torch.inference_mode()
|
||||
def test_mxfp8_native_moe(T, H, inter, E, top_k):
|
||||
from sglang.srt.layers.moe.moe_runner.triton_utils.mxfp8_moe_amd_gfx95 import (
|
||||
fused_moe_mxfp8_native,
|
||||
)
|
||||
|
||||
torch.manual_seed(0)
|
||||
alpha, beta, limit = 1.702, 1.0, 7.0
|
||||
w13_bf16 = torch.randn(E, 2 * inter, H, device=DEVICE, dtype=torch.bfloat16) * 0.1
|
||||
w2_bf16 = torch.randn(E, H, inter, device=DEVICE, dtype=torch.bfloat16) * 0.1
|
||||
w13_fp8, w13_scale = _mxfp8_e4m3_quantize_torch(w13_bf16)
|
||||
w2_fp8, w2_scale = _mxfp8_e4m3_quantize_torch(w2_bf16)
|
||||
|
||||
x = torch.randn(T, H, device=DEVICE, dtype=torch.bfloat16) * 0.5
|
||||
logits = torch.randn(T, E, device=DEVICE, dtype=torch.float32)
|
||||
topk_weights, topk_ids = logits.softmax(dim=-1).topk(top_k, dim=-1)
|
||||
topk_weights = topk_weights.to(torch.float32)
|
||||
topk_ids = topk_ids.to(torch.int32)
|
||||
|
||||
got = fused_moe_mxfp8_native(
|
||||
x,
|
||||
w13_fp8,
|
||||
w13_scale,
|
||||
w2_fp8,
|
||||
w2_scale,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
alpha=alpha,
|
||||
beta=beta,
|
||||
limit=limit,
|
||||
)
|
||||
# Reference consumes the dequantized weights (same bits the kernel reads).
|
||||
w13_deq = dequant_mxfp8_to_bf16(w13_fp8, w13_scale)
|
||||
w2_deq = dequant_mxfp8_to_bf16(w2_fp8, w2_scale)
|
||||
ref = _ref_moe(x, w13_deq, w2_deq, topk_weights, topk_ids, alpha, beta, limit)
|
||||
assert got.shape == (T, H)
|
||||
assert _relerr(got, ref) < 5e-2
|
||||
|
||||
|
||||
@requires_gfx950
|
||||
@torch.inference_mode()
|
||||
def test_mxfp8_native_moe_ep_expert_map_filters_non_local_routes():
|
||||
from sglang.srt.layers.moe.moe_runner.triton_utils.mxfp8_moe_amd_gfx95 import (
|
||||
fused_moe_mxfp8_native,
|
||||
)
|
||||
|
||||
torch.manual_seed(0)
|
||||
T, H, inter = 4, 256, 512
|
||||
local_E = 3
|
||||
alpha, beta, limit = 1.702, 1.0, 7.0
|
||||
|
||||
w13_bf16 = (
|
||||
torch.randn(local_E, 2 * inter, H, device=DEVICE, dtype=torch.bfloat16) * 0.1
|
||||
)
|
||||
w2_bf16 = torch.randn(local_E, H, inter, device=DEVICE, dtype=torch.bfloat16) * 0.1
|
||||
w13_fp8, w13_scale = _mxfp8_e4m3_quantize_torch(w13_bf16)
|
||||
w2_fp8, w2_scale = _mxfp8_e4m3_quantize_torch(w2_bf16)
|
||||
|
||||
x = torch.randn(T, H, device=DEVICE, dtype=torch.bfloat16) * 0.5
|
||||
topk_ids_global = torch.tensor(
|
||||
[[0, 1, 4], [2, 3, 5], [4, 0, 3], [5, 1, 2]],
|
||||
device=DEVICE,
|
||||
dtype=torch.int32,
|
||||
)
|
||||
topk_weights = torch.tensor(
|
||||
[
|
||||
[0.50, 0.25, 0.25],
|
||||
[0.40, 0.30, 0.30],
|
||||
[0.70, 0.20, 0.10],
|
||||
[0.60, 0.30, 0.10],
|
||||
],
|
||||
device=DEVICE,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
expert_map = torch.tensor([0, -1, 1, -1, 2, -1], device=DEVICE, dtype=torch.int32)
|
||||
|
||||
got = fused_moe_mxfp8_native(
|
||||
x,
|
||||
w13_fp8,
|
||||
w13_scale,
|
||||
w2_fp8,
|
||||
w2_scale,
|
||||
topk_weights,
|
||||
topk_ids_global,
|
||||
alpha=alpha,
|
||||
beta=beta,
|
||||
limit=limit,
|
||||
expert_map=expert_map,
|
||||
)
|
||||
|
||||
topk_ids_local = expert_map[topk_ids_global.long()]
|
||||
w13_deq = dequant_mxfp8_to_bf16(w13_fp8, w13_scale)
|
||||
w2_deq = dequant_mxfp8_to_bf16(w2_fp8, w2_scale)
|
||||
ref = _ref_moe(x, w13_deq, w2_deq, topk_weights, topk_ids_local, alpha, beta, limit)
|
||||
assert got.shape == (T, H)
|
||||
assert _relerr(got, ref) < 5e-2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -0,0 +1,78 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Reference tests for MiniMax-M3 ROCm Gemma RMSNorm Triton kernels."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
if not is_hip():
|
||||
pytest.skip(
|
||||
"MiniMax-M3 Gemma RMSNorm Triton kernels are ROCm-only.",
|
||||
allow_module_level=True,
|
||||
)
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("Requires a GPU.", allow_module_level=True)
|
||||
|
||||
from sglang.jit_kernel.minimax_m3.rmsnorm import ( # noqa: E402
|
||||
gemma_fused_add_rmsnorm,
|
||||
gemma_rmsnorm,
|
||||
)
|
||||
|
||||
DEVICE = "cuda"
|
||||
EPS = 1e-6
|
||||
|
||||
|
||||
def _gemma_rmsnorm_ref(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
|
||||
orig_dtype = x.dtype
|
||||
x_f = x.float()
|
||||
variance = x_f.pow(2).mean(dim=-1, keepdim=True)
|
||||
out = x_f * torch.rsqrt(variance + EPS)
|
||||
out = out * (1.0 + weight.float())
|
||||
return out.to(orig_dtype)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(1, 512), (64, 6144), (257, 6144)])
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@torch.inference_mode()
|
||||
def test_gemma_rmsnorm_matches_reference(shape, dtype):
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn(*shape, device=DEVICE, dtype=dtype)
|
||||
weight = torch.randn(shape[-1], device=DEVICE, dtype=torch.float32)
|
||||
|
||||
got = gemma_rmsnorm(x, weight, EPS)
|
||||
ref = _gemma_rmsnorm_ref(x, weight)
|
||||
|
||||
torch.testing.assert_close(got, ref, atol=2e-2, rtol=2e-2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@torch.inference_mode()
|
||||
def test_gemma_rmsnorm_accepts_strided_2d_input(dtype):
|
||||
torch.manual_seed(0)
|
||||
base = torch.randn(128, 1024, device=DEVICE, dtype=dtype)
|
||||
x = base[:, ::2]
|
||||
weight = torch.randn(x.shape[-1], device=DEVICE, dtype=torch.float32)
|
||||
|
||||
assert not x.is_contiguous()
|
||||
got = gemma_rmsnorm(x, weight, EPS)
|
||||
ref = _gemma_rmsnorm_ref(x, weight)
|
||||
|
||||
torch.testing.assert_close(got, ref, atol=2e-2, rtol=2e-2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [(1, 512), (64, 6144), (257, 6144)])
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@torch.inference_mode()
|
||||
def test_gemma_fused_add_rmsnorm_matches_reference(shape, dtype):
|
||||
torch.manual_seed(0)
|
||||
x = torch.randn(*shape, device=DEVICE, dtype=dtype)
|
||||
residual = torch.randn(*shape, device=DEVICE, dtype=dtype)
|
||||
weight = torch.randn(shape[-1], device=DEVICE, dtype=torch.float32)
|
||||
|
||||
got, residual_out = gemma_fused_add_rmsnorm(x, residual, weight, EPS)
|
||||
ref_residual = x + residual
|
||||
ref = _gemma_rmsnorm_ref(ref_residual, weight)
|
||||
|
||||
torch.testing.assert_close(residual_out, ref_residual, atol=2e-2, rtol=2e-2)
|
||||
torch.testing.assert_close(got, ref, atol=2e-2, rtol=2e-2)
|
||||
@@ -0,0 +1,493 @@
|
||||
"""
|
||||
Correctness tests for the moe_topk_sigmoid JIT kernel.
|
||||
|
||||
Validates against a pure-PyTorch reference and, when sgl_kernel is available,
|
||||
cross-checks against the AOT implementation.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.moe_topk_sigmoid import topk_sigmoid
|
||||
|
||||
try:
|
||||
from sgl_kernel import topk_sigmoid as topk_sigmoid_aot
|
||||
|
||||
AOT_AVAILABLE = True
|
||||
except ImportError:
|
||||
AOT_AVAILABLE = False
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CI / full-range helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_is_ci = (
|
||||
os.getenv("CI", "false").lower() == "true"
|
||||
or os.getenv("GITHUB_ACTIONS", "false").lower() == "true"
|
||||
)
|
||||
|
||||
# Power-of-2 configs covered by static dispatch (num_experts 1–256)
|
||||
# Plus 48 (non-power-of-2) to exercise the fallback path
|
||||
NUM_TOKENS_FULL = [1, 16, 128, 512, 1024, 2048]
|
||||
NUM_TOKENS_CI = [1, 128, 1024]
|
||||
|
||||
NUM_EXPERTS_FULL = [16, 32, 64, 128, 256, 48] # 48 = fallback path
|
||||
NUM_EXPERTS_CI = [16, 64, 48]
|
||||
|
||||
TOPK_FULL = [1, 2, 4, 8]
|
||||
TOPK_CI = [1, 4]
|
||||
|
||||
DTYPES_FULL = [torch.float32]
|
||||
DTYPES_CI = [torch.float32, torch.bfloat16]
|
||||
|
||||
NUM_TOKENS = NUM_TOKENS_CI if _is_ci else NUM_TOKENS_FULL
|
||||
NUM_EXPERTS = NUM_EXPERTS_CI if _is_ci else NUM_EXPERTS_FULL
|
||||
TOPK_LIST = TOPK_CI if _is_ci else TOPK_FULL
|
||||
DTYPES = DTYPES_CI if _is_ci else DTYPES_FULL
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pure-PyTorch reference
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def grouped_topk_gpu(
|
||||
gating_output: torch.Tensor,
|
||||
topk: int,
|
||||
renormalize: bool,
|
||||
num_expert_group: Optional[int] = None,
|
||||
topk_group: Optional[int] = None,
|
||||
num_fused_shared_experts: int = 0,
|
||||
routed_scaling_factor: Optional[float] = None,
|
||||
apply_routed_scaling_factor_on_output: Optional[bool] = False,
|
||||
scoring_func: str = "softmax",
|
||||
):
|
||||
|
||||
# Scoring function: softmax or sigmoid
|
||||
if scoring_func == "softmax":
|
||||
scores = torch.softmax(gating_output, dim=-1)
|
||||
elif scoring_func == "sigmoid":
|
||||
scores = gating_output.sigmoid()
|
||||
else:
|
||||
raise ValueError(f"Unsupported scoring function: {scoring_func}")
|
||||
|
||||
num_token = scores.shape[0]
|
||||
num_experts = scores.shape[1]
|
||||
group_scores = (
|
||||
scores.view(num_token, num_expert_group, -1).max(dim=-1).values
|
||||
) # [n, n_group]
|
||||
|
||||
group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=False)[
|
||||
1
|
||||
] # [n, top_k_group]
|
||||
|
||||
group_mask = torch.zeros_like(group_scores) # [n, n_group]
|
||||
group_mask.scatter_(1, group_idx, 1) # [n, n_group]
|
||||
score_mask = (
|
||||
group_mask.unsqueeze(-1)
|
||||
.expand(num_token, num_expert_group, scores.shape[-1] // num_expert_group)
|
||||
.reshape(num_token, -1)
|
||||
) # [n, e]
|
||||
tmp_scores = scores.masked_fill(
|
||||
~score_mask.bool(), float("-inf")
|
||||
) # [n, e] - use -inf like VLLM
|
||||
|
||||
topk_weights, topk_ids = torch.topk(
|
||||
tmp_scores,
|
||||
k=topk,
|
||||
dim=-1,
|
||||
sorted=(True if num_fused_shared_experts > 0 else True),
|
||||
)
|
||||
|
||||
if num_fused_shared_experts:
|
||||
topk_ids[:, -1] = torch.randint(
|
||||
low=num_experts,
|
||||
high=num_experts + num_fused_shared_experts,
|
||||
size=(topk_ids.size(0),),
|
||||
dtype=topk_ids.dtype,
|
||||
device=topk_ids.device,
|
||||
)
|
||||
if routed_scaling_factor is not None:
|
||||
topk_weights[:, -1] = (
|
||||
topk_weights[:, :-1].sum(dim=-1) / routed_scaling_factor
|
||||
)
|
||||
|
||||
if renormalize:
|
||||
topk_weights_sum = (
|
||||
topk_weights.sum(dim=-1, keepdim=True)
|
||||
if num_fused_shared_experts == 0
|
||||
else topk_weights[:, :-1].sum(dim=-1, keepdim=True)
|
||||
)
|
||||
topk_weights = topk_weights / topk_weights_sum
|
||||
if apply_routed_scaling_factor_on_output:
|
||||
topk_weights *= routed_scaling_factor
|
||||
|
||||
topk_weights, topk_ids = topk_weights.to(torch.float32), topk_ids.to(torch.int32)
|
||||
return topk_weights, topk_ids
|
||||
|
||||
|
||||
def biased_grouped_topk_impl(
|
||||
gating_output: torch.Tensor,
|
||||
correction_bias: torch.Tensor,
|
||||
topk: int,
|
||||
renormalize: bool,
|
||||
num_expert_group: Optional[int] = None,
|
||||
topk_group: Optional[int] = None,
|
||||
num_fused_shared_experts: int = 0,
|
||||
routed_scaling_factor: Optional[float] = None,
|
||||
apply_routed_scaling_factor_on_output: Optional[bool] = False,
|
||||
):
|
||||
scores = gating_output.sigmoid()
|
||||
|
||||
num_token = scores.shape[0]
|
||||
num_experts = scores.shape[1]
|
||||
scores_for_choice = scores.view(num_token, -1) + correction_bias.unsqueeze(0)
|
||||
group_scores = (
|
||||
scores_for_choice.view(num_token, num_expert_group, -1)
|
||||
.topk(2, dim=-1)[0]
|
||||
.sum(dim=-1)
|
||||
) # [n, n_group]
|
||||
|
||||
group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=False)[
|
||||
1
|
||||
] # [n, top_k_group]
|
||||
|
||||
group_mask = torch.zeros_like(group_scores) # [n, n_group]
|
||||
group_mask.scatter_(1, group_idx, 1) # [n, n_group]
|
||||
score_mask = (
|
||||
group_mask.unsqueeze(-1)
|
||||
.expand(num_token, num_expert_group, scores.shape[-1] // num_expert_group)
|
||||
.reshape(num_token, -1)
|
||||
) # [n, e]
|
||||
tmp_scores = scores_for_choice.masked_fill(
|
||||
~score_mask.bool(), float("-inf")
|
||||
) # [n, e]
|
||||
|
||||
_, topk_ids = torch.topk(
|
||||
tmp_scores,
|
||||
k=topk,
|
||||
dim=-1,
|
||||
sorted=(True if num_fused_shared_experts > 0 else True),
|
||||
)
|
||||
topk_weights = scores.gather(1, topk_ids)
|
||||
|
||||
if num_fused_shared_experts:
|
||||
topk_ids[:, -1] = torch.randint(
|
||||
low=num_experts,
|
||||
high=num_experts + num_fused_shared_experts,
|
||||
size=(topk_ids.size(0),),
|
||||
dtype=topk_ids.dtype,
|
||||
device=topk_ids.device,
|
||||
)
|
||||
if routed_scaling_factor is not None:
|
||||
topk_weights[:, -1] = (
|
||||
topk_weights[:, :-1].sum(dim=-1) / routed_scaling_factor
|
||||
)
|
||||
if renormalize:
|
||||
topk_weights_sum = (
|
||||
topk_weights.sum(dim=-1, keepdim=True)
|
||||
if num_fused_shared_experts == 0
|
||||
else topk_weights[:, :-1].sum(dim=-1, keepdim=True)
|
||||
)
|
||||
topk_weights = topk_weights / topk_weights_sum
|
||||
if apply_routed_scaling_factor_on_output:
|
||||
topk_weights *= routed_scaling_factor
|
||||
|
||||
topk_weights, topk_ids = topk_weights.to(torch.float32), topk_ids.to(torch.int32)
|
||||
return topk_weights, topk_ids
|
||||
|
||||
|
||||
def topk_sigmoid_torch_ref(
|
||||
gating_output: torch.Tensor,
|
||||
topk: int,
|
||||
renormalize: bool,
|
||||
correction_bias: torch.Tensor | None,
|
||||
num_fused_shared_experts: int = 0,
|
||||
routed_scaling_factor: float = 1.0,
|
||||
apply_routed_scaling_factor_on_output: bool = True,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Reference: sigmoid → (add bias) → topk → (renormalize).
|
||||
Indices are selected on biased scores; weights are the unbiased sigmoid values.
|
||||
"""
|
||||
num_experts = gating_output.shape[1]
|
||||
scores = gating_output.float().sigmoid()
|
||||
biased = scores if correction_bias is None else scores + correction_bias.float()
|
||||
_, ref_ids = torch.topk(biased, k=topk, dim=-1)
|
||||
ref_weights = scores.gather(1, ref_ids)
|
||||
if num_fused_shared_experts > 0:
|
||||
ref_ids[:, -1] = torch.randint(
|
||||
low=num_experts,
|
||||
high=num_experts + num_fused_shared_experts,
|
||||
size=(ref_ids.size(0),),
|
||||
dtype=ref_ids.dtype,
|
||||
device=ref_ids.device,
|
||||
)
|
||||
ref_weights[:, -1] = ref_weights[:, :-1].sum(dim=-1) / routed_scaling_factor
|
||||
if renormalize:
|
||||
topk_weights_sum = (
|
||||
ref_weights.sum(dim=-1, keepdim=True)
|
||||
if num_fused_shared_experts == 0
|
||||
else ref_weights[:, :-1].sum(dim=-1, keepdim=True)
|
||||
)
|
||||
ref_weights = ref_weights / topk_weights_sum
|
||||
if apply_routed_scaling_factor_on_output:
|
||||
ref_weights *= routed_scaling_factor
|
||||
return ref_weights.float(), ref_ids.int()
|
||||
|
||||
|
||||
def topk_sigmoid_grouped_ref(
|
||||
gating_output: torch.Tensor,
|
||||
topk: int,
|
||||
renormalize: bool,
|
||||
correction_bias: torch.Tensor | None,
|
||||
num_fused_shared_experts: int = 0,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
if correction_bias is not None:
|
||||
return biased_grouped_topk_impl(
|
||||
gating_output,
|
||||
correction_bias,
|
||||
topk,
|
||||
renormalize,
|
||||
num_expert_group=1,
|
||||
topk_group=1,
|
||||
num_fused_shared_experts=num_fused_shared_experts,
|
||||
routed_scaling_factor=1.0,
|
||||
apply_routed_scaling_factor_on_output=True,
|
||||
)
|
||||
else:
|
||||
return grouped_topk_gpu(
|
||||
gating_output,
|
||||
topk,
|
||||
renormalize,
|
||||
num_expert_group=1,
|
||||
topk_group=1,
|
||||
num_fused_shared_experts=num_fused_shared_experts,
|
||||
routed_scaling_factor=1.0,
|
||||
apply_routed_scaling_factor_on_output=True,
|
||||
scoring_func="sigmoid",
|
||||
)
|
||||
|
||||
|
||||
def topk_sigmoid_ref(
|
||||
gating_output: torch.Tensor,
|
||||
topk: int,
|
||||
renormalize: bool,
|
||||
correction_bias: torch.Tensor | None,
|
||||
num_fused_shared_experts: int = 0,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
return topk_sigmoid_torch_ref(
|
||||
gating_output, topk, renormalize, correction_bias, num_fused_shared_experts
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Correctness: JIT vs PyTorch reference
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens, num_experts, topk",
|
||||
list(itertools.product(NUM_TOKENS, NUM_EXPERTS, TOPK_LIST)),
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", DTYPES)
|
||||
@pytest.mark.parametrize("renormalize", [False, True])
|
||||
def test_topk_sigmoid_vs_ref(num_tokens, num_experts, topk, dtype, renormalize):
|
||||
if topk > num_experts:
|
||||
pytest.skip("topk > num_experts")
|
||||
|
||||
torch.manual_seed(num_tokens * num_experts)
|
||||
gating = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
|
||||
|
||||
topk_w = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
|
||||
topk_i = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
|
||||
topk_sigmoid(topk_w, topk_i, gating, renormalize=renormalize)
|
||||
|
||||
ref_w, ref_i = topk_sigmoid_ref(gating, topk, renormalize, correction_bias=None)
|
||||
|
||||
# Compare sorted weights (indices may differ for ties when dtype != float32)
|
||||
assert torch.allclose(
|
||||
topk_w.sort(dim=-1)[0],
|
||||
ref_w.sort(dim=-1)[0],
|
||||
atol=1e-3,
|
||||
rtol=1e-3,
|
||||
), f"Weight mismatch (dtype={dtype}, n_exp={num_experts}, topk={topk}, renorm={renormalize})"
|
||||
# Exact index match is only reliable for float32 (fp16/bf16 tie-breaking may differ)
|
||||
if dtype == torch.float32:
|
||||
assert torch.equal(
|
||||
topk_i, ref_i
|
||||
), f"Index mismatch (dtype={dtype}, n_exp={num_experts}, topk={topk})"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Correctness: with correction_bias
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens, num_experts, topk",
|
||||
list(itertools.product(NUM_TOKENS, NUM_EXPERTS, TOPK_LIST)),
|
||||
)
|
||||
@pytest.mark.parametrize("renormalize", [False, True])
|
||||
def test_topk_sigmoid_with_correction_bias(num_tokens, num_experts, topk, renormalize):
|
||||
if topk > num_experts:
|
||||
pytest.skip("topk > num_experts")
|
||||
|
||||
torch.manual_seed(num_tokens + num_experts + topk)
|
||||
gating = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda")
|
||||
bias = torch.randn(num_experts, dtype=torch.float32, device="cuda")
|
||||
|
||||
topk_w = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
|
||||
topk_i = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
|
||||
topk_sigmoid(topk_w, topk_i, gating, renormalize=renormalize, correction_bias=bias)
|
||||
|
||||
ref_w, ref_i = topk_sigmoid_ref(gating, topk, renormalize, correction_bias=bias)
|
||||
|
||||
assert torch.allclose(
|
||||
topk_w, ref_w, atol=1e-3, rtol=1e-3
|
||||
), f"Weight mismatch with bias (n_exp={num_experts}, topk={topk}, renorm={renormalize})"
|
||||
assert torch.equal(
|
||||
topk_i, ref_i
|
||||
), f"Index mismatch with bias (n_exp={num_experts}, topk={topk})"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Correctness: with fused shared experts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens, num_experts, topk",
|
||||
list(itertools.product(NUM_TOKENS, NUM_EXPERTS, TOPK_LIST)),
|
||||
)
|
||||
@pytest.mark.parametrize("renormalize", [False, True])
|
||||
def test_topk_sigmoid_with_fused_shared_experts(
|
||||
num_tokens, num_experts, topk, renormalize
|
||||
):
|
||||
if topk + 1 > num_experts:
|
||||
pytest.skip("topk > num_experts")
|
||||
|
||||
torch.manual_seed(num_tokens + num_experts)
|
||||
gating = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda")
|
||||
bias = torch.randn(num_experts, dtype=torch.float32, device="cuda")
|
||||
|
||||
topk_w = torch.empty((num_tokens, topk + 1), dtype=torch.float32, device="cuda")
|
||||
topk_i = torch.empty((num_tokens, topk + 1), dtype=torch.int32, device="cuda")
|
||||
topk_sigmoid(
|
||||
topk_w,
|
||||
topk_i,
|
||||
gating,
|
||||
renormalize=renormalize,
|
||||
correction_bias=bias,
|
||||
num_fused_shared_experts=1,
|
||||
)
|
||||
|
||||
ref_w, ref_i = topk_sigmoid_ref(
|
||||
gating, topk + 1, renormalize, correction_bias=bias, num_fused_shared_experts=1
|
||||
)
|
||||
|
||||
assert torch.allclose(
|
||||
topk_w, ref_w, atol=1e-3, rtol=1e-3
|
||||
), f"Weight mismatch with bias (n_exp={num_experts}, topk={topk}, renorm={renormalize})"
|
||||
assert torch.equal(
|
||||
topk_i, ref_i
|
||||
), f"Index mismatch with bias (n_exp={num_experts}, topk={topk})"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Renormalization: weights should sum to 1 per row
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_tokens, num_experts, topk", [(128, 64, 4), (1, 8, 2)])
|
||||
def test_renormalize_sums_to_one(num_tokens, num_experts, topk):
|
||||
gating = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda")
|
||||
topk_w = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
|
||||
topk_i = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
|
||||
topk_sigmoid(topk_w, topk_i, gating, renormalize=True)
|
||||
row_sums = topk_w.sum(dim=-1)
|
||||
torch.testing.assert_close(
|
||||
row_sums, torch.ones(num_tokens, device="cuda"), rtol=1e-4, atol=1e-4
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Output shape and dtype
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_output_shapes_and_dtypes():
|
||||
num_tokens, num_experts, topk = 64, 128, 4
|
||||
gating = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda")
|
||||
topk_w = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
|
||||
topk_i = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
|
||||
topk_sigmoid(topk_w, topk_i, gating)
|
||||
|
||||
assert topk_w.shape == (num_tokens, topk)
|
||||
assert topk_i.shape == (num_tokens, topk)
|
||||
assert topk_w.dtype == torch.float32
|
||||
assert topk_i.dtype == torch.int32
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fallback path (non-power-of-2 experts)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("num_experts", [48, 96])
|
||||
def test_fallback_non_power_of_two(num_experts):
|
||||
num_tokens, topk = 64, 2
|
||||
gating = torch.randn((num_tokens, num_experts), dtype=torch.float32, device="cuda")
|
||||
topk_w = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
|
||||
topk_i = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
|
||||
topk_sigmoid(topk_w, topk_i, gating, renormalize=True)
|
||||
|
||||
# Weights should be positive and sum to 1
|
||||
assert torch.all(topk_w > 0)
|
||||
torch.testing.assert_close(
|
||||
topk_w.sum(dim=-1), torch.ones(num_tokens, device="cuda"), rtol=1e-4, atol=1e-4
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cross-validation against AOT sgl_kernel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.skipif(not AOT_AVAILABLE, reason="sgl_kernel not available")
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens, num_experts, topk",
|
||||
list(itertools.product([1, 128, 1024], [8, 64, 128], [1, 4])),
|
||||
)
|
||||
@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
|
||||
@pytest.mark.parametrize("renormalize", [False, True])
|
||||
def test_topk_sigmoid_vs_aot(num_tokens, num_experts, topk, dtype, renormalize):
|
||||
if topk > num_experts:
|
||||
pytest.skip("topk > num_experts")
|
||||
|
||||
torch.manual_seed(42)
|
||||
gating = torch.randn((num_tokens, num_experts), dtype=dtype, device="cuda")
|
||||
|
||||
topk_w_jit = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
|
||||
topk_i_jit = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
|
||||
topk_sigmoid(topk_w_jit, topk_i_jit, gating, renormalize=renormalize)
|
||||
|
||||
topk_w_aot = torch.empty((num_tokens, topk), dtype=torch.float32, device="cuda")
|
||||
topk_i_aot = torch.empty((num_tokens, topk), dtype=torch.int32, device="cuda")
|
||||
topk_sigmoid_aot(topk_w_aot, topk_i_aot, gating, renormalize=renormalize)
|
||||
|
||||
assert torch.allclose(
|
||||
topk_w_jit, topk_w_aot, atol=1e-3, rtol=1e-3
|
||||
), f"JIT vs AOT weight mismatch (dtype={dtype}, n_exp={num_experts}, topk={topk})"
|
||||
assert torch.equal(
|
||||
topk_i_jit, topk_i_aot
|
||||
), f"JIT vs AOT index mismatch (dtype={dtype}, n_exp={num_experts}, topk={topk})"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -127,6 +127,51 @@ def get_dsa_index_head_dim(config: PretrainedConfig) -> int:
|
||||
return config.index_head_dim
|
||||
|
||||
|
||||
def is_minimax_sparse(config: PretrainedConfig) -> bool:
|
||||
arch = (config.architectures or [None])[0]
|
||||
return arch in (
|
||||
"MiniMaxM3SparseForCausalLM",
|
||||
"MiniMaxM3SparseForConditionalGeneration",
|
||||
)
|
||||
|
||||
|
||||
def get_minimax_sparse_attention_config(config: PretrainedConfig) -> dict:
|
||||
text_cfg = getattr(config, "text_config", None)
|
||||
cfg = (
|
||||
getattr(text_cfg, "sparse_attention_config", None)
|
||||
if text_cfg is not None
|
||||
else None
|
||||
)
|
||||
if cfg is None:
|
||||
cfg = getattr(config, "sparse_attention_config", None)
|
||||
if cfg is None:
|
||||
raise ValueError("Could not find sparse config. Is it MiniMax M3 Sparse model?")
|
||||
return cfg
|
||||
|
||||
|
||||
def get_minimax_sparse_layer_ids(sparse_cfg: dict) -> tuple[list[int], list[int]]:
|
||||
sparse_freq = sparse_cfg["sparse_attention_freq"]
|
||||
dense_layer_ids = [i for i, f in enumerate(sparse_freq) if f == 0]
|
||||
sparse_layer_ids = [i for i, f in enumerate(sparse_freq) if f != 0]
|
||||
return dense_layer_ids, sparse_layer_ids
|
||||
|
||||
|
||||
def get_minimax_sparse_disable_value_layer_ids(sparse_cfg: dict) -> list[int]:
|
||||
flags = sparse_cfg.get("sparse_disable_index_value")
|
||||
if flags is None:
|
||||
return []
|
||||
return [i for i, f in enumerate(flags) if f != 0]
|
||||
|
||||
|
||||
def get_minimax_sparse_score_type(sparse_cfg: dict) -> str:
|
||||
score_type = sparse_cfg.get("sparse_score_type", "max")
|
||||
assert score_type in (
|
||||
"max",
|
||||
"lse",
|
||||
), f"sparse_score_type must be 'max' or 'lse', got {score_type!r}"
|
||||
return score_type
|
||||
|
||||
|
||||
def get_dsa_index_topk(config: PretrainedConfig) -> int:
|
||||
assert is_deepseek_dsa(config)
|
||||
return config.index_topk
|
||||
@@ -378,8 +423,15 @@ class ModelConfig:
|
||||
self.hf_config.architectures
|
||||
)
|
||||
self.use_ngram_embedding = getattr(self.hf_config, "use_ngram_embedding", False)
|
||||
# A multimodal arch is piecewise-incompatible until its LM prefill is validated.
|
||||
self.is_piecewise_cuda_graph_disabled_model = (
|
||||
is_piecewise_cuda_graph_disabled_model(self.hf_config.architectures)
|
||||
or (
|
||||
self.is_multimodal
|
||||
and not is_multimodal_piecewise_cuda_graph_supported(
|
||||
self.hf_config.architectures
|
||||
)
|
||||
)
|
||||
)
|
||||
# Multimodal archs whose language-model prefill is verified safe to capture
|
||||
# under piecewise CUDA graph. ServerArgs otherwise disables prefill piecewise
|
||||
@@ -1215,6 +1267,7 @@ class ModelConfig:
|
||||
"petit_nvfp4",
|
||||
"quark",
|
||||
"mxfp4",
|
||||
"mxfp8",
|
||||
"auto-round",
|
||||
"quark_int4fp8_moe",
|
||||
"quark_mxfp4",
|
||||
@@ -1321,11 +1374,16 @@ class ModelConfig:
|
||||
f"({self.quantization})."
|
||||
)
|
||||
|
||||
# Check if the scale_fmt is ue8m0, and warn user if deepgemm is enabled for non-ue8m0 models on blackwell
|
||||
# Warn if DeepGemm is enabled for a non-ue8m0 checkpoint on Blackwell.
|
||||
# MXFP8 stores E8M0 block scales that DeepGemm consumes losslessly, so skip the warning there.
|
||||
self.use_scale_ue8m0 = quant_cfg.get("scale_fmt", None) == "ue8m0"
|
||||
from sglang.srt.layers import deep_gemm_wrapper
|
||||
|
||||
if not self.use_scale_ue8m0 and deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0:
|
||||
if (
|
||||
not self.use_scale_ue8m0
|
||||
and deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0
|
||||
and self.quantization != "mxfp8"
|
||||
):
|
||||
logger.warning(
|
||||
"DeepGemm is enabled but the scale_fmt of checkpoint is not ue8m0. This might cause accuracy degradation on Blackwell."
|
||||
)
|
||||
@@ -1589,6 +1647,7 @@ multimodal_model_archs = [
|
||||
"Cohere2VisionForConditionalGeneration",
|
||||
"DeepseekVL2ForCausalLM",
|
||||
"Ernie4_5_VLMoeForConditionalGeneration",
|
||||
"MiniMaxM3SparseForConditionalGeneration",
|
||||
"Gemma3ForConditionalGeneration",
|
||||
"Gemma3nForConditionalGeneration",
|
||||
"Gemma4ForConditionalGeneration",
|
||||
@@ -1725,6 +1784,14 @@ def is_piecewise_cuda_graph_disabled_model(model_architectures: List[str]):
|
||||
)
|
||||
|
||||
|
||||
# Multimodal archs whose LM-decoder prefill is validated under piecewise CUDA
|
||||
# graph (capture wraps only the decoder; the image encoder runs eager).
|
||||
multimodal_piecewise_cuda_graph_supported_archs = [
|
||||
"MiniMaxM3SparseForCausalLM",
|
||||
"MiniMaxM3SparseForConditionalGeneration",
|
||||
]
|
||||
|
||||
|
||||
def is_multimodal_piecewise_cuda_graph_supported(model_architectures: List[str]):
|
||||
"""Whether a multimodal arch may keep prefill piecewise CUDA graph enabled."""
|
||||
return any(
|
||||
|
||||
@@ -826,6 +826,13 @@ class Envs:
|
||||
SGLANG_OPT_USE_JIT_KERNEL_FUSED_TOPK = EnvBool(True)
|
||||
SGLANG_OPT_USE_TOPK_V2 = EnvBool(True)
|
||||
|
||||
# MiniMax-M3 sparse decode indexer: single JIT radix-select kernel replaces the 2-stage split-K Triton topk.
|
||||
SGLANG_OPT_USE_MINIMAX_DECODE_TOPK_RADIX = EnvBool(True)
|
||||
|
||||
# MiniMax-M3 MXFP8 MoE experimental fusion toggles (default off; A/B only).
|
||||
SGLANG_MINIMAX_M3_FUSED_SWIGLU_MXFP8 = EnvBool(False)
|
||||
SGLANG_MINIMAX_M3_FUSED_MOE_COMBINE = EnvBool(False)
|
||||
|
||||
# GEMM / kernel fusion
|
||||
SGLANG_OPT_FP8_WO_A_GEMM = EnvBool(True)
|
||||
SGLANG_OPT_BF16_FP32_GEMM_ALGO = EnvStr("cublas")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import torch
|
||||
|
||||
|
||||
def topk_index_reduce(tensor: torch.Tensor, dim: int) -> torch.Tensor:
|
||||
"""
|
||||
Reduces a specific dimension by computing the union of all top-k indices along that dimension.
|
||||
The resulting tensor will have the 'dim' removed, and the last dimension expanded.
|
||||
|
||||
Example:
|
||||
Input: [10, num_heads, seq_len, max_topk] with dim=0
|
||||
Output: [num_heads, seq_len, 10 * max_topk] (Left-aligned, padded with -1)
|
||||
|
||||
Args:
|
||||
tensor (torch.Tensor): Input tensor of shape [..., dim_size, ..., max_topk].
|
||||
dim (int): The dimension to reduce (collapse).
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Reduced tensor with shape [..., new_max_topk].
|
||||
Where new_max_topk = dim_size * original_max_topk.
|
||||
"""
|
||||
# 1. Shape Transformation
|
||||
# We need to merge the target 'dim' with the last dimension 'max_topk'.
|
||||
# Step A: Move the target dim to the second-to-last position (-2).
|
||||
# e.g., [10, H, S, K] (dim=0) -> [H, S, 10, K]
|
||||
tensor_permuted = torch.movedim(tensor, source=dim, destination=-2)
|
||||
|
||||
# Step B: Flatten the last two dimensions.
|
||||
# e.g., [H, S, 10, K] -> [H, S, 10 * K]
|
||||
combined = tensor_permuted.flatten(start_dim=-2)
|
||||
|
||||
# --- The following logic is identical to 'topk_index_union' ---
|
||||
|
||||
# 2. Sort row-wise.
|
||||
# Groups identical values together. -1 padding sorts to the left.
|
||||
sorted_vals, _ = combined.sort(dim=-1)
|
||||
|
||||
# 3. Deduplication (Delta Check).
|
||||
# Keep value if it differs from the previous one.
|
||||
is_new_element = sorted_vals[..., 1:] != sorted_vals[..., :-1]
|
||||
|
||||
# First column is always new
|
||||
first_col_true = torch.ones_like(sorted_vals[..., :1], dtype=torch.bool)
|
||||
non_duplicate_mask = torch.cat([first_col_true, is_new_element], dim=-1)
|
||||
|
||||
# 4. Filter.
|
||||
# Valid if non-duplicate AND not -1 padding.
|
||||
valid_mask = non_duplicate_mask & (sorted_vals != -1)
|
||||
|
||||
# 5. Packing (Left-Alignment).
|
||||
# Move valid elements to the left.
|
||||
sort_idx = torch.argsort((~valid_mask).int(), dim=-1, stable=True)
|
||||
result = torch.gather(sorted_vals, -1, sort_idx)
|
||||
|
||||
# 6. Re-masking the right side.
|
||||
# Fill garbage values on the right with -1.
|
||||
valid_count = valid_mask.sum(dim=-1, keepdim=True)
|
||||
total_cols = result.size(-1)
|
||||
|
||||
# Broadcasting check:
|
||||
# valid_count shape: [..., 1]
|
||||
# idx_range shape: [total_cols]
|
||||
# result shape: [..., total_cols]
|
||||
idx_range = torch.arange(total_cols, device=tensor.device)
|
||||
|
||||
final_result = torch.where(idx_range < valid_count, result, -1)
|
||||
|
||||
return final_result
|
||||
@@ -0,0 +1,262 @@
|
||||
# Copyright 2025 XunhaoLai. All rights reserved.
|
||||
|
||||
import functools
|
||||
from collections import deque
|
||||
from typing import Any, Callable, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
_tma_keep_alive_buf = deque(maxlen=200)
|
||||
|
||||
# Q is always bf16/fp16. The paged main K/V cache may be fp8 (unit-scaled) under
|
||||
# --kv-cache-dtype fp8_*; the kernel widens it to the Q dtype on load (IS_FP8
|
||||
# branch). Accepted on both HIP and CUDA (the bf16->fp8 cache write is unit-scaled,
|
||||
# so the widening cast is the exact inverse dequant). The bf16/fp16-only MSA
|
||||
# (fmha_sm100) kernel is excluded for fp8 KV by the backend use_msa gate.
|
||||
SPARSE_KV_FP8_DTYPES = (
|
||||
torch.float8_e4m3fn,
|
||||
torch.float8_e5m2,
|
||||
torch.float8_e4m3fnuz,
|
||||
)
|
||||
|
||||
|
||||
def check_sparse_kv_fp8(
|
||||
q: torch.Tensor,
|
||||
k_cache: torch.Tensor,
|
||||
v_cache: torch.Tensor,
|
||||
*,
|
||||
label: str,
|
||||
) -> bool:
|
||||
"""Validate the sparse-attention KV cache dtype contract.
|
||||
|
||||
Returns True iff the K cache is fp8 (then widened to Q dtype in the kernel).
|
||||
Raises AssertionError otherwise, mirroring the contract the decode and prefill
|
||||
topk kernels both enforce. fp8 is accepted on both HIP and CUDA.
|
||||
"""
|
||||
assert q.dtype in (torch.bfloat16, torch.float16)
|
||||
is_fp8 = k_cache.dtype in SPARSE_KV_FP8_DTYPES
|
||||
assert k_cache.dtype == q.dtype or is_fp8, (
|
||||
f"sparse {label} expects K cache dtype == Q dtype ({q.dtype}) "
|
||||
f"or fp8, got {k_cache.dtype}"
|
||||
)
|
||||
assert v_cache.dtype == k_cache.dtype
|
||||
return is_fp8
|
||||
|
||||
|
||||
try:
|
||||
make_tensor_descriptor = tl.make_tensor_descriptor
|
||||
except Exception:
|
||||
make_tensor_descriptor = tl._experimental_make_tensor_descriptor
|
||||
|
||||
|
||||
def robust_allocator(size: int, alignment: int, stream: int = None):
|
||||
"""Allocator for Triton TMA descriptors.
|
||||
|
||||
We keep reference in deque to prevent GC from collecting the buffer.
|
||||
"""
|
||||
tensor = torch.empty(size, device="cuda", dtype=torch.uint8)
|
||||
_tma_keep_alive_buf.append(tensor)
|
||||
return tensor
|
||||
|
||||
|
||||
def tensor_cache(maxsize: int = 8):
|
||||
"""
|
||||
Cache function results using identity comparison.
|
||||
Zero-overhead cache hit: no hash, no DtoH, just pointer comparison.
|
||||
|
||||
Args:
|
||||
maxsize: Maximum number of cached entries. Supports multi-GPU scenarios
|
||||
where different devices have different tensor arguments.
|
||||
"""
|
||||
|
||||
def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
|
||||
# LRU-style cache: list of (args, kwargs, result) tuples
|
||||
# Most recently used at the end
|
||||
_cache: list = []
|
||||
|
||||
def _args_match(args: tuple, cached_args: tuple) -> bool:
|
||||
if len(args) != len(cached_args):
|
||||
return False
|
||||
for i in range(len(args)):
|
||||
if args[i] is not cached_args[i]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _kwargs_match(kwargs: dict, cached_kwargs: dict) -> bool:
|
||||
if not kwargs and not cached_kwargs:
|
||||
return True
|
||||
if kwargs.keys() != cached_kwargs.keys():
|
||||
return False
|
||||
for k, v in kwargs.items():
|
||||
if v is not cached_kwargs[k]:
|
||||
return False
|
||||
return True
|
||||
|
||||
@functools.wraps(fn)
|
||||
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
||||
# Search cache (most recent first for better hit rate)
|
||||
for i in range(len(_cache) - 1, -1, -1):
|
||||
cached_args, cached_kwargs, cached_result = _cache[i]
|
||||
if _args_match(args, cached_args) and _kwargs_match(
|
||||
kwargs, cached_kwargs
|
||||
):
|
||||
# Move to end (most recently used)
|
||||
if i != len(_cache) - 1:
|
||||
_cache.append(_cache.pop(i))
|
||||
return cached_result
|
||||
|
||||
# Cache miss
|
||||
result = fn(*args, **kwargs)
|
||||
|
||||
# Add to cache
|
||||
if len(_cache) >= maxsize:
|
||||
_cache.pop(0) # Remove oldest
|
||||
_cache.append((args, kwargs, result))
|
||||
return result
|
||||
|
||||
# Expose cache for manual clearing if needed
|
||||
wrapper.cache_clear = lambda: _cache.clear()
|
||||
wrapper.cache_info = lambda: {"size": len(_cache), "maxsize": maxsize}
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@tensor_cache(maxsize=8)
|
||||
def get_cu_seqblocks(
|
||||
cu_seqlens: torch.Tensor,
|
||||
max_seqlen: int,
|
||||
block_size_q: int,
|
||||
block_size_k: int,
|
||||
seqlens_cpu: Optional[List[int]] = None,
|
||||
) -> Tuple[torch.Tensor, int, int, torch.Tensor, int, int]:
|
||||
"""Compute cumulative sequence block indices for blocked sparse attention.
|
||||
|
||||
Converts token-level cumulative sequence lengths to block-level indices,
|
||||
which are needed for block-sparse attention kernels.
|
||||
|
||||
Note:
|
||||
Results are cached (maxsize=8) based on input arguments. Repeated calls
|
||||
with the same cu_seqlens, max_seqlen, and block sizes will return cached
|
||||
results without recomputation.
|
||||
|
||||
Args:
|
||||
cu_seqlens: Cumulative sequence lengths. Shape: [batch_size + 1], dtype: int32.
|
||||
max_seqlen: Maximum sequence length in the batch.
|
||||
block_size_q: Query block size.
|
||||
block_size_k: Key-value block size.
|
||||
seqlens_cpu: Optional host copy of ``torch.diff(cu_seqlens)``; when given,
|
||||
``all_seqblock_q/k`` are summed on the host to avoid a per-layer sync.
|
||||
|
||||
Returns:
|
||||
A tuple of 6 values:
|
||||
- cu_seqblocks_q: Cumulative query block indices. Shape: [batch_size + 1]
|
||||
- max_seqblock_q: Maximum number of query blocks per sequence.
|
||||
- all_seqblock_q: Total number of query blocks across all sequences.
|
||||
- cu_seqblocks_k: Cumulative key block indices. Shape: [batch_size + 1]
|
||||
- max_seqblock_k: Maximum number of key blocks per sequence.
|
||||
- all_seqblock_k: Total number of key blocks across all sequences.
|
||||
"""
|
||||
cu_seqblocks_q = torch.zeros_like(cu_seqlens)
|
||||
cu_seqblocks_k = torch.zeros_like(cu_seqlens)
|
||||
seq_lens = torch.diff(cu_seqlens)
|
||||
seqblocks_q = (seq_lens + block_size_q - 1) // block_size_q
|
||||
seqblocks_k = (seq_lens + block_size_k - 1) // block_size_k
|
||||
max_seqblock_q = (max_seqlen + block_size_q - 1) // block_size_q
|
||||
max_seqblock_k = (max_seqlen + block_size_k - 1) // block_size_k
|
||||
cu_seqblocks_q[1:] = seqblocks_q
|
||||
cu_seqblocks_k[1:] = seqblocks_k
|
||||
cu_seqblocks_q.cumsum_(0)
|
||||
cu_seqblocks_k.cumsum_(0)
|
||||
if seqlens_cpu is not None:
|
||||
# Bit-identical to seqblocks.sum().item() but no device->host sync.
|
||||
all_seqblock_q = sum(
|
||||
(s + block_size_q - 1) // block_size_q for s in seqlens_cpu
|
||||
)
|
||||
all_seqblock_k = sum(
|
||||
(s + block_size_k - 1) // block_size_k for s in seqlens_cpu
|
||||
)
|
||||
else:
|
||||
all_seqblock_q = seqblocks_q.sum().item()
|
||||
all_seqblock_k = seqblocks_k.sum().item()
|
||||
return (
|
||||
cu_seqblocks_q,
|
||||
max_seqblock_q,
|
||||
all_seqblock_q,
|
||||
cu_seqblocks_k,
|
||||
max_seqblock_k,
|
||||
all_seqblock_k,
|
||||
)
|
||||
|
||||
|
||||
# Bitonic-sort compare-and-swap primitives shared by the decode and prefill
|
||||
# topk-index kernels. Identical copies previously lived in both
|
||||
# decode/flash_with_topk_idx.py and prefill/flash_with_topk_idx.py.
|
||||
@triton.jit
|
||||
def _compare_and_swap(
|
||||
x,
|
||||
ids,
|
||||
flip,
|
||||
i: tl.constexpr,
|
||||
n_dims: tl.constexpr,
|
||||
):
|
||||
n_outer: tl.constexpr = x.numel >> n_dims
|
||||
shape: tl.constexpr = [n_outer * 2**i, 2, 2 ** (n_dims - i - 1)]
|
||||
y = tl.reshape(x, shape)
|
||||
# slice left/right with 'stride' 2**(n_dims - i - 1)
|
||||
mask = tl.arange(0, 2)[None, :, None]
|
||||
left = tl.broadcast_to(tl.sum(y * (1 - mask), 1)[:, None, :], shape).to(y.dtype)
|
||||
right = tl.broadcast_to(tl.sum(y * mask, 1)[:, None, :], shape).to(y.dtype)
|
||||
left = tl.reshape(left, x.shape)
|
||||
right = tl.reshape(right, x.shape)
|
||||
# idx
|
||||
y_idx = tl.reshape(ids, shape)
|
||||
left_idx = tl.broadcast_to(tl.sum(y_idx * (1 - mask), 1)[:, None, :], shape)
|
||||
right_idx = tl.broadcast_to(tl.sum(y_idx * mask, 1)[:, None, :], shape)
|
||||
left_idx = tl.reshape(left_idx, x.shape).to(y_idx.dtype)
|
||||
right_idx = tl.reshape(right_idx, x.shape).to(y_idx.dtype)
|
||||
# actual compare-and-swap
|
||||
idtype = tl.core.get_int_dtype(bitwidth=x.dtype.primitive_bitwidth, signed=True)
|
||||
ileft = left.to(idtype, bitcast=True)
|
||||
iright = right.to(idtype, bitcast=True)
|
||||
ix = x.to(idtype, bitcast=True)
|
||||
|
||||
cond = (left > right) != flip
|
||||
ret = ix ^ tl.where(cond, ileft ^ iright, tl.zeros_like(ix))
|
||||
new_ids = ids ^ tl.where(cond, left_idx ^ right_idx, tl.zeros_like(ids))
|
||||
return ret.to(x.dtype, bitcast=True), new_ids
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _bitonic_merge(
|
||||
x,
|
||||
ids,
|
||||
stage: tl.constexpr,
|
||||
order: tl.constexpr,
|
||||
n_dims: tl.constexpr,
|
||||
):
|
||||
n_outer: tl.constexpr = x.numel >> n_dims
|
||||
tl.static_assert(stage <= n_dims)
|
||||
# flip denotes whether to re-arrange sub-sequences of elements in ascending or
|
||||
# descending order.
|
||||
# if flip = 00000000... then all elements will be re-arranged ascendingly at this stage
|
||||
# if flip = 00110011... then all the elements will be re-arranged alternatingly (with
|
||||
# a stride of 2) at this stage
|
||||
if order == 2:
|
||||
shape: tl.constexpr = [
|
||||
n_outer * 2 ** (n_dims - 1 - stage),
|
||||
2,
|
||||
2**stage,
|
||||
]
|
||||
flip = tl.reshape(
|
||||
tl.broadcast_to(tl.arange(0, 2)[None, :, None], shape), x.shape
|
||||
)
|
||||
else:
|
||||
flip = order
|
||||
# perform `stage` rounds of `compare-and-swap`
|
||||
for i in tl.static_range(stage):
|
||||
x, ids = _compare_and_swap(x, ids, flip, i + (n_dims - stage), n_dims)
|
||||
return x, ids
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,419 @@
|
||||
# Copyright 2025 XunhaoLai. All rights reserved.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from ..common.utils import check_sparse_kv_fp8, robust_allocator
|
||||
|
||||
|
||||
@triton.heuristics(
|
||||
{
|
||||
"BLOCK_SIZE_H": lambda args: max(
|
||||
16, triton.next_power_of_2(args["gqa_group_size"])
|
||||
),
|
||||
"BLOCK_SIZE_D": lambda args: triton.next_power_of_2(args["head_dim"]),
|
||||
"BLOCK_SIZE_T": lambda args: triton.next_power_of_2(args["max_topk"]),
|
||||
"HAS_SINK": lambda args: args["sink_ptr"] is not None,
|
||||
"BATCH_SIZE_BUCKET": lambda args: triton.next_power_of_2(args["batch_size"]),
|
||||
}
|
||||
)
|
||||
@triton.autotune(
|
||||
configs=[
|
||||
triton.Config({}, num_warps=nw, num_stages=ns)
|
||||
for nw in [4, 8]
|
||||
for ns in [2, 3, 4, 5]
|
||||
],
|
||||
key=["BATCH_SIZE_BUCKET", "gqa_group_size", "head_dim", "block_size", "HAS_SINK"],
|
||||
)
|
||||
@triton.jit
|
||||
def _gqa_share_sparse_decode_kernel(
|
||||
q_ptr, # Q: b x qh x d
|
||||
sink_ptr, # Sink: qh x d
|
||||
k_cache_ptr, # K paged: max_slots x kh x d
|
||||
v_cache_ptr, # V paged: max_slots x kh x d
|
||||
req_to_token_ptr, # req_to_token: max_reqs x max_kv_len
|
||||
idx_ptr, # topk index: qh x b x topk
|
||||
o_ptr, # O partial: c x b x qh x d
|
||||
lse_ptr, # lse partial: c x b x qh
|
||||
seq_lens,
|
||||
slot_ids,
|
||||
# shape
|
||||
max_slots,
|
||||
batch_size,
|
||||
gqa_group_size,
|
||||
head_dim,
|
||||
max_topk,
|
||||
max_kv_len,
|
||||
# sm_scale
|
||||
sm_scale,
|
||||
# stride
|
||||
stride_q_b,
|
||||
stride_q_h,
|
||||
stride_q_d,
|
||||
stride_sink_h,
|
||||
stride_sink_d,
|
||||
stride_k_s,
|
||||
stride_k_h,
|
||||
stride_k_d,
|
||||
stride_v_s,
|
||||
stride_v_h,
|
||||
stride_v_d,
|
||||
stride_r2t_b,
|
||||
stride_ti_h,
|
||||
stride_ti_b,
|
||||
stride_ti_t,
|
||||
stride_o_c,
|
||||
stride_o_b,
|
||||
stride_o_h,
|
||||
stride_o_d,
|
||||
stride_l_c,
|
||||
stride_l_b,
|
||||
stride_l_h,
|
||||
# META parameters
|
||||
BATCH_SIZE_BUCKET: tl.constexpr,
|
||||
BLOCK_SIZE_H: tl.constexpr,
|
||||
BLOCK_SIZE_N: tl.constexpr,
|
||||
BLOCK_SIZE_D: tl.constexpr,
|
||||
BLOCK_SIZE_T: tl.constexpr,
|
||||
NUM_TOPK_CHUNKS: tl.constexpr,
|
||||
HAS_SINK: tl.constexpr,
|
||||
IS_FP8: tl.constexpr,
|
||||
):
|
||||
# decode program ids: split-K over the topk dimension to give every SM
|
||||
# something to do at small batch. pid(0) folds (batch, chunk) together so
|
||||
# the grid size = batch_size * NUM_TOPK_CHUNKS.
|
||||
pid_bc, pid_kh = tl.program_id(0), tl.program_id(1)
|
||||
pid_b = pid_bc % batch_size
|
||||
pid_c = pid_bc // batch_size
|
||||
pid_h = pid_kh * gqa_group_size
|
||||
# per-chunk topk range. chunk_size is *runtime* (depends on max_topk which
|
||||
# is a runtime arg, not constexpr), so don't annotate as tl.constexpr —
|
||||
# doing so produces undefined behavior in Triton.
|
||||
chunk_size_topk = (max_topk + NUM_TOPK_CHUNKS - 1) // NUM_TOPK_CHUNKS
|
||||
chunk_start_topk = pid_c * chunk_size_topk
|
||||
chunk_end_topk_compiletime = chunk_start_topk + chunk_size_topk
|
||||
# get q k start and len after rmpad
|
||||
seq_len = tl.minimum(tl.load(seq_lens + pid_b), max_kv_len)
|
||||
sid = (
|
||||
tl.load(slot_ids + pid_b).to(tl.int64) + max_slots
|
||||
) % max_slots # to avoid bugs when slot_ids is negative
|
||||
# get real topk
|
||||
off_t = tl.arange(0, BLOCK_SIZE_T)
|
||||
idx_base = idx_ptr + pid_kh * stride_ti_h + pid_b * stride_ti_b
|
||||
topk_idx = tl.load(idx_base + off_t * stride_ti_t, mask=off_t < max_topk, other=-1)
|
||||
valid_idx = tl.where(topk_idx >= 0, off_t, -1)
|
||||
real_topk = tl.sum(valid_idx != -1, axis=0)
|
||||
chunk_end_topk = tl.minimum(chunk_end_topk_compiletime, real_topk)
|
||||
# init pointer
|
||||
off_n = tl.arange(0, BLOCK_SIZE_N)
|
||||
off_d = tl.arange(0, BLOCK_SIZE_D)
|
||||
dim_mask = off_d < head_dim
|
||||
# init statistics — kept at -inf so empty chunks (chunk_start >= real_topk)
|
||||
# naturally fall out as weight=0 in the merge step.
|
||||
if HAS_SINK and pid_c == 0:
|
||||
q_ptrs = tl.make_block_ptr(
|
||||
base=q_ptr + pid_b * stride_q_b + pid_h * stride_q_h,
|
||||
shape=(gqa_group_size, head_dim),
|
||||
strides=(stride_q_h, stride_q_d),
|
||||
offsets=(0, 0),
|
||||
block_shape=(BLOCK_SIZE_H, BLOCK_SIZE_D),
|
||||
order=(1, 0),
|
||||
)
|
||||
q = tl.load(q_ptrs, boundary_check=(0, 1), padding_option="zero")
|
||||
sink_ptrs = tl.make_block_ptr(
|
||||
base=sink_ptr + pid_h * stride_sink_h,
|
||||
shape=(gqa_group_size, head_dim),
|
||||
strides=(stride_sink_h, stride_sink_d),
|
||||
offsets=(0, 0),
|
||||
block_shape=(BLOCK_SIZE_H, BLOCK_SIZE_D),
|
||||
order=(1, 0),
|
||||
)
|
||||
sink = tl.load(sink_ptrs, boundary_check=(0, 1), padding_option="zero").to(
|
||||
tl.float32
|
||||
)
|
||||
qsink = tl.sum(q.to(tl.float32) * sink, axis=1) * sm_scale # (BLOCK_SIZE_H,)
|
||||
m_i = qsink
|
||||
lse_i = qsink
|
||||
else:
|
||||
m_i = tl.full((BLOCK_SIZE_H,), float("-inf"), dtype=tl.float32)
|
||||
lse_i = tl.full((BLOCK_SIZE_H,), float("-inf"), dtype=tl.float32)
|
||||
q_ptrs = tl.make_block_ptr(
|
||||
base=q_ptr + pid_b * stride_q_b + pid_h * stride_q_h,
|
||||
shape=(gqa_group_size, head_dim),
|
||||
strides=(stride_q_h, stride_q_d),
|
||||
offsets=(0, 0),
|
||||
block_shape=(BLOCK_SIZE_H, BLOCK_SIZE_D),
|
||||
order=(1, 0),
|
||||
)
|
||||
q = tl.load(q_ptrs, boundary_check=(0, 1), padding_option="zero")
|
||||
acc_o = tl.full((BLOCK_SIZE_H, BLOCK_SIZE_D), 0, dtype=tl.float32)
|
||||
# only iterate over this chunk's topk slice. the load must respect the
|
||||
# per-chunk start offset.
|
||||
cur_idx_ptr = idx_base + chunk_start_topk * stride_ti_t
|
||||
for _ in tl.range(chunk_start_topk, chunk_end_topk):
|
||||
# load index
|
||||
c = tl.load(cur_idx_ptr).to(tl.int32) * BLOCK_SIZE_N
|
||||
cur_idx_ptr = cur_idx_ptr + stride_ti_t
|
||||
# resolve slots for this block via req_to_token
|
||||
pos = c + off_n
|
||||
pos_mask = pos < seq_len
|
||||
slots = tl.load(
|
||||
req_to_token_ptr + sid * stride_r2t_b + pos,
|
||||
mask=pos_mask,
|
||||
other=0,
|
||||
).to(tl.int64)
|
||||
slots = (slots + max_slots) % max_slots # safety against negative
|
||||
# load K as (head_dim, BLOCK_SIZE_N) via indirect addressing
|
||||
k_off = (
|
||||
slots[None, :] * stride_k_s
|
||||
+ pid_kh * stride_k_h
|
||||
+ off_d[:, None] * stride_k_d
|
||||
)
|
||||
k = tl.load(
|
||||
k_cache_ptr + k_off,
|
||||
mask=dim_mask[:, None] & pos_mask[None, :],
|
||||
other=0.0,
|
||||
)
|
||||
if IS_FP8:
|
||||
# fp8 KV cache is unit-scaled (set_kv_buffer casts bf16->fp8 with no
|
||||
# scale), so dequant is just a widening cast to the Q compute dtype
|
||||
# before the tl.dot. Matches the bf16 path bit-for-bit when the cache
|
||||
# is bf16 (IS_FP8 False -> this branch is compiled out).
|
||||
k = k.to(q.dtype)
|
||||
# load V as (BLOCK_SIZE_N, head_dim) via indirect addressing
|
||||
v_off = (
|
||||
slots[:, None] * stride_v_s
|
||||
+ pid_kh * stride_v_h
|
||||
+ off_d[None, :] * stride_v_d
|
||||
)
|
||||
v = tl.load(
|
||||
v_cache_ptr + v_off,
|
||||
mask=pos_mask[:, None] & dim_mask[None, :],
|
||||
other=0.0,
|
||||
)
|
||||
if IS_FP8:
|
||||
# Widen V before the P@V dot. This also makes the `p.to(v.dtype)`
|
||||
# below cast P to the compute dtype (not to fp8, which would be
|
||||
# catastrophic precision loss on the attention weights).
|
||||
v = v.to(q.dtype)
|
||||
# compute qk
|
||||
qk = tl.zeros((BLOCK_SIZE_H, BLOCK_SIZE_N), dtype=tl.float32)
|
||||
qk += tl.where(off_n[None, :] < seq_len - c, 0, float("-inf"))
|
||||
# [H, D], [D, N] -> [H, N]
|
||||
qk += tl.dot(q, k) * sm_scale
|
||||
# compute m_ij and l_ij
|
||||
m_ij = tl.maximum(m_i, tl.max(qk, axis=1))
|
||||
p = tl.exp(qk - m_ij[:, None])
|
||||
l_ij = tl.sum(p, axis=1)
|
||||
# scale acc_o
|
||||
acc_o_scale = tl.exp(m_i - m_ij)
|
||||
acc_o = acc_o * acc_o_scale[:, None]
|
||||
# load v and update acc_o
|
||||
p = p.to(v.dtype)
|
||||
# [H, N], [N, D] -> [H, D]
|
||||
acc_o += tl.dot(p.to(v.dtype), v)
|
||||
# update statistics
|
||||
m_i = m_ij
|
||||
lse_i = m_ij + tl.log(tl.exp(lse_i - m_ij) + l_ij)
|
||||
# final scale (matches the old non-split kernel for chunks where lse_i>-inf).
|
||||
# For empty chunks (chunk_start_topk >= real_topk) the inner loop never
|
||||
# runs, so m_i = lse_i = -inf and naive `tl.exp(m_i - lse_i)` would compute
|
||||
# exp(-inf - (-inf)) = exp(NaN) = NaN, then 0 * NaN = NaN poisons o_partial
|
||||
# and the merge result. Gate the scale with tl.where so empty chunks emit a
|
||||
# clean zero (lse_i stays -inf which the merge correctly turns into weight=0).
|
||||
scale = tl.where(
|
||||
lse_i > float("-inf"),
|
||||
tl.exp(m_i - lse_i),
|
||||
tl.zeros_like(lse_i),
|
||||
)
|
||||
acc_o = acc_o * scale[:, None]
|
||||
# save partial output and lse for the merge step
|
||||
o_ptrs = tl.make_block_ptr(
|
||||
base=o_ptr + pid_c * stride_o_c + pid_b * stride_o_b + pid_h * stride_o_h,
|
||||
shape=(gqa_group_size, head_dim),
|
||||
strides=(stride_o_h, stride_o_d),
|
||||
offsets=(0, 0),
|
||||
block_shape=(BLOCK_SIZE_H, BLOCK_SIZE_D),
|
||||
order=(1, 0),
|
||||
)
|
||||
tl.store(o_ptrs, acc_o.to(o_ptr.dtype.element_ty), boundary_check=(0, 1))
|
||||
lse_ptrs = tl.make_block_ptr(
|
||||
base=lse_ptr + pid_c * stride_l_c + pid_b * stride_l_b + pid_h * stride_l_h,
|
||||
shape=(gqa_group_size,),
|
||||
strides=(stride_l_h,),
|
||||
offsets=(0,),
|
||||
block_shape=(BLOCK_SIZE_H,),
|
||||
order=(0,),
|
||||
)
|
||||
tl.store(lse_ptrs, lse_i.to(lse_ptr.dtype.element_ty), boundary_check=(0,))
|
||||
|
||||
|
||||
@triton.heuristics(
|
||||
{
|
||||
"BLOCK_SIZE_D": lambda args: triton.next_power_of_2(args["head_dim"]),
|
||||
}
|
||||
)
|
||||
@triton.jit
|
||||
def _merge_topk_attn_out_kernel(
|
||||
o_ptr, # [NUM_TOPK_CHUNKS, BS, NQH, D] — partials in, merged out at chunk 0
|
||||
lse_ptr, # [NUM_TOPK_CHUNKS, BS, NQH]
|
||||
head_dim,
|
||||
stride_o_c,
|
||||
stride_o_b,
|
||||
stride_o_h,
|
||||
stride_o_d,
|
||||
stride_l_c,
|
||||
stride_l_b,
|
||||
stride_l_h,
|
||||
NUM_TOPK_CHUNKS: tl.constexpr,
|
||||
BLOCK_SIZE_D: tl.constexpr,
|
||||
):
|
||||
pid_b, pid_h = tl.program_id(0), tl.program_id(1)
|
||||
off_c = tl.arange(0, NUM_TOPK_CHUNKS)
|
||||
off_d = tl.arange(0, BLOCK_SIZE_D)
|
||||
o_ptrs = tl.make_block_ptr(
|
||||
base=o_ptr + pid_b * stride_o_b + pid_h * stride_o_h,
|
||||
shape=(NUM_TOPK_CHUNKS, head_dim),
|
||||
strides=(stride_o_c, stride_o_d),
|
||||
offsets=(0, 0),
|
||||
block_shape=(NUM_TOPK_CHUNKS, BLOCK_SIZE_D),
|
||||
order=(1, 0),
|
||||
)
|
||||
lse_ptrs = lse_ptr + pid_b * stride_l_b + pid_h * stride_l_h + off_c * stride_l_c
|
||||
o = tl.load(o_ptrs, boundary_check=(0, 1), padding_option="zero")
|
||||
lse = tl.load(lse_ptrs) # empty chunks contribute -inf -> weight 0
|
||||
# standard flash-decoding merge in linear (not log2) space, matching the
|
||||
# decode kernel which uses tl.exp / tl.log.
|
||||
lse_max = tl.max(lse, axis=0)
|
||||
weights = tl.exp(lse - lse_max)
|
||||
weights = weights / tl.sum(weights, axis=0)
|
||||
o_merged = tl.sum(o * weights[:, None], axis=0)
|
||||
o_out_ptrs = o_ptr + pid_b * stride_o_b + pid_h * stride_o_h + off_d * stride_o_d
|
||||
tl.store(o_out_ptrs, o_merged.to(o_ptr.dtype.element_ty), mask=off_d < head_dim)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def flash_decode_with_gqa_share_sparse(
|
||||
q: torch.Tensor, # [batch_size, num_q_heads, head_dim]
|
||||
sink: Optional[torch.Tensor],
|
||||
k_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim] (paged)
|
||||
v_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim] (paged)
|
||||
req_to_token: torch.Tensor, # [max_reqs, max_kv_len]
|
||||
seq_lens: torch.Tensor, # [batch_size, ]
|
||||
slot_ids: torch.Tensor, # [batch_size, ]
|
||||
block_size: int,
|
||||
topk_idx: torch.Tensor, # [num_kv_heads, batch_size, topk]
|
||||
sm_scale: Optional[float] = None,
|
||||
use_tma: bool = True,
|
||||
) -> torch.Tensor:
|
||||
triton.set_allocator(robust_allocator)
|
||||
is_fp8 = check_sparse_kv_fp8(q, k_cache, v_cache, label="decode")
|
||||
# shape
|
||||
batch_size, num_q_heads, head_dim = q.shape
|
||||
max_slots, num_kv_heads, _ = k_cache.shape
|
||||
assert slot_ids.shape[0] == batch_size and seq_lens.shape[0] == batch_size
|
||||
assert topk_idx.shape[0] == num_kv_heads
|
||||
assert (
|
||||
triton.next_power_of_2(block_size) == block_size
|
||||
), f"block_size must be a power of 2, but got {block_size}"
|
||||
# assert slot_ids.max() < max_slots, f"get slot_ids {slot_ids}, but kv_cache shape is {kv_cache.shape}"
|
||||
max_kv_len = req_to_token.shape[1]
|
||||
# gqa
|
||||
assert num_q_heads % num_kv_heads == 0
|
||||
gqa_group_size = num_q_heads // num_kv_heads
|
||||
max_topk = topk_idx.shape[2]
|
||||
# sm scale
|
||||
if sm_scale is None:
|
||||
sm_scale = head_dim**-0.5
|
||||
# Pick NUM_TOPK_CHUNKS so total grid ≈ TARGET_GRID. Same constraints as
|
||||
# flash_decode_with_topk_idx: must be power of 2 (Triton arange) and must
|
||||
# only depend on shape constants (so grid is fixed within a cuda graph).
|
||||
# Capped by max_topk because chunks beyond real_topk early-fall-through to
|
||||
# the merge-as-zero path; capping avoids wasting blocks at tiny topk.
|
||||
TARGET_GRID = 256
|
||||
target = max(
|
||||
1,
|
||||
min(max_topk, TARGET_GRID // max(1, batch_size * num_kv_heads)),
|
||||
)
|
||||
NUM_TOPK_CHUNKS = 1 << (target.bit_length() - 1)
|
||||
# output tensor: split-K partials, merged into chunk 0 by the merge kernel
|
||||
o_partial = torch.empty(
|
||||
NUM_TOPK_CHUNKS,
|
||||
batch_size,
|
||||
num_q_heads,
|
||||
head_dim,
|
||||
dtype=q.dtype,
|
||||
device=q.device,
|
||||
)
|
||||
lse_partial = torch.empty(
|
||||
NUM_TOPK_CHUNKS,
|
||||
batch_size,
|
||||
num_q_heads,
|
||||
dtype=torch.float32,
|
||||
device=q.device,
|
||||
)
|
||||
# launch attention kernel
|
||||
grid = (batch_size * NUM_TOPK_CHUNKS, num_kv_heads)
|
||||
_gqa_share_sparse_decode_kernel[grid](
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
topk_idx,
|
||||
o_partial,
|
||||
lse_partial,
|
||||
seq_lens,
|
||||
slot_ids,
|
||||
max_slots,
|
||||
batch_size,
|
||||
gqa_group_size,
|
||||
head_dim,
|
||||
max_topk,
|
||||
max_kv_len,
|
||||
sm_scale,
|
||||
q.stride(0),
|
||||
q.stride(1),
|
||||
q.stride(2),
|
||||
sink.stride(0) if sink is not None else 0,
|
||||
sink.stride(1) if sink is not None else 0,
|
||||
k_cache.stride(0),
|
||||
k_cache.stride(1),
|
||||
k_cache.stride(2),
|
||||
v_cache.stride(0),
|
||||
v_cache.stride(1),
|
||||
v_cache.stride(2),
|
||||
req_to_token.stride(0),
|
||||
topk_idx.stride(0),
|
||||
topk_idx.stride(1),
|
||||
topk_idx.stride(2),
|
||||
o_partial.stride(0),
|
||||
o_partial.stride(1),
|
||||
o_partial.stride(2),
|
||||
o_partial.stride(3),
|
||||
lse_partial.stride(0),
|
||||
lse_partial.stride(1),
|
||||
lse_partial.stride(2),
|
||||
BLOCK_SIZE_N=block_size,
|
||||
NUM_TOPK_CHUNKS=NUM_TOPK_CHUNKS,
|
||||
IS_FP8=is_fp8,
|
||||
)
|
||||
# merge partials into chunk 0
|
||||
merge_grid = (batch_size, num_q_heads)
|
||||
_merge_topk_attn_out_kernel[merge_grid](
|
||||
o_partial,
|
||||
lse_partial,
|
||||
head_dim,
|
||||
o_partial.stride(0),
|
||||
o_partial.stride(1),
|
||||
o_partial.stride(2),
|
||||
o_partial.stride(3),
|
||||
lse_partial.stride(0),
|
||||
lse_partial.stride(1),
|
||||
lse_partial.stride(2),
|
||||
NUM_TOPK_CHUNKS=NUM_TOPK_CHUNKS,
|
||||
)
|
||||
return o_partial[0].contiguous()
|
||||
@@ -0,0 +1,234 @@
|
||||
# Copyright 2025 XunhaoLai. All rights reserved.
|
||||
|
||||
from typing import Callable, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from .common.index import topk_index_reduce
|
||||
from .common.utils import get_cu_seqblocks
|
||||
from .decode.flash_with_topk_idx import flash_decode_with_topk_idx
|
||||
from .decode.topk_sparse import flash_decode_with_gqa_share_sparse
|
||||
from .prefill.flash_with_topk_idx import flash_prefill_with_topk_index
|
||||
from .prefill.topk_sparse import flash_prefill_with_gqa_share_sparse
|
||||
|
||||
|
||||
def minimax_sparse_prefill(
|
||||
q: torch.Tensor, # [total_extend_tokens, num_q_heads, qk_head_dim]
|
||||
k_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim] (paged main)
|
||||
v_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim] (paged main)
|
||||
sink: Optional[torch.Tensor], # [num_q_heads, qk_head_dim]
|
||||
idx_q: torch.Tensor, # [total_extend_tokens, num_idx_heads, idx_head_dim]
|
||||
idx_k_cache: torch.Tensor, # [max_slots, 1, idx_head_dim] (paged index)
|
||||
idx_v_cache: Optional[
|
||||
torch.Tensor
|
||||
], # [max_slots, 1, idx_head_dim] (paged index); None when disable_index_value
|
||||
idx_sink: Optional[torch.Tensor], # [num_idx_heads, idx_head_dim]
|
||||
req_to_token: torch.Tensor, # [max_reqs, max_kv_len]
|
||||
slot_ids: torch.Tensor, # [batch_size, ]
|
||||
cu_seqlens: torch.Tensor, # [batch_size + 1, ] (Q-side cumulative)
|
||||
seq_lens: torch.Tensor, # [batch_size, ] total K length (prefix + chunk)
|
||||
prefix_lens: torch.Tensor, # [batch_size, ]
|
||||
max_seqlen_q: int,
|
||||
max_seqlen_k: int,
|
||||
block_size_q: int,
|
||||
block_size_k: int,
|
||||
topk: int,
|
||||
init_blocks: int,
|
||||
local_blocks: int,
|
||||
sm_scale: Optional[float] = None,
|
||||
idx_sm_scale: Optional[float] = None,
|
||||
score_type: str = "max",
|
||||
disable_index_value: bool = False,
|
||||
use_msa: bool = False,
|
||||
cu_seqblocks_q: Optional[torch.Tensor] = None,
|
||||
max_seqblock_q: Optional[int] = None,
|
||||
all_seqblock_q: Optional[int] = None,
|
||||
seqlens_cpu: Optional[List[int]] = None,
|
||||
):
|
||||
"""Run MiniMax-M3 sparse prefill.
|
||||
|
||||
``cu_seqblocks_q``, ``max_seqblock_q``, and ``all_seqblock_q`` are optional
|
||||
precomputed query-block metadata shared by the index and value sparse
|
||||
kernels. Supplying them avoids recomputing the same block layout twice.
|
||||
``seqlens_cpu`` (host copy of ``torch.diff(cu_seqlens)``) is forwarded to
|
||||
``get_cu_seqblocks`` to avoid a per-layer device sync when it recomputes.
|
||||
"""
|
||||
if cu_seqblocks_q is None or max_seqblock_q is None or all_seqblock_q is None:
|
||||
cu_seqblocks_q, max_seqblock_q, all_seqblock_q, _, _, _ = get_cu_seqblocks(
|
||||
cu_seqlens, max_seqlen_q, block_size_q, block_size_k, seqlens_cpu
|
||||
)
|
||||
|
||||
# All seqlen is less than topk, use full attention
|
||||
# Step 1: Flash attention with topk index (using index head)
|
||||
idx_o, topk_idx = flash_prefill_with_topk_index(
|
||||
q=idx_q,
|
||||
k_cache=idx_k_cache,
|
||||
v_cache=idx_v_cache,
|
||||
sink=idx_sink,
|
||||
req_to_token=req_to_token,
|
||||
slot_ids=slot_ids,
|
||||
cu_seqlens=cu_seqlens,
|
||||
seq_lens=seq_lens,
|
||||
prefix_lens=prefix_lens,
|
||||
max_seqlen_q=max_seqlen_q,
|
||||
max_seqlen_k=max_seqlen_k,
|
||||
block_size_q=block_size_q,
|
||||
block_size_k=block_size_k,
|
||||
topk=topk,
|
||||
init_blocks=init_blocks,
|
||||
local_blocks=local_blocks,
|
||||
sm_scale=idx_sm_scale,
|
||||
score_type=score_type,
|
||||
disable_index_value=disable_index_value,
|
||||
cu_seqblocks_q=cu_seqblocks_q,
|
||||
max_seqblock_q=max_seqblock_q,
|
||||
all_seqblock_q=all_seqblock_q,
|
||||
)
|
||||
# Step 2: Reduce topk idx if num_idx_heads > num_kv_heads
|
||||
num_idx_heads = idx_q.shape[1]
|
||||
num_kv_heads = k_cache.shape[1]
|
||||
idx_group_size = num_idx_heads // num_kv_heads
|
||||
if idx_group_size > 1:
|
||||
topk_idx = topk_index_reduce(
|
||||
topk_idx.view(num_kv_heads, idx_group_size, -1, topk), dim=1
|
||||
)
|
||||
# Step 3: Sparse attention using topk index (main head). The MSA path only
|
||||
# replaces this step; the indexer above is unchanged. MSA has no attn-sink
|
||||
# input, so keep the Triton path when sink is present.
|
||||
if use_msa and sink is None:
|
||||
from .msa import msa_sparse_prefill_main
|
||||
|
||||
o = msa_sparse_prefill_main(
|
||||
q=q,
|
||||
k_cache=k_cache,
|
||||
v_cache=v_cache,
|
||||
topk_idx=topk_idx,
|
||||
req_to_token=req_to_token,
|
||||
slot_ids=slot_ids,
|
||||
cu_seqlens=cu_seqlens,
|
||||
seq_lens=seq_lens,
|
||||
prefix_lens=prefix_lens,
|
||||
block_size_k=block_size_k,
|
||||
sm_scale=sm_scale,
|
||||
)
|
||||
else:
|
||||
o = flash_prefill_with_gqa_share_sparse(
|
||||
q=q,
|
||||
k_cache=k_cache,
|
||||
v_cache=v_cache,
|
||||
sink=sink,
|
||||
req_to_token=req_to_token,
|
||||
slot_ids=slot_ids,
|
||||
topk_idx=topk_idx,
|
||||
block_size_q=block_size_q,
|
||||
block_size_k=block_size_k,
|
||||
cu_seqlens=cu_seqlens,
|
||||
seq_lens=seq_lens,
|
||||
prefix_lens=prefix_lens,
|
||||
max_seqlen_q=max_seqlen_q,
|
||||
sm_scale=sm_scale,
|
||||
cu_seqblocks_q=cu_seqblocks_q,
|
||||
max_seqblock_q=max_seqblock_q,
|
||||
)
|
||||
return idx_o, o
|
||||
|
||||
|
||||
def minimax_sparse_decode(
|
||||
q: torch.Tensor, # [batch_size, num_q_heads, qk_head_dim]
|
||||
sink: Optional[torch.Tensor], # [num_q_heads, qk_head_dim]
|
||||
k_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim] (paged)
|
||||
v_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim] (paged)
|
||||
idx_q: torch.Tensor, # [batch_size, num_idx_heads, idx_head_dim], num_idx_heads >= num_kv_heads
|
||||
idx_sink: Optional[torch.Tensor], # [num_idx_heads, idx_head_dim]
|
||||
idx_k_cache: torch.Tensor, # [max_slots, 1, idx_head_dim] (paged)
|
||||
idx_v_cache: Optional[
|
||||
torch.Tensor
|
||||
], # [max_slots, 1, idx_head_dim] (paged); None when disable_index_value
|
||||
req_to_token: torch.Tensor, # [max_reqs, max_kv_len]
|
||||
slot_ids: torch.Tensor, # [batch_size, ]
|
||||
seq_lens: torch.Tensor, # [batch_size, ]
|
||||
max_seqlen: int, # max of seq_lens, passed from caller to avoid sync during CUDA graph capture
|
||||
block_size_q: int, # useless for now, will always be 1
|
||||
block_size_k: int,
|
||||
topk: int,
|
||||
init_blocks: int,
|
||||
local_blocks: int,
|
||||
sm_scale: Optional[float] = None,
|
||||
idx_sm_scale: Optional[float] = None,
|
||||
score_type: str = "max",
|
||||
disable_index_value: bool = False,
|
||||
dense_main_attn_fn: Optional[Callable] = None,
|
||||
page_size: int = 1,
|
||||
use_msa: bool = False,
|
||||
msa_kv_indices: Optional[
|
||||
torch.Tensor
|
||||
] = None, # per-forward MSA page table (cached)
|
||||
msa_plan=None, # per-forward MSA fmha_sm100 plan (cached)
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
# Step 1: Flash decode with topk index (using index head). When the dense main
|
||||
# attention is used, the indexer emits the page table directly (fused
|
||||
# transform) instead of block ids, plus the per-query effective KV length.
|
||||
idx_o, topk_idx, real_seq_lens = flash_decode_with_topk_idx(
|
||||
q=idx_q,
|
||||
sink=idx_sink,
|
||||
k_cache=idx_k_cache,
|
||||
v_cache=idx_v_cache,
|
||||
req_to_token=req_to_token,
|
||||
seq_lens=seq_lens,
|
||||
max_seqlen=max_seqlen,
|
||||
slot_ids=slot_ids,
|
||||
block_size=block_size_k,
|
||||
topk=topk,
|
||||
init_blocks=init_blocks,
|
||||
local_blocks=local_blocks,
|
||||
sm_scale=idx_sm_scale,
|
||||
score_type=score_type,
|
||||
disable_index_value=disable_index_value,
|
||||
use_dense_main_attn=dense_main_attn_fn is not None,
|
||||
page_size=page_size,
|
||||
)
|
||||
num_idx_heads = idx_q.shape[1]
|
||||
num_kv_heads = k_cache.shape[1]
|
||||
idx_group_size = num_idx_heads // num_kv_heads
|
||||
if dense_main_attn_fn is not None:
|
||||
# topk_idx is the page table; real_seq_lens is the per-query cache_seqlens
|
||||
assert idx_group_size == 1
|
||||
o = dense_main_attn_fn(q, topk_idx, real_seq_lens)
|
||||
else:
|
||||
# Step 2: Reduce topk idx if num_idx_heads > num_kv_heads
|
||||
if idx_group_size > 1:
|
||||
topk_idx = topk_index_reduce(
|
||||
topk_idx.view(num_kv_heads, idx_group_size, -1, topk), dim=1
|
||||
)
|
||||
# Step 3: Sparse attention using topk index (main head). The MSA path
|
||||
# only replaces this step; keep the Triton path when sink is present.
|
||||
if use_msa and sink is None:
|
||||
from .msa import msa_sparse_decode_main
|
||||
|
||||
o = msa_sparse_decode_main(
|
||||
q=q,
|
||||
k_cache=k_cache,
|
||||
v_cache=v_cache,
|
||||
topk_idx=topk_idx,
|
||||
req_to_token=req_to_token,
|
||||
slot_ids=slot_ids,
|
||||
seq_lens=seq_lens,
|
||||
block_size_k=block_size_k,
|
||||
sm_scale=sm_scale,
|
||||
kv_indices=msa_kv_indices,
|
||||
plan=msa_plan,
|
||||
)
|
||||
else:
|
||||
o = flash_decode_with_gqa_share_sparse(
|
||||
q=q,
|
||||
sink=sink,
|
||||
k_cache=k_cache,
|
||||
v_cache=v_cache,
|
||||
req_to_token=req_to_token,
|
||||
seq_lens=seq_lens,
|
||||
slot_ids=slot_ids,
|
||||
block_size=block_size_k,
|
||||
topk_idx=topk_idx,
|
||||
sm_scale=sm_scale,
|
||||
)
|
||||
return idx_o, o
|
||||
@@ -0,0 +1,353 @@
|
||||
# MSA (fmha_sm100) drop-in for the MiniMax-M3 main sparse-attention step.
|
||||
#
|
||||
# Replaces only step 3 of MiniMax sparse prefill/decode. The lightning indexer
|
||||
# (steps 1-2) is unchanged and still produces `topk_idx`.
|
||||
# NVIDIA Blackwell (SM100/sm_103) only; callers gate on `msa_available()`.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def msa_available() -> bool:
|
||||
"""True iff the fmha_sm100 sparse kernels are importable on this device."""
|
||||
try:
|
||||
cap = torch.cuda.get_device_capability()
|
||||
except Exception:
|
||||
return False
|
||||
# SM100 family: B200 (10,0) and B300 (10,3). fmha_sm100/jit.py emits both
|
||||
# sm_100a and sm_103a; the kernels run on either.
|
||||
if cap[0] != 10 or cap[1] not in (0, 3):
|
||||
return False
|
||||
try:
|
||||
import fmha_sm100 # noqa: F401
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _build_page_table(
|
||||
req_to_token: torch.Tensor, # [max_reqs, max_kv_len], physical slot per logical pos
|
||||
slot_ids: torch.Tensor, # [batch]
|
||||
seq_lens: torch.Tensor, # [batch] total K length (prefix + chunk)
|
||||
page_size: int,
|
||||
) -> torch.Tensor:
|
||||
"""Flattened physical page ids per request (MSA `kv_indices`).
|
||||
|
||||
sglang's paged allocator stores page_size contiguous physical slots per page, so the
|
||||
physical page of logical position p is ``req_to_token[req, p] // page_size`` and is the
|
||||
same for every p within a page. We read one slot per logical page to recover the table.
|
||||
|
||||
Vectorized (no per-request Python loop): pages are packed contiguously by request in the
|
||||
same order MSA's planner expects (``kv_page_indptr = cumsum(ceil(seq_lens/page_size))``).
|
||||
``searchsorted`` maps each packed page slot back to its request; one ``.item()`` recovers
|
||||
the total page count (this runs eagerly, outside CUDA-graph capture).
|
||||
"""
|
||||
P = page_size
|
||||
n_pages = (seq_lens.to(torch.int64) + (P - 1)) // P # [batch]
|
||||
offsets = (
|
||||
torch.cumsum(n_pages, 0) - n_pages
|
||||
) # [batch] exclusive page offset per request
|
||||
total = int(n_pages.sum().item())
|
||||
idx = torch.arange(
|
||||
total, device=req_to_token.device
|
||||
) # packed page slot -> (req, page)
|
||||
req = torch.searchsorted(
|
||||
offsets + n_pages, idx, right=True
|
||||
) # request id per packed slot
|
||||
logical_first = (idx - offsets[req]) * P # first logical position of that page
|
||||
rows = slot_ids[req].to(torch.int64)
|
||||
return (req_to_token[rows, logical_first] // P).to(torch.int32)
|
||||
|
||||
|
||||
def msa_sparse_prefill_main(
|
||||
q: torch.Tensor, # [total_q, num_q_heads, head_dim]
|
||||
k_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim] (slot-major NHD)
|
||||
v_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim]
|
||||
topk_idx: torch.Tensor, # [num_kv_heads, total_q, topk] (0-based, -1 pad) -- step1/2 output
|
||||
req_to_token: torch.Tensor, # [max_reqs, max_kv_len]
|
||||
slot_ids: torch.Tensor, # [batch]
|
||||
cu_seqlens: torch.Tensor, # [batch+1] cumulative Q lengths
|
||||
seq_lens: torch.Tensor, # [batch] total K length (prefix + chunk)
|
||||
prefix_lens: torch.Tensor, # [batch]
|
||||
block_size_k: int, # == page_size == 128 for M3
|
||||
sm_scale: Optional[float] = None,
|
||||
) -> torch.Tensor:
|
||||
"""Drop-in for flash_prefill_with_gqa_share_sparse using MSA fmha_sm100.
|
||||
|
||||
Returns o [total_q, num_q_heads, head_dim].
|
||||
"""
|
||||
from fmha_sm100 import fmha_sm100, fmha_sm100_plan
|
||||
|
||||
max_slots, num_kv_heads, head_dim = k_cache.shape
|
||||
num_q_heads = q.shape[1]
|
||||
P = block_size_k
|
||||
topk = topk_idx.shape[-1]
|
||||
if max_slots % P != 0:
|
||||
raise ValueError(f"max_slots={max_slots} not divisible by page_size={P}")
|
||||
if sm_scale is None:
|
||||
sm_scale = head_dim**-0.5
|
||||
|
||||
# Whole pool as MSA paged KV: [num_phys_pages, num_kv_heads, P, head_dim].
|
||||
n_phys_pages = max_slots // P
|
||||
k_paged = k_cache.view(n_phys_pages, P, num_kv_heads, head_dim).permute(0, 2, 1, 3)
|
||||
v_paged = v_cache.view(n_phys_pages, P, num_kv_heads, head_dim).permute(0, 2, 1, 3)
|
||||
|
||||
# Per-request Q lengths (extend) and physical page table.
|
||||
qo_segment_lens = (cu_seqlens[1:] - cu_seqlens[:-1]).to(torch.int32)
|
||||
kv_indices = _build_page_table(req_to_token, slot_ids, seq_lens, P)
|
||||
|
||||
# topk_idx [Hkv, total_q, topk] -> kv_block_indexes [total_q, Hkv, topk].
|
||||
kv_block_indexes = topk_idx.permute(1, 0, 2).contiguous().to(torch.int32)
|
||||
|
||||
plan = fmha_sm100_plan(
|
||||
qo_segment_lens,
|
||||
seq_lens.to(torch.int32),
|
||||
num_q_heads,
|
||||
num_kv_heads=num_kv_heads,
|
||||
page_size=P,
|
||||
kv_block_num=topk,
|
||||
causal=True,
|
||||
qo_offset=prefix_lens.to(torch.int32),
|
||||
)
|
||||
o, _ = fmha_sm100(
|
||||
q,
|
||||
k_paged,
|
||||
v_paged,
|
||||
plan,
|
||||
sm_scale=sm_scale,
|
||||
kv_indices=kv_indices,
|
||||
kv_block_indexes=kv_block_indexes,
|
||||
)
|
||||
return o
|
||||
|
||||
|
||||
def build_msa_decode_meta(
|
||||
k_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim]
|
||||
req_to_token: torch.Tensor,
|
||||
slot_ids: torch.Tensor, # [batch]
|
||||
seq_lens: torch.Tensor, # [batch] cached KV length per request
|
||||
num_q_heads: int,
|
||||
block_size_k: int,
|
||||
topk: int,
|
||||
):
|
||||
"""Per-forward MSA decode metadata (page table + fmha plan), shared across layers.
|
||||
|
||||
Within one decode forward every sparse layer has the same batch, seq_lens, page size
|
||||
and topk, so the physical page table and the fmha_sm100 plan are identical for all of
|
||||
them — only ``kv_block_indexes`` (the per-layer top-k selection) changes. Building these
|
||||
once per forward instead of once per layer removes the dominant host-side overhead
|
||||
(page-table build + the host-side ``fmha_sm100_plan``) from the 57-layer decode loop.
|
||||
|
||||
Used only by the standalone parity harnesses; the serving backend builds eager-decode
|
||||
metadata via ``build_msa_decode_cg_plan`` + ``update_msa_decode_cg_meta`` instead.
|
||||
"""
|
||||
from fmha_sm100 import fmha_sm100_plan
|
||||
|
||||
max_slots, num_kv_heads, _ = k_cache.shape
|
||||
P = block_size_k
|
||||
if max_slots % P != 0:
|
||||
raise ValueError(f"max_slots={max_slots} not divisible by page_size={P}")
|
||||
B = slot_ids.shape[0]
|
||||
kv_indices = _build_page_table(req_to_token, slot_ids, seq_lens, P)
|
||||
seq_lens_i32 = seq_lens.to(torch.int32)
|
||||
plan = fmha_sm100_plan(
|
||||
torch.ones(B, dtype=torch.int32),
|
||||
seq_lens_i32,
|
||||
num_q_heads,
|
||||
num_kv_heads=num_kv_heads,
|
||||
page_size=P,
|
||||
kv_block_num=topk,
|
||||
causal=False,
|
||||
qo_offset=seq_lens_i32 - 1, # decode query sits at the last cached position
|
||||
)
|
||||
return kv_indices, plan
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Eager-only MSA decode plan (NOT used under CUDA graph)
|
||||
#
|
||||
# WARNING: the fmha_sm100 sparse decode kernel is NOT cuda-graph-safe — captured
|
||||
# and replayed it returns silently wrong results that compound across replays
|
||||
# (~14% GSM8K loss on B200). The backend routes decode to the cuda-graph-safe
|
||||
# Triton sparse path whenever decode runs under a CUDA graph (see
|
||||
# MiniMaxSparseAttnBackend._use_msa_decode); this plan is reachable ONLY in eager
|
||||
# decode (no decode CUDA graph), where there is no capture/replay. Do NOT wire it
|
||||
# back into a captured graph — that reintroduces the ~14% regression.
|
||||
#
|
||||
# The build-once / replay-update structure below (refreshing the four length
|
||||
# tensors ``{kv_segment_lens, kv_segment_offsets, kv_page_indptr, qo_offset}`` and
|
||||
# the page table in place) is a leftover from the abandoned capture-once attempt;
|
||||
# it is kept only because eager decode reuses one per-forward plan across layers.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_MSA_CG_LEN_KEYS = (
|
||||
"kv_segment_lens",
|
||||
"kv_segment_offsets",
|
||||
"kv_page_indptr",
|
||||
"qo_offset",
|
||||
)
|
||||
|
||||
|
||||
def _check_cg_plan_layout(plan) -> None:
|
||||
"""Fail fast if fmha_sm100's plan layout drifted from what replay-update
|
||||
assumes (dict at tuple index 3 holding the four length tensors) — these are
|
||||
undocumented fmha_sm100 internals."""
|
||||
if not (isinstance(plan, tuple) and len(plan) > 3 and isinstance(plan[3], dict)):
|
||||
raise RuntimeError(
|
||||
"fmha_sm100_plan no longer returns a tuple with a metadata dict at index 3; "
|
||||
"the MSA CUDA-graph decode path must be revalidated against this fmha_sm100 "
|
||||
"version. Set SGLANG_DISABLE_MSA=1 to use the Triton path meanwhile."
|
||||
)
|
||||
missing = [k for k in _MSA_CG_LEN_KEYS if not torch.is_tensor(plan[3].get(k))]
|
||||
if missing:
|
||||
raise RuntimeError(
|
||||
f"fmha_sm100 plan is missing length tensors {missing}; the MSA CUDA-graph "
|
||||
"decode path must be revalidated against this fmha_sm100 version. "
|
||||
"Set SGLANG_DISABLE_MSA=1 to use the Triton path meanwhile."
|
||||
)
|
||||
|
||||
|
||||
def build_msa_decode_cg_plan(
|
||||
num_q_heads: int,
|
||||
num_kv_heads: int,
|
||||
block_size_k: int,
|
||||
topk: int,
|
||||
batch_size: int,
|
||||
device: Optional[torch.device] = None,
|
||||
):
|
||||
"""Persistent fmha_sm100 decode plan for one batch size (CUDA-graph stable).
|
||||
|
||||
Built once per captured batch size. The worklist is length-independent (it uses
|
||||
``topk * page_size`` internally), so the reference KV length here only has to make
|
||||
every topk block valid; the length-dependent tensors are overwritten each step by
|
||||
``update_msa_decode_cg_meta``. Returns the plan tuple to pass to ``fmha_sm100``.
|
||||
"""
|
||||
from fmha_sm100 import fmha_sm100_plan
|
||||
|
||||
P = block_size_k
|
||||
ref_len = topk * P # length at which all topk blocks exist -> full worklist
|
||||
qo = torch.ones(batch_size, dtype=torch.int32)
|
||||
kv = torch.full((batch_size,), ref_len, dtype=torch.int32)
|
||||
plan = fmha_sm100_plan(
|
||||
qo,
|
||||
kv,
|
||||
num_q_heads,
|
||||
num_kv_heads=num_kv_heads,
|
||||
page_size=P,
|
||||
kv_block_num=topk,
|
||||
causal=False,
|
||||
qo_offset=kv - 1,
|
||||
device=device,
|
||||
)
|
||||
_check_cg_plan_layout(plan)
|
||||
return plan
|
||||
|
||||
|
||||
def update_msa_decode_cg_meta(
|
||||
plan,
|
||||
kv_indices_buf: torch.Tensor, # persistent page-table buffer [batch * max_pages]
|
||||
req_to_token: torch.Tensor,
|
||||
slot_ids: torch.Tensor, # [batch]
|
||||
seq_lens: torch.Tensor, # [batch] cached KV length per request
|
||||
block_size_k: int,
|
||||
topk: int,
|
||||
num_q_heads: int,
|
||||
num_kv_heads: int,
|
||||
):
|
||||
"""Refresh the persistent decode plan's length-dependent tensors + page table IN PLACE.
|
||||
|
||||
Host-side (calls fmha_sm100_plan and one ``.item()``); MUST run outside CUDA-graph
|
||||
capture — i.e. only from ``init_forward_metadata_out_graph``. The captured graph then
|
||||
reads the same plan-tensor and ``kv_indices_buf`` addresses on replay.
|
||||
"""
|
||||
from fmha_sm100 import fmha_sm100_plan
|
||||
|
||||
P = block_size_k
|
||||
B = seq_lens.shape[0]
|
||||
seq_lens_i32 = seq_lens.to(torch.int32)
|
||||
# Fresh plan for the real lengths; copy only its four length-dependent tensors into the
|
||||
# persistent plan (same shapes — they depend on batch size, not length). The fresh
|
||||
# worklist is identical to the persistent one (topk*P based) and is discarded.
|
||||
# qo_offset is clamped: graph replay pads the batch with seq_len==0 slots
|
||||
# (masked via kv_segment_lens==0, but seq_len-1 would be -1).
|
||||
fresh = fmha_sm100_plan(
|
||||
torch.ones(B, dtype=torch.int32),
|
||||
seq_lens_i32,
|
||||
num_q_heads,
|
||||
num_kv_heads=num_kv_heads,
|
||||
page_size=P,
|
||||
kv_block_num=topk,
|
||||
causal=False,
|
||||
qo_offset=(seq_lens_i32 - 1).clamp_min(0),
|
||||
device=seq_lens.device,
|
||||
)
|
||||
_check_cg_plan_layout(fresh)
|
||||
pd, fd = plan[3], fresh[3]
|
||||
for k in _MSA_CG_LEN_KEYS:
|
||||
pd[k].copy_(fd[k])
|
||||
table = _build_page_table(req_to_token, slot_ids, seq_lens, P)
|
||||
kv_indices_buf[: table.numel()].copy_(table)
|
||||
|
||||
|
||||
def msa_sparse_decode_main(
|
||||
q: torch.Tensor, # [batch, num_q_heads, head_dim] (1 query token per request)
|
||||
k_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim] (slot-major NHD)
|
||||
v_cache: torch.Tensor, # [max_slots, num_kv_heads, head_dim]
|
||||
topk_idx: torch.Tensor, # [num_kv_heads, batch, topk] (0-based, -1 pad)
|
||||
req_to_token: torch.Tensor, # [max_reqs, max_kv_len]
|
||||
slot_ids: torch.Tensor, # [batch]
|
||||
seq_lens: torch.Tensor, # [batch] cached KV length per request
|
||||
block_size_k: int, # == page_size == 128
|
||||
sm_scale: Optional[float] = None,
|
||||
kv_indices: Optional[
|
||||
torch.Tensor
|
||||
] = None, # precomputed page table (per-forward cache)
|
||||
plan=None, # precomputed fmha_sm100 plan (per-forward cache)
|
||||
) -> torch.Tensor:
|
||||
"""Drop-in for flash_decode_with_gqa_share_sparse using MSA fmha_sm100.
|
||||
|
||||
Each request is one decode query at absolute position seq_len-1 attending to its
|
||||
cached KV through the topk selected 128-blocks. Returns o [batch, num_q_heads, head_dim].
|
||||
|
||||
``kv_indices`` / ``plan`` are shared across all sparse layers of a forward; the serving
|
||||
backend builds them once via ``build_msa_decode_cg_plan`` + ``update_msa_decode_cg_meta``
|
||||
(eager decode only) and passes them in. When omitted (only the standalone parity
|
||||
harnesses) they are built here via ``build_msa_decode_meta``.
|
||||
"""
|
||||
from fmha_sm100 import fmha_sm100
|
||||
|
||||
max_slots, num_kv_heads, head_dim = k_cache.shape
|
||||
H = q.shape[1]
|
||||
P = block_size_k
|
||||
topk = topk_idx.shape[-1]
|
||||
if max_slots % P != 0:
|
||||
raise ValueError(f"max_slots={max_slots} not divisible by page_size={P}")
|
||||
if sm_scale is None:
|
||||
sm_scale = head_dim**-0.5
|
||||
|
||||
n_phys_pages = max_slots // P
|
||||
k_paged = k_cache.view(n_phys_pages, P, num_kv_heads, head_dim).permute(0, 2, 1, 3)
|
||||
v_paged = v_cache.view(n_phys_pages, P, num_kv_heads, head_dim).permute(0, 2, 1, 3)
|
||||
|
||||
if kv_indices is None or plan is None:
|
||||
kv_indices, plan = build_msa_decode_meta(
|
||||
k_cache, req_to_token, slot_ids, seq_lens, H, P, topk
|
||||
)
|
||||
kv_block_indexes = topk_idx.permute(1, 0, 2).contiguous().to(torch.int32)
|
||||
|
||||
o, _ = fmha_sm100(
|
||||
q,
|
||||
k_paged,
|
||||
v_paged,
|
||||
plan,
|
||||
sm_scale=sm_scale,
|
||||
kv_indices=kv_indices,
|
||||
kv_block_indexes=kv_block_indexes,
|
||||
)
|
||||
return o
|
||||
@@ -0,0 +1,84 @@
|
||||
# Copyright 2025 XunhaoLai. All rights reserved.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
from einops import einsum, rearrange
|
||||
|
||||
|
||||
def naive_flash_decode_with_topk_idx(
|
||||
q: torch.Tensor, # [batch_size, num_heads, head_dim]
|
||||
sink: Optional[torch.Tensor], # [num_heads, head_dim]
|
||||
kv_cache: torch.Tensor, # [max_slots, 2, max_len, num_heads, head_dim]
|
||||
seq_lens: torch.Tensor, # [batch_size, ]
|
||||
max_seqlen: int,
|
||||
slot_ids: torch.Tensor, # [batch_size, ]
|
||||
block_size: int,
|
||||
topk: int,
|
||||
sm_scale: Optional[float] = None,
|
||||
init_blocks: int = 0,
|
||||
local_blocks: int = 0,
|
||||
):
|
||||
assert (
|
||||
kv_cache.shape[2] % block_size == 0
|
||||
), "max cache len must be divisible by block size"
|
||||
if sm_scale is None:
|
||||
sm_scale = q.shape[-1] ** -0.5
|
||||
original_dtype = q.dtype
|
||||
batch_size = q.shape[0]
|
||||
num_q_heads = q.shape[1]
|
||||
num_kv_heads = kv_cache.shape[3]
|
||||
q = rearrange(q.float(), "b (h g) d -> b h g d", h=num_kv_heads)
|
||||
kv_cache_float = kv_cache.float()
|
||||
qk = (
|
||||
einsum(q, kv_cache_float[slot_ids, 0, ...], "b h g d, b n h d -> b h g n")
|
||||
* sm_scale
|
||||
)
|
||||
mask = torch.arange(kv_cache.shape[2], device=q.device) < seq_lens[:, None]
|
||||
qk = qk.masked_fill(~mask[:, None, None, :], float("-inf"))
|
||||
# get score
|
||||
score = qk.clone().to(torch.float32)
|
||||
score = rearrange(score, "b h g (n s) -> b (h g) n s", s=block_size)
|
||||
score = score.max(dim=-1).values # [batch_size, num_q_heads, num_blocks]
|
||||
# post-process score for init_blocks and local_blocks
|
||||
INIT_SCORE = 1e30
|
||||
LOCAL_SCORE = 1e29
|
||||
if init_blocks > 0:
|
||||
score[:, :, :init_blocks] = INIT_SCORE
|
||||
if local_blocks > 0:
|
||||
num_blocks_per_batch = (seq_lens + block_size - 1) // block_size
|
||||
for b in range(batch_size):
|
||||
num_blks = num_blocks_per_batch[b].item()
|
||||
local_start = max(0, num_blks - local_blocks)
|
||||
score[b, :, local_start:num_blks] = LOCAL_SCORE
|
||||
# compute topk indices per (batch, head)
|
||||
# score shape: [batch_size, num_q_heads, num_blocks]
|
||||
topk_idx = torch.full(
|
||||
(num_q_heads, batch_size, topk),
|
||||
fill_value=-1,
|
||||
device=score.device,
|
||||
dtype=torch.int32,
|
||||
)
|
||||
num_blocks_per_batch = (seq_lens + block_size - 1) // block_size
|
||||
for b in range(batch_size):
|
||||
num_blks = num_blocks_per_batch[b].item()
|
||||
actual_topk = min(topk, num_blks)
|
||||
for h in range(num_q_heads):
|
||||
# get topk indices for this (batch, head)
|
||||
_, indices = torch.topk(score[b, h, :num_blks], k=actual_topk, dim=-1)
|
||||
topk_idx[h, b, :actual_topk] = indices.to(torch.int32)
|
||||
# compute attention output with sink
|
||||
if sink is not None:
|
||||
# sink: [num_q_heads, head_dim] -> reshape to match q: [b, h, g, d]
|
||||
sink_reshaped = rearrange(sink.float(), "(h g) d -> h g d", h=num_kv_heads)
|
||||
qsink = (
|
||||
einsum(q, sink_reshaped, "b h g d, h g d -> b h g") * sm_scale
|
||||
) # [b, h, g]
|
||||
qk_with_sink = torch.cat([qsink[..., None], qk], dim=-1) # [b, h, g, n+1]
|
||||
attn = qk_with_sink.softmax(dim=-1, dtype=torch.float32)
|
||||
attn = attn[..., 1:] # remove sink score
|
||||
else:
|
||||
attn = qk.softmax(dim=-1, dtype=torch.float32)
|
||||
o = einsum(attn, kv_cache_float[slot_ids, 1, ...], "b h g n, b n h d -> b h g d")
|
||||
o = rearrange(o, "b h g d -> b (h g) d", h=num_kv_heads)
|
||||
return o.to(original_dtype), topk_idx
|
||||
@@ -0,0 +1,88 @@
|
||||
# Copyright 2025 XunhaoLai. All rights reserved.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def naive_flash_decode_with_gqa_share_sparse(
|
||||
q: torch.Tensor, # [batch_size, num_q_heads, head_dim]
|
||||
sink: Optional[torch.Tensor], # [num_q_heads, head_dim]
|
||||
kv_cache: torch.Tensor, # [max_slots, 2, max_len, num_kv_heads, head_dim]
|
||||
seq_lens: torch.Tensor, # [batch_size, ]
|
||||
slot_ids: torch.Tensor, # [batch_size, ]
|
||||
block_size: int,
|
||||
topk_idx: torch.Tensor, # [num_kv_heads, batch_size, topk]
|
||||
sm_scale: Optional[float] = None,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Naive implementation of sparse attention with GQA group sharing.
|
||||
Each GQA group (all q heads sharing the same kv head) uses the same topk_idx.
|
||||
"""
|
||||
if sm_scale is None:
|
||||
sm_scale = q.shape[-1] ** -0.5
|
||||
|
||||
original_dtype = q.dtype
|
||||
batch_size, num_q_heads, head_dim = q.shape
|
||||
max_slots, _, max_kv_len, num_kv_heads, _ = kv_cache.shape
|
||||
gqa_group_size = num_q_heads // num_kv_heads
|
||||
# Output tensor
|
||||
o = torch.zeros(batch_size, num_q_heads, head_dim, dtype=q.dtype, device=q.device)
|
||||
|
||||
for b in range(batch_size):
|
||||
seq_len = seq_lens[b].item()
|
||||
sid = slot_ids[b].item() % max_slots
|
||||
|
||||
for kh in range(num_kv_heads):
|
||||
# Get topk indices for this (kv_head, batch)
|
||||
block_indices = topk_idx[kh, b, :].tolist()
|
||||
|
||||
# Collect selected K and V blocks
|
||||
selected_k_blocks = []
|
||||
selected_v_blocks = []
|
||||
for block_idx in block_indices:
|
||||
if block_idx < 0:
|
||||
continue # Invalid index
|
||||
start = block_idx * block_size
|
||||
end = min(start + block_size, seq_len)
|
||||
if start >= seq_len:
|
||||
continue
|
||||
# K: [block_len, head_dim]
|
||||
k_block = kv_cache[sid, 0, start:end, kh, :]
|
||||
# V: [block_len, head_dim]
|
||||
v_block = kv_cache[sid, 1, start:end, kh, :]
|
||||
selected_k_blocks.append(k_block)
|
||||
selected_v_blocks.append(v_block)
|
||||
|
||||
if len(selected_k_blocks) == 0:
|
||||
continue
|
||||
|
||||
# Concatenate selected blocks: [total_selected_len, head_dim]
|
||||
k_selected = torch.cat(selected_k_blocks, dim=0)
|
||||
v_selected = torch.cat(selected_v_blocks, dim=0)
|
||||
|
||||
# Compute attention for all q heads in this GQA group
|
||||
for g in range(gqa_group_size):
|
||||
qh = kh * gqa_group_size + g
|
||||
# q_vec: [head_dim]
|
||||
q_vec = q[b, qh, :].float()
|
||||
|
||||
# Compute attention scores: [total_selected_len]
|
||||
scores = torch.matmul(q_vec, k_selected.float().T) * sm_scale
|
||||
|
||||
# Add sink to softmax normalization if present
|
||||
if sink is not None:
|
||||
sink_vec = sink[qh, :].float()
|
||||
qsink = torch.dot(q_vec, sink_vec) * sm_scale
|
||||
# Concatenate sink score with regular scores for softmax
|
||||
scores_with_sink = torch.cat([qsink.unsqueeze(0), scores], dim=0)
|
||||
attn_weights_with_sink = torch.softmax(scores_with_sink, dim=-1)
|
||||
# Remove sink weight (only used for normalization)
|
||||
attn_weights = attn_weights_with_sink[1:]
|
||||
else:
|
||||
attn_weights = torch.softmax(scores, dim=-1)
|
||||
|
||||
# Compute output: [head_dim]
|
||||
o[b, qh, :] = torch.matmul(attn_weights.to(original_dtype), v_selected)
|
||||
|
||||
return o
|
||||
@@ -0,0 +1,563 @@
|
||||
# Copyright 2025 XunhaoLai. All rights reserved.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from ..common.utils import _bitonic_merge, get_cu_seqblocks, robust_allocator
|
||||
|
||||
|
||||
@triton.heuristics(
|
||||
{
|
||||
"BLOCK_SIZE_KD": lambda args: triton.next_power_of_2(args["qk_head_dim"]),
|
||||
"BLOCK_SIZE_VD": lambda args: triton.next_power_of_2(args["v_head_dim"]),
|
||||
"HAS_SINK": lambda args: args["sink_ptr"] is not None,
|
||||
}
|
||||
)
|
||||
@triton.autotune(
|
||||
configs=[
|
||||
# Small block (64x64): low shared mem, can use higher num_stages
|
||||
triton.Config(
|
||||
{"BLOCK_SIZE_Q": 64, "BLOCK_SIZE_K": 64}, num_warps=4, num_stages=2
|
||||
),
|
||||
triton.Config(
|
||||
{"BLOCK_SIZE_Q": 64, "BLOCK_SIZE_K": 64}, num_warps=4, num_stages=3
|
||||
),
|
||||
triton.Config(
|
||||
{"BLOCK_SIZE_Q": 64, "BLOCK_SIZE_K": 64}, num_warps=4, num_stages=4
|
||||
),
|
||||
# Medium block (64x128, 128x64): moderate shared mem, ns=2,3
|
||||
triton.Config(
|
||||
{"BLOCK_SIZE_Q": 64, "BLOCK_SIZE_K": 128}, num_warps=8, num_stages=2
|
||||
),
|
||||
triton.Config(
|
||||
{"BLOCK_SIZE_Q": 64, "BLOCK_SIZE_K": 128}, num_warps=8, num_stages=3
|
||||
),
|
||||
triton.Config(
|
||||
{"BLOCK_SIZE_Q": 128, "BLOCK_SIZE_K": 64}, num_warps=8, num_stages=2
|
||||
),
|
||||
triton.Config(
|
||||
{"BLOCK_SIZE_Q": 128, "BLOCK_SIZE_K": 64}, num_warps=8, num_stages=3
|
||||
),
|
||||
# Large block (128x128): high shared mem, ns=2,3 only, nw=8
|
||||
triton.Config(
|
||||
{"BLOCK_SIZE_Q": 128, "BLOCK_SIZE_K": 128}, num_warps=8, num_stages=2
|
||||
),
|
||||
triton.Config(
|
||||
{"BLOCK_SIZE_Q": 128, "BLOCK_SIZE_K": 128}, num_warps=8, num_stages=3
|
||||
),
|
||||
],
|
||||
key=[
|
||||
"qk_head_dim",
|
||||
"v_head_dim",
|
||||
"block_size",
|
||||
"use_gumbel_topk",
|
||||
"SCORE_TYPE",
|
||||
"DISABLE_INDEX_VALUE",
|
||||
],
|
||||
)
|
||||
@triton.jit
|
||||
def _flash_attn_fwd_with_block_score_kernel(
|
||||
q_ptr, # Q: n x h x d
|
||||
k_cache_ptr, # K paged: max_slots x kh x d
|
||||
v_cache_ptr, # V paged: max_slots x kh x d
|
||||
sink_ptr, # Sink: h x d
|
||||
o_ptr, # O: n x h x d
|
||||
score_ptr, # Score: h x n x max_seqblock
|
||||
req_to_token_ptr, # req_to_token: max_reqs x max_kv_len
|
||||
# seqlens
|
||||
cu_seqlens,
|
||||
seq_lens,
|
||||
prefix_lens,
|
||||
slot_ids,
|
||||
# shape
|
||||
max_slots,
|
||||
num_heads,
|
||||
gqa_group_size,
|
||||
qk_head_dim,
|
||||
v_head_dim,
|
||||
block_size: tl.constexpr,
|
||||
# sm_scale
|
||||
sm_scale,
|
||||
# gumbel topk
|
||||
use_gumbel_topk: tl.constexpr,
|
||||
gumbel_seed,
|
||||
# stride
|
||||
stride_q_n,
|
||||
stride_q_h,
|
||||
stride_q_d,
|
||||
stride_k_s,
|
||||
stride_k_h,
|
||||
stride_k_d,
|
||||
stride_v_s,
|
||||
stride_v_h,
|
||||
stride_v_d,
|
||||
stride_sink_h,
|
||||
stride_sink_d,
|
||||
stride_o_n,
|
||||
stride_o_h,
|
||||
stride_o_d,
|
||||
stride_s_h,
|
||||
stride_s_q,
|
||||
stride_s_k,
|
||||
stride_r2t_b,
|
||||
# META parameters
|
||||
BLOCK_SIZE_Q: tl.constexpr, # q block size
|
||||
BLOCK_SIZE_K: tl.constexpr, # k block size
|
||||
BLOCK_SIZE_KD: tl.constexpr,
|
||||
BLOCK_SIZE_VD: tl.constexpr,
|
||||
# has sink
|
||||
HAS_SINK: tl.constexpr,
|
||||
SCORE_TYPE: tl.constexpr,
|
||||
DISABLE_INDEX_VALUE: tl.constexpr,
|
||||
):
|
||||
tl.static_assert(SCORE_TYPE == "max" or SCORE_TYPE == "lse")
|
||||
sm_scale_log2e = sm_scale * 1.4426950409
|
||||
tl.static_assert(BLOCK_SIZE_K >= block_size)
|
||||
BLOCKS_PER_K_BLOCK: tl.constexpr = BLOCK_SIZE_K // block_size
|
||||
# get batch id and head id
|
||||
pid_q, pid_bh = tl.program_id(0), tl.program_id(1)
|
||||
pid_b = pid_bh // num_heads
|
||||
pid_h = pid_bh % num_heads
|
||||
pid_kh = pid_h // gqa_group_size
|
||||
# get q k start and len after rmpad
|
||||
seq_start = tl.load(cu_seqlens + pid_b)
|
||||
q_len = tl.load(cu_seqlens + pid_b + 1) - seq_start
|
||||
seq_len = tl.load(seq_lens + pid_b)
|
||||
prefix_len = tl.load(prefix_lens + pid_b)
|
||||
sid = (
|
||||
tl.load(slot_ids + pid_b).to(tl.int64) + max_slots
|
||||
) % max_slots # safety against negative
|
||||
if BLOCK_SIZE_Q * pid_q >= q_len:
|
||||
return
|
||||
block_num = (seq_len + block_size - 1) // block_size
|
||||
# init qkv pointer
|
||||
q_ptrs = tl.make_block_ptr(
|
||||
base=q_ptr + seq_start * stride_q_n + pid_h * stride_q_h,
|
||||
shape=(q_len, qk_head_dim),
|
||||
strides=(stride_q_n, stride_q_d),
|
||||
offsets=(pid_q * BLOCK_SIZE_Q, 0),
|
||||
block_shape=(BLOCK_SIZE_Q, BLOCK_SIZE_KD),
|
||||
order=(1, 0),
|
||||
)
|
||||
s_ptrs = tl.make_block_ptr(
|
||||
base=score_ptr + seq_start * stride_s_q + pid_h * stride_s_h,
|
||||
shape=(q_len, block_num),
|
||||
strides=(stride_s_q, stride_s_k),
|
||||
offsets=(pid_q * BLOCK_SIZE_Q, 0),
|
||||
block_shape=(BLOCK_SIZE_Q, BLOCKS_PER_K_BLOCK),
|
||||
order=(1, 0),
|
||||
)
|
||||
# load q
|
||||
q = tl.load(q_ptrs, boundary_check=(0, 1), padding_option="zero")
|
||||
if HAS_SINK:
|
||||
off_d = tl.arange(0, BLOCK_SIZE_KD)
|
||||
sink = tl.load(
|
||||
sink_ptr + pid_h * stride_sink_h + off_d * stride_sink_d,
|
||||
mask=off_d < qk_head_dim,
|
||||
other=0,
|
||||
)
|
||||
# init statistics
|
||||
off_q = tl.arange(0, BLOCK_SIZE_Q) + pid_q * BLOCK_SIZE_Q + prefix_len
|
||||
off_k = tl.arange(0, BLOCK_SIZE_K)
|
||||
off_kd = tl.arange(0, BLOCK_SIZE_KD)
|
||||
off_vd = tl.arange(0, BLOCK_SIZE_VD)
|
||||
off_bpk = tl.arange(0, BLOCKS_PER_K_BLOCK)
|
||||
kd_mask = off_kd < qk_head_dim
|
||||
vd_mask = off_vd < v_head_dim
|
||||
if HAS_SINK:
|
||||
m_i = tl.zeros((BLOCK_SIZE_Q,), dtype=tl.float32)
|
||||
lse_i = tl.zeros((BLOCK_SIZE_Q,), dtype=tl.float32)
|
||||
qsink = tl.sum(q * sink[None, :], axis=1) * sm_scale_log2e # (BLOCK_SIZE_Q,)
|
||||
m_i += qsink
|
||||
lse_i += qsink
|
||||
else:
|
||||
m_i = tl.full((BLOCK_SIZE_Q,), float("-inf"), dtype=tl.float32)
|
||||
lse_i = tl.full((BLOCK_SIZE_Q,), float("-inf"), dtype=tl.float32)
|
||||
acc_o = tl.full((BLOCK_SIZE_Q, BLOCK_SIZE_VD), 0, dtype=tl.float32)
|
||||
# attention
|
||||
diag_start = (prefix_len + pid_q * BLOCK_SIZE_Q) // BLOCK_SIZE_K * BLOCK_SIZE_K
|
||||
hi = min(seq_len, prefix_len + (pid_q + 1) * BLOCK_SIZE_Q)
|
||||
for i in tl.range(0, hi, BLOCK_SIZE_K):
|
||||
# paged load K via req_to_token: pos -> slot -> k_cache
|
||||
pos = i + off_k
|
||||
pos_mask = pos < seq_len
|
||||
slots = tl.load(
|
||||
req_to_token_ptr + sid * stride_r2t_b + pos,
|
||||
mask=pos_mask,
|
||||
other=0,
|
||||
).to(tl.int64)
|
||||
slots = (slots + max_slots) % max_slots # safety against negative
|
||||
# k shape: [BLOCK_SIZE_KD, BLOCK_SIZE_K] (transposed for tl.dot)
|
||||
k = tl.load(
|
||||
k_cache_ptr
|
||||
+ slots[None, :] * stride_k_s
|
||||
+ pid_kh * stride_k_h
|
||||
+ off_kd[:, None] * stride_k_d,
|
||||
mask=kd_mask[:, None] & pos_mask[None, :],
|
||||
other=0.0,
|
||||
)
|
||||
# compute qk
|
||||
qk = tl.dot(q, k) * sm_scale_log2e
|
||||
if i >= diag_start:
|
||||
qk = tl.where(off_q[:, None] >= (i + off_k)[None, :], qk, float("-inf"))
|
||||
# K boundary mask: positions beyond seq_len contribute -inf
|
||||
qk += tl.where(pos_mask[None, :], 0, float("-inf"))
|
||||
# save score
|
||||
score = tl.reshape(
|
||||
qk, (BLOCK_SIZE_Q, BLOCKS_PER_K_BLOCK, block_size), can_reorder=False
|
||||
)
|
||||
sub_max = tl.max(score, axis=2)
|
||||
if SCORE_TYPE == "max":
|
||||
score = sub_max
|
||||
else: # "lse"
|
||||
# fully-masked sub-blocks produce NaN via -inf - (-inf); clamp
|
||||
# back to -inf so downstream bitonic sort sees a clean sentinel.
|
||||
score = sub_max + tl.log2(
|
||||
tl.sum(tl.exp2(score - sub_max[:, :, None]), axis=2)
|
||||
)
|
||||
score = tl.where(score != score, float("-inf"), score)
|
||||
if use_gumbel_topk:
|
||||
# generate non-conflicting offset for random generation
|
||||
# noise_offset shape: (BLOCK_SIZE_Q, BLOCKS_PER_K_BLOCK)
|
||||
# random seed include head id, batch id and gumbel seed
|
||||
# (Head low 7 bits | Batch middle 12 bits | Other high bits)
|
||||
local_seed = (pid_h | (pid_b << 7) | (gumbel_seed << 19)).to(tl.int32)
|
||||
# noise offset include q index and k block index
|
||||
# [31-13: Q (19bits)] | [12-0: K_Block (13bits)]
|
||||
noise_offset = (off_q << 13)[:, None] | (off_bpk + i // block_size)[None, :]
|
||||
# gumbel noise (scaled to log2 scale to match sm_scale_log2e)
|
||||
noise = tl.rand(local_seed, offset=noise_offset)
|
||||
noise = tl.clamp(noise, min=1e-9, max=1 - 1e-9) # avoid log(0)
|
||||
noise = -tl.log(-tl.log(noise)) * 1.4426950409
|
||||
score += noise
|
||||
tl.store(s_ptrs, score.to(score_ptr.dtype.element_ty), boundary_check=(0, 1))
|
||||
if not DISABLE_INDEX_VALUE:
|
||||
# compute m_ij and l_ij
|
||||
m_ij = tl.maximum(m_i, tl.max(qk, axis=1))
|
||||
p = tl.exp2(qk - m_ij[:, None])
|
||||
l_ij = tl.sum(p, axis=1)
|
||||
# scale acc_o
|
||||
acc_o_scale = tl.exp2(m_i - m_ij)
|
||||
acc_o = acc_o * acc_o_scale[:, None]
|
||||
# paged load V
|
||||
v = tl.load(
|
||||
v_cache_ptr
|
||||
+ slots[:, None] * stride_v_s
|
||||
+ pid_kh * stride_v_h
|
||||
+ off_vd[None, :] * stride_v_d,
|
||||
mask=pos_mask[:, None] & vd_mask[None, :],
|
||||
other=0.0,
|
||||
)
|
||||
p = p.to(v.dtype)
|
||||
acc_o += tl.dot(p, v)
|
||||
# update statistics
|
||||
m_i = m_ij
|
||||
lse_i = m_ij + tl.log2(tl.exp2(lse_i - m_ij) + l_ij)
|
||||
# update ptrs
|
||||
s_ptrs = tl.advance(s_ptrs, (0, BLOCKS_PER_K_BLOCK))
|
||||
if not DISABLE_INDEX_VALUE:
|
||||
# final scale
|
||||
acc_o = acc_o * tl.exp2(m_i - lse_i)[:, None]
|
||||
# save output
|
||||
o_ptrs = tl.make_block_ptr(
|
||||
base=o_ptr + seq_start * stride_o_n + pid_h * stride_o_h,
|
||||
shape=(q_len, v_head_dim),
|
||||
strides=(stride_o_n, stride_o_d),
|
||||
offsets=(pid_q * BLOCK_SIZE_Q, 0),
|
||||
block_shape=(BLOCK_SIZE_Q, BLOCK_SIZE_VD),
|
||||
order=(1, 0),
|
||||
)
|
||||
tl.store(o_ptrs, acc_o.to(o_ptr.dtype.element_ty), boundary_check=(0, 1))
|
||||
|
||||
|
||||
@triton.heuristics({"BLOCK_SIZE_T": lambda args: triton.next_power_of_2(args["topk"])})
|
||||
@triton.autotune(
|
||||
configs=[
|
||||
# Large configs for H200/B200 to support larger topk
|
||||
triton.Config({"BLOCK_SIZE_K": 2048}, num_warps=8, num_stages=2),
|
||||
triton.Config({"BLOCK_SIZE_K": 1024}, num_warps=8, num_stages=2),
|
||||
triton.Config({"BLOCK_SIZE_K": 512}, num_warps=8, num_stages=2),
|
||||
triton.Config({"BLOCK_SIZE_K": 256}, num_warps=8, num_stages=2),
|
||||
triton.Config({"BLOCK_SIZE_K": 256}, num_warps=4, num_stages=2),
|
||||
triton.Config({"BLOCK_SIZE_K": 128}, num_warps=4, num_stages=2),
|
||||
triton.Config({"BLOCK_SIZE_K": 128}, num_warps=4, num_stages=3),
|
||||
triton.Config({"BLOCK_SIZE_K": 64}, num_warps=2, num_stages=2),
|
||||
],
|
||||
key=[
|
||||
"BLOCK_SIZE_T"
|
||||
], # use BLOCK_SIZE_T instead of topk to reduce autotune frequency
|
||||
)
|
||||
@triton.jit
|
||||
def _topk_index_kernel(
|
||||
s_ptr, # Score: h x n x max_seqblock
|
||||
ti_ptr, # topk_idx: h x n x topk
|
||||
# size
|
||||
sample_interval: tl.constexpr,
|
||||
block_size: tl.constexpr,
|
||||
# seqlens
|
||||
cu_seqlens,
|
||||
cu_seqblocks_q,
|
||||
prefix_lens,
|
||||
# shape
|
||||
topk, # not constexpr to avoid recompilation when topk changes
|
||||
init_blocks: tl.constexpr,
|
||||
local_blocks: tl.constexpr,
|
||||
# stride
|
||||
stride_s_h,
|
||||
stride_s_n,
|
||||
stride_s_k,
|
||||
stride_ti_h,
|
||||
stride_ti_n,
|
||||
stride_ti_t,
|
||||
# META parameters
|
||||
BLOCK_SIZE_K: tl.constexpr,
|
||||
BLOCK_SIZE_T: tl.constexpr,
|
||||
MASK_INIT: tl.constexpr,
|
||||
MASK_LOCAL: tl.constexpr,
|
||||
):
|
||||
tl.static_assert(
|
||||
BLOCK_SIZE_K > BLOCK_SIZE_T
|
||||
) # use BLOCK_SIZE_T instead of topk (stricter but safe)
|
||||
# get batch id and head id
|
||||
pid_q = tl.program_id(0)
|
||||
pid_b = tl.program_id(1)
|
||||
pid_h = tl.program_id(2)
|
||||
# get q k start and len after rmpad
|
||||
seq_start = tl.load(cu_seqlens + pid_b)
|
||||
block_start = tl.load(cu_seqblocks_q + pid_b)
|
||||
block_num = tl.load(cu_seqblocks_q + pid_b + 1) - block_start
|
||||
prefix_len = tl.load(prefix_lens + pid_b)
|
||||
if pid_q >= block_num:
|
||||
return
|
||||
# offsets
|
||||
off_k = tl.arange(0, BLOCK_SIZE_K)
|
||||
off_t = tl.arange(0, BLOCK_SIZE_T)
|
||||
# init qkv pointer
|
||||
s_ptrs = (
|
||||
s_ptr
|
||||
+ (seq_start + pid_q * sample_interval) * stride_s_n
|
||||
+ pid_h * stride_s_h
|
||||
+ off_k * stride_s_k
|
||||
)
|
||||
# init statistics
|
||||
topk_score = tl.full((BLOCK_SIZE_K,), -1e30, dtype=tl.float32)
|
||||
topk_idx = tl.full((BLOCK_SIZE_K,), 0, dtype=tl.int32)
|
||||
left_half_mask = tl.arange(0, BLOCK_SIZE_K) < BLOCK_SIZE_K // 2
|
||||
# compute topk
|
||||
valid_blocks = (prefix_len + pid_q * sample_interval + block_size) // block_size
|
||||
for i in tl.range(0, valid_blocks, BLOCK_SIZE_K):
|
||||
# masks
|
||||
causal_mask = i + off_k < valid_blocks
|
||||
local_mask = i + off_k >= max(0, valid_blocks - local_blocks)
|
||||
init_mask = i + off_k < init_blocks
|
||||
# load score
|
||||
score = tl.load(s_ptrs, mask=causal_mask, other=-1e30).to(tl.float32)
|
||||
# handle NaN: NaN inputs cause bitonic sort to fail, resulting in invalid indices (-2)
|
||||
# appearing in the topk list. We replace NaN with -inf to maintain sort order.
|
||||
score = tl.where(score != score, -1e30, score)
|
||||
s_ptrs = s_ptrs + stride_s_k * BLOCK_SIZE_K
|
||||
# fill init and local part, make sure init part is always in topk
|
||||
# and at the first position. Note: must use causal_mask to protect
|
||||
# init_mask to avoid selecting blocks outside causal window
|
||||
if MASK_INIT:
|
||||
score = tl.where(causal_mask & init_mask, score - 1e29, score)
|
||||
else:
|
||||
score = tl.where(causal_mask & init_mask, 1e30, score)
|
||||
if MASK_LOCAL:
|
||||
score = tl.where(causal_mask & local_mask, score - 1e28, score)
|
||||
else:
|
||||
score = tl.where(causal_mask & local_mask, 1e29, score)
|
||||
# bitonic merge
|
||||
topk_score, last_topk_score = score, topk_score
|
||||
topk_idx, last_topk_idx = (tl.where(causal_mask, i + off_k + 1, 0), topk_idx)
|
||||
n_dims: tl.constexpr = tl.standard._log2(BLOCK_SIZE_K)
|
||||
for j in tl.static_range(1, n_dims):
|
||||
topk_score, topk_idx = _bitonic_merge(
|
||||
topk_score, topk_idx.to(tl.int32), j, 2, n_dims
|
||||
)
|
||||
if i != 0:
|
||||
topk_score, topk_idx = _bitonic_merge(
|
||||
topk_score, topk_idx.to(tl.int32), n_dims, False, n_dims
|
||||
)
|
||||
topk_score_new = last_topk_score * left_half_mask + topk_score * (
|
||||
1 - left_half_mask
|
||||
)
|
||||
topk_idx_new = last_topk_idx * left_half_mask + topk_idx * (
|
||||
1 - left_half_mask
|
||||
)
|
||||
topk_score, topk_idx = _bitonic_merge(
|
||||
topk_score_new, topk_idx_new.to(tl.int32), n_dims, True, n_dims
|
||||
)
|
||||
else:
|
||||
topk_score, topk_idx = _bitonic_merge(
|
||||
topk_score, topk_idx.to(tl.int32), n_dims, True, n_dims
|
||||
)
|
||||
# get topk, shape: [BLOCK_SIZE_T,]
|
||||
topk_mask = tl.arange(0, BLOCK_SIZE_K // BLOCK_SIZE_T) == 0
|
||||
topk_idx = tl.sum(
|
||||
topk_mask[:, None]
|
||||
* tl.reshape(topk_idx - 1, [BLOCK_SIZE_K // BLOCK_SIZE_T, BLOCK_SIZE_T]),
|
||||
axis=0,
|
||||
)
|
||||
# save topk
|
||||
ti_ptrs = (
|
||||
ti_ptr
|
||||
+ (block_start + pid_q) * stride_ti_n
|
||||
+ pid_h * stride_ti_h
|
||||
+ off_t * stride_ti_t
|
||||
)
|
||||
topk_mask = tl.arange(0, BLOCK_SIZE_T) < min(topk, valid_blocks)
|
||||
tl.store(ti_ptrs, topk_idx.to(ti_ptrs.dtype.element_ty), mask=topk_mask)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def flash_prefill_with_topk_index(
|
||||
q: torch.Tensor,
|
||||
k_cache: torch.Tensor, # paged
|
||||
v_cache: Optional[torch.Tensor], # paged; ignored when disable_index_value=True
|
||||
sink: Optional[torch.Tensor],
|
||||
req_to_token: torch.Tensor,
|
||||
slot_ids: torch.Tensor,
|
||||
cu_seqlens: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
prefix_lens: torch.Tensor,
|
||||
max_seqlen_q: int,
|
||||
max_seqlen_k: int,
|
||||
block_size_q: int,
|
||||
block_size_k: int,
|
||||
topk: int,
|
||||
init_blocks: int = 1,
|
||||
local_blocks: int = 2,
|
||||
sm_scale: Optional[float] = None,
|
||||
use_tma: bool = False,
|
||||
score_type: str = "max",
|
||||
disable_index_value: bool = False,
|
||||
cu_seqblocks_q: Optional[torch.Tensor] = None,
|
||||
max_seqblock_q: Optional[int] = None,
|
||||
all_seqblock_q: Optional[int] = None,
|
||||
):
|
||||
assert score_type in (
|
||||
"max",
|
||||
"lse",
|
||||
), f"score_type must be 'max' or 'lse', got {score_type!r}"
|
||||
triton.set_allocator(robust_allocator)
|
||||
# dtype check
|
||||
assert q.dtype == torch.bfloat16 or q.dtype == torch.float16
|
||||
assert k_cache.dtype == q.dtype
|
||||
assert cu_seqlens.dtype == torch.int32
|
||||
# shape
|
||||
total_q, num_heads, qk_head_dim = q.shape
|
||||
max_slots, num_kv_heads, _ = k_cache.shape
|
||||
if disable_index_value:
|
||||
# placeholder for BLOCK_SIZE_VD; V is never loaded
|
||||
v_head_dim = qk_head_dim
|
||||
else:
|
||||
assert v_cache is not None and v_cache.dtype == q.dtype
|
||||
assert v_cache.shape[1] == k_cache.shape[1]
|
||||
v_head_dim = v_cache.shape[-1]
|
||||
gqa_group_size = num_heads // num_kv_heads
|
||||
batch_size = cu_seqlens.shape[0] - 1
|
||||
assert qk_head_dim <= 256 and v_head_dim <= 256, "head_dim must be less than 256"
|
||||
if sink is not None:
|
||||
assert sink.shape[0] == num_heads and sink.shape[1] == qk_head_dim
|
||||
assert (
|
||||
init_blocks + local_blocks <= topk
|
||||
), "init_blocks + local_blocks must be less than topk"
|
||||
if sm_scale is None:
|
||||
sm_scale = qk_head_dim**-0.5
|
||||
if cu_seqblocks_q is None or max_seqblock_q is None or all_seqblock_q is None:
|
||||
cu_seqblocks_q, max_seqblock_q, all_seqblock_q, _, _, _ = get_cu_seqblocks(
|
||||
cu_seqlens, max_seqlen_q, block_size_q, block_size_k
|
||||
)
|
||||
max_seqblock_k = triton.cdiv(max_seqlen_k, block_size_k)
|
||||
if disable_index_value:
|
||||
o = None
|
||||
else:
|
||||
o = torch.empty(total_q, num_heads, v_head_dim, dtype=q.dtype, device=q.device)
|
||||
score = torch.full(
|
||||
(num_heads, total_q, max_seqblock_k),
|
||||
float("-inf"),
|
||||
dtype=torch.float32,
|
||||
device=q.device,
|
||||
)
|
||||
|
||||
# launch kernel
|
||||
def grid(META):
|
||||
return (triton.cdiv(max_seqlen_q, META["BLOCK_SIZE_Q"]), batch_size * num_heads)
|
||||
|
||||
_flash_attn_fwd_with_block_score_kernel[grid](
|
||||
q,
|
||||
k_cache,
|
||||
v_cache,
|
||||
sink,
|
||||
o,
|
||||
score,
|
||||
req_to_token,
|
||||
cu_seqlens,
|
||||
seq_lens,
|
||||
prefix_lens,
|
||||
slot_ids,
|
||||
max_slots,
|
||||
num_heads,
|
||||
gqa_group_size,
|
||||
qk_head_dim,
|
||||
v_head_dim,
|
||||
block_size_k,
|
||||
sm_scale,
|
||||
False,
|
||||
1,
|
||||
q.stride(0),
|
||||
q.stride(1),
|
||||
q.stride(2),
|
||||
k_cache.stride(0),
|
||||
k_cache.stride(1),
|
||||
k_cache.stride(2),
|
||||
v_cache.stride(0) if v_cache is not None else 0,
|
||||
v_cache.stride(1) if v_cache is not None else 0,
|
||||
v_cache.stride(2) if v_cache is not None else 0,
|
||||
sink.stride(0) if sink is not None else 0,
|
||||
sink.stride(1) if sink is not None else 0,
|
||||
o.stride(0) if o is not None else 0,
|
||||
o.stride(1) if o is not None else 0,
|
||||
o.stride(2) if o is not None else 0,
|
||||
score.stride(0),
|
||||
score.stride(1),
|
||||
score.stride(2),
|
||||
req_to_token.stride(0),
|
||||
SCORE_TYPE=score_type,
|
||||
DISABLE_INDEX_VALUE=disable_index_value,
|
||||
)
|
||||
|
||||
# topk extraction kernel
|
||||
topk_idx = torch.full(
|
||||
(num_heads, all_seqblock_q, topk),
|
||||
fill_value=-1,
|
||||
device=score.device,
|
||||
dtype=torch.int32,
|
||||
)
|
||||
# launch kernel
|
||||
grid = (max_seqblock_q, batch_size, num_heads)
|
||||
_topk_index_kernel[grid](
|
||||
score,
|
||||
topk_idx,
|
||||
block_size_q,
|
||||
block_size_k,
|
||||
cu_seqlens,
|
||||
cu_seqblocks_q,
|
||||
prefix_lens,
|
||||
topk,
|
||||
init_blocks,
|
||||
local_blocks,
|
||||
score.stride(0),
|
||||
score.stride(1),
|
||||
score.stride(2),
|
||||
topk_idx.stride(0),
|
||||
topk_idx.stride(1),
|
||||
topk_idx.stride(2),
|
||||
MASK_INIT=False,
|
||||
MASK_LOCAL=False,
|
||||
)
|
||||
return o, topk_idx
|
||||
@@ -0,0 +1,356 @@
|
||||
# Copyright 2025 XunhaoLai. All rights reserved.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from ..common.utils import check_sparse_kv_fp8, get_cu_seqblocks, robust_allocator
|
||||
|
||||
|
||||
@triton.heuristics(
|
||||
{
|
||||
"BLOCK_SIZE_KD": lambda args: triton.next_power_of_2(args["qk_head_dim"]),
|
||||
"BLOCK_SIZE_VD": lambda args: triton.next_power_of_2(args["v_head_dim"]),
|
||||
"BLOCK_SIZE_H": lambda args: triton.next_power_of_2(
|
||||
max(
|
||||
16 // args["BLOCK_SIZE_Q"],
|
||||
triton.next_power_of_2(args["gqa_group_size"]),
|
||||
)
|
||||
),
|
||||
"BLOCK_SIZE_T": lambda args: triton.next_power_of_2(args["max_topk"]),
|
||||
"BLOCK_SIZE_QH": lambda args: args["BLOCK_SIZE_Q"] * args["BLOCK_SIZE_H"],
|
||||
"HAS_SINK": lambda args: args["sink_ptr"] is not None,
|
||||
}
|
||||
)
|
||||
@triton.autotune(
|
||||
# Configs that fail to compile on the target arch are skipped, so widening
|
||||
# the num_warps x num_stages grid only adds candidates, never a bad kernel.
|
||||
configs=[
|
||||
triton.Config({}, num_warps=nw, num_stages=ns)
|
||||
for nw in (2, 4, 8)
|
||||
for ns in (2, 3, 4)
|
||||
],
|
||||
key=[
|
||||
"BLOCK_SIZE_Q",
|
||||
"BLOCK_SIZE_K",
|
||||
"qk_head_dim",
|
||||
"v_head_dim",
|
||||
"gqa_group_size",
|
||||
],
|
||||
)
|
||||
@triton.jit
|
||||
def _gqa_share_sparse_fwd_kernel(
|
||||
q_ptr, # Q: n x h x d
|
||||
k_cache_ptr, # K paged: max_slots x kh x d
|
||||
v_cache_ptr, # V paged: max_slots x kh x d
|
||||
sink_ptr, # Sink: h x d
|
||||
t_ptr, # topk_idx: kh x n x k
|
||||
o_ptr, # O: n x h x d
|
||||
req_to_token_ptr, # req_to_token: max_reqs x max_kv_len
|
||||
# seqlens
|
||||
cu_seqlens_q,
|
||||
cu_seqblocks_q,
|
||||
seq_lens,
|
||||
prefix_lens,
|
||||
slot_ids,
|
||||
# shape
|
||||
max_slots,
|
||||
num_kv_heads,
|
||||
gqa_group_size,
|
||||
qk_head_dim,
|
||||
v_head_dim,
|
||||
max_topk,
|
||||
# q loop num
|
||||
num_q_loop,
|
||||
# sm_scale
|
||||
sm_scale,
|
||||
# stride
|
||||
stride_qn,
|
||||
stride_qh,
|
||||
stride_qd,
|
||||
stride_ks,
|
||||
stride_kh,
|
||||
stride_kd,
|
||||
stride_vs,
|
||||
stride_vh,
|
||||
stride_vd,
|
||||
stride_sh,
|
||||
stride_sd,
|
||||
stride_th,
|
||||
stride_tn,
|
||||
stride_tk,
|
||||
stride_on,
|
||||
stride_oh,
|
||||
stride_od,
|
||||
stride_r2t_b,
|
||||
# META parameters
|
||||
BLOCK_SIZE_Q: tl.constexpr, # q block size
|
||||
BLOCK_SIZE_K: tl.constexpr, # k block size
|
||||
BLOCK_SIZE_KD: tl.constexpr,
|
||||
BLOCK_SIZE_VD: tl.constexpr,
|
||||
BLOCK_SIZE_H: tl.constexpr,
|
||||
BLOCK_SIZE_T: tl.constexpr,
|
||||
BLOCK_SIZE_QH: tl.constexpr,
|
||||
# has sink
|
||||
HAS_SINK: tl.constexpr,
|
||||
USE_TMA: tl.constexpr,
|
||||
IS_FP8: tl.constexpr,
|
||||
):
|
||||
sm_scale_log2e = sm_scale * 1.4426950409
|
||||
# get batch id and head id
|
||||
pid_q = tl.program_id(0)
|
||||
pid_kh = tl.program_id(1)
|
||||
pid_b = tl.program_id(2)
|
||||
pid_h = pid_kh * gqa_group_size
|
||||
# get q k start and len after rmpad
|
||||
q_start = tl.load(cu_seqlens_q + pid_b)
|
||||
q_len = tl.load(cu_seqlens_q + pid_b + 1) - q_start
|
||||
q_block_start = tl.load(cu_seqblocks_q + pid_b)
|
||||
q_block_len = tl.load(cu_seqblocks_q + pid_b + 1) - q_block_start
|
||||
seq_len = tl.load(seq_lens + pid_b)
|
||||
prefix_len = tl.load(prefix_lens + pid_b)
|
||||
sid = (
|
||||
tl.load(slot_ids + pid_b).to(tl.int64) + max_slots
|
||||
) % max_slots # safety against negative
|
||||
if pid_q * num_q_loop >= q_block_len:
|
||||
return
|
||||
real_q_loop = min(num_q_loop, q_block_len - pid_q * num_q_loop)
|
||||
if HAS_SINK:
|
||||
sink_ptrs = tl.make_block_ptr(
|
||||
base=sink_ptr + pid_h * stride_sh,
|
||||
shape=(gqa_group_size, qk_head_dim),
|
||||
strides=(stride_sh, stride_sd),
|
||||
offsets=(0, 0),
|
||||
block_shape=(BLOCK_SIZE_H, BLOCK_SIZE_KD),
|
||||
order=(1, 0),
|
||||
)
|
||||
sink = tl.load(sink_ptrs, boundary_check=(0, 1), padding_option="zero").to(
|
||||
tl.float32
|
||||
)
|
||||
# offsets for paged K/V load
|
||||
off_n = tl.arange(0, BLOCK_SIZE_K)
|
||||
off_kd = tl.arange(0, BLOCK_SIZE_KD)
|
||||
off_vd = tl.arange(0, BLOCK_SIZE_VD)
|
||||
kd_mask = off_kd < qk_head_dim
|
||||
vd_mask = off_vd < v_head_dim
|
||||
for j in range(real_q_loop):
|
||||
pid_q_j = pid_q * num_q_loop + j
|
||||
# init topk idx pointer
|
||||
t_ptr_j = t_ptr + (q_block_start + pid_q_j) * stride_tn + pid_kh * stride_th
|
||||
# we assume that the topk_idx is right padded with -1
|
||||
off_t = tl.arange(0, BLOCK_SIZE_T)
|
||||
topk_idx = tl.load(t_ptr_j + off_t * stride_tk, mask=off_t < max_topk, other=-1)
|
||||
valid_idx = tl.where(topk_idx >= 0, off_t, -1)
|
||||
real_topk = tl.sum(valid_idx != -1, axis=0)
|
||||
# init qkv pointer
|
||||
q_ptrs = tl.make_block_ptr(
|
||||
base=q_ptr + q_start * stride_qn + pid_h * stride_qh,
|
||||
shape=(q_len, gqa_group_size, qk_head_dim),
|
||||
strides=(stride_qn, stride_qh, stride_qd),
|
||||
offsets=(pid_q_j * BLOCK_SIZE_Q, 0, 0),
|
||||
block_shape=(BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_KD),
|
||||
order=(2, 1, 0),
|
||||
)
|
||||
# load q, shape: [BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_D] -> [BLOCK_SIZE_QH, BLOCK_SIZE_D]
|
||||
q = tl.load(q_ptrs, boundary_check=(0, 1, 2), padding_option="zero")
|
||||
# init statistics
|
||||
off_q_k = (
|
||||
tl.arange(0, BLOCK_SIZE_Q)[:, None]
|
||||
+ pid_q_j * BLOCK_SIZE_Q
|
||||
+ prefix_len
|
||||
- tl.arange(0, BLOCK_SIZE_K)[None, :]
|
||||
)
|
||||
if HAS_SINK:
|
||||
m_i = tl.zeros((BLOCK_SIZE_Q, BLOCK_SIZE_H), dtype=tl.float32)
|
||||
lse_i = tl.zeros((BLOCK_SIZE_Q, BLOCK_SIZE_H), dtype=tl.float32)
|
||||
qsink = (
|
||||
tl.sum(q.to(tl.float32) * sink[None, :, :], axis=2) * sm_scale_log2e
|
||||
) # (BLOCK_SIZE_Q, BLOCK_SIZE_H)
|
||||
m_i += qsink
|
||||
lse_i += qsink
|
||||
m_i = tl.reshape(m_i, BLOCK_SIZE_QH)
|
||||
lse_i = tl.reshape(lse_i, BLOCK_SIZE_QH)
|
||||
else:
|
||||
m_i = tl.full((BLOCK_SIZE_QH,), float("-inf"), dtype=tl.float32)
|
||||
lse_i = tl.full((BLOCK_SIZE_QH,), float("-inf"), dtype=tl.float32)
|
||||
acc_o = tl.full((BLOCK_SIZE_QH, BLOCK_SIZE_VD), 0, dtype=tl.float32)
|
||||
q = tl.reshape(q, BLOCK_SIZE_QH, BLOCK_SIZE_KD)
|
||||
# sparse attention
|
||||
for i in range(real_topk):
|
||||
# get current block start index (absolute K position)
|
||||
c = tl.load(t_ptr_j).to(tl.int32) * BLOCK_SIZE_K
|
||||
t_ptr_j = t_ptr_j + stride_tk
|
||||
# paged load K via req_to_token: pos -> slot -> k_cache
|
||||
pos = c + off_n
|
||||
pos_mask = pos < seq_len
|
||||
slots = tl.load(
|
||||
req_to_token_ptr + sid * stride_r2t_b + pos,
|
||||
mask=pos_mask,
|
||||
other=0,
|
||||
).to(tl.int64)
|
||||
slots = (slots + max_slots) % max_slots # safety against negative
|
||||
# k shape: [BLOCK_SIZE_KD, BLOCK_SIZE_K] (transposed for tl.dot)
|
||||
k = tl.load(
|
||||
k_cache_ptr
|
||||
+ slots[None, :] * stride_ks
|
||||
+ pid_kh * stride_kh
|
||||
+ off_kd[:, None] * stride_kd,
|
||||
mask=kd_mask[:, None] & pos_mask[None, :],
|
||||
other=0.0,
|
||||
)
|
||||
if IS_FP8:
|
||||
# fp8 main K cache is unit-scaled; widen to the Q compute dtype
|
||||
# before the tl.dot (compiled out when the cache is bf16).
|
||||
k = k.to(q.dtype)
|
||||
# compute qk
|
||||
qk = tl.zeros((BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_K), dtype=tl.float32)
|
||||
# causal mask
|
||||
qk += tl.where(off_q_k[:, None, :] >= c, 0, float("-inf"))
|
||||
qk = tl.reshape(qk, BLOCK_SIZE_QH, BLOCK_SIZE_K)
|
||||
# [BLOCK_SIZE_QH, qk_head_dim] @ [qk_head_dim, BLOCK_SIZE_K]
|
||||
# -> [BLOCK_SIZE_QH, BLOCK_SIZE_K]
|
||||
qk += tl.dot(q, k) * sm_scale_log2e
|
||||
# K boundary mask: positions beyond seq_len contribute -inf
|
||||
qk += tl.where(pos_mask[None, :], 0, float("-inf"))
|
||||
# compute m_ij and l_ij
|
||||
m_ij = tl.maximum(m_i, tl.max(qk, axis=1))
|
||||
p = tl.exp2(qk - m_ij[:, None])
|
||||
l_ij = tl.sum(p, axis=1)
|
||||
# scale acc_o
|
||||
acc_o_scale = tl.exp2(m_i - m_ij)
|
||||
acc_o = acc_o * acc_o_scale[:, None]
|
||||
# paged load V
|
||||
v = tl.load(
|
||||
v_cache_ptr
|
||||
+ slots[:, None] * stride_vs
|
||||
+ pid_kh * stride_vh
|
||||
+ off_vd[None, :] * stride_vd,
|
||||
mask=pos_mask[:, None] & vd_mask[None, :],
|
||||
other=0.0,
|
||||
)
|
||||
if IS_FP8:
|
||||
# Widen V so `p.to(v.dtype)` casts P to the compute dtype rather
|
||||
# than to fp8 (which would wreck attention-weight precision).
|
||||
v = v.to(q.dtype)
|
||||
p = p.to(v.dtype)
|
||||
acc_o += tl.dot(p, v)
|
||||
# update statistics
|
||||
m_i = m_ij
|
||||
lse_i = m_ij + tl.log2(tl.exp2(lse_i - m_ij) + l_ij)
|
||||
# final scale
|
||||
acc_o = acc_o * tl.exp2(m_i - lse_i)[:, None]
|
||||
# save output
|
||||
acc_o = tl.reshape(acc_o, BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_VD)
|
||||
o_ptrs = tl.make_block_ptr(
|
||||
base=o_ptr + q_start * stride_on + pid_h * stride_oh,
|
||||
shape=(q_len, gqa_group_size, v_head_dim),
|
||||
strides=(stride_on, stride_oh, stride_od),
|
||||
offsets=(pid_q_j * BLOCK_SIZE_Q, 0, 0),
|
||||
block_shape=(BLOCK_SIZE_Q, BLOCK_SIZE_H, BLOCK_SIZE_VD),
|
||||
order=(2, 1, 0),
|
||||
)
|
||||
tl.store(o_ptrs, acc_o.to(o_ptr.dtype.element_ty), boundary_check=(0, 1, 2))
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def flash_prefill_with_gqa_share_sparse(
|
||||
q: torch.Tensor,
|
||||
k_cache: torch.Tensor,
|
||||
v_cache: torch.Tensor,
|
||||
sink: Optional[torch.Tensor],
|
||||
req_to_token: torch.Tensor,
|
||||
slot_ids: torch.Tensor,
|
||||
topk_idx: torch.Tensor,
|
||||
block_size_q: int,
|
||||
block_size_k: int,
|
||||
cu_seqlens: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
prefix_lens: torch.Tensor,
|
||||
max_seqlen_q: int,
|
||||
sm_scale: Optional[float] = None,
|
||||
use_tma: bool = True,
|
||||
cu_seqblocks_q: Optional[torch.Tensor] = None,
|
||||
max_seqblock_q: Optional[int] = None,
|
||||
) -> torch.Tensor:
|
||||
triton.set_allocator(robust_allocator)
|
||||
is_fp8 = check_sparse_kv_fp8(q, k_cache, v_cache, label="prefill")
|
||||
assert block_size_q in {1, 2, 4, 8, 16, 32, 64}
|
||||
assert block_size_k in {16, 32, 64, 128}
|
||||
# shape
|
||||
total_q, num_q_heads, qk_head_dim = q.shape
|
||||
max_slots, num_k_heads, _ = k_cache.shape
|
||||
_, num_v_heads, v_head_dim = v_cache.shape
|
||||
batch_size = cu_seqlens.shape[0] - 1
|
||||
topk = topk_idx.shape[-1]
|
||||
assert topk_idx.shape[0] == num_k_heads
|
||||
# gqa
|
||||
assert num_k_heads == num_v_heads
|
||||
assert num_q_heads % num_k_heads == 0
|
||||
gqa_group_size = num_q_heads // num_k_heads
|
||||
assert gqa_group_size * block_size_q <= 128
|
||||
if sm_scale is None:
|
||||
sm_scale = qk_head_dim**-0.5
|
||||
if cu_seqblocks_q is None or max_seqblock_q is None:
|
||||
cu_seqblocks_q, max_seqblock_q, _, _, _, _ = get_cu_seqblocks(
|
||||
cu_seqlens, max_seqlen_q, block_size_q, block_size_k
|
||||
)
|
||||
# output tensor
|
||||
o = torch.empty(total_q, num_q_heads, v_head_dim, device=q.device, dtype=q.dtype)
|
||||
# launch kernel
|
||||
num_q_loop = (
|
||||
max_seqblock_q // 131072 + 1
|
||||
) # calculate multiple queries in one kernel if seqlence length is too long
|
||||
BLOCK_SIZE_Q = triton.next_power_of_2(block_size_q)
|
||||
BLOCK_SIZE_K = triton.next_power_of_2(block_size_k)
|
||||
grid = (
|
||||
triton.cdiv(triton.cdiv(max_seqlen_q, block_size_q), num_q_loop),
|
||||
num_k_heads,
|
||||
batch_size,
|
||||
)
|
||||
_gqa_share_sparse_fwd_kernel[grid](
|
||||
q,
|
||||
k_cache,
|
||||
v_cache,
|
||||
sink,
|
||||
topk_idx,
|
||||
o,
|
||||
req_to_token,
|
||||
cu_seqlens,
|
||||
cu_seqblocks_q,
|
||||
seq_lens,
|
||||
prefix_lens,
|
||||
slot_ids,
|
||||
max_slots,
|
||||
num_k_heads,
|
||||
gqa_group_size,
|
||||
qk_head_dim,
|
||||
v_head_dim,
|
||||
topk,
|
||||
num_q_loop,
|
||||
sm_scale,
|
||||
q.stride(0),
|
||||
q.stride(1),
|
||||
q.stride(2),
|
||||
k_cache.stride(0),
|
||||
k_cache.stride(1),
|
||||
k_cache.stride(2),
|
||||
v_cache.stride(0),
|
||||
v_cache.stride(1),
|
||||
v_cache.stride(2),
|
||||
sink.stride(0) if sink is not None else 0,
|
||||
sink.stride(1) if sink is not None else 0,
|
||||
topk_idx.stride(0),
|
||||
topk_idx.stride(1),
|
||||
topk_idx.stride(2),
|
||||
o.stride(0),
|
||||
o.stride(1),
|
||||
o.stride(2),
|
||||
req_to_token.stride(0),
|
||||
BLOCK_SIZE_Q=BLOCK_SIZE_Q,
|
||||
BLOCK_SIZE_K=BLOCK_SIZE_K,
|
||||
USE_TMA=use_tma,
|
||||
IS_FP8=is_fp8,
|
||||
)
|
||||
return o
|
||||
+480
@@ -0,0 +1,480 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.attention.minimax_sparse_ops.decode.flash_with_topk_idx import (
|
||||
flash_decode_with_topk_idx,
|
||||
)
|
||||
|
||||
DEVICE = "cuda"
|
||||
RTOL_VS_REF = 5e-3
|
||||
ATOL_VS_REF = 5e-3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Reference & helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytorch_reference(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens,
|
||||
slot_ids,
|
||||
block_size,
|
||||
topk,
|
||||
init_blocks,
|
||||
local_blocks,
|
||||
sm_scale=None,
|
||||
score_type="max",
|
||||
):
|
||||
batch_size, num_q_heads, head_dim = q.shape
|
||||
num_kv_heads = k_cache.shape[1]
|
||||
gqa_group_size = num_q_heads // num_kv_heads
|
||||
if sm_scale is None:
|
||||
sm_scale = head_dim**-0.5
|
||||
max_sl = seq_lens.max().item()
|
||||
|
||||
# Gather K/V for all batches: [BS, max_sl, kv_heads, hd] -> [BS, q_heads, max_sl, hd]
|
||||
all_slots = req_to_token[:, :max_sl].long()
|
||||
k = (
|
||||
k_cache[all_slots]
|
||||
.float()
|
||||
.permute(0, 2, 1, 3)
|
||||
.repeat_interleave(gqa_group_size, dim=1)
|
||||
)
|
||||
v = (
|
||||
v_cache[all_slots]
|
||||
.float()
|
||||
.permute(0, 2, 1, 3)
|
||||
.repeat_interleave(gqa_group_size, dim=1)
|
||||
)
|
||||
|
||||
# Batched QK: [BS, q_heads, max_sl]
|
||||
qk = (q.float().unsqueeze(2) @ k.transpose(-1, -2)).squeeze(2) * sm_scale
|
||||
seq_mask = torch.arange(max_sl, device=q.device).unsqueeze(0) < seq_lens.unsqueeze(
|
||||
1
|
||||
)
|
||||
qk = qk.masked_fill(~seq_mask.unsqueeze(1), float("-inf"))
|
||||
|
||||
# Attention output (full attention on all tokens)
|
||||
if sink is not None:
|
||||
sink_score = (q.float() * sink.float().unsqueeze(0)).sum(
|
||||
dim=-1, keepdim=True
|
||||
) * sm_scale
|
||||
attn = torch.softmax(torch.cat([sink_score, qk], dim=-1), dim=-1)
|
||||
o = (attn[:, :, 1:].unsqueeze(2) @ v).squeeze(2)
|
||||
else:
|
||||
attn = torch.softmax(qk, dim=-1)
|
||||
o = (attn.unsqueeze(2) @ v).squeeze(2)
|
||||
|
||||
# Block scores + topk
|
||||
topk_idx = torch.full(
|
||||
(num_q_heads, batch_size, topk), -1, dtype=torch.int32, device=q.device
|
||||
)
|
||||
max_num_blocks = (max_sl + block_size - 1) // block_size
|
||||
padded_len = max_num_blocks * block_size
|
||||
qk_padded = torch.full(
|
||||
(batch_size, num_q_heads, padded_len), float("-inf"), device=q.device
|
||||
)
|
||||
qk_padded[:, :, :max_sl] = qk
|
||||
block_scores = qk_padded.reshape(
|
||||
batch_size, num_q_heads, max_num_blocks, block_size
|
||||
)
|
||||
if score_type == "max":
|
||||
block_scores = block_scores.max(dim=-1).values
|
||||
else:
|
||||
bmax = block_scores.max(dim=-1, keepdim=True).values
|
||||
block_scores = bmax.squeeze(-1) + torch.log(
|
||||
torch.sum(torch.exp(block_scores - bmax), dim=-1)
|
||||
)
|
||||
block_scores = torch.where(block_scores.isnan(), float("-inf"), block_scores)
|
||||
|
||||
for b in range(batch_size):
|
||||
sl = seq_lens[b].item()
|
||||
num_blocks = (sl + block_size - 1) // block_size
|
||||
bs_b = block_scores[b, :, :num_blocks].clone()
|
||||
if init_blocks > 0:
|
||||
bs_b[:, :init_blocks] = 1e30
|
||||
if local_blocks > 0:
|
||||
local_start = max(0, num_blocks - local_blocks)
|
||||
bs_b[:, local_start:num_blocks] = 1e29
|
||||
actual_topk = min(topk, num_blocks)
|
||||
_, tidx = bs_b.topk(actual_topk, dim=-1)
|
||||
topk_idx[:, b, :actual_topk] = tidx.to(torch.int32)
|
||||
|
||||
return o, topk_idx
|
||||
|
||||
|
||||
def build_inputs(
|
||||
batch_size,
|
||||
num_q_heads,
|
||||
num_kv_heads,
|
||||
head_dim,
|
||||
seq_lens_list,
|
||||
max_kv_len=None,
|
||||
with_sink=False,
|
||||
dtype=torch.bfloat16,
|
||||
):
|
||||
if max_kv_len is None:
|
||||
max_kv_len = max(seq_lens_list)
|
||||
max_slots = batch_size * max_kv_len
|
||||
k_cache = torch.randn(max_slots, num_kv_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
v_cache = torch.randn(max_slots, num_kv_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
q = torch.randn(batch_size, num_q_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
req_to_token = torch.zeros(batch_size, max_kv_len, dtype=torch.int32, device=DEVICE)
|
||||
slot_ids = torch.zeros(batch_size, dtype=torch.int64, device=DEVICE)
|
||||
seq_lens = torch.tensor(seq_lens_list, dtype=torch.int32, device=DEVICE)
|
||||
for i in range(batch_size):
|
||||
base = i * max_kv_len
|
||||
slot_ids[i] = i
|
||||
req_to_token[i, :max_kv_len] = torch.arange(
|
||||
base, base + max_kv_len, device=DEVICE
|
||||
)
|
||||
sink = (
|
||||
torch.randn(num_q_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
if with_sink
|
||||
else None
|
||||
)
|
||||
return q, sink, k_cache, v_cache, req_to_token, seq_lens, max_kv_len, slot_ids
|
||||
|
||||
|
||||
def make_seq_lens(pattern, batch_size, block_size):
|
||||
if pattern == "aligned":
|
||||
sl = [1024] * batch_size
|
||||
elif pattern == "unaligned":
|
||||
base = [513, 1023, 257, 769]
|
||||
sl = (base * ((batch_size + len(base) - 1) // len(base)))[:batch_size]
|
||||
elif pattern == "mixed":
|
||||
base = [64, 2048, 512, 128]
|
||||
sl = (base * ((batch_size + len(base) - 1) // len(base)))[:batch_size]
|
||||
elif pattern == "few_blocks":
|
||||
base = [block_size, block_size * 2]
|
||||
sl = (base * ((batch_size + len(base) - 1) // len(base)))[:batch_size]
|
||||
elif pattern == "long":
|
||||
sl = [524288] * batch_size
|
||||
return sl, max(sl)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test cases: compact set covering all code paths + long sequence.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _case(bs, nqh, nkh, hd, blk, sink, seq_pat, ib, lb, tk):
|
||||
tag = (
|
||||
f"bs{bs}_gqa{nqh}:{nkh}_hd{hd}_blk{blk}"
|
||||
f"_{'sink' if sink else 'nosink'}_{seq_pat}"
|
||||
f"_init{ib}_local{lb}_topk{tk}"
|
||||
)
|
||||
return pytest.param(bs, nqh, nkh, hd, blk, sink, seq_pat, ib, lb, tk, id=tag)
|
||||
|
||||
|
||||
# fmt: off
|
||||
CASES = [
|
||||
# -- Core code paths --
|
||||
_case(2, 8, 1, 128, 64, False, "aligned", 0, 0, 16), # baseline
|
||||
_case(2, 8, 1, 128, 64, False, "unaligned", 2, 4, 16), # unaligned + init+local
|
||||
_case(2, 8, 1, 128, 32, False, "mixed", 0, 0, 16), # block_size=32 + mixed
|
||||
_case(2, 8, 1, 128, 64, True, "aligned", 0, 0, 16), # sink
|
||||
_case(8, 8, 1, 128, 64, True, "unaligned", 2, 4, 16), # sink + init+local
|
||||
_case(8, 8, 1, 128, 64, False, "aligned", 0, 0, 1), # topk=1
|
||||
_case(32, 8, 1, 128, 64, False, "few_blocks", 0, 0, 32), # topk > num_blocks
|
||||
_case(32, 32, 8, 128, 64, False, "aligned", 0, 0, 16), # GQA 32:8
|
||||
_case(128, 8, 4, 64, 64, False, "aligned", 0, 0, 16), # head_dim=64
|
||||
_case(128, 8, 4, 128, 64, False, "few_blocks", 0, 0, 1), # single batch, 1 block, topk=1
|
||||
# -- Long sequence (512k) --
|
||||
_case(1, 8, 1, 128, 64, False, "long", 0, 0, 16), # 512k baseline
|
||||
_case(1, 8, 1, 128, 64, False, "long", 2, 4, 16), # 512k + init+local
|
||||
_case(1, 8, 1, 128, 64, True, "long", 0, 0, 16), # 512k + sink
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("score_type", ["max", "lse"])
|
||||
@pytest.mark.parametrize(
|
||||
"bs,nqh,nkh,hd,blk,with_sink,seq_pat,ib,lb,tk",
|
||||
CASES,
|
||||
)
|
||||
def test_flash_decode_with_topk_idx(
|
||||
bs, nqh, nkh, hd, blk, with_sink, seq_pat, ib, lb, tk, score_type
|
||||
):
|
||||
torch.manual_seed(42)
|
||||
seq_lens, mkl = make_seq_lens(seq_pat, bs, blk)
|
||||
|
||||
q, sink, k_cache, v_cache, req_to_token, seq_lens_t, mkl, slot_ids = build_inputs(
|
||||
bs,
|
||||
nqh,
|
||||
nkh,
|
||||
hd,
|
||||
seq_lens,
|
||||
max_kv_len=mkl,
|
||||
with_sink=with_sink,
|
||||
)
|
||||
|
||||
o_new, topk_new, _ = flash_decode_with_topk_idx(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens_t,
|
||||
mkl,
|
||||
slot_ids,
|
||||
blk,
|
||||
tk,
|
||||
ib,
|
||||
lb,
|
||||
score_type=score_type,
|
||||
)
|
||||
o_ref, topk_ref = pytorch_reference(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens_t,
|
||||
slot_ids,
|
||||
blk,
|
||||
tk,
|
||||
ib,
|
||||
lb,
|
||||
score_type=score_type,
|
||||
)
|
||||
o_ref = o_ref.to(o_new.dtype)
|
||||
|
||||
# --- attention output vs reference ---
|
||||
assert torch.allclose(
|
||||
o_new.float(), o_ref.float(), rtol=RTOL_VS_REF, atol=ATOL_VS_REF
|
||||
), f"vs ref max abs diff {(o_new.float() - o_ref.float()).abs().max().item():.4e}"
|
||||
|
||||
# --- topk set match vs reference ---
|
||||
for h in range(nqh):
|
||||
for b in range(bs):
|
||||
sl = seq_lens[b]
|
||||
num_blocks = (sl + blk - 1) // blk
|
||||
actual_k = min(tk, num_blocks)
|
||||
set_new = set(topk_new[h, b, :actual_k].tolist())
|
||||
set_ref = set(topk_ref[h, b, :actual_k].tolist())
|
||||
assert (
|
||||
set_new == set_ref
|
||||
), f"topk mismatch at h={h} b={b}: kernel={set_new} ref={set_ref}"
|
||||
|
||||
# --- topk sentinel: invalid positions must be -1 ---
|
||||
for b in range(bs):
|
||||
sl = seq_lens[b]
|
||||
num_blocks = (sl + blk - 1) // blk
|
||||
actual_k = min(tk, num_blocks)
|
||||
if actual_k < tk:
|
||||
invalid = topk_new[:, b, actual_k:]
|
||||
assert (
|
||||
invalid == -1
|
||||
).all(), f"sentinel fail at b={b}: expected -1, got {invalid[invalid != -1].tolist()}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("score_type", ["max", "lse"])
|
||||
@pytest.mark.parametrize(
|
||||
"bs,nqh,nkh,hd,blk,with_sink,seq_pat,ib,lb,tk",
|
||||
CASES,
|
||||
)
|
||||
def test_flash_decode_score_only(
|
||||
bs, nqh, nkh, hd, blk, with_sink, seq_pat, ib, lb, tk, score_type
|
||||
):
|
||||
torch.manual_seed(42)
|
||||
seq_lens, mkl = make_seq_lens(seq_pat, bs, blk)
|
||||
|
||||
q, sink, k_cache, v_cache, req_to_token, seq_lens_t, mkl, slot_ids = build_inputs(
|
||||
bs,
|
||||
nqh,
|
||||
nkh,
|
||||
hd,
|
||||
seq_lens,
|
||||
max_kv_len=mkl,
|
||||
with_sink=with_sink,
|
||||
)
|
||||
|
||||
o_new, topk_new, _ = flash_decode_with_topk_idx(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens_t,
|
||||
mkl,
|
||||
slot_ids,
|
||||
blk,
|
||||
tk,
|
||||
ib,
|
||||
lb,
|
||||
disable_index_value=True,
|
||||
score_type=score_type,
|
||||
)
|
||||
_, topk_ref = pytorch_reference(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens_t,
|
||||
slot_ids,
|
||||
blk,
|
||||
tk,
|
||||
ib,
|
||||
lb,
|
||||
score_type=score_type,
|
||||
)
|
||||
|
||||
assert o_new is None, "expected None output when disable_index_value=True"
|
||||
|
||||
for h in range(nqh):
|
||||
for b in range(bs):
|
||||
sl = seq_lens[b]
|
||||
num_blocks = (sl + blk - 1) // blk
|
||||
actual_k = min(tk, num_blocks)
|
||||
set_new = set(topk_new[h, b, :actual_k].tolist())
|
||||
set_ref = set(topk_ref[h, b, :actual_k].tolist())
|
||||
assert (
|
||||
set_new == set_ref
|
||||
), f"topk mismatch at h={h} b={b}: kernel={set_new} ref={set_ref}"
|
||||
|
||||
for b in range(bs):
|
||||
sl = seq_lens[b]
|
||||
num_blocks = (sl + blk - 1) // blk
|
||||
actual_k = min(tk, num_blocks)
|
||||
if actual_k < tk:
|
||||
invalid = topk_new[:, b, actual_k:]
|
||||
assert (
|
||||
invalid == -1
|
||||
).all(), f"sentinel fail at b={b}: expected -1, got {invalid[invalid != -1].tolist()}"
|
||||
|
||||
|
||||
def test_flash_decode_jit_topk_trivial_rows_skip_score_writes():
|
||||
torch.manual_seed(123)
|
||||
bs, nqh, nkh, hd, blk, tk = 4, 8, 1, 128, 64, 32
|
||||
seq_lens, mkl = make_seq_lens("few_blocks", bs, blk)
|
||||
|
||||
q, sink, k_cache, v_cache, req_to_token, seq_lens_t, mkl, slot_ids = build_inputs(
|
||||
bs,
|
||||
nqh,
|
||||
nkh,
|
||||
hd,
|
||||
seq_lens,
|
||||
max_kv_len=mkl,
|
||||
)
|
||||
|
||||
with envs.SGLANG_OPT_USE_MINIMAX_DECODE_TOPK_RADIX.override(True):
|
||||
o_new, topk_new, real_seq_lens = flash_decode_with_topk_idx(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens_t,
|
||||
mkl,
|
||||
slot_ids,
|
||||
blk,
|
||||
tk,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
o_ref, topk_ref = pytorch_reference(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens_t,
|
||||
slot_ids,
|
||||
blk,
|
||||
tk,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
assert real_seq_lens is None
|
||||
assert torch.allclose(
|
||||
o_new.float(), o_ref.float(), rtol=RTOL_VS_REF, atol=ATOL_VS_REF
|
||||
)
|
||||
for h in range(nqh):
|
||||
for b in range(bs):
|
||||
sl = seq_lens[b]
|
||||
num_blocks = (sl + blk - 1) // blk
|
||||
actual_k = min(tk, num_blocks)
|
||||
assert set(topk_new[h, b, :actual_k].tolist()) == set(
|
||||
topk_ref[h, b, :actual_k].tolist()
|
||||
)
|
||||
assert (topk_new[h, b, actual_k:] == -1).all()
|
||||
|
||||
|
||||
def test_flash_decode_dense_page_table_trivial_rows_skip_score_writes():
|
||||
torch.manual_seed(321)
|
||||
bs, nqh, nkh, hd, blk, tk, page_size = 3, 4, 1, 128, 64, 32, 1
|
||||
seq_lens, mkl = make_seq_lens("few_blocks", bs, blk)
|
||||
|
||||
q, sink, k_cache, v_cache, req_to_token, seq_lens_t, mkl, slot_ids = build_inputs(
|
||||
bs,
|
||||
nqh,
|
||||
nkh,
|
||||
hd,
|
||||
seq_lens,
|
||||
max_kv_len=mkl,
|
||||
)
|
||||
|
||||
o_new, page_table, real_seq_lens = flash_decode_with_topk_idx(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens_t,
|
||||
mkl,
|
||||
slot_ids,
|
||||
blk,
|
||||
tk,
|
||||
0,
|
||||
0,
|
||||
use_dense_main_attn=True,
|
||||
page_size=page_size,
|
||||
)
|
||||
o_ref, _ = pytorch_reference(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens_t,
|
||||
slot_ids,
|
||||
blk,
|
||||
tk,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
assert torch.allclose(
|
||||
o_new.float(), o_ref.float(), rtol=RTOL_VS_REF, atol=ATOL_VS_REF
|
||||
)
|
||||
assert page_table.shape == (bs * nqh, tk * blk // page_size)
|
||||
assert torch.equal(real_seq_lens.cpu(), seq_lens_t.repeat_interleave(nqh).cpu())
|
||||
|
||||
page_table_cpu = page_table.cpu()
|
||||
req_to_token_cpu = req_to_token.cpu()
|
||||
for b, seq_len in enumerate(seq_lens):
|
||||
valid_pages = seq_len // page_size
|
||||
for h in range(nqh):
|
||||
row = b * nqh + h
|
||||
expected = req_to_token_cpu[b, :seq_len:page_size] // page_size * nqh + h
|
||||
assert torch.equal(page_table_cpu[row, :valid_pages], expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,362 @@
|
||||
"""Unit tests for flash_decode_with_gqa_share_sparse (sparse GQA attention).
|
||||
|
||||
Tests the Triton sparse GQA kernel against a PyTorch reference that computes
|
||||
attention only on the topk blocks via standard softmax, covering GQA ratios,
|
||||
sink tokens, paged KV (randperm), variable seq_lens, and edge cases.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.attention.minimax_sparse_ops.decode.topk_sparse import (
|
||||
flash_decode_with_gqa_share_sparse,
|
||||
)
|
||||
|
||||
DEVICE = "cuda"
|
||||
RTOL = 5e-3
|
||||
ATOL = 5e-3
|
||||
|
||||
|
||||
def pytorch_sparse_gqa_reference(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens,
|
||||
block_size,
|
||||
topk_idx,
|
||||
sm_scale=None,
|
||||
):
|
||||
"""PyTorch reference: gather topk block tokens, then batched attention."""
|
||||
batch_size, num_q_heads, head_dim = q.shape
|
||||
num_kv_heads = k_cache.shape[1]
|
||||
gqa_group_size = num_q_heads // num_kv_heads
|
||||
topk = topk_idx.shape[2]
|
||||
if sm_scale is None:
|
||||
sm_scale = head_dim**-0.5
|
||||
|
||||
# Build per-batch token positions from topk block indices, padded to uniform length
|
||||
max_tokens = topk * block_size
|
||||
all_slots = torch.zeros(batch_size, max_tokens, dtype=torch.long, device=q.device)
|
||||
mask = torch.zeros(batch_size, max_tokens, dtype=torch.bool, device=q.device)
|
||||
|
||||
for b in range(batch_size):
|
||||
sl = seq_lens[b].item()
|
||||
offset = 0
|
||||
for t in range(topk):
|
||||
bi = topk_idx[0, b, t].item()
|
||||
if bi < 0:
|
||||
continue
|
||||
start = bi * block_size
|
||||
end = min(start + block_size, sl)
|
||||
n = end - start
|
||||
positions = torch.arange(start, end, device=q.device)
|
||||
all_slots[b, offset : offset + n] = req_to_token[b, positions].long()
|
||||
mask[b, offset : offset + n] = True
|
||||
offset += n
|
||||
|
||||
# Gather K/V: [BS, max_tokens, num_kv_heads, hd] -> [BS, num_q_heads, max_tokens, hd]
|
||||
k = k_cache[all_slots].float() # [BS, max_tokens, num_kv_heads, hd]
|
||||
v = v_cache[all_slots].float()
|
||||
k = k.permute(0, 2, 1, 3).repeat_interleave(
|
||||
gqa_group_size, dim=1
|
||||
) # [BS, num_q_heads, max_tokens, hd]
|
||||
v = v.permute(0, 2, 1, 3).repeat_interleave(gqa_group_size, dim=1)
|
||||
|
||||
# QK: [BS, num_q_heads, 1, hd] @ [BS, num_q_heads, hd, max_tokens] -> [BS, num_q_heads, max_tokens]
|
||||
qk = (q.float().unsqueeze(2) @ k.transpose(-1, -2)).squeeze(2) * sm_scale
|
||||
# Mask invalid positions
|
||||
qk = qk.masked_fill(~mask.unsqueeze(1), float("-inf"))
|
||||
|
||||
if sink is not None:
|
||||
sink_score = (q.float() * sink.float().unsqueeze(0)).sum(
|
||||
dim=-1, keepdim=True
|
||||
) * sm_scale # [BS, num_q_heads, 1]
|
||||
qk = torch.cat([sink_score, qk], dim=-1) # [BS, num_q_heads, 1+max_tokens]
|
||||
attn = torch.softmax(qk, dim=-1)
|
||||
o = (attn[:, :, 1:].unsqueeze(2) @ v).squeeze(2) # [BS, num_q_heads, hd]
|
||||
else:
|
||||
attn = torch.softmax(qk, dim=-1)
|
||||
o = (attn.unsqueeze(2) @ v).squeeze(2)
|
||||
|
||||
return o
|
||||
|
||||
|
||||
def build_inputs(
|
||||
batch_size,
|
||||
num_q_heads,
|
||||
num_kv_heads,
|
||||
head_dim,
|
||||
seq_lens_list,
|
||||
block_size,
|
||||
topk,
|
||||
with_sink=False,
|
||||
paged=True,
|
||||
dtype=torch.bfloat16,
|
||||
):
|
||||
max_kv_len = max(seq_lens_list)
|
||||
max_slots = batch_size * max_kv_len
|
||||
|
||||
q = torch.randn(batch_size, num_q_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
k_cache = torch.randn(max_slots, num_kv_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
v_cache = torch.randn(max_slots, num_kv_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
req_to_token = torch.zeros(batch_size, max_kv_len, dtype=torch.int32, device=DEVICE)
|
||||
slot_ids = torch.zeros(batch_size, dtype=torch.int64, device=DEVICE)
|
||||
seq_lens = torch.tensor(seq_lens_list, dtype=torch.int32, device=DEVICE)
|
||||
|
||||
for i in range(batch_size):
|
||||
base = i * max_kv_len
|
||||
slot_ids[i] = i
|
||||
if paged:
|
||||
req_to_token[i, :max_kv_len] = (
|
||||
torch.randperm(max_kv_len, device=DEVICE) + base
|
||||
).to(torch.int32)
|
||||
else:
|
||||
req_to_token[i, :max_kv_len] = torch.arange(
|
||||
base, base + max_kv_len, device=DEVICE
|
||||
).to(torch.int32)
|
||||
|
||||
num_blocks_list = [(sl + block_size - 1) // block_size for sl in seq_lens_list]
|
||||
actual_topk = min(topk, min(num_blocks_list))
|
||||
topk_idx = torch.zeros(
|
||||
num_kv_heads, batch_size, topk, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
for kh in range(num_kv_heads):
|
||||
for b in range(batch_size):
|
||||
nb = num_blocks_list[b]
|
||||
ak = min(topk, nb)
|
||||
perm = torch.randperm(nb, device=DEVICE)[:ak]
|
||||
topk_idx[kh, b, :ak] = perm.to(torch.int32)
|
||||
if ak < topk:
|
||||
topk_idx[kh, b, ak:] = -1
|
||||
|
||||
sink = (
|
||||
torch.randn(num_q_heads, head_dim, dtype=dtype, device=DEVICE)
|
||||
if with_sink
|
||||
else None
|
||||
)
|
||||
|
||||
return q, sink, k_cache, v_cache, req_to_token, seq_lens, slot_ids, topk_idx
|
||||
|
||||
|
||||
def _case(bs, nqh, nkh, hd, blk, tk, sink, seq_pat, paged=True):
|
||||
tag = (
|
||||
f"bs{bs}_gqa{nqh}:{nkh}_hd{hd}_blk{blk}_topk{tk}"
|
||||
f"_{'sink' if sink else 'nosink'}_{seq_pat}"
|
||||
f"{'_paged' if paged else '_contig'}"
|
||||
)
|
||||
return pytest.param(bs, nqh, nkh, hd, blk, tk, sink, seq_pat, paged, id=tag)
|
||||
|
||||
|
||||
def make_seq_lens(pattern, batch_size, block_size):
|
||||
if pattern == "aligned":
|
||||
return [1024] * batch_size
|
||||
elif pattern == "unaligned":
|
||||
base = [513, 1023, 257, 769]
|
||||
return (base * ((batch_size + len(base) - 1) // len(base)))[:batch_size]
|
||||
elif pattern == "short":
|
||||
return [block_size * 2] * batch_size
|
||||
elif pattern == "mixed":
|
||||
base = [block_size, block_size * 4, block_size * 16, block_size * 2]
|
||||
return (base * ((batch_size + len(base) - 1) // len(base)))[:batch_size]
|
||||
elif pattern == "long":
|
||||
return [block_size * 512] * batch_size
|
||||
|
||||
|
||||
CASES = [
|
||||
# Baseline: GQA 8:1
|
||||
_case(1, 8, 1, 128, 64, 16, False, "aligned"),
|
||||
_case(2, 8, 1, 128, 64, 16, False, "aligned"),
|
||||
_case(4, 8, 1, 128, 64, 32, False, "aligned"),
|
||||
# Unaligned seq_lens
|
||||
_case(4, 8, 1, 128, 64, 16, False, "unaligned"),
|
||||
# Short sequences (topk ~ num_blocks)
|
||||
_case(2, 8, 1, 128, 64, 16, False, "short"),
|
||||
# topk=1
|
||||
_case(2, 8, 1, 128, 64, 1, False, "aligned"),
|
||||
# topk=32 (production config)
|
||||
_case(2, 8, 1, 128, 64, 32, False, "aligned"),
|
||||
# With sink
|
||||
_case(2, 8, 1, 128, 64, 16, True, "aligned"),
|
||||
_case(4, 8, 1, 128, 64, 16, True, "unaligned"),
|
||||
# GQA 32:8
|
||||
_case(2, 32, 8, 128, 64, 16, False, "aligned"),
|
||||
# GQA 16:1 (production)
|
||||
_case(2, 16, 1, 128, 64, 32, False, "aligned"),
|
||||
# head_dim=64
|
||||
_case(2, 8, 4, 64, 64, 16, False, "aligned"),
|
||||
# block_size=32
|
||||
_case(2, 8, 1, 128, 32, 16, False, "aligned"),
|
||||
# Large BS
|
||||
_case(32, 8, 1, 128, 64, 16, False, "aligned"),
|
||||
_case(128, 8, 1, 128, 64, 32, False, "short"),
|
||||
# Contiguous (non-paged) KV
|
||||
_case(2, 8, 1, 128, 64, 16, False, "aligned", paged=False),
|
||||
# Mixed seq_lens in batch
|
||||
_case(4, 8, 1, 128, 64, 8, False, "mixed"),
|
||||
# Long sequence
|
||||
_case(1, 8, 1, 128, 64, 32, False, "long"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bs,nqh,nkh,hd,blk,tk,with_sink,seq_pat,paged", CASES)
|
||||
def test_sparse_gqa_vs_reference(bs, nqh, nkh, hd, blk, tk, with_sink, seq_pat, paged):
|
||||
"""Kernel output must match PyTorch reference (attend only to topk blocks)."""
|
||||
torch.manual_seed(42)
|
||||
seq_lens_list = make_seq_lens(seq_pat, bs, blk)
|
||||
|
||||
q, sink, k_cache, v_cache, req_to_token, seq_lens, slot_ids, topk_idx = (
|
||||
build_inputs(
|
||||
bs,
|
||||
nqh,
|
||||
nkh,
|
||||
hd,
|
||||
seq_lens_list,
|
||||
blk,
|
||||
tk,
|
||||
with_sink=with_sink,
|
||||
paged=paged,
|
||||
)
|
||||
)
|
||||
|
||||
o_kernel = flash_decode_with_gqa_share_sparse(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens,
|
||||
slot_ids,
|
||||
blk,
|
||||
topk_idx,
|
||||
)
|
||||
o_ref = pytorch_sparse_gqa_reference(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens,
|
||||
blk,
|
||||
topk_idx,
|
||||
)
|
||||
o_ref = o_ref.to(o_kernel.dtype)
|
||||
|
||||
assert torch.allclose(
|
||||
o_kernel.float(), o_ref.float(), rtol=RTOL, atol=ATOL
|
||||
), f"max abs diff {(o_kernel.float() - o_ref.float()).abs().max().item():.4e}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bs,nqh,nkh,hd,blk,tk,with_sink,seq_pat,paged",
|
||||
[
|
||||
_case(2, 8, 1, 128, 64, 32, False, "short"),
|
||||
],
|
||||
)
|
||||
def test_sparse_gqa_topk_exceeds_blocks(
|
||||
bs, nqh, nkh, hd, blk, tk, with_sink, seq_pat, paged
|
||||
):
|
||||
"""topk > num_blocks: kernel should handle gracefully (some topk_idx = -1)."""
|
||||
torch.manual_seed(42)
|
||||
seq_lens_list = [blk] * bs
|
||||
|
||||
q, sink, k_cache, v_cache, req_to_token, seq_lens, slot_ids, topk_idx = (
|
||||
build_inputs(
|
||||
bs,
|
||||
nqh,
|
||||
nkh,
|
||||
hd,
|
||||
seq_lens_list,
|
||||
blk,
|
||||
tk,
|
||||
with_sink=with_sink,
|
||||
paged=paged,
|
||||
)
|
||||
)
|
||||
|
||||
o_kernel = flash_decode_with_gqa_share_sparse(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens,
|
||||
slot_ids,
|
||||
blk,
|
||||
topk_idx,
|
||||
)
|
||||
o_ref = pytorch_sparse_gqa_reference(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens,
|
||||
blk,
|
||||
topk_idx,
|
||||
)
|
||||
o_ref = o_ref.to(o_kernel.dtype)
|
||||
|
||||
assert torch.allclose(
|
||||
o_kernel.float(), o_ref.float(), rtol=RTOL, atol=ATOL
|
||||
), f"max abs diff {(o_kernel.float() - o_ref.float()).abs().max().item():.4e}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bs,nqh,nkh,hd,blk,tk,with_sink,seq_pat,paged",
|
||||
[
|
||||
_case(4, 8, 1, 128, 64, 16, False, "aligned"),
|
||||
],
|
||||
)
|
||||
def test_sparse_gqa_deterministic(bs, nqh, nkh, hd, blk, tk, with_sink, seq_pat, paged):
|
||||
"""Two calls with same inputs must produce identical outputs."""
|
||||
torch.manual_seed(42)
|
||||
seq_lens_list = make_seq_lens(seq_pat, bs, blk)
|
||||
|
||||
q, sink, k_cache, v_cache, req_to_token, seq_lens, slot_ids, topk_idx = (
|
||||
build_inputs(
|
||||
bs,
|
||||
nqh,
|
||||
nkh,
|
||||
hd,
|
||||
seq_lens_list,
|
||||
blk,
|
||||
tk,
|
||||
with_sink=with_sink,
|
||||
paged=paged,
|
||||
)
|
||||
)
|
||||
|
||||
o1 = flash_decode_with_gqa_share_sparse(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens,
|
||||
slot_ids,
|
||||
blk,
|
||||
topk_idx,
|
||||
)
|
||||
o2 = flash_decode_with_gqa_share_sparse(
|
||||
q,
|
||||
sink,
|
||||
k_cache,
|
||||
v_cache,
|
||||
req_to_token,
|
||||
seq_lens,
|
||||
slot_ids,
|
||||
blk,
|
||||
topk_idx,
|
||||
)
|
||||
|
||||
assert torch.equal(
|
||||
o1, o2
|
||||
), f"non-deterministic: max diff {(o1.float() - o2.float()).abs().max().item():.4e}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -179,6 +179,7 @@ class FusedMoE(torch.nn.Module):
|
||||
with_bias=False,
|
||||
routing_method_type: Optional[RoutingMethodType] = None,
|
||||
is_gated: bool = True,
|
||||
gate_up_interleaved: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
if params_dtype is None:
|
||||
@@ -265,6 +266,7 @@ class FusedMoE(torch.nn.Module):
|
||||
swiglu_limit=swiglu_limit,
|
||||
is_gated=is_gated,
|
||||
routing_method_type=routing_method_type,
|
||||
gate_up_interleaved=gate_up_interleaved,
|
||||
)
|
||||
|
||||
self.quant_method: Optional[FusedMoEMethodBase] = None
|
||||
|
||||
@@ -65,6 +65,10 @@ class MoeRunnerConfig:
|
||||
gemm1_alpha: Optional[float] = None
|
||||
gemm1_clamp_limit: Optional[float] = None
|
||||
swiglu_limit: Optional[float] = None
|
||||
# Whether gate/up weights are stored interleaved (vs split). Only the
|
||||
# silu+is_gated swiglu path consumes it (interleaved -> swiglu_gpt_oss_*,
|
||||
# otherwise chunk gate/up then apply alpha/limit).
|
||||
gate_up_interleaved: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -16,6 +16,7 @@ from sglang.srt.layers.moe.moe_runner.base import (
|
||||
register_pre_permute,
|
||||
)
|
||||
from sglang.srt.layers.moe.utils import MoeRunnerBackend
|
||||
from sglang.srt.utils import is_cuda, is_gfx95_supported, is_hip
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.layers.moe.token_dispatcher.standard import (
|
||||
@@ -55,6 +56,7 @@ class TritonMoeQuantInfo(MoeQuantInfo):
|
||||
w2_weight: torch.Tensor
|
||||
b13: Optional[torch.Tensor] = None
|
||||
b2: Optional[torch.Tensor] = None
|
||||
use_mxfp8: bool = False
|
||||
use_fp8_w8a8: bool = False
|
||||
use_int8_w8a8: bool = False
|
||||
use_int8_w8a16: bool = False
|
||||
@@ -81,6 +83,40 @@ class TritonRunnerCore(MoeRunnerCore):
|
||||
running_state: dict,
|
||||
hooks: Optional[Any] = None,
|
||||
) -> TritonRunnerOutput:
|
||||
if quant_info.use_mxfp8 and is_hip() and is_gfx95_supported():
|
||||
from sglang.srt.layers.moe.moe_runner.triton_utils.mxfp8_moe_amd_gfx95 import (
|
||||
fused_experts_mxfp8,
|
||||
)
|
||||
|
||||
out = fused_experts_mxfp8(
|
||||
runner_input.hidden_states,
|
||||
quant_info.w13_weight,
|
||||
quant_info.w2_weight,
|
||||
runner_input.topk_weights,
|
||||
runner_input.topk_ids,
|
||||
quant_info.w13_scale,
|
||||
quant_info.w2_scale,
|
||||
b1=quant_info.b13,
|
||||
b2=quant_info.b2,
|
||||
activation=self.config.activation,
|
||||
is_gated=self.config.is_gated,
|
||||
no_combine=self.config.no_combine,
|
||||
inplace=self.config.inplace,
|
||||
apply_router_weight_on_input=self.config.apply_router_weight_on_input,
|
||||
routed_scaling_factor=self.config.routed_scaling_factor,
|
||||
gemm1_alpha=self.config.gemm1_alpha,
|
||||
gemm1_limit=self.config.gemm1_clamp_limit,
|
||||
swiglu_limit=self.config.swiglu_limit,
|
||||
gate_up_interleaved=self.config.gate_up_interleaved,
|
||||
)
|
||||
return TritonRunnerOutput(hidden_states=out)
|
||||
|
||||
if quant_info.use_mxfp8 and is_cuda():
|
||||
raise NotImplementedError(
|
||||
"Triton MoE runner does not support NVIDIA MXFP8; use "
|
||||
"--moe-runner-backend deep_gemm (or flashinfer_trtllm/cutlass)."
|
||||
)
|
||||
|
||||
from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe import (
|
||||
_fused_moe_kernel_sequence,
|
||||
)
|
||||
@@ -142,30 +178,66 @@ def fused_experts_none_to_triton(
|
||||
quant_info: TritonMoeQuantInfo,
|
||||
runner_config: MoeRunnerConfig,
|
||||
) -> StandardCombineInput:
|
||||
from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe import fused_experts
|
||||
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
|
||||
|
||||
output = fused_experts(
|
||||
hidden_states=dispatch_output.hidden_states,
|
||||
w1=quant_info.w13_weight,
|
||||
w2=quant_info.w2_weight,
|
||||
topk_output=dispatch_output.topk_output,
|
||||
moe_runner_config=runner_config,
|
||||
b1=quant_info.b13,
|
||||
b2=quant_info.b2,
|
||||
use_fp8_w8a8=quant_info.use_fp8_w8a8,
|
||||
use_int8_w8a8=quant_info.use_int8_w8a8,
|
||||
use_int8_w8a16=quant_info.use_int8_w8a16,
|
||||
use_int4_w4a16=quant_info.use_int4_w4a16,
|
||||
per_channel_quant=quant_info.per_channel_quant,
|
||||
w1_scale=quant_info.w13_scale,
|
||||
w2_scale=quant_info.w2_scale,
|
||||
w1_zp=quant_info.w13_zp,
|
||||
w2_zp=quant_info.w2_zp,
|
||||
a1_scale=quant_info.a13_scale,
|
||||
a2_scale=quant_info.a2_scale,
|
||||
block_shape=quant_info.block_shape,
|
||||
)
|
||||
if quant_info.use_mxfp8 and is_hip() and is_gfx95_supported():
|
||||
from sglang.srt.layers.moe.moe_runner.triton_utils.mxfp8_moe_amd_gfx95 import (
|
||||
fused_experts_mxfp8,
|
||||
)
|
||||
|
||||
topk_weights, topk_ids, _ = dispatch_output.topk_output
|
||||
output = fused_experts_mxfp8(
|
||||
hidden_states=dispatch_output.hidden_states,
|
||||
w1=quant_info.w13_weight,
|
||||
w2=quant_info.w2_weight,
|
||||
topk_weights=topk_weights,
|
||||
topk_ids=topk_ids,
|
||||
w1_scale=quant_info.w13_scale,
|
||||
w2_scale=quant_info.w2_scale,
|
||||
b1=quant_info.b13,
|
||||
b2=quant_info.b2,
|
||||
activation=runner_config.activation,
|
||||
is_gated=runner_config.is_gated,
|
||||
no_combine=runner_config.no_combine,
|
||||
inplace=runner_config.inplace,
|
||||
apply_router_weight_on_input=runner_config.apply_router_weight_on_input,
|
||||
routed_scaling_factor=runner_config.routed_scaling_factor,
|
||||
gemm1_alpha=runner_config.gemm1_alpha,
|
||||
gemm1_limit=runner_config.gemm1_clamp_limit,
|
||||
swiglu_limit=runner_config.swiglu_limit,
|
||||
gate_up_interleaved=runner_config.gate_up_interleaved,
|
||||
)
|
||||
else:
|
||||
if quant_info.use_mxfp8 and is_cuda():
|
||||
raise NotImplementedError(
|
||||
"Triton MoE runner does not support NVIDIA MXFP8; use "
|
||||
"--moe-runner-backend deep_gemm (or flashinfer_trtllm/cutlass)."
|
||||
)
|
||||
from sglang.srt.layers.moe.moe_runner.triton_utils.fused_moe import (
|
||||
fused_experts,
|
||||
)
|
||||
|
||||
output = fused_experts(
|
||||
hidden_states=dispatch_output.hidden_states,
|
||||
w1=quant_info.w13_weight,
|
||||
w2=quant_info.w2_weight,
|
||||
topk_output=dispatch_output.topk_output,
|
||||
moe_runner_config=runner_config,
|
||||
b1=quant_info.b13,
|
||||
b2=quant_info.b2,
|
||||
use_fp8_w8a8=quant_info.use_fp8_w8a8,
|
||||
use_int8_w8a8=quant_info.use_int8_w8a8,
|
||||
use_int8_w8a16=quant_info.use_int8_w8a16,
|
||||
use_int4_w4a16=quant_info.use_int4_w4a16,
|
||||
per_channel_quant=quant_info.per_channel_quant,
|
||||
w1_scale=quant_info.w13_scale,
|
||||
w2_scale=quant_info.w2_scale,
|
||||
w1_zp=quant_info.w13_zp,
|
||||
w2_zp=quant_info.w2_zp,
|
||||
a1_scale=quant_info.a13_scale,
|
||||
a2_scale=quant_info.a2_scale,
|
||||
block_shape=quant_info.block_shape,
|
||||
)
|
||||
|
||||
return StandardCombineInput(
|
||||
hidden_states=output,
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"model": "MiniMax-M3",
|
||||
"device": "gfx950",
|
||||
"experts": 128,
|
||||
"hidden_size": 6144,
|
||||
"intermediate_size": 384,
|
||||
"top_k": 4,
|
||||
"tokens": {
|
||||
"1": {
|
||||
"best_gemm1": {"block_m": 128, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 4},
|
||||
"best_gemm2": {"block_m": 128, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 4}
|
||||
},
|
||||
"2": {
|
||||
"best_gemm1": {"block_m": 64, "block_n": 128, "block_k": 128, "num_stages": 2, "num_warps": 8},
|
||||
"best_gemm2": {"block_m": 64, "block_n": 256, "block_k": 128, "num_stages": 2, "num_warps": 4}
|
||||
},
|
||||
"4": {
|
||||
"best_gemm1": {"block_m": 128, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 4},
|
||||
"best_gemm2": {"block_m": 128, "block_n": 128, "block_k": 64, "num_stages": 1, "num_warps": 4}
|
||||
},
|
||||
"8": {
|
||||
"best_gemm1": {"block_m": 128, "block_n": 128, "block_k": 256, "num_stages": 1, "num_warps": 4},
|
||||
"best_gemm2": {"block_m": 128, "block_n": 256, "block_k": 128, "num_stages": 2, "num_warps": 8}
|
||||
},
|
||||
"16": {
|
||||
"best_gemm1": {"block_m": 64, "block_n": 128, "block_k": 256, "num_stages": 2, "num_warps": 8},
|
||||
"best_gemm2": {"block_m": 64, "block_n": 256, "block_k": 128, "num_stages": 2, "num_warps": 4}
|
||||
},
|
||||
"32": {
|
||||
"best_gemm1": {"block_m": 64, "block_n": 128, "block_k": 128, "num_stages": 2, "num_warps": 8},
|
||||
"best_gemm2": {"block_m": 64, "block_n": 128, "block_k": 64, "num_stages": 2, "num_warps": 4}
|
||||
},
|
||||
"64": {
|
||||
"best_gemm1": {"block_m": 32, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 4},
|
||||
"best_gemm2": {"block_m": 32, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 4}
|
||||
},
|
||||
"128": {
|
||||
"best_gemm1": {"block_m": 64, "block_n": 256, "block_k": 256, "num_stages": 1, "num_warps": 4},
|
||||
"best_gemm2": {"block_m": 32, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 8}
|
||||
},
|
||||
"256": {
|
||||
"best_gemm1": {"block_m": 32, "block_n": 128, "block_k": 256, "num_stages": 1, "num_warps": 4},
|
||||
"best_gemm2": {"block_m": 32, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 8}
|
||||
},
|
||||
"512": {
|
||||
"best_gemm1": {"block_m": 64, "block_n": 128, "block_k": 256, "num_stages": 1, "num_warps": 4},
|
||||
"best_gemm2": {"block_m": 32, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 8}
|
||||
},
|
||||
"1024": {
|
||||
"best_gemm1": {"block_m": 64, "block_n": 128, "block_k": 256, "num_stages": 2, "num_warps": 8},
|
||||
"best_gemm2": {"block_m": 64, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 8}
|
||||
},
|
||||
"1536": {
|
||||
"best_gemm1": {"block_m": 64, "block_n": 128, "block_k": 256, "num_stages": 2, "num_warps": 8},
|
||||
"best_gemm2": {"block_m": 64, "block_n": 128, "block_k": 128, "num_stages": 1, "num_warps": 4}
|
||||
},
|
||||
"2048": {
|
||||
"best_gemm1": {"block_m": 128, "block_n": 128, "block_k": 256, "num_stages": 2, "num_warps": 8},
|
||||
"best_gemm2": {"block_m": 128, "block_n": 128, "block_k": 128, "num_stages": 2, "num_warps": 8}
|
||||
},
|
||||
"3072": {
|
||||
"best_gemm1": {"block_m": 128, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 4},
|
||||
"best_gemm2": {"block_m": 128, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 4}
|
||||
},
|
||||
"4096": {
|
||||
"best_gemm1": {"block_m": 128, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 4},
|
||||
"best_gemm2": {"block_m": 128, "block_n": 256, "block_k": 128, "num_stages": 1, "num_warps": 4}
|
||||
}
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"1": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 16,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"2": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 16,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"4": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 16,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 8,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"8": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 32,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 1,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"16": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 32,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 1,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"24": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"32": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"48": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"64": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 8,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"96": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 4,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"128": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 8,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"256": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 8,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"512": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 4,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"1024": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 8,
|
||||
"num_warps": 4,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"1536": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 4,
|
||||
"num_warps": 2,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"2048": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 8,
|
||||
"num_warps": 4,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"3072": {
|
||||
"BLOCK_SIZE_M": 128,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 8,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
},
|
||||
"4096": {
|
||||
"BLOCK_SIZE_M": 128,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 8,
|
||||
"num_warps": 4,
|
||||
"num_stages": 2,
|
||||
"waves_per_eu": 0
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"1": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 32,
|
||||
"BLOCK_SIZE_K": 256,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"2": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 32,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 5
|
||||
},
|
||||
"4": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 32,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 64,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"8": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 32,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"16": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"24": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 2
|
||||
},
|
||||
"32": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 2
|
||||
},
|
||||
"48": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 32,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 64,
|
||||
"num_warps": 4,
|
||||
"num_stages": 2
|
||||
},
|
||||
"64": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"96": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 3
|
||||
},
|
||||
"128": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 8,
|
||||
"num_stages": 3
|
||||
},
|
||||
"256": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 3
|
||||
},
|
||||
"512": {
|
||||
"BLOCK_SIZE_M": 32,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 2
|
||||
},
|
||||
"1024": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 3
|
||||
},
|
||||
"1536": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 3
|
||||
},
|
||||
"2048": {
|
||||
"BLOCK_SIZE_M": 128,
|
||||
"BLOCK_SIZE_N": 256,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 4
|
||||
},
|
||||
"3072": {
|
||||
"BLOCK_SIZE_M": 128,
|
||||
"BLOCK_SIZE_N": 256,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 8,
|
||||
"num_stages": 4
|
||||
},
|
||||
"4096": {
|
||||
"BLOCK_SIZE_M": 128,
|
||||
"BLOCK_SIZE_N": 256,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 4
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,7 @@ def inplace_fused_experts(
|
||||
gemm1_limit: Optional[float] = None,
|
||||
filter_expert: bool = True,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
gate_up_interleaved: bool = True,
|
||||
) -> None:
|
||||
fused_experts_impl(
|
||||
hidden_states,
|
||||
@@ -153,6 +154,7 @@ def inplace_fused_experts(
|
||||
gemm1_limit,
|
||||
filter_expert,
|
||||
swiglu_limit=swiglu_limit,
|
||||
gate_up_interleaved=gate_up_interleaved,
|
||||
)
|
||||
|
||||
|
||||
@@ -186,6 +188,7 @@ def outplace_fused_experts(
|
||||
gemm1_limit: Optional[float] = None,
|
||||
filter_expert: bool = True,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
gate_up_interleaved: bool = True,
|
||||
) -> torch.Tensor:
|
||||
return fused_experts_impl(
|
||||
hidden_states,
|
||||
@@ -217,6 +220,7 @@ def outplace_fused_experts(
|
||||
gemm1_limit=gemm1_limit,
|
||||
filter_expert=filter_expert,
|
||||
swiglu_limit=swiglu_limit,
|
||||
gate_up_interleaved=gate_up_interleaved,
|
||||
)
|
||||
|
||||
|
||||
@@ -276,6 +280,7 @@ def fused_experts(
|
||||
moe_runner_config.gemm1_clamp_limit,
|
||||
filter_expert,
|
||||
swiglu_limit=moe_runner_config.swiglu_limit,
|
||||
gate_up_interleaved=moe_runner_config.gate_up_interleaved,
|
||||
)
|
||||
return hidden_states
|
||||
else:
|
||||
@@ -308,6 +313,7 @@ def fused_experts(
|
||||
gemm1_limit=moe_runner_config.gemm1_clamp_limit,
|
||||
filter_expert=filter_expert,
|
||||
swiglu_limit=moe_runner_config.swiglu_limit,
|
||||
gate_up_interleaved=moe_runner_config.gate_up_interleaved,
|
||||
)
|
||||
|
||||
|
||||
@@ -336,6 +342,14 @@ def swiglu_gpt_oss_sigmoid_alpha(x, gemm1_alpha, gemm1_limit):
|
||||
return gate * torch.sigmoid(gate * gemm1_alpha) * (up + 1)
|
||||
|
||||
|
||||
@torch.compile
|
||||
def swiglu_no_interleaved_with_alpha_and_limit(x, gemm1_alpha, gemm1_limit):
|
||||
gate, up = x.chunk(2, dim=-1)
|
||||
gate = gate.clamp(min=None, max=gemm1_limit)
|
||||
up = up.clamp(min=-gemm1_limit, max=gemm1_limit)
|
||||
return gate * torch.sigmoid(gate * gemm1_alpha) * (up + 1)
|
||||
|
||||
|
||||
@functools.lru_cache()
|
||||
def _down_moe_use_tma():
|
||||
return support_tensor_descriptor()
|
||||
@@ -441,6 +455,7 @@ def _fused_moe_kernel_sequence(
|
||||
filter_expert: bool,
|
||||
hooks: Optional[Any] = None,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
gate_up_interleaved: bool = True,
|
||||
) -> torch.Tensor:
|
||||
"""Run the MoE kernel/activation/kernel/combine sequence in a single shot.
|
||||
|
||||
@@ -538,9 +553,18 @@ def _fused_moe_kernel_sequence(
|
||||
# - swiglu_limit != None: DeepSeek V4 swiglu clamp + silu_and_mul (CUDA/HIP only)
|
||||
if gemm1_alpha is not None:
|
||||
assert gemm1_limit is not None
|
||||
intermediate_cache2 = swiglu_gpt_oss_sigmoid_alpha(
|
||||
intermediate_cache1.view(-1, N), gemm1_alpha, gemm1_limit
|
||||
)
|
||||
if gate_up_interleaved:
|
||||
intermediate_cache2 = swiglu_gpt_oss_sigmoid_alpha(
|
||||
intermediate_cache1.view(-1, N),
|
||||
gemm1_alpha,
|
||||
gemm1_limit,
|
||||
)
|
||||
else:
|
||||
intermediate_cache2 = swiglu_no_interleaved_with_alpha_and_limit(
|
||||
intermediate_cache1.view(-1, N),
|
||||
gemm1_alpha,
|
||||
gemm1_limit,
|
||||
)
|
||||
elif gemm1_limit is not None:
|
||||
intermediate_cache2 = _swiglu_silu_clamp_mul(
|
||||
intermediate_cache1.view(-1, N), gemm1_limit
|
||||
@@ -820,6 +844,7 @@ def fused_experts_impl(
|
||||
gemm1_limit: Optional[float] = None,
|
||||
filter_expert: bool = True,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
gate_up_interleaved: bool = True,
|
||||
):
|
||||
padded_size = padding_size
|
||||
if not (use_fp8_w8a8 or use_int8_w8a8) or block_shape is not None or _use_aiter:
|
||||
@@ -895,6 +920,7 @@ def fused_experts_impl(
|
||||
filter_expert=filter_expert,
|
||||
hooks=None,
|
||||
swiglu_limit=swiglu_limit,
|
||||
gate_up_interleaved=gate_up_interleaved,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
"""Native MXFP8 (1x32 block, E8M0 scale) MoE for AMD CDNA4 (gfx950).
|
||||
|
||||
Replaces the prior SGLang MXFP8 MoE family (dense / hybrid / packed /
|
||||
grouped_gemm1 / grouped_gemm12 / compact / fused_act) with a single grouped
|
||||
``tl.dot_scaled`` kernel. Instead of an explicit ``argsort`` + ``index_select``
|
||||
gather, a materialized intermediate, a separate activation quant, and a
|
||||
``tl.atomic_add`` combine:
|
||||
|
||||
* tokens are sorted by expert with ``moe_align_block_size``;
|
||||
* GEMM1 reads the activation by token-id indirection (``a_row = token // top_k``)
|
||||
so the hidden states are MXFP8-quantized exactly ONCE (not top_k times);
|
||||
* the SwiGLU-OAI activation is the fused fp32 Triton kernel (split layout);
|
||||
* GEMM2 applies the top-k weight inside the kernel and writes each route to a
|
||||
distinct output row (no atomics); the final reduction is a strided sum.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.moe.moe_runner.triton_utils.moe_align_block_size import (
|
||||
moe_align_block_size,
|
||||
)
|
||||
from sglang.srt.layers.quantization.mxfp8_amd_gfx95 import mxfp8_e4m3_quantize
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _mxfp8_grouped_gemm_kernel(
|
||||
a_ptr,
|
||||
a_scale_ptr,
|
||||
b_ptr,
|
||||
b_scale_ptr,
|
||||
c_ptr,
|
||||
topk_weights_ptr,
|
||||
sorted_token_ids_ptr,
|
||||
expert_ids_ptr,
|
||||
num_tokens_post_padded_ptr,
|
||||
E,
|
||||
N,
|
||||
K,
|
||||
num_valid_tokens,
|
||||
top_k,
|
||||
stride_am,
|
||||
stride_ak,
|
||||
stride_asm,
|
||||
stride_ask,
|
||||
stride_be,
|
||||
stride_bn,
|
||||
stride_bk,
|
||||
stride_bse,
|
||||
stride_bsn,
|
||||
stride_bsk,
|
||||
stride_cm,
|
||||
stride_cn,
|
||||
A_DIV: tl.constexpr,
|
||||
MUL_WEIGHT: tl.constexpr,
|
||||
BLOCK_M: tl.constexpr,
|
||||
BLOCK_N: tl.constexpr,
|
||||
BLOCK_K: tl.constexpr,
|
||||
):
|
||||
pid_m = tl.program_id(0)
|
||||
pid_n = tl.program_id(1)
|
||||
num_post = tl.load(num_tokens_post_padded_ptr)
|
||||
if pid_m * BLOCK_M >= num_post:
|
||||
return
|
||||
|
||||
offs_tid = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
||||
offs_token = tl.load(sorted_token_ids_ptr + offs_tid).to(tl.int64)
|
||||
token_mask = offs_token < num_valid_tokens
|
||||
off_e = tl.load(expert_ids_ptr + pid_m).to(tl.int64)
|
||||
valid_expert = (off_e >= 0) & (off_e < E)
|
||||
|
||||
offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
|
||||
offs_k = tl.arange(0, BLOCK_K)
|
||||
offs_sk = tl.arange(0, BLOCK_K // 32)
|
||||
a_row = offs_token // A_DIV
|
||||
|
||||
a_ptrs = a_ptr + a_row[:, None] * stride_am + offs_k[None, :] * stride_ak
|
||||
as_ptrs = a_scale_ptr + a_row[:, None] * stride_asm + offs_sk[None, :] * stride_ask
|
||||
b_ptrs = (
|
||||
b_ptr
|
||||
+ off_e * stride_be
|
||||
+ offs_n[:, None] * stride_bn
|
||||
+ offs_k[None, :] * stride_bk
|
||||
)
|
||||
bs_ptrs = (
|
||||
b_scale_ptr
|
||||
+ off_e * stride_bse
|
||||
+ offs_n[:, None] * stride_bsn
|
||||
+ offs_sk[None, :] * stride_bsk
|
||||
)
|
||||
|
||||
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
|
||||
n_mask = offs_n < N
|
||||
for _ in range(0, tl.cdiv(K, BLOCK_K)):
|
||||
a = tl.load(a_ptrs, mask=token_mask[:, None], other=0.0)
|
||||
b = tl.load(b_ptrs, mask=valid_expert & n_mask[:, None], other=0.0)
|
||||
asc = tl.load(as_ptrs, mask=token_mask[:, None], other=0)
|
||||
bsc = tl.load(bs_ptrs, mask=valid_expert & n_mask[:, None], other=0)
|
||||
acc += tl.dot_scaled(a, asc, "e4m3", b.T, bsc, "e4m3")
|
||||
|
||||
a_ptrs += BLOCK_K * stride_ak
|
||||
b_ptrs += BLOCK_K * stride_bk
|
||||
as_ptrs += (BLOCK_K // 32) * stride_ask
|
||||
bs_ptrs += (BLOCK_K // 32) * stride_bsk
|
||||
|
||||
if MUL_WEIGHT:
|
||||
w = tl.load(topk_weights_ptr + offs_token, mask=token_mask, other=0.0)
|
||||
acc = acc * w[:, None]
|
||||
|
||||
c_ptrs = c_ptr + offs_token[:, None] * stride_cm + offs_n[None, :] * stride_cn
|
||||
tl.store(
|
||||
c_ptrs,
|
||||
acc.to(c_ptr.dtype.element_ty),
|
||||
mask=token_mask[:, None] & n_mask[None, :],
|
||||
)
|
||||
|
||||
|
||||
def _grouped_gemm_mxfp8(
|
||||
a_q: torch.Tensor, # [M, K] fp8 e4m3
|
||||
a_scale: torch.Tensor, # [M, K//32] uint8 (E8M0)
|
||||
w: torch.Tensor, # [E, N, K] fp8 e4m3
|
||||
w_scale: torch.Tensor, # [E, N, K//32] uint8 (E8M0)
|
||||
sorted_token_ids: torch.Tensor,
|
||||
expert_ids: torch.Tensor,
|
||||
num_tokens_post_padded: torch.Tensor,
|
||||
num_valid_tokens: int,
|
||||
top_k: int,
|
||||
block_m: int,
|
||||
out_dtype: torch.dtype,
|
||||
a_div: int,
|
||||
mul_weight_by: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
M_routed = num_valid_tokens
|
||||
E, N, K = w.shape
|
||||
assert K % 128 == 0, f"MXFP8 native MoE requires K%128==0, got K={K}"
|
||||
# Keep zero-fill: moe_align_block_size reserves an extra expert bucket for
|
||||
# filtered routes, which should contribute zeros if present.
|
||||
out = torch.zeros((M_routed, N), dtype=out_dtype, device=a_q.device)
|
||||
if a_div == top_k and M_routed <= 32 and K >= 3072:
|
||||
BLOCK_N = 64
|
||||
num_warps = 4
|
||||
else:
|
||||
BLOCK_N = 128
|
||||
num_warps = 8
|
||||
BLOCK_K = 128
|
||||
grid = (triton.cdiv(sorted_token_ids.shape[0], block_m), triton.cdiv(N, BLOCK_N))
|
||||
_mxfp8_grouped_gemm_kernel[grid](
|
||||
a_q,
|
||||
a_scale,
|
||||
w,
|
||||
w_scale,
|
||||
out,
|
||||
mul_weight_by if mul_weight_by is not None else a_q,
|
||||
sorted_token_ids,
|
||||
expert_ids,
|
||||
num_tokens_post_padded,
|
||||
E,
|
||||
N,
|
||||
K,
|
||||
num_valid_tokens,
|
||||
top_k,
|
||||
a_q.stride(0),
|
||||
a_q.stride(1),
|
||||
a_scale.stride(0),
|
||||
a_scale.stride(1),
|
||||
w.stride(0),
|
||||
w.stride(1),
|
||||
w.stride(2),
|
||||
w_scale.stride(0),
|
||||
w_scale.stride(1),
|
||||
w_scale.stride(2),
|
||||
out.stride(0),
|
||||
out.stride(1),
|
||||
A_DIV=a_div,
|
||||
MUL_WEIGHT=mul_weight_by is not None,
|
||||
BLOCK_M=block_m,
|
||||
BLOCK_N=BLOCK_N,
|
||||
BLOCK_K=BLOCK_K,
|
||||
num_warps=num_warps,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _combine_topk_routes_kernel(
|
||||
route_ptr,
|
||||
out_ptr,
|
||||
H,
|
||||
top_k: tl.constexpr,
|
||||
stride_rm,
|
||||
stride_rh,
|
||||
stride_ot,
|
||||
stride_oh,
|
||||
BLOCK_H: tl.constexpr,
|
||||
):
|
||||
token_id = tl.program_id(0)
|
||||
h_block = tl.program_id(1)
|
||||
offs_h = h_block * BLOCK_H + tl.arange(0, BLOCK_H)
|
||||
h_mask = offs_h < H
|
||||
|
||||
acc = tl.zeros((BLOCK_H,), dtype=tl.float32)
|
||||
for route_idx in range(0, top_k):
|
||||
route_row = token_id * top_k + route_idx
|
||||
vals = tl.load(
|
||||
route_ptr + route_row * stride_rm + offs_h * stride_rh,
|
||||
mask=h_mask,
|
||||
other=0.0,
|
||||
)
|
||||
acc += vals.to(tl.float32)
|
||||
|
||||
tl.store(out_ptr + token_id * stride_ot + offs_h * stride_oh, acc, mask=h_mask)
|
||||
|
||||
|
||||
def _combine_topk_routes(
|
||||
route_outputs: torch.Tensor,
|
||||
num_tokens: int,
|
||||
top_k: int,
|
||||
hidden_size: int,
|
||||
out_dtype: torch.dtype,
|
||||
) -> torch.Tensor:
|
||||
out = torch.empty(
|
||||
(num_tokens, hidden_size), dtype=out_dtype, device=route_outputs.device
|
||||
)
|
||||
block_h = 1024
|
||||
grid = (num_tokens, triton.cdiv(hidden_size, block_h))
|
||||
_combine_topk_routes_kernel[grid](
|
||||
route_outputs,
|
||||
out,
|
||||
hidden_size,
|
||||
top_k,
|
||||
route_outputs.stride(0),
|
||||
route_outputs.stride(1),
|
||||
out.stride(0),
|
||||
out.stride(1),
|
||||
BLOCK_H=block_h,
|
||||
num_warps=8,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def fused_moe_mxfp8_native(
|
||||
hidden_states: torch.Tensor, # [T, H] bf16
|
||||
w13: torch.Tensor, # [E, 2I, H] fp8
|
||||
w13_scale: torch.Tensor, # [E, 2I, H//32] uint8
|
||||
w2: torch.Tensor, # [E, H, I] fp8
|
||||
w2_scale: torch.Tensor, # [E, H, I//32] uint8
|
||||
topk_weights: torch.Tensor, # [T, top_k]
|
||||
topk_ids: torch.Tensor, # [T, top_k] (local expert ids; -1 for non-local EP)
|
||||
*,
|
||||
alpha: float,
|
||||
beta: float,
|
||||
limit: Optional[float],
|
||||
no_combine: bool = False,
|
||||
expert_map: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
# Lazy import: the jit_kernel package pulls in Triton at first use; importing
|
||||
# at call time avoids any import-time cycle with the moe runner package.
|
||||
from sglang.jit_kernel.minimax_m3 import swiglu_oai_mxfp8_quant, swiglu_oai_split
|
||||
|
||||
T, H = hidden_states.shape
|
||||
top_k = topk_ids.shape[1]
|
||||
M = T * top_k
|
||||
local_num_experts = w13.shape[0]
|
||||
|
||||
if expert_map is not None:
|
||||
valid_global = (topk_ids >= 0) & (topk_ids < expert_map.numel())
|
||||
topk_ids = expert_map[topk_ids.clamp(0, expert_map.numel() - 1).long()].to(
|
||||
torch.int32
|
||||
)
|
||||
topk_ids.masked_fill_(
|
||||
~valid_global | (topk_ids < 0) | (topk_ids >= local_num_experts), -1
|
||||
)
|
||||
else:
|
||||
topk_ids = topk_ids.to(torch.int32, copy=True)
|
||||
topk_ids.masked_fill_((topk_ids < 0) | (topk_ids >= local_num_experts), -1)
|
||||
|
||||
block_m = 64
|
||||
sorted_ids, expert_ids, num_post = moe_align_block_size(
|
||||
topk_ids, block_m, local_num_experts
|
||||
)
|
||||
|
||||
# GEMM1: x (mxfp8) @ w13^T -> [M, 2I]. The activation is quantized ONCE over
|
||||
# the T hidden rows; the kernel gathers per route via a_row = token // top_k.
|
||||
a_q, a_s = mxfp8_e4m3_quantize(hidden_states)
|
||||
g1 = _grouped_gemm_mxfp8(
|
||||
a_q,
|
||||
a_s,
|
||||
w13,
|
||||
w13_scale,
|
||||
sorted_ids,
|
||||
expert_ids,
|
||||
num_post,
|
||||
M,
|
||||
top_k,
|
||||
block_m,
|
||||
hidden_states.dtype,
|
||||
a_div=top_k,
|
||||
) # [M, 2I]
|
||||
|
||||
if envs.SGLANG_MINIMAX_M3_FUSED_SWIGLU_MXFP8.get():
|
||||
# SwiGLU-OAI (split layout) + MiniMax MXFP8 quant in one kernel, fp32 all
|
||||
# the way to the E8M0 scale (no bf16 activation round-trip; matches the
|
||||
# vLLM/ame fused kernel). Opt-in until full-model accuracy is re-qualified.
|
||||
act_q, act_s = swiglu_oai_mxfp8_quant(g1, alpha=alpha, beta=beta, limit=limit)
|
||||
else:
|
||||
# Default accuracy path: keep the historical two-kernel boundary.
|
||||
act = swiglu_oai_split(
|
||||
g1, alpha=alpha, beta=beta, limit=limit, out_dtype=hidden_states.dtype
|
||||
)
|
||||
act_q, act_s = mxfp8_e4m3_quantize(act)
|
||||
|
||||
if no_combine:
|
||||
# Per-route outputs, unweighted, no reduction: [T, top_k, H].
|
||||
g2 = _grouped_gemm_mxfp8(
|
||||
act_q,
|
||||
act_s,
|
||||
w2,
|
||||
w2_scale,
|
||||
sorted_ids,
|
||||
expert_ids,
|
||||
num_post,
|
||||
M,
|
||||
top_k,
|
||||
block_m,
|
||||
hidden_states.dtype,
|
||||
a_div=1,
|
||||
)
|
||||
return g2.view(T, top_k, H)
|
||||
|
||||
# GEMM2: act (mxfp8) @ w2^T -> [M, H], weighted by topk_weights, then reduce.
|
||||
g2 = _grouped_gemm_mxfp8(
|
||||
act_q,
|
||||
act_s,
|
||||
w2,
|
||||
w2_scale,
|
||||
sorted_ids,
|
||||
expert_ids,
|
||||
num_post,
|
||||
M,
|
||||
top_k,
|
||||
block_m,
|
||||
torch.float32,
|
||||
a_div=1,
|
||||
mul_weight_by=topk_weights.reshape(-1).to(torch.float32),
|
||||
) # [M, H] == [T*top_k, H]
|
||||
|
||||
if envs.SGLANG_MINIMAX_M3_FUSED_MOE_COMBINE.get():
|
||||
return _combine_topk_routes(g2, T, top_k, H, hidden_states.dtype)
|
||||
return g2.view(T, top_k, H).sum(dim=1).to(hidden_states.dtype)
|
||||
|
||||
|
||||
def fused_experts_mxfp8(
|
||||
hidden_states: torch.Tensor,
|
||||
w1: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
w1_scale: torch.Tensor,
|
||||
w2_scale: torch.Tensor,
|
||||
*,
|
||||
b1: Optional[torch.Tensor] = None,
|
||||
b2: Optional[torch.Tensor] = None,
|
||||
activation: str = "silu",
|
||||
is_gated: bool = True,
|
||||
no_combine: bool = False,
|
||||
inplace: bool = False,
|
||||
apply_router_weight_on_input: bool = False,
|
||||
routed_scaling_factor: Optional[float] = None,
|
||||
gemm1_alpha: Optional[float] = None,
|
||||
gemm1_limit: Optional[float] = None,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
gate_up_interleaved: bool = True,
|
||||
expert_map: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
"""Native MXFP8 MoE entry (CDNA4 ``dot_scaled``).
|
||||
|
||||
Keeps the SGLang ``fused_experts_mxfp8`` call contract but routes to the
|
||||
single grouped-GEMM kernel. Only the MiniMax-M3 SwiGLU-OAI (split,
|
||||
uninterleaved, gated silu with ``gemm1_alpha``/``gemm1_limit``) configuration
|
||||
is supported -- the unsupported cases below never occur on the M3 path.
|
||||
"""
|
||||
if not (activation == "silu" and is_gated):
|
||||
raise NotImplementedError(
|
||||
f"native MXFP8 MoE only supports gated swiglu-oai, got "
|
||||
f"{activation=} {is_gated=}."
|
||||
)
|
||||
if b1 is not None or b2 is not None:
|
||||
raise NotImplementedError("native MXFP8 MoE does not support expert bias.")
|
||||
if apply_router_weight_on_input:
|
||||
raise NotImplementedError(
|
||||
"native MXFP8 MoE does not support apply_router_weight_on_input."
|
||||
)
|
||||
if gate_up_interleaved:
|
||||
raise NotImplementedError(
|
||||
"native MXFP8 MoE expects uninterleaved (split) gate/up layout."
|
||||
)
|
||||
|
||||
# SwiGLU-OAI default activation alpha (gpt-oss); M3 may override via gemm1_alpha.
|
||||
alpha = 1.702 if gemm1_alpha is None else float(gemm1_alpha)
|
||||
beta = 1.0
|
||||
limit = None if gemm1_limit is None else float(gemm1_limit)
|
||||
# NOTE: routed_scaling_factor is intentionally NOT re-applied here. For M3
|
||||
# (sigmoid routing with apply_routed_scaling_factor_on_output=True) it is
|
||||
# already folded into topk_weights by the topk kernel; re-applying would
|
||||
# double-count it. This matches the prior SGLang MXFP8 behaviour.
|
||||
|
||||
out = fused_moe_mxfp8_native(
|
||||
hidden_states,
|
||||
w1,
|
||||
w1_scale,
|
||||
w2,
|
||||
w2_scale,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
alpha=alpha,
|
||||
beta=beta,
|
||||
limit=limit,
|
||||
no_combine=no_combine,
|
||||
expert_map=expert_map,
|
||||
)
|
||||
|
||||
if no_combine:
|
||||
return out
|
||||
if inplace:
|
||||
hidden_states.copy_(out)
|
||||
return hidden_states
|
||||
return out
|
||||
@@ -0,0 +1,333 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Native MXFP8 (1x32 block, E8M0 scale) ops for AMD CDNA4 (gfx950).
|
||||
|
||||
* per-token MXFP8 activation quant (single fused Triton pass)
|
||||
* dense GEMM via Triton ``tl.dot_scaled`` (consumes FP8 E4M3 weights + E8M0
|
||||
block scales directly, no dequant-to-BF16), lowering to the CDNA4 native MX
|
||||
matrix-core ops; ``K % 128 != 0`` falls back to dequant + ``F.linear``.
|
||||
|
||||
Replaces the FlyDSL ``v_mfma_scale_f32_32x32x64`` dense path with a single
|
||||
Triton ``dot_scaled`` GEMM: no load-time weight reformat (fp8 + E8M0 are
|
||||
consumed as-is) and the activation is MXFP8-quantized in one fused pass.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
# MXFP8 constants (OCP microscaling: 1x32 block, E8M0 shared scale).
|
||||
MXFP8_VALUE_DTYPE = torch.float8_e4m3fn
|
||||
MXFP8_SCALE_DTYPE = torch.uint8
|
||||
MXFP8_BLOCK_SIZE = 32
|
||||
MXFP8_E4M3_MAX = 448.0 # max representable magnitude of float8_e4m3fn
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# MXFP8 quantization (per-32-block E8M0 scale + FP8-E4M3 values)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _mxfp8_e4m3_quantize_torch(x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Naive (reference) MXFP8 quantization.
|
||||
|
||||
For each block of 32 elements along the last dim, compute a shared E8M0
|
||||
scale and quantize each element to float8_e4m3fn. The E8M0 exponent is
|
||||
rounded *up* -- ``ceil(log2(amax / e4m3_max)) + 127`` -- so the block amax
|
||||
stays inside the e4m3 range (no clipping) and the full dynamic range is
|
||||
used, matching ``triton_kernels`` ``downcast_to_mxfp`` (ROUND_UP) and the
|
||||
SGLang fp8 quant kernels. Returns ``(values [same shape, fp8], scales
|
||||
[..., K//32] u8)``.
|
||||
"""
|
||||
assert x.shape[-1] % MXFP8_BLOCK_SIZE == 0
|
||||
orig_shape = x.shape
|
||||
num_blocks = x.shape[-1] // MXFP8_BLOCK_SIZE
|
||||
|
||||
x_fp32 = x.to(torch.float32)
|
||||
x_blocked = x_fp32.view(*orig_shape[:-1], num_blocks, MXFP8_BLOCK_SIZE)
|
||||
|
||||
amax = x_blocked.abs().amax(dim=-1)
|
||||
amax = amax.clamp(min=torch.finfo(torch.float32).tiny)
|
||||
scale_biased = (torch.ceil(torch.log2(amax / MXFP8_E4M3_MAX)) + 127.0).clamp(0, 254)
|
||||
scales_uint8 = scale_biased.to(torch.uint8)
|
||||
|
||||
descale = torch.exp2(scale_biased - 127.0)
|
||||
x_scaled = (x_blocked / descale.unsqueeze(-1)).clamp(
|
||||
-MXFP8_E4M3_MAX, MXFP8_E4M3_MAX
|
||||
)
|
||||
x_fp8 = x_scaled.view(orig_shape).to(MXFP8_VALUE_DTYPE)
|
||||
|
||||
scales_uint8 = scales_uint8.view(*orig_shape[:-1], num_blocks)
|
||||
return x_fp8, scales_uint8
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _mxfp8_quant_kernel(
|
||||
x_ptr,
|
||||
xq_ptr,
|
||||
s_ptr,
|
||||
M,
|
||||
K,
|
||||
sxm,
|
||||
sxk,
|
||||
sqm,
|
||||
sqk,
|
||||
ssm,
|
||||
ssk,
|
||||
BLOCK_M: tl.constexpr,
|
||||
):
|
||||
"""Per-32-block E8M0 scale + FP8-E4M3 quant, one program per ``[BLOCK_M, 32]``."""
|
||||
pid_m = tl.program_id(0)
|
||||
pid_b = tl.program_id(1) # which 32-element block along K
|
||||
offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
||||
offs_k = pid_b * 32 + tl.arange(0, 32)
|
||||
m_mask = offs_m < M
|
||||
x = tl.load(
|
||||
x_ptr + offs_m[:, None] * sxm + offs_k[None, :] * sxk,
|
||||
mask=m_mask[:, None],
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
amax = tl.maximum(tl.max(tl.abs(x), axis=1), 1e-30) # [BLOCK_M]
|
||||
# Round the E8M0 exponent up (ceil(log2(amax / e4m3_max))) so the block amax
|
||||
# stays inside the e4m3 range and the full dynamic range is used.
|
||||
sb = tl.ceil(tl.log2(amax / 448.0)) + 127.0
|
||||
sb = tl.minimum(tl.maximum(sb, 0.0), 254.0)
|
||||
descale = tl.exp2(sb - 127.0)
|
||||
xq = tl.clamp(x / descale[:, None], -448.0, 448.0).to(xq_ptr.dtype.element_ty)
|
||||
tl.store(
|
||||
xq_ptr + offs_m[:, None] * sqm + offs_k[None, :] * sqk,
|
||||
xq,
|
||||
mask=m_mask[:, None],
|
||||
)
|
||||
tl.store(s_ptr + offs_m * ssm + pid_b * ssk, sb.to(tl.uint8), mask=m_mask)
|
||||
|
||||
|
||||
def _mxfp8_e4m3_quantize_triton(
|
||||
x: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Fused 2D MXFP8 quant (row-major [M, K//32] UE8M0 scales)."""
|
||||
M, K = x.shape
|
||||
x = x.contiguous()
|
||||
xq = torch.empty((M, K), dtype=MXFP8_VALUE_DTYPE, device=x.device)
|
||||
scales = torch.empty(
|
||||
(M, K // MXFP8_BLOCK_SIZE), dtype=MXFP8_SCALE_DTYPE, device=x.device
|
||||
)
|
||||
BLOCK_M = 64
|
||||
grid = (triton.cdiv(M, BLOCK_M), K // MXFP8_BLOCK_SIZE)
|
||||
_mxfp8_quant_kernel[grid](
|
||||
x,
|
||||
xq,
|
||||
scales,
|
||||
M,
|
||||
K,
|
||||
x.stride(0),
|
||||
x.stride(1),
|
||||
xq.stride(0),
|
||||
xq.stride(1),
|
||||
scales.stride(0),
|
||||
scales.stride(1),
|
||||
BLOCK_M=BLOCK_M,
|
||||
)
|
||||
return xq, scales
|
||||
|
||||
|
||||
def mxfp8_e4m3_quantize(x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Per-token MXFP8 quant -> (fp8 values, [.., K//32] uint8 UE8M0 scales).
|
||||
|
||||
Uses the single fused Triton kernel for the common 2D, ``K % 32 == 0`` case
|
||||
(activations); falls back to the torch reference otherwise.
|
||||
"""
|
||||
if x.ndim == 2 and x.shape[-1] % MXFP8_BLOCK_SIZE == 0 and x.is_cuda:
|
||||
return _mxfp8_e4m3_quantize_triton(x.contiguous())
|
||||
return _mxfp8_e4m3_quantize_torch(x)
|
||||
|
||||
|
||||
def dequant_mxfp8_to_bf16(x: torch.Tensor, scales: torch.Tensor) -> torch.Tensor:
|
||||
"""Dequantize an MXFP8 tensor (fp8 values + UE8M0 scales) to BF16."""
|
||||
x_float = x.to(torch.float32)
|
||||
num_blocks = x.shape[-1] // MXFP8_BLOCK_SIZE
|
||||
x_blocked = x_float.view(*x.shape[:-1], num_blocks, MXFP8_BLOCK_SIZE)
|
||||
descale = torch.exp2(scales.to(torch.float32) - 127.0)
|
||||
dequantized = (x_blocked * descale.unsqueeze(-1)).view(*x.shape)
|
||||
return dequantized.to(torch.bfloat16)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Dense MXFP8 linear via Triton tl.dot_scaled (CDNA4 native microscaling)
|
||||
# --------------------------------------------------------------------------- #
|
||||
@triton.jit
|
||||
def _mxfp8_linear_kernel(
|
||||
x_ptr,
|
||||
xs_ptr,
|
||||
w_ptr,
|
||||
ws_ptr,
|
||||
out_ptr,
|
||||
M,
|
||||
N,
|
||||
K,
|
||||
stride_xm,
|
||||
stride_xk,
|
||||
stride_xsm,
|
||||
stride_xsk,
|
||||
stride_wn,
|
||||
stride_wk,
|
||||
stride_wsn,
|
||||
stride_wsk,
|
||||
stride_om,
|
||||
stride_on,
|
||||
BLOCK_M: tl.constexpr,
|
||||
BLOCK_N: tl.constexpr,
|
||||
BLOCK_K: tl.constexpr,
|
||||
):
|
||||
pid_m = tl.program_id(0)
|
||||
pid_n = tl.program_id(1)
|
||||
offs_m = pid_m * BLOCK_M + tl.arange(0, BLOCK_M)
|
||||
offs_n = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
|
||||
offs_k = tl.arange(0, BLOCK_K)
|
||||
offs_sk = tl.arange(0, BLOCK_K // 32)
|
||||
m_mask = offs_m < M
|
||||
n_mask = offs_n < N
|
||||
|
||||
x_ptrs = x_ptr + offs_m[:, None] * stride_xm + offs_k[None, :] * stride_xk
|
||||
xs_ptrs = xs_ptr + offs_m[:, None] * stride_xsm + offs_sk[None, :] * stride_xsk
|
||||
w_ptrs = w_ptr + offs_n[:, None] * stride_wn + offs_k[None, :] * stride_wk
|
||||
ws_ptrs = ws_ptr + offs_n[:, None] * stride_wsn + offs_sk[None, :] * stride_wsk
|
||||
|
||||
acc = tl.zeros((BLOCK_M, BLOCK_N), dtype=tl.float32)
|
||||
for _ in range(0, tl.cdiv(K, BLOCK_K)):
|
||||
x = tl.load(x_ptrs, mask=m_mask[:, None], other=0.0)
|
||||
w = tl.load(w_ptrs, mask=n_mask[:, None], other=0.0)
|
||||
xs = tl.load(xs_ptrs, mask=m_mask[:, None], other=0)
|
||||
ws = tl.load(ws_ptrs, mask=n_mask[:, None], other=0)
|
||||
acc += tl.dot_scaled(x, xs, "e4m3", w.T, ws, "e4m3")
|
||||
x_ptrs += BLOCK_K * stride_xk
|
||||
w_ptrs += BLOCK_K * stride_wk
|
||||
xs_ptrs += (BLOCK_K // 32) * stride_xsk
|
||||
ws_ptrs += (BLOCK_K // 32) * stride_wsk
|
||||
|
||||
o_ptrs = out_ptr + offs_m[:, None] * stride_om + offs_n[None, :] * stride_on
|
||||
tl.store(
|
||||
o_ptrs,
|
||||
acc.to(out_ptr.dtype.element_ty),
|
||||
mask=m_mask[:, None] & n_mask[None, :],
|
||||
)
|
||||
|
||||
|
||||
def _run_mxfp8_linear_kernel(
|
||||
x_q: torch.Tensor, # [M, K] fp8 e4m3
|
||||
x_scale: torch.Tensor, # [M, K//32] uint8 (E8M0)
|
||||
w: torch.Tensor, # [N, K] fp8 e4m3
|
||||
w_scale: torch.Tensor, # [N, K//32] uint8 (E8M0)
|
||||
out_dtype: torch.dtype,
|
||||
) -> torch.Tensor:
|
||||
M, K = x_q.shape
|
||||
N = w.shape[0]
|
||||
out = torch.empty((M, N), dtype=out_dtype, device=x_q.device)
|
||||
BLOCK_M, BLOCK_K = 64, 128
|
||||
if M <= 512 and (K >= 4096 or (N == 6144 and K in (2048, 3072))):
|
||||
BLOCK_N, num_warps = 64, 4
|
||||
else:
|
||||
BLOCK_N, num_warps = 128, 8
|
||||
grid = (triton.cdiv(M, BLOCK_M), triton.cdiv(N, BLOCK_N))
|
||||
_mxfp8_linear_kernel[grid](
|
||||
x_q,
|
||||
x_scale,
|
||||
w,
|
||||
w_scale,
|
||||
out,
|
||||
M,
|
||||
N,
|
||||
K,
|
||||
x_q.stride(0),
|
||||
x_q.stride(1),
|
||||
x_scale.stride(0),
|
||||
x_scale.stride(1),
|
||||
w.stride(0),
|
||||
w.stride(1),
|
||||
w_scale.stride(0),
|
||||
w_scale.stride(1),
|
||||
out.stride(0),
|
||||
out.stride(1),
|
||||
BLOCK_M=BLOCK_M,
|
||||
BLOCK_N=BLOCK_N,
|
||||
BLOCK_K=BLOCK_K,
|
||||
num_warps=num_warps,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _mxfp8_dot_scaled_linear(
|
||||
x: torch.Tensor, # [M, K] bf16/fp16
|
||||
w: torch.Tensor, # [N, K] fp8 e4m3
|
||||
w_scale: torch.Tensor, # [N, K//32] uint8 (E8M0)
|
||||
) -> torch.Tensor:
|
||||
"""bf16/fp16 input -> per-token MXFP8 quant -> dot_scaled GEMM."""
|
||||
x_q, x_scale = mxfp8_e4m3_quantize(x)
|
||||
return _run_mxfp8_linear_kernel(x_q, x_scale, w, w_scale, x.dtype)
|
||||
|
||||
|
||||
def dot_scaled_mxfp8_blockscaled_linear(
|
||||
input: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
input_scale: Optional[torch.Tensor] = None,
|
||||
bias: Optional[torch.Tensor] = None,
|
||||
output_dtype: Optional[torch.dtype] = None,
|
||||
) -> torch.Tensor:
|
||||
"""Native dense MXFP8 linear (CDNA4 ``tl.dot_scaled``).
|
||||
|
||||
Consumes FP8 E4M3 ``weight`` + canonical 2D UE8M0 ``weight_scale`` [N, K//32]
|
||||
directly. Activations are MXFP8-quantized per token inside the kernel path.
|
||||
Drop-in for the SGLang ``w8a8_mxfp8_linear`` callable signature.
|
||||
"""
|
||||
assert weight.dtype == torch.float8_e4m3fn, "MXFP8 weight must be FP8 E4M3."
|
||||
assert weight_scale.dtype == torch.uint8, "MXFP8 weight_scale must be UE8M0 uint8."
|
||||
assert weight_scale.dim() == 2, (
|
||||
"dot_scaled MXFP8 linear expects canonical 2D [N, K//32] weight scales, "
|
||||
f"got {weight_scale.dim()}D."
|
||||
)
|
||||
|
||||
input_2d = input.view(-1, input.shape[-1]).contiguous()
|
||||
output_shape = [*input.shape[:-1], weight.shape[0]]
|
||||
if output_dtype is None:
|
||||
output_dtype = (
|
||||
input_2d.dtype
|
||||
if input_2d.dtype in (torch.float16, torch.bfloat16, torch.float32)
|
||||
else torch.bfloat16
|
||||
)
|
||||
|
||||
m, k = input_2d.shape
|
||||
n, k_w = weight.shape
|
||||
assert k == k_w, f"{k=} does not match {k_w=}"
|
||||
|
||||
if k % 128 == 0:
|
||||
if input_scale is None:
|
||||
# Quantize the bf16/fp16 activations per token inside the path.
|
||||
x_q, x_scale = mxfp8_e4m3_quantize(input_2d)
|
||||
kernel_out_dtype = input_2d.dtype
|
||||
else:
|
||||
# Activations already MXFP8-quantized by a fused upstream op.
|
||||
assert (
|
||||
input_2d.dtype == MXFP8_VALUE_DTYPE
|
||||
), "pre-quantized input must be FP8 E4M3 when input_scale is given."
|
||||
assert input_scale.dtype == torch.uint8 and input_scale.shape == (
|
||||
m,
|
||||
k // 32,
|
||||
), "input_scale must be UE8M0 uint8 [M, K//32]."
|
||||
x_q, x_scale = input_2d, input_scale
|
||||
kernel_out_dtype = output_dtype
|
||||
out = _run_mxfp8_linear_kernel(
|
||||
x_q, x_scale, weight, weight_scale, kernel_out_dtype
|
||||
)
|
||||
else:
|
||||
# dot_scaled tiling needs K % 128 == 0; dequantize fallback otherwise.
|
||||
w_bf16 = dequant_mxfp8_to_bf16(weight, weight_scale)
|
||||
if input_scale is not None:
|
||||
input_2d = dequant_mxfp8_to_bf16(input_2d, input_scale)
|
||||
out = F.linear(input_2d.to(w_bf16.dtype), w_bf16).to(output_dtype)
|
||||
|
||||
if bias is not None:
|
||||
out = out + bias
|
||||
return out.to(output_dtype).view(*output_shape)
|
||||
@@ -0,0 +1,121 @@
|
||||
"""Benchmark: MiniMax-M3 single-stage radix-select decode topk (JIT CUDA) vs the
|
||||
2-stage split-K Triton baseline (_topk_index_partial_kernel + _topk_index_merge_kernel).
|
||||
|
||||
Both consume the decode score tensor [num_heads, batch, max_seqblock] and produce
|
||||
topk_idx [num_heads, batch, topk]. The JIT kernel is one launch with no
|
||||
intermediate buffers; the baseline is two launches with split-K partials.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import triton
|
||||
|
||||
from sglang.jit_kernel.benchmark import marker
|
||||
from sglang.jit_kernel.minimax_decode_topk import minimax_decode_topk
|
||||
from sglang.srt.layers.attention.minimax_sparse_ops.decode.flash_with_topk_idx import (
|
||||
_topk_index_merge_kernel,
|
||||
_topk_index_partial_kernel,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=8, suite="base-b-kernel-benchmark-1-gpu-large")
|
||||
|
||||
BLOCK_SIZE = 128
|
||||
TOPK = 16
|
||||
NUM_HEADS = 1 # per-rank index heads at TP>=4
|
||||
|
||||
|
||||
def _triton_2stage(score, seq_lens):
|
||||
num_q_heads, batch_size, max_seqblock = score.shape
|
||||
TOPK_TARGET_GRID = 64
|
||||
MAX_NUM_TOPK_CHUNKS = 16
|
||||
t = max(
|
||||
1,
|
||||
min(MAX_NUM_TOPK_CHUNKS, TOPK_TARGET_GRID // max(1, batch_size * num_q_heads)),
|
||||
)
|
||||
nchunks = 1 << (t.bit_length() - 1)
|
||||
bt = triton.next_power_of_2(TOPK)
|
||||
chunk_blocks = (max_seqblock + nchunks - 1) // nchunks
|
||||
out = torch.empty(
|
||||
(num_q_heads, batch_size, TOPK), device=score.device, dtype=torch.int32
|
||||
)
|
||||
tsp = torch.empty(
|
||||
nchunks, num_q_heads, batch_size, bt, dtype=torch.float32, device=score.device
|
||||
)
|
||||
tip = torch.empty(
|
||||
nchunks, num_q_heads, batch_size, bt, dtype=torch.int32, device=score.device
|
||||
)
|
||||
_topk_index_partial_kernel[(batch_size, num_q_heads, nchunks)](
|
||||
score,
|
||||
tsp,
|
||||
tip,
|
||||
seq_lens,
|
||||
BLOCK_SIZE,
|
||||
TOPK,
|
||||
chunk_blocks,
|
||||
score.stride(0),
|
||||
score.stride(1),
|
||||
score.stride(2),
|
||||
tsp.stride(0),
|
||||
tsp.stride(1),
|
||||
tsp.stride(2),
|
||||
tsp.stride(3),
|
||||
tip.stride(0),
|
||||
tip.stride(1),
|
||||
tip.stride(2),
|
||||
tip.stride(3),
|
||||
)
|
||||
_topk_index_merge_kernel[(batch_size, num_q_heads)](
|
||||
tsp,
|
||||
tip,
|
||||
out,
|
||||
seq_lens,
|
||||
BLOCK_SIZE,
|
||||
TOPK,
|
||||
tsp.stride(0),
|
||||
tsp.stride(1),
|
||||
tsp.stride(2),
|
||||
tsp.stride(3),
|
||||
tip.stride(0),
|
||||
tip.stride(1),
|
||||
tip.stride(2),
|
||||
tip.stride(3),
|
||||
out.stride(0),
|
||||
out.stride(1),
|
||||
out.stride(2),
|
||||
NUM_TOPK_CHUNKS=nchunks,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _jit(score, seq_lens):
|
||||
return minimax_decode_topk(score, seq_lens, BLOCK_SIZE, TOPK)
|
||||
|
||||
|
||||
FN_MAP = {"jit": _jit, "triton_2stage": _triton_2stage}
|
||||
|
||||
|
||||
@marker.parametrize("ctx", [4096, 32768, 131072, 524288], [4096, 524288])
|
||||
@marker.parametrize("batch", [1, 4, 16, 64, 256], [1, 64])
|
||||
@marker.benchmark("impl", ["jit", "triton_2stage"])
|
||||
def benchmark(ctx: int, batch: int, impl: str):
|
||||
max_seqblock = (524288 + BLOCK_SIZE - 1) // BLOCK_SIZE
|
||||
nb = min((ctx + BLOCK_SIZE - 1) // BLOCK_SIZE, max_seqblock)
|
||||
score = torch.full(
|
||||
(NUM_HEADS, batch, max_seqblock),
|
||||
float("-inf"),
|
||||
dtype=torch.float32,
|
||||
device="cuda",
|
||||
)
|
||||
score[:, :, :nb] = torch.randn(NUM_HEADS, batch, nb, device="cuda") * 5.0
|
||||
score[:, :, nb - 1] = 1e29 # forced local block
|
||||
seq_lens = torch.full((batch,), ctx, device="cuda", dtype=torch.int32)
|
||||
return marker.do_bench(
|
||||
FN_MAP[impl],
|
||||
input_args=(score, seq_lens),
|
||||
graph_clone_args=(0, 1), # both read-only inputs
|
||||
memory_args=(score,),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
benchmark.run()
|
||||
@@ -0,0 +1,146 @@
|
||||
"""Benchmark: fused MiniMax-M3 Gemma-RMSNorm + partial NeoX RoPE (1 in-place
|
||||
launch) vs the unfused path (GemmaRMSNorm(q) + GemmaRMSNorm(k) + rotary_emb,
|
||||
3 launches + intermediates). Main attention branch, per-rank TP8 shape (nq=8, nk=1).
|
||||
"""
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.benchmark import marker
|
||||
from sglang.jit_kernel.minimax_qknorm_rope import (
|
||||
minimax_qknorm_rope,
|
||||
minimax_qknorm_rope_grouped,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=6, suite="base-b-kernel-benchmark-1-gpu-large")
|
||||
|
||||
HEAD_DIM, ROTARY_DIM, BASE, EPS, MAXPOS = 128, 64, 5_000_000, 1e-6, 131072
|
||||
NQ, NK = 64, 4
|
||||
|
||||
|
||||
def _cache():
|
||||
inv_freq = 1.0 / (
|
||||
BASE
|
||||
** (
|
||||
torch.arange(0, ROTARY_DIM, 2, dtype=torch.float, device="cuda")
|
||||
/ ROTARY_DIM
|
||||
)
|
||||
)
|
||||
t = torch.arange(MAXPOS, dtype=torch.float, device="cuda")
|
||||
freqs = torch.outer(t, inv_freq)
|
||||
return torch.cat([freqs.cos(), freqs.sin()], dim=-1).contiguous()
|
||||
|
||||
|
||||
def _unfused(qkv, wq, wk, cache, positions):
|
||||
# GemmaRMSNorm (1+w) on q,k head-wise + partial neox rope, in plain torch
|
||||
# (representative of the separate norm + rope launches).
|
||||
T = qkv.shape[0]
|
||||
q, k, v = qkv.split([NQ * HEAD_DIM, NK * HEAD_DIM, NK * HEAD_DIM], dim=-1)
|
||||
|
||||
def norm(x, w, nh):
|
||||
x = x.reshape(T, nh, HEAD_DIM).float()
|
||||
var = x.pow(2).mean(-1, keepdim=True)
|
||||
return (x * torch.rsqrt(var + EPS) * (1.0 + w.float())).to(torch.bfloat16)
|
||||
|
||||
qn, kn = norm(q, wq, NQ), norm(k, wk, NK)
|
||||
cs = cache.index_select(0, positions).float()
|
||||
cos, sin = cs[:, None, :32], cs[:, None, 32:]
|
||||
|
||||
def rope(x):
|
||||
x1, x2 = x[..., :32].float(), x[..., 32:64].float()
|
||||
o1 = x1 * cos - x2 * sin
|
||||
o2 = x2 * cos + x1 * sin
|
||||
return torch.cat([o1, o2, x[..., 64:].float()], dim=-1).to(torch.bfloat16)
|
||||
|
||||
return rope(qn), rope(kn)
|
||||
|
||||
|
||||
def _fused(qkv, wq, wk, cache, positions):
|
||||
minimax_qknorm_rope(qkv, wq, wk, cache, positions, NQ, NK, NK, EPS)
|
||||
return qkv
|
||||
|
||||
|
||||
FN_MAP = {"fused": _fused, "unfused_torch": _unfused}
|
||||
|
||||
|
||||
@marker.parametrize("T", [1, 16, 64, 256, 1024, 8192], [64, 1024])
|
||||
@marker.benchmark("impl", ["fused", "unfused_torch"])
|
||||
def benchmark(T: int, impl: str):
|
||||
cache = _cache()
|
||||
wq = (torch.randn(HEAD_DIM, device="cuda") * 0.1).to(torch.bfloat16)
|
||||
wk = (torch.randn(HEAD_DIM, device="cuda") * 0.1).to(torch.bfloat16)
|
||||
qkv = torch.randn(T, (NQ + 2 * NK) * HEAD_DIM, dtype=torch.bfloat16, device="cuda")
|
||||
positions = torch.randint(0, MAXPOS, (T,), device="cuda", dtype=torch.int64)
|
||||
return marker.do_bench(
|
||||
FN_MAP[impl],
|
||||
input_args=(qkv, wq, wk, cache, positions),
|
||||
graph_clone_args=(0,),
|
||||
memory_args=None,
|
||||
)
|
||||
|
||||
|
||||
# --- Combined main + index single launch (the fused qkv+index_qkv GEMM path) ---
|
||||
# Per-rank TP8 sparse shape: main q=8/k=1/v=1 + index idx_q=1/idx_k=1 (value
|
||||
# disabled). One grouped launch (q, k, idx_q, idx_k) vs two separate launches.
|
||||
C_NQ, C_NKV, C_NIQ = 8, 1, 1
|
||||
C_OFF_Q = 0
|
||||
C_OFF_K = C_NQ
|
||||
C_OFF_V = C_NQ + C_NKV
|
||||
C_OFF_IQ = C_NQ + 2 * C_NKV
|
||||
C_OFF_IK = C_OFF_IQ + C_NIQ
|
||||
C_TOTAL_HEADS = C_OFF_IK + 1
|
||||
|
||||
|
||||
def _combined_one(args):
|
||||
qkv, wq, wk, wiq, wik, cache, positions = args
|
||||
minimax_qknorm_rope_grouped(
|
||||
qkv,
|
||||
[
|
||||
(wq, C_OFF_Q, C_NQ),
|
||||
(wk, C_OFF_K, C_NKV),
|
||||
(wiq, C_OFF_IQ, C_NIQ),
|
||||
(wik, C_OFF_IK, 1),
|
||||
],
|
||||
cache,
|
||||
positions,
|
||||
EPS,
|
||||
)
|
||||
return qkv
|
||||
|
||||
|
||||
def _combined_two(args):
|
||||
# Two launches over the same buffer: main (q,k) then index (idx_q, idx_k).
|
||||
qkv, wq, wk, wiq, wik, cache, positions = args
|
||||
minimax_qknorm_rope_grouped(
|
||||
qkv, [(wq, C_OFF_Q, C_NQ), (wk, C_OFF_K, C_NKV)], cache, positions, EPS
|
||||
)
|
||||
minimax_qknorm_rope_grouped(
|
||||
qkv, [(wiq, C_OFF_IQ, C_NIQ), (wik, C_OFF_IK, 1)], cache, positions, EPS
|
||||
)
|
||||
return qkv
|
||||
|
||||
|
||||
C_FN_MAP = {"combined_one_launch": _combined_one, "two_launches": _combined_two}
|
||||
|
||||
|
||||
@marker.parametrize("T", [1, 16, 64, 256, 1024, 8192], [64, 1024])
|
||||
@marker.benchmark("impl", ["combined_one_launch", "two_launches"])
|
||||
def benchmark_combined(T: int, impl: str):
|
||||
cache = _cache()
|
||||
ws = [
|
||||
(torch.randn(HEAD_DIM, device="cuda") * 0.1).to(torch.bfloat16)
|
||||
for _ in range(4)
|
||||
]
|
||||
qkv = torch.randn(T, C_TOTAL_HEADS * HEAD_DIM, dtype=torch.bfloat16, device="cuda")
|
||||
positions = torch.randint(0, MAXPOS, (T,), device="cuda", dtype=torch.int64)
|
||||
return marker.do_bench(
|
||||
C_FN_MAP[impl],
|
||||
input_args=((qkv, *ws, cache, positions),),
|
||||
graph_clone_args=(0,),
|
||||
memory_args=None,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
benchmark.run()
|
||||
benchmark_combined.run()
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Benchmark: fused MiniMax-M3 KV + index cache store (1 launch) vs the separate
|
||||
per-buffer index_put_ stores (main K, main V, index K, optional index V)."""
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.benchmark import marker
|
||||
from sglang.jit_kernel.minimax_store_kv_index import store_kv_index
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=6, suite="base-b-kernel-benchmark-1-gpu-large")
|
||||
|
||||
HEAD_DIM = 128
|
||||
NUM_KV_HEADS = 1
|
||||
HAS_V = False
|
||||
N = 1 << 20
|
||||
DTYPE = torch.bfloat16
|
||||
|
||||
|
||||
def _separate(k, v, kc, vc, ik, ikc, loc, **_):
|
||||
kc[loc] = k
|
||||
vc[loc] = v
|
||||
ikc[loc] = ik
|
||||
|
||||
|
||||
def _fused(k, v, kc, vc, ik, ikc, loc, *, num_kv_heads, head_bytes):
|
||||
store_kv_index(
|
||||
k,
|
||||
v,
|
||||
kc,
|
||||
vc,
|
||||
ik,
|
||||
ikc,
|
||||
None,
|
||||
None,
|
||||
loc,
|
||||
num_kv_heads=num_kv_heads,
|
||||
head_bytes=head_bytes,
|
||||
)
|
||||
|
||||
|
||||
FN_MAP = {"fused": _fused, "separate": _separate}
|
||||
|
||||
|
||||
@marker.parametrize("T", [16, 64, 256, 1024, 4096, 16384], [256, 4096])
|
||||
@marker.benchmark("impl", ["fused", "separate"])
|
||||
def benchmark(T: int, impl: str):
|
||||
k = torch.randn(T, NUM_KV_HEADS * HEAD_DIM, dtype=DTYPE, device="cuda")
|
||||
v = torch.randn_like(k)
|
||||
ik = torch.randn(T, HEAD_DIM, dtype=DTYPE, device="cuda")
|
||||
kc = torch.zeros(N, NUM_KV_HEADS * HEAD_DIM, dtype=DTYPE, device="cuda")
|
||||
vc = torch.zeros_like(kc)
|
||||
ikc = torch.zeros(N, HEAD_DIM, dtype=DTYPE, device="cuda")
|
||||
loc = torch.randperm(N, device="cuda")[:T]
|
||||
extra_kwargs = dict(num_kv_heads=NUM_KV_HEADS, head_bytes=HEAD_DIM * DTYPE.itemsize)
|
||||
return marker.do_bench(
|
||||
FN_MAP[impl],
|
||||
input_args=(k, v, kc, vc, ik, ikc, loc),
|
||||
input_kwargs=extra_kwargs if impl == "fused" else {},
|
||||
# Read inputs cloned per iter; caches are write targets (kept hot).
|
||||
graph_clone_args=(0, 1, 4, 6),
|
||||
memory_args=(k, v, ik, loc),
|
||||
memory_output=(k, v, ik),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
benchmark.run()
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Correctness tests for the MiniMax-M3 single-stage radix-select decode topk.
|
||||
|
||||
The kernel selects, per (head, batch) row, the indices of the ``topk`` largest
|
||||
block scores among the row's first ``num_blocks = ceil(seq_len / block_size)``
|
||||
entries, front-packing valid block ids and ``-1``-padding the tail. This mirrors
|
||||
the consumer ``_gqa_share_sparse_decode_kernel`` contract.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.minimax_decode_topk import minimax_decode_topk
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=40, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=40, suite="base-b-kernel-unit-1-gpu-b200")
|
||||
|
||||
|
||||
def _ref(score, seq_lens, block_size, topk):
|
||||
H, B, S = score.shape
|
||||
out = torch.full((H, B, topk), -1, dtype=torch.int32, device=score.device)
|
||||
for h in range(H):
|
||||
for b in range(B):
|
||||
sl = int(seq_lens[b])
|
||||
nb = min((sl + block_size - 1) // block_size, S)
|
||||
if nb <= topk:
|
||||
for i in range(nb):
|
||||
out[h, b, i] = i
|
||||
continue
|
||||
keff = min(topk, nb)
|
||||
_, idx = torch.topk(score[h, b, :nb], keff)
|
||||
out[h, b, :keff] = idx.to(torch.int32)
|
||||
return out
|
||||
|
||||
|
||||
def _selected_scores_sorted(score, out):
|
||||
"""Per-row sorted-desc multiset of the scores the kernel selected (tie-robust)."""
|
||||
H, B, _ = score.shape
|
||||
rows = []
|
||||
for h in range(H):
|
||||
for b in range(B):
|
||||
sel = out[h, b]
|
||||
sel = sel[sel >= 0].long()
|
||||
assert len(sel.unique()) == len(sel), f"duplicate idx h{h} b{b}: {sel}"
|
||||
rows.append(torch.sort(score[h, b, sel], descending=True).values)
|
||||
return rows
|
||||
|
||||
|
||||
def _check_contract(out, seq_lens, block_size, topk, S):
|
||||
H, B, _ = out.shape
|
||||
for h in range(H):
|
||||
for b in range(B):
|
||||
o = out[h, b]
|
||||
nvalid = int((o >= 0).sum())
|
||||
# valid entries are front-packed, -1 after
|
||||
assert torch.all(o[:nvalid] >= 0) and torch.all(o[nvalid:] == -1), o
|
||||
nb = min((int(seq_lens[b]) + block_size - 1) // block_size, S)
|
||||
assert nvalid == min(topk, nb)
|
||||
assert torch.all(o[:nvalid] < nb)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype_sl", [torch.int32, torch.int64])
|
||||
@pytest.mark.parametrize("H", [1, 2])
|
||||
@pytest.mark.parametrize("B", [1, 5, 32])
|
||||
@pytest.mark.parametrize("topk", [16, 32, 64])
|
||||
@pytest.mark.parametrize("max_ctx", [4096, 131072, 524288])
|
||||
def test_decode_topk_distinct(dtype_sl, H, B, topk, max_ctx):
|
||||
torch.manual_seed(1234)
|
||||
block_size = 128
|
||||
S = (max_ctx + block_size - 1) // block_size
|
||||
# distinct scores per row -> exact index-set match against torch.topk
|
||||
score = torch.empty(H, B, S, dtype=torch.float32, device="cuda")
|
||||
for h in range(H):
|
||||
for b in range(B):
|
||||
score[h, b] = torch.randperm(S, device="cuda").float() + torch.rand(
|
||||
1, device="cuda"
|
||||
)
|
||||
seq_lens = torch.randint(1, max_ctx + 1, (B,), device="cuda", dtype=dtype_sl)
|
||||
|
||||
out = minimax_decode_topk(score, seq_lens, block_size, topk)
|
||||
ref = _ref(score, seq_lens, block_size, topk)
|
||||
_check_contract(out, seq_lens, block_size, topk, S)
|
||||
# exact index-set equality (distinct scores)
|
||||
for h in range(H):
|
||||
for b in range(B):
|
||||
assert set(out[h, b][out[h, b] >= 0].tolist()) == set(
|
||||
ref[h, b][ref[h, b] >= 0].tolist()
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("kind", ["ties", "negative", "neg_inf_padding", "all_equal"])
|
||||
def test_decode_topk_adversarial(kind):
|
||||
torch.manual_seed(7)
|
||||
block_size = 128
|
||||
H, B, S, topk = 1, 6, 1024, 16
|
||||
if kind == "ties":
|
||||
score = torch.randint(0, 4, (H, B, S), device="cuda").float()
|
||||
elif kind == "negative":
|
||||
score = -torch.rand(H, B, S, device="cuda") * 1000 - 1.0
|
||||
elif kind == "neg_inf_padding":
|
||||
score = torch.randn(H, B, S, device="cuda")
|
||||
score[:, :, ::7] = float("-inf") # scattered -inf in valid range
|
||||
else: # all_equal
|
||||
score = torch.full((H, B, S), 3.14, dtype=torch.float32, device="cuda")
|
||||
seq_lens = torch.randint(1, S * block_size, (B,), device="cuda", dtype=torch.int32)
|
||||
|
||||
out = minimax_decode_topk(score, seq_lens, block_size, topk)
|
||||
ref = _ref(score, seq_lens, block_size, topk)
|
||||
_check_contract(out, seq_lens, block_size, topk, S)
|
||||
# tie-robust: the multiset of selected scores must match torch.topk's
|
||||
for a, b in zip(
|
||||
_selected_scores_sorted(score, out), _selected_scores_sorted(score, ref)
|
||||
):
|
||||
torch.testing.assert_close(a, b, rtol=0, atol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seq_len", [1, 128, 129, 2048, 2049])
|
||||
def test_decode_topk_small_num_blocks(seq_len):
|
||||
# num_blocks around / below topk -> naive identity path and boundary.
|
||||
block_size = 128
|
||||
H, B, S, topk = 1, 1, 64, 16
|
||||
score = torch.randn(H, B, S, dtype=torch.float32, device="cuda")
|
||||
seq_lens = torch.tensor([seq_len], device="cuda", dtype=torch.int32)
|
||||
out = minimax_decode_topk(score, seq_lens, block_size, topk)
|
||||
_check_contract(out, seq_lens, block_size, topk, S)
|
||||
nb = min((seq_len + block_size - 1) // block_size, S)
|
||||
if nb <= topk:
|
||||
assert out[0, 0, :nb].tolist() == list(range(nb))
|
||||
|
||||
|
||||
def test_decode_topk_out_param():
|
||||
block_size = 128
|
||||
H, B, S, topk = 1, 4, 1024, 16
|
||||
score = torch.randn(H, B, S, dtype=torch.float32, device="cuda")
|
||||
seq_lens = torch.full((B,), 100000, device="cuda", dtype=torch.int32)
|
||||
out = torch.empty((H, B, topk), dtype=torch.int32, device="cuda")
|
||||
res = minimax_decode_topk(score, seq_lens, block_size, topk, out=out)
|
||||
assert res is out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,178 @@
|
||||
"""Fused decode top-k + page-table transform.
|
||||
|
||||
`minimax_decode_topk_page_table` selects the top-k blocks (same as the block-id
|
||||
`minimax_decode_topk`) and emits the per-query paged page table consumed by the
|
||||
dense backend (trtllm_mha), instead of block ids. This checks the fused output
|
||||
end-to-end: trtllm decode over the emitted page table matches the custom
|
||||
`_gqa_share_sparse_decode_kernel` fed the block-id selection from the same score.
|
||||
Only the TP>=4 case (num_kv_heads == 1) is covered.
|
||||
"""
|
||||
|
||||
import random
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
flashinfer = pytest.importorskip("flashinfer")
|
||||
|
||||
from sglang.jit_kernel.minimax_decode_topk import (
|
||||
minimax_decode_topk,
|
||||
minimax_decode_topk_page_table,
|
||||
)
|
||||
from sglang.srt.layers.attention.minimax_sparse_ops.decode.topk_sparse import (
|
||||
flash_decode_with_gqa_share_sparse,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=25, suite="base-b-kernel-unit-1-gpu-b200")
|
||||
|
||||
dev = "cuda"
|
||||
|
||||
|
||||
def _effective_kv_from_selection(ti, seq_lens, block):
|
||||
# Ground-truth effective KV length: sum of valid tokens over the selected
|
||||
# blocks (only the final block can be partial), per query.
|
||||
bs = seq_lens.shape[0]
|
||||
out = torch.zeros(bs, dtype=torch.int32, device=ti.device)
|
||||
for b in range(bs):
|
||||
sl = int(seq_lens[b])
|
||||
tot = 0
|
||||
for c in ti[0, b].tolist():
|
||||
if c < 0:
|
||||
continue
|
||||
tot += min(block, sl - c * block)
|
||||
out[b] = tot
|
||||
return out
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bs,seq_len",
|
||||
[
|
||||
(1, 5000),
|
||||
(2, 300),
|
||||
(3, 160),
|
||||
(4, 2048),
|
||||
(8, 4096),
|
||||
(16, 8000),
|
||||
(2, 40000), # num_blocks=313 -> medium radix path
|
||||
(1, 90000), # num_blocks=704 -> large compaction path
|
||||
(1, 480000), # num_blocks=3750 -> large path near kMaxNumBlocks
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("nqh", [8, 16])
|
||||
def test_fused_page_table_matches_custom(bs, seq_len, nqh):
|
||||
arch_major, _ = torch.cuda.get_device_capability(torch.cuda.current_device())
|
||||
if arch_major < 10:
|
||||
pytest.skip("trtllm-gen decode is Blackwell (sm100)")
|
||||
nkv, D, block, topk, ps = 1, 128, 128, 16, 64
|
||||
torch.manual_seed(bs * 31 + seq_len + nqh)
|
||||
random.seed(bs * 31 + seq_len)
|
||||
|
||||
ppr = (seq_len + ps - 1) // ps
|
||||
npages = bs * ppr + 4
|
||||
kf = torch.randn(npages * ps, nkv, D, device=dev, dtype=torch.bfloat16) * 0.5
|
||||
vf = torch.randn(npages * ps, nkv, D, device=dev, dtype=torch.bfloat16) * 0.5
|
||||
q = torch.randn(bs, nqh, D, device=dev, dtype=torch.bfloat16) * 0.5
|
||||
|
||||
r2t = torch.zeros(bs, seq_len, dtype=torch.int32, device=dev)
|
||||
sl = torch.full((bs,), seq_len, dtype=torch.int32, device=dev)
|
||||
sid = torch.arange(bs, dtype=torch.int64, device=dev)
|
||||
idxs = torch.arange(seq_len, device=dev)
|
||||
for b in range(bs):
|
||||
r2t[b] = ((b * ppr + idxs // ps) * ps + idxs % ps).int()
|
||||
|
||||
nb = (seq_len + block - 1) // block
|
||||
score = torch.full((1, bs, nb), -float("inf"), device=dev, dtype=torch.float32)
|
||||
score[0, :, :nb] = torch.randn(bs, nb, device=dev)
|
||||
score[0, :, nb - 1] = 1e30 # final (local) block always selected
|
||||
|
||||
# block-id selection -> custom kernel (reference)
|
||||
ti = minimax_decode_topk(score, sl, block, topk)
|
||||
ref = flash_decode_with_gqa_share_sparse(
|
||||
q, None, kf, vf, r2t, sl, sid, block, ti, sm_scale=D**-0.5
|
||||
)
|
||||
|
||||
# fused page-table + effective KV length -> trtllm (allocated + returned)
|
||||
pt, cache = minimax_decode_topk_page_table(score, sl, r2t, sid, block, topk, ps)
|
||||
# the kernel's effective KV length must match the actual block selection
|
||||
expect_cache = _effective_kv_from_selection(ti, sl, block)
|
||||
assert torch.equal(cache, expect_cache), f"{cache} != {expect_cache}"
|
||||
ws = torch.zeros(128 * 1024 * 1024, dtype=torch.int8, device=dev)
|
||||
kv = (
|
||||
kf.view(npages, ps, nkv, D).permute(0, 2, 1, 3),
|
||||
vf.view(npages, ps, nkv, D).permute(0, 2, 1, 3),
|
||||
)
|
||||
o = flashinfer.decode.trtllm_batch_decode_with_kv_cache(
|
||||
query=q,
|
||||
kv_cache=kv,
|
||||
workspace_buffer=ws,
|
||||
block_tables=pt,
|
||||
seq_lens=cache,
|
||||
max_seq_len=topk * block,
|
||||
bmm1_scale=D**-0.5,
|
||||
bmm2_scale=1.0,
|
||||
)
|
||||
cos = torch.nn.functional.cosine_similarity(
|
||||
ref.float().flatten(), o.float().flatten(), dim=0
|
||||
).item()
|
||||
assert cos > 0.999, f"cos={cos}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seq_len", [300, 5000, 90000])
|
||||
@pytest.mark.parametrize("bs", [1, 3])
|
||||
@pytest.mark.parametrize("nkv", [2, 4])
|
||||
def test_dp_flattened_page_table(nkv, bs, seq_len):
|
||||
"""DP attention (num_kv_heads>1): each kv head selects its own blocks, flattened
|
||||
into bs*nkv pseudo-requests (row = b*nkv + h). Validate the flattened page table
|
||||
+ effective KV length against the per-head block-id selection, including the
|
||||
head-minor head-encoded page index (base_page*nkv + h, the index into an HND
|
||||
cache [num_pages, nkv, ps, D] reshaped to [num_pages*nkv, 1, ps, D])."""
|
||||
D, block, topk, ps = 128, 128, 16, 64
|
||||
ppb = block // ps
|
||||
torch.manual_seed(nkv * 131 + bs * 31 + seq_len)
|
||||
ppr = (seq_len + ps - 1) // ps
|
||||
max_kv = seq_len # req_to_token width
|
||||
|
||||
r2t = torch.zeros(bs, seq_len, dtype=torch.int32, device=dev)
|
||||
sl = torch.full((bs,), seq_len, dtype=torch.int32, device=dev)
|
||||
sid = torch.arange(bs, dtype=torch.int64, device=dev)
|
||||
idxs = torch.arange(seq_len, device=dev)
|
||||
for b in range(bs):
|
||||
r2t[b] = ((b * ppr + idxs // ps) * ps + idxs % ps).int()
|
||||
|
||||
nb = (seq_len + block - 1) // block
|
||||
score = torch.full((nkv, bs, nb), -float("inf"), device=dev, dtype=torch.float32)
|
||||
score[:, :, :nb] = torch.randn(nkv, bs, nb, device=dev)
|
||||
score[:, :, nb - 1] = 1e30 # final (local) block always selected
|
||||
|
||||
# per-head block-id selection is the reference for the flattened page table
|
||||
ti = minimax_decode_topk(score, sl, block, topk) # [nkv, bs, topk]
|
||||
pt, cache = minimax_decode_topk_page_table(score, sl, r2t, sid, block, topk, ps)
|
||||
msp = topk * ppb
|
||||
assert pt.shape == (bs * nkv, msp) and cache.shape == (bs * nkv,)
|
||||
|
||||
r2t_cpu = r2t.cpu()
|
||||
for b in range(bs):
|
||||
for h in range(nkv):
|
||||
blocks = sorted(c for c in ti[h, b].tolist() if c >= 0)
|
||||
row = b * nkv + h
|
||||
# effective KV length = sum of valid tokens over selected blocks
|
||||
exp_kv = sum(min(block, seq_len - c * block) for c in blocks)
|
||||
assert (
|
||||
int(cache[row]) == exp_kv
|
||||
), f"row {row}: {int(cache[row])} != {exp_kv}"
|
||||
# page table: each block -> ppb pages via req_to_token, head-minor encoded
|
||||
for e in range(len(blocks) * ppb):
|
||||
c = blocks[e // ppb]
|
||||
tok = c * block + (e % ppb) * ps
|
||||
if tok >= max_kv:
|
||||
tok = max_kv - 1
|
||||
exp = int(r2t_cpu[b, tok]) // ps * nkv + h
|
||||
assert (
|
||||
int(pt[row, e]) == exp
|
||||
), f"row {row} e {e}: {int(pt[row,e])} != {exp}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
@@ -0,0 +1,178 @@
|
||||
"""Correctness for the fused MiniMax-M3 Gemma-RMSNorm + partial NeoX RoPE kernel.
|
||||
|
||||
Verifies the in-place fused kernel reproduces GemmaRMSNorm((1+w)) + partial NeoX
|
||||
RoPE to the bf16 round-off floor, leaves V untouched, and matches sglang's RoPE
|
||||
convention (cos|sin cache, neox pairs (i, i+rotary_dim/2)).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.minimax_qknorm_rope import (
|
||||
minimax_qknorm_rope,
|
||||
minimax_qknorm_rope_grouped,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-b200")
|
||||
|
||||
dev = "cuda"
|
||||
HEAD_DIM, ROTARY_DIM, BASE, EPS = 128, 64, 5_000_000, 1e-6
|
||||
|
||||
|
||||
def _build_cache(max_pos):
|
||||
inv_freq = 1.0 / (
|
||||
BASE
|
||||
** (torch.arange(0, ROTARY_DIM, 2, dtype=torch.float, device=dev) / ROTARY_DIM)
|
||||
) # [32]
|
||||
t = torch.arange(max_pos, dtype=torch.float, device=dev)
|
||||
freqs = torch.outer(t, inv_freq) # [max_pos, 32]
|
||||
return torch.cat([freqs.cos(), freqs.sin()], dim=-1).contiguous() # [max_pos, 64]
|
||||
|
||||
|
||||
def _ref(q, k, wq, wk, cache, positions, nq, nk):
|
||||
T = q.shape[0]
|
||||
|
||||
def norm(x, w, nh):
|
||||
x = x.reshape(T, nh, HEAD_DIM).float()
|
||||
var = x.pow(2).mean(-1, keepdim=True)
|
||||
return x * torch.rsqrt(var + EPS) * (1.0 + w.float())
|
||||
|
||||
cs = cache.index_select(0, positions).float()
|
||||
cos, sin = cs[:, :32], cs[:, 32:]
|
||||
|
||||
def rope(x):
|
||||
x1, x2 = x[..., :32], x[..., 32:64]
|
||||
o1 = x1 * cos[:, None, :] - x2 * sin[:, None, :]
|
||||
o2 = x2 * cos[:, None, :] + x1 * sin[:, None, :]
|
||||
return torch.cat([o1, o2, x[..., 64:]], dim=-1)
|
||||
|
||||
return rope(norm(q, wq, nq)).reshape(T, -1), rope(norm(k, wk, nk)).reshape(T, -1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nq,nk", [(8, 1), (64, 8), (16, 2)])
|
||||
@pytest.mark.parametrize("T", [1, 7, 64, 1024, 4096])
|
||||
@pytest.mark.parametrize("pos_dtype", [torch.int32, torch.int64])
|
||||
def test_fused_qknorm_rope(nq, nk, T, pos_dtype):
|
||||
torch.manual_seed(T * 131 + nq)
|
||||
max_pos = 8192
|
||||
cache = _build_cache(max_pos)
|
||||
wq = (torch.randn(HEAD_DIM, device=dev) * 0.1).to(torch.bfloat16)
|
||||
wk = (torch.randn(HEAD_DIM, device=dev) * 0.1).to(torch.bfloat16)
|
||||
q = torch.randn(T, nq * HEAD_DIM, dtype=torch.bfloat16, device=dev)
|
||||
k = torch.randn(T, nk * HEAD_DIM, dtype=torch.bfloat16, device=dev)
|
||||
v = torch.randn(T, nk * HEAD_DIM, dtype=torch.bfloat16, device=dev)
|
||||
positions = torch.randint(0, max_pos, (T,), device=dev, dtype=pos_dtype)
|
||||
|
||||
qr, kr = _ref(q, k, wq, wk, cache, positions.long(), nq, nk)
|
||||
|
||||
qkv = torch.cat([q, k, v], dim=-1).contiguous()
|
||||
minimax_qknorm_rope(qkv, wq, wk, cache, positions, nq, nk, nk, EPS)
|
||||
qf, kf, vf = qkv.split([nq * HEAD_DIM, nk * HEAD_DIM, nk * HEAD_DIM], dim=-1)
|
||||
|
||||
floor = (qr.bfloat16().float() - qr.float()).abs().max().item()
|
||||
assert (qf.float() - qr.float()).abs().max().item() <= 2 * floor + 1e-3
|
||||
assert (kf.float() - kr.float()).abs().max().item() <= 2 * floor + 1e-3
|
||||
assert (vf.float() - v.float()).abs().max().item() == 0.0 # V untouched
|
||||
|
||||
|
||||
def test_index_branch_shapes():
|
||||
# idx_q (nq=num_idx_heads, nk=0) and idx_k (nq=1) in-place calls.
|
||||
torch.manual_seed(0)
|
||||
max_pos = 4096
|
||||
cache = _build_cache(max_pos)
|
||||
w = (torch.randn(HEAD_DIM, device=dev) * 0.1).to(torch.bfloat16)
|
||||
T, num_idx = 33, 4
|
||||
positions = torch.randint(0, max_pos, (T,), device=dev, dtype=torch.int64)
|
||||
for nq in (num_idx, 1):
|
||||
x = torch.randn(T, nq * HEAD_DIM, dtype=torch.bfloat16, device=dev)
|
||||
ref, _ = _ref(x, x[:, :HEAD_DIM], w, w, cache, positions, nq, 1)
|
||||
xc = x.clone()
|
||||
minimax_qknorm_rope(xc, w, w, cache, positions, nq, 0, 0, EPS)
|
||||
floor = (ref.bfloat16().float() - ref.float()).abs().max().item()
|
||||
assert (xc.float() - ref.float()).abs().max().item() <= 2 * floor + 1e-3
|
||||
|
||||
|
||||
def _norm_rope_one(x_heads, w, cache, positions):
|
||||
# x_heads: [T, nh, HEAD_DIM] fp32; returns same shape, GemmaRMSNorm(1+w)+rope.
|
||||
var = x_heads.pow(2).mean(-1, keepdim=True)
|
||||
y = x_heads * torch.rsqrt(var + EPS) * (1.0 + w.float())
|
||||
cs = cache.index_select(0, positions).float()
|
||||
cos, sin = cs[:, None, :32], cs[:, None, 32:]
|
||||
x1, x2 = y[..., :32], y[..., 32:64]
|
||||
o1 = x1 * cos - x2 * sin
|
||||
o2 = x2 * cos + x1 * sin
|
||||
return torch.cat([o1, o2, y[..., 64:]], dim=-1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("nq,nkv,niq", [(8, 1, 1), (8, 1, 4), (16, 2, 2)])
|
||||
@pytest.mark.parametrize("idx_v", [0, 1])
|
||||
@pytest.mark.parametrize("T", [1, 7, 64, 1024])
|
||||
@pytest.mark.parametrize("pos_dtype", [torch.int32, torch.int64])
|
||||
def test_combined_main_index_grouped(nq, nkv, niq, idx_v, T, pos_dtype):
|
||||
"""Combined main(q,k,v) + index(idx_q,idx_k,[idx_v]) layout in one launch.
|
||||
|
||||
Mirrors the fused qkv+index_qkv GEMM output: a uniform [total_heads, 128]
|
||||
grid where Q / K main heads and index-Q / index-K heads are normed+roped in
|
||||
one pass and the V / index-V heads are left untouched.
|
||||
"""
|
||||
torch.manual_seed(T * 17 + nq * 3 + niq + idx_v)
|
||||
max_pos = 8192
|
||||
cache = _build_cache(max_pos)
|
||||
positions = torch.randint(0, max_pos, (T,), device=dev, dtype=pos_dtype)
|
||||
|
||||
# head layout: q(nq) k(nkv) v(nkv) idx_q(niq) idx_k(1) [idx_v(1)]
|
||||
off_q = 0
|
||||
off_k = nq
|
||||
off_v = nq + nkv
|
||||
off_iq = nq + 2 * nkv
|
||||
off_ik = off_iq + niq
|
||||
total_heads = off_ik + 1 + idx_v
|
||||
|
||||
wq = (torch.randn(HEAD_DIM, device=dev) * 0.1).to(torch.bfloat16)
|
||||
wk = (torch.randn(HEAD_DIM, device=dev) * 0.1).to(torch.bfloat16)
|
||||
wiq = (torch.randn(HEAD_DIM, device=dev) * 0.1).to(torch.bfloat16)
|
||||
wik = (torch.randn(HEAD_DIM, device=dev) * 0.1).to(torch.bfloat16)
|
||||
|
||||
x = torch.randn(T, total_heads, HEAD_DIM, dtype=torch.bfloat16, device=dev)
|
||||
ref = x.float().clone()
|
||||
ref[:, off_q:off_k] = _norm_rope_one(
|
||||
ref[:, off_q:off_k], wq, cache, positions.long()
|
||||
)
|
||||
ref[:, off_k:off_v] = _norm_rope_one(
|
||||
ref[:, off_k:off_v], wk, cache, positions.long()
|
||||
)
|
||||
ref[:, off_iq:off_ik] = _norm_rope_one(
|
||||
ref[:, off_iq:off_ik], wiq, cache, positions.long()
|
||||
)
|
||||
ref[:, off_ik : off_ik + 1] = _norm_rope_one(
|
||||
ref[:, off_ik : off_ik + 1], wik, cache, positions.long()
|
||||
)
|
||||
|
||||
qkv = x.reshape(T, total_heads * HEAD_DIM).contiguous()
|
||||
minimax_qknorm_rope_grouped(
|
||||
qkv,
|
||||
[(wq, off_q, nq), (wk, off_k, nkv), (wiq, off_iq, niq), (wik, off_ik, 1)],
|
||||
cache,
|
||||
positions,
|
||||
EPS,
|
||||
)
|
||||
out = qkv.reshape(T, total_heads, HEAD_DIM)
|
||||
|
||||
floor = (ref.bfloat16().float() - ref).abs().max().item()
|
||||
assert (out.float() - ref).abs().max().item() <= 2 * floor + 1e-3
|
||||
# V and index-V heads untouched (bit-exact).
|
||||
assert (
|
||||
out[:, off_v:off_iq].float() - x[:, off_v:off_iq].float()
|
||||
).abs().max() == 0.0
|
||||
if idx_v:
|
||||
assert (
|
||||
out[:, off_ik + 1 :].float() - x[:, off_ik + 1 :].float()
|
||||
).abs().max() == 0.0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Correctness for the fused MiniMax-M3 KV + index cache store kernel.
|
||||
|
||||
Verifies the single fused launch writes the main K/V, the index K, and the
|
||||
optional index V into their pools at out_cache_loc rows exactly as the separate
|
||||
index_put_ stores would, for both value modes and int32/int64 indices.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.minimax_store_kv_index import store_kv_index
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-large")
|
||||
register_cuda_ci(est_time=30, suite="base-b-kernel-unit-1-gpu-b200")
|
||||
|
||||
dev = "cuda"
|
||||
HEAD_DIM = 128
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@pytest.mark.parametrize("num_kv_heads", [1, 4, 8])
|
||||
@pytest.mark.parametrize("has_v", [False, True])
|
||||
@pytest.mark.parametrize("idx_dtype", [torch.int32, torch.int64])
|
||||
@pytest.mark.parametrize("T", [1, 7, 128, 513])
|
||||
def test_store_kv_index(dtype, num_kv_heads, has_v, idx_dtype, T):
|
||||
torch.manual_seed(T * 17 + num_kv_heads)
|
||||
head_bytes = HEAD_DIM * dtype.itemsize
|
||||
N = 4096
|
||||
|
||||
def rnd(*shape):
|
||||
return torch.randn(*shape, dtype=dtype, device=dev)
|
||||
|
||||
k = rnd(T, num_kv_heads * HEAD_DIM)
|
||||
v = rnd(T, num_kv_heads * HEAD_DIM)
|
||||
idx_k = rnd(T, HEAD_DIM)
|
||||
idx_v = rnd(T, HEAD_DIM) if has_v else None
|
||||
|
||||
k_cache = torch.zeros(N, num_kv_heads * HEAD_DIM, dtype=dtype, device=dev)
|
||||
v_cache = torch.zeros_like(k_cache)
|
||||
idx_k_cache = torch.zeros(N, HEAD_DIM, dtype=dtype, device=dev)
|
||||
idx_v_cache = torch.zeros_like(idx_k_cache) if has_v else None
|
||||
|
||||
loc = torch.randperm(N, device=dev)[:T].to(idx_dtype)
|
||||
|
||||
store_kv_index(
|
||||
k,
|
||||
v,
|
||||
k_cache,
|
||||
v_cache,
|
||||
idx_k,
|
||||
idx_k_cache,
|
||||
idx_v,
|
||||
idx_v_cache,
|
||||
loc,
|
||||
num_kv_heads=num_kv_heads,
|
||||
head_bytes=head_bytes,
|
||||
)
|
||||
|
||||
ll = loc.long()
|
||||
k_ref = torch.zeros_like(k_cache)
|
||||
v_ref = torch.zeros_like(v_cache)
|
||||
ik_ref = torch.zeros_like(idx_k_cache)
|
||||
k_ref[ll], v_ref[ll], ik_ref[ll] = k, v, idx_k
|
||||
|
||||
assert torch.equal(k_cache, k_ref)
|
||||
assert torch.equal(v_cache, v_ref)
|
||||
assert torch.equal(idx_k_cache, ik_ref)
|
||||
if has_v:
|
||||
iv_ref = torch.zeros_like(idx_v_cache)
|
||||
iv_ref[ll] = idx_v
|
||||
assert torch.equal(idx_v_cache, iv_ref)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
@@ -0,0 +1,334 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""Reference tests for MiniMax-M3 fused Q/K Gemma RMSNorm + RoPE."""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
if not is_hip():
|
||||
pytest.skip(
|
||||
"MiniMax-M3 fused Q/K norm + RoPE kernel is ROCm-only.",
|
||||
allow_module_level=True,
|
||||
)
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip("Requires a GPU.", allow_module_level=True)
|
||||
|
||||
from sglang.jit_kernel.minimax_m3.qk_norm_rope import ( # noqa: E402
|
||||
qk_gemma_rmsnorm_rope,
|
||||
sparse_qk_index_gemma_rmsnorm_rope,
|
||||
sparse_qk_index_gemma_rmsnorm_rope_cache,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_amd_ci # noqa: E402
|
||||
|
||||
# ROCm-only fused kernel; runs in the AMD jit-kernel unit suite.
|
||||
register_amd_ci(est_time=30, suite="jit-kernel-unit-test-amd")
|
||||
|
||||
DEVICE = "cuda"
|
||||
EPS = 1e-6
|
||||
|
||||
|
||||
def _gemma_norm_by_head(x: torch.Tensor, weight: torch.Tensor, head_dim: int):
|
||||
orig_shape = x.shape
|
||||
orig_dtype = x.dtype
|
||||
xh = x.view(x.shape[0], -1, head_dim).float()
|
||||
var = xh.pow(2).mean(dim=-1, keepdim=True)
|
||||
out = xh * torch.rsqrt(var + EPS) * (1.0 + weight.float())
|
||||
return out.to(orig_dtype).reshape(orig_shape)
|
||||
|
||||
|
||||
def _apply_rope_ref(
|
||||
x: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
cos_sin_cache: torch.Tensor,
|
||||
head_dim: int,
|
||||
rotary_dim: int,
|
||||
is_neox_style: bool,
|
||||
):
|
||||
orig_shape = x.shape
|
||||
xh = x.view(x.shape[0], -1, head_dim)
|
||||
x_rot = xh[..., :rotary_dim].float()
|
||||
x_pass = xh[..., rotary_dim:]
|
||||
cos_sin = cos_sin_cache.index_select(0, positions)
|
||||
cos, sin = cos_sin.chunk(2, dim=-1)
|
||||
cos = cos[:, None, :].float()
|
||||
sin = sin[:, None, :].float()
|
||||
|
||||
if is_neox_style:
|
||||
x1, x2 = x_rot.chunk(2, dim=-1)
|
||||
y_rot = torch.cat((x1 * cos - x2 * sin, x2 * cos + x1 * sin), dim=-1)
|
||||
else:
|
||||
x1 = x_rot[..., ::2]
|
||||
x2 = x_rot[..., 1::2]
|
||||
y_rot = torch.stack((x1 * cos - x2 * sin, x2 * cos + x1 * sin), dim=-1)
|
||||
y_rot = y_rot.flatten(-2)
|
||||
|
||||
return torch.cat((y_rot.to(x.dtype), x_pass), dim=-1).reshape(orig_shape)
|
||||
|
||||
|
||||
def _reference(
|
||||
q,
|
||||
k,
|
||||
q_weight,
|
||||
k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
is_neox_style,
|
||||
):
|
||||
q_norm = _gemma_norm_by_head(q, q_weight, head_dim)
|
||||
k_norm = _gemma_norm_by_head(k, k_weight, head_dim)
|
||||
q_ref = _apply_rope_ref(
|
||||
q_norm, positions, cos_sin_cache, head_dim, rotary_dim, is_neox_style
|
||||
)
|
||||
k_ref = _apply_rope_ref(
|
||||
k_norm, positions, cos_sin_cache, head_dim, rotary_dim, is_neox_style
|
||||
)
|
||||
return q_ref, k_ref
|
||||
|
||||
|
||||
def _sparse_reference(
|
||||
q,
|
||||
k,
|
||||
idx_q,
|
||||
idx_k,
|
||||
q_weight,
|
||||
k_weight,
|
||||
idx_q_weight,
|
||||
idx_k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
is_neox_style,
|
||||
):
|
||||
q_ref, k_ref = _reference(
|
||||
q,
|
||||
k,
|
||||
q_weight,
|
||||
k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
is_neox_style,
|
||||
)
|
||||
idx_q_norm = _gemma_norm_by_head(idx_q, idx_q_weight, head_dim)
|
||||
idx_k_norm = _gemma_norm_by_head(idx_k, idx_k_weight, head_dim)
|
||||
idx_q_ref = _apply_rope_ref(
|
||||
idx_q_norm, positions, cos_sin_cache, head_dim, rotary_dim, is_neox_style
|
||||
)
|
||||
idx_k_ref = _apply_rope_ref(
|
||||
idx_k_norm, positions, cos_sin_cache, head_dim, rotary_dim, is_neox_style
|
||||
)
|
||||
return q_ref, k_ref, idx_q_ref, idx_k_ref
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@pytest.mark.parametrize("is_neox_style", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens,q_heads,k_heads,head_dim,rotary_dim",
|
||||
[(1, 16, 1, 128, 64), (17, 16, 1, 128, 64), (64, 4, 1, 128, 64)],
|
||||
)
|
||||
@torch.inference_mode()
|
||||
def test_qk_gemma_rmsnorm_rope_matches_reference(
|
||||
dtype, is_neox_style, num_tokens, q_heads, k_heads, head_dim, rotary_dim
|
||||
):
|
||||
torch.manual_seed(0)
|
||||
q_dim = q_heads * head_dim
|
||||
k_dim = k_heads * head_dim
|
||||
padding_dim = 37
|
||||
qkv = torch.randn(
|
||||
num_tokens, q_dim + k_dim + padding_dim, device=DEVICE, dtype=dtype
|
||||
)
|
||||
q, k, _ = qkv.split([q_dim, k_dim, padding_dim], dim=-1)
|
||||
if num_tokens > 1:
|
||||
assert not q.is_contiguous()
|
||||
assert not k.is_contiguous()
|
||||
|
||||
q_weight = torch.randn(head_dim, device=DEVICE, dtype=torch.float32)
|
||||
k_weight = torch.randn(head_dim, device=DEVICE, dtype=torch.float32)
|
||||
positions = torch.randint(0, 512, (num_tokens,), device=DEVICE, dtype=torch.long)
|
||||
cos_sin_cache = torch.randn(512, rotary_dim, device=DEVICE, dtype=dtype)
|
||||
|
||||
got_q, got_k = qk_gemma_rmsnorm_rope(
|
||||
q,
|
||||
k,
|
||||
q_weight,
|
||||
k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
EPS,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
is_neox_style,
|
||||
)
|
||||
ref_q, ref_k = _reference(
|
||||
q,
|
||||
k,
|
||||
q_weight,
|
||||
k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
is_neox_style,
|
||||
)
|
||||
|
||||
torch.testing.assert_close(got_q, ref_q, atol=3e-2, rtol=3e-2)
|
||||
torch.testing.assert_close(got_k, ref_k, atol=3e-2, rtol=3e-2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@pytest.mark.parametrize("is_neox_style", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
"num_tokens,q_heads,k_heads,idx_q_heads,head_dim,rotary_dim",
|
||||
[(1, 16, 1, 16, 128, 64), (19, 16, 1, 16, 128, 64)],
|
||||
)
|
||||
@torch.inference_mode()
|
||||
def test_sparse_qk_index_gemma_rmsnorm_rope_matches_reference(
|
||||
dtype,
|
||||
is_neox_style,
|
||||
num_tokens,
|
||||
q_heads,
|
||||
k_heads,
|
||||
idx_q_heads,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
):
|
||||
torch.manual_seed(1)
|
||||
q = torch.randn(num_tokens, q_heads * head_dim, device=DEVICE, dtype=dtype)
|
||||
k = torch.randn(num_tokens, k_heads * head_dim, device=DEVICE, dtype=dtype)
|
||||
idx_q = torch.randn(num_tokens, idx_q_heads * head_dim, device=DEVICE, dtype=dtype)
|
||||
idx_k = torch.randn(num_tokens, head_dim, device=DEVICE, dtype=dtype)
|
||||
q_weight = torch.randn(head_dim, device=DEVICE, dtype=torch.float32)
|
||||
k_weight = torch.randn(head_dim, device=DEVICE, dtype=torch.float32)
|
||||
idx_q_weight = torch.randn(head_dim, device=DEVICE, dtype=torch.float32)
|
||||
idx_k_weight = torch.randn(head_dim, device=DEVICE, dtype=torch.float32)
|
||||
positions = torch.randint(0, 512, (num_tokens,), device=DEVICE, dtype=torch.long)
|
||||
cos_sin_cache = torch.randn(512, rotary_dim, device=DEVICE, dtype=dtype)
|
||||
|
||||
got = sparse_qk_index_gemma_rmsnorm_rope(
|
||||
q,
|
||||
k,
|
||||
idx_q,
|
||||
idx_k,
|
||||
q_weight,
|
||||
k_weight,
|
||||
idx_q_weight,
|
||||
idx_k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
EPS,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
is_neox_style,
|
||||
)
|
||||
ref = _sparse_reference(
|
||||
q,
|
||||
k,
|
||||
idx_q,
|
||||
idx_k,
|
||||
q_weight,
|
||||
k_weight,
|
||||
idx_q_weight,
|
||||
idx_k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
is_neox_style,
|
||||
)
|
||||
for got_tensor, ref_tensor in zip(got, ref):
|
||||
torch.testing.assert_close(got_tensor, ref_tensor, atol=3e-2, rtol=3e-2)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16, torch.float16])
|
||||
@pytest.mark.parametrize("is_neox_style", [True, False])
|
||||
@torch.inference_mode()
|
||||
def test_sparse_qk_index_gemma_rmsnorm_rope_cache_matches_reference(
|
||||
dtype, is_neox_style
|
||||
):
|
||||
torch.manual_seed(2)
|
||||
num_tokens, q_heads, k_heads, idx_q_heads = 11, 16, 1, 16
|
||||
head_dim, rotary_dim = 128, 64
|
||||
q = torch.randn(num_tokens, q_heads * head_dim, device=DEVICE, dtype=dtype)
|
||||
k = torch.randn(num_tokens, k_heads * head_dim, device=DEVICE, dtype=dtype)
|
||||
v = torch.randn(num_tokens, k_heads * head_dim, device=DEVICE, dtype=dtype)
|
||||
idx_q = torch.randn(num_tokens, idx_q_heads * head_dim, device=DEVICE, dtype=dtype)
|
||||
idx_k = torch.randn(num_tokens, head_dim, device=DEVICE, dtype=dtype)
|
||||
q_weight = torch.randn(head_dim, device=DEVICE, dtype=torch.float32)
|
||||
k_weight = torch.randn(head_dim, device=DEVICE, dtype=torch.float32)
|
||||
idx_q_weight = torch.randn(head_dim, device=DEVICE, dtype=torch.float32)
|
||||
idx_k_weight = torch.randn(head_dim, device=DEVICE, dtype=torch.float32)
|
||||
positions = torch.randint(0, 512, (num_tokens,), device=DEVICE, dtype=torch.long)
|
||||
cos_sin_cache = torch.randn(512, rotary_dim, device=DEVICE, dtype=dtype)
|
||||
out_cache_loc = torch.randperm(64, device=DEVICE, dtype=torch.int64)[:num_tokens]
|
||||
k_cache = torch.empty(64, k_heads, head_dim, device=DEVICE, dtype=dtype)
|
||||
v_cache = torch.empty(64, k_heads, head_dim, device=DEVICE, dtype=dtype)
|
||||
idx_k_cache = torch.empty(64, 1, head_dim, device=DEVICE, dtype=dtype)
|
||||
|
||||
got = sparse_qk_index_gemma_rmsnorm_rope_cache(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
idx_q,
|
||||
idx_k,
|
||||
k_cache,
|
||||
v_cache,
|
||||
idx_k_cache,
|
||||
out_cache_loc,
|
||||
q_weight,
|
||||
k_weight,
|
||||
idx_q_weight,
|
||||
idx_k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
EPS,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
is_neox_style,
|
||||
)
|
||||
ref = _sparse_reference(
|
||||
q,
|
||||
k,
|
||||
idx_q,
|
||||
idx_k,
|
||||
q_weight,
|
||||
k_weight,
|
||||
idx_q_weight,
|
||||
idx_k_weight,
|
||||
positions,
|
||||
cos_sin_cache,
|
||||
head_dim,
|
||||
rotary_dim,
|
||||
is_neox_style,
|
||||
)
|
||||
for got_tensor, ref_tensor in zip(got, ref):
|
||||
torch.testing.assert_close(got_tensor, ref_tensor, atol=3e-2, rtol=3e-2)
|
||||
|
||||
torch.testing.assert_close(
|
||||
k_cache.index_select(0, out_cache_loc),
|
||||
ref[1].view(num_tokens, k_heads, head_dim),
|
||||
atol=3e-2,
|
||||
rtol=3e-2,
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
v_cache.index_select(0, out_cache_loc),
|
||||
v.view(num_tokens, k_heads, head_dim),
|
||||
atol=0,
|
||||
rtol=0,
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
idx_k_cache.index_select(0, out_cache_loc),
|
||||
ref[3].view(num_tokens, 1, head_dim),
|
||||
atol=3e-2,
|
||||
rtol=3e-2,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
Reference in New Issue
Block a user