dsv4.1: standalone kernels and Python wrappers (#39646)

This commit is contained in:
Liangsheng Yin
2026-09-15 22:31:18 -07:00
committed by GitHub
parent afde31a2f5
commit 91f691c490
25 changed files with 2741 additions and 19 deletions
@@ -0,0 +1,276 @@
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/utils.cuh>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
// Tile-scheduler metadata for FlashMLA's split-KV decode.
//
// FlashMLA computes this itself when handed no metadata, in a <<<1, 32>>> kernel
// whose whole partition walk runs on thread 0 and stores each 32-byte entry
// straight to global memory. Inside a cuda graph that sits fully exposed on the
// critical path, and it cannot be hoisted out because the schedule depends on
// the per-request `topk_length` of the step being replayed.
//
// This kernel produces the same output bit for bit; the shared-memory layout
// matches FlashMLA's exactly because the idle-tail fill below reads one entry
// past `first_block_idx_shared`, as FlashMLA does.
namespace sglang {
namespace flashmla {
// sizeof(DecodingSchedMeta)/4: begin/end req, begin/end block, begin split,
// the two per-end split flags, one pad word.
constexpr int kMetaInts = 8;
constexpr int kBlockSize = 256;
struct Params {
int b;
int block_size_n;
int fixed_overhead_num_blocks;
int topk; // -1 for a dense model
int extra_topk; // 0 when there is no extra cache
const int* __restrict__ topk_length;
const int* __restrict__ extra_topk_length;
const int* __restrict__ seqlens_k; // dense only
int* __restrict__ tile_scheduler_metadata;
int* __restrict__ num_splits;
int num_sm_parts;
};
__device__ __forceinline__ int ceil_div_i(int a, int b) {
return (a + b - 1) / b;
}
// ku::ceil: round up to a multiple of b.
__device__ __forceinline__ int ceil_to_i(int a, int b) {
return (a + b - 1) / b * b;
}
__global__ void __launch_bounds__(kBlockSize) flashmla_sched_meta_kernel(__grid_constant__ const Params p) {
extern __shared__ int smem[];
const int b = p.b;
int* num_blocks_shared = smem; // [b]
int* num_splits_shared = smem + b; // [b + 1]
int* seqlens_k_shared = smem + b * 2 + 1; // [b]
int* first_block_idx_shared = smem + b * 3 + 1; // [b]
int* last_block_idx_shared = smem + b * 4 + 1; // [b]
int* out_shared = smem + b * 5 + 1; // [num_sm_parts * kMetaInts]
__shared__ int total_num_blocks_shared;
int partial = 0;
for (int i = threadIdx.x; i < b; i += kBlockSize) {
int cur_s_k;
if (p.topk == -1) {
cur_s_k = __ldg(p.seqlens_k + i);
} else {
cur_s_k = p.topk_length ? __ldg(p.topk_length + i) : p.topk;
if (cur_s_k == 0) cur_s_k = 1; // the main loop must never be empty
if (p.extra_topk) {
cur_s_k = ceil_to_i(cur_s_k, p.block_size_n);
cur_s_k += p.extra_topk_length ? __ldg(p.extra_topk_length + i) : p.extra_topk;
}
}
seqlens_k_shared[i] = cur_s_k;
const int last_token_idx = max(cur_s_k - 1, 0);
const int cur_first_block_idx = 0; // first_token_idx is always 0
const int cur_last_block_idx = last_token_idx / p.block_size_n;
const int num_blocks = cur_last_block_idx - cur_first_block_idx + 1;
partial += num_blocks + p.fixed_overhead_num_blocks;
num_blocks_shared[i] = num_blocks;
first_block_idx_shared[i] = cur_first_block_idx;
last_block_idx_shared[i] = cur_last_block_idx;
}
// Integer sum, so the tree order does not change the result.
for (int offset = 16; offset >= 1; offset /= 2) {
partial += __shfl_xor_sync(uint32_t(-1), partial, offset);
}
__shared__ int warp_sums[kBlockSize / 32];
if ((threadIdx.x & 31) == 0) warp_sums[threadIdx.x >> 5] = partial;
__syncthreads();
if (threadIdx.x == 0) {
int total = 0;
#pragma unroll
for (int w = 0; w < kBlockSize / 32; ++w)
total += warp_sums[w];
total_num_blocks_shared = total;
}
__syncthreads();
const int fixed_overhead_num_blocks = p.fixed_overhead_num_blocks;
__shared__ int first_idle_part_shared;
if (threadIdx.x == 0) {
const int payload = ceil_div_i(total_num_blocks_shared, p.num_sm_parts) + fixed_overhead_num_blocks;
int now_req_idx = 0, now_block = 0, now_n_split_idx = 0, cum_num_splits = 0;
// The request being consumed, and the one before it, held across partitions.
int cur_c = num_blocks_shared[0], cur_lb = last_block_idx_shared[0], cur_sk = seqlens_k_shared[0];
int prev_lb = 0, prev_sk = 0;
num_splits_shared[0] = 0;
int i = 0;
for (; i < p.num_sm_parts; ++i) {
int* meta = out_shared + i * kMetaInts;
const int begin_req_idx = now_req_idx;
// first_block_idx is 0 for every request: the first token index is 0.
const int begin_block_idx = now_block;
const int begin_split_idx = now_n_split_idx;
int is_first_req_splitted = (now_block != 0);
int remain_payload = payload;
while (now_req_idx < b) {
const int now_remain_blocks = cur_c - now_block;
if (remain_payload >= now_remain_blocks + fixed_overhead_num_blocks) {
cum_num_splits += now_n_split_idx + 1;
num_splits_shared[now_req_idx + 1] = cum_num_splits;
remain_payload -= now_remain_blocks + fixed_overhead_num_blocks;
++now_req_idx;
now_block = 0;
now_n_split_idx = 0;
prev_lb = cur_lb;
prev_sk = cur_sk;
if (now_req_idx < b) {
cur_c = num_blocks_shared[now_req_idx];
cur_lb = last_block_idx_shared[now_req_idx];
cur_sk = seqlens_k_shared[now_req_idx];
}
} else {
if (remain_payload - fixed_overhead_num_blocks > 0) {
now_block += remain_payload - fixed_overhead_num_blocks;
++now_n_split_idx;
remain_payload = 0;
}
break;
}
}
const int split_open = now_block > 0;
const int end_req_idx = split_open ? now_req_idx : now_req_idx - 1;
const int end_lb = split_open ? cur_lb : prev_lb;
const int end_sk = split_open ? cur_sk : prev_sk;
const int end_block_idx = split_open ? now_block : (end_sk == 0 ? 0 : end_lb + 1);
int is_last_req_splitted = (end_block_idx != end_lb + 1) && (end_sk != 0);
if (begin_req_idx == end_req_idx) {
is_first_req_splitted = is_last_req_splitted = is_first_req_splitted || is_last_req_splitted;
}
meta[0] = begin_req_idx;
meta[1] = end_req_idx;
meta[2] = begin_block_idx;
meta[3] = end_block_idx;
meta[4] = begin_split_idx;
meta[5] = is_first_req_splitted;
meta[6] = is_last_req_splitted;
meta[7] = 0;
if (now_req_idx == b) {
++i;
break;
}
}
first_idle_part_shared = i;
}
__syncthreads();
// Every partition past the walk describes the same empty range.
{
const int lb_last = last_block_idx_shared[b - 1];
const int sk_last = seqlens_k_shared[b - 1];
const int end_block_idx = (sk_last == 0) ? 0 : lb_last + 1;
// FlashMLA reads first_block_idx_shared[batch_size] for these, one past the
// end of that array, which aliases last_block_idx_shared[0].
const int begin_block_idx = last_block_idx_shared[0];
const int is_last_req_splitted = (end_block_idx != lb_last + 1) && (sk_last != 0);
for (int i = first_idle_part_shared + threadIdx.x; i < p.num_sm_parts; i += kBlockSize) {
int* meta = out_shared + i * kMetaInts;
meta[0] = b;
meta[1] = b - 1;
meta[2] = begin_block_idx;
meta[3] = end_block_idx;
meta[4] = 0;
meta[5] = 0;
meta[6] = is_last_req_splitted;
meta[7] = 0;
}
}
__syncthreads();
const int meta_words = p.num_sm_parts * kMetaInts;
for (int i = threadIdx.x; i < meta_words; i += kBlockSize) {
p.tile_scheduler_metadata[i] = out_shared[i];
}
for (int i = threadIdx.x; i <= b; i += kBlockSize) {
p.num_splits[i] = num_splits_shared[i];
}
}
} // namespace flashmla
void flashmla_sched_meta(
tvm::ffi::TensorView tile_scheduler_metadata,
tvm::ffi::TensorView num_splits,
tvm::ffi::Optional<tvm::ffi::TensorView> topk_length,
tvm::ffi::Optional<tvm::ffi::TensorView> extra_topk_length,
tvm::ffi::Optional<tvm::ffi::TensorView> seqlens_k,
int64_t block_size_n,
int64_t fixed_overhead_num_blocks,
int64_t topk,
int64_t extra_topk) {
using namespace host;
using namespace flashmla;
auto parts = SymbolicSize{"num_sm_parts"};
auto meta_ints = SymbolicSize{"meta_ints"};
auto b_plus_one = SymbolicSize{"batch_size_plus_one"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLGPU>();
TensorMatcher({parts, meta_ints}).with_dtype<int32_t>().with_device(device_).verify(tile_scheduler_metadata);
TensorMatcher({b_plus_one}).with_strides({1}).with_dtype<int32_t>().with_device(device_).verify(num_splits);
const int num_sm_parts = static_cast<int>(parts.unwrap());
const int b = static_cast<int>(b_plus_one.unwrap()) - 1;
RuntimeCheck(
static_cast<int>(meta_ints.unwrap()) == kMetaInts,
"tile_scheduler_metadata must be [num_sm_parts, ",
kMetaInts,
"], got last dim ",
meta_ints.unwrap());
RuntimeCheck(b >= 1, "batch size must be positive, got ", b);
RuntimeCheck(num_sm_parts >= 1, "num_sm_parts must be positive, got ", num_sm_parts);
RuntimeCheck(block_size_n >= 1, "block_size_n must be positive, got ", block_size_n);
auto opt_ptr = [&](const tvm::ffi::Optional<tvm::ffi::TensorView>& t, const char* name) -> const int* {
if (!t.has_value()) return nullptr;
auto n = SymbolicSize{"batch_size"};
auto dev = SymbolicDevice{};
dev.set_options<kDLGPU>();
TensorMatcher({n}).with_strides({1}).with_dtype<int32_t>().with_device(dev).verify(t.value());
RuntimeCheck(static_cast<int>(n.unwrap()) == b, name, " must have ", b, " entries, got ", n.unwrap());
return static_cast<const int*>(t.value().data_ptr());
};
RuntimeCheck(topk != -1 || seqlens_k.has_value(), "a dense schedule (topk == -1) needs seqlens_k");
const Params p{
b,
static_cast<int>(block_size_n),
static_cast<int>(fixed_overhead_num_blocks),
static_cast<int>(topk),
static_cast<int>(extra_topk),
opt_ptr(topk_length, "topk_length"),
opt_ptr(extra_topk_length, "extra_topk_length"),
opt_ptr(seqlens_k, "seqlens_k"),
static_cast<int*>(tile_scheduler_metadata.data_ptr()),
static_cast<int*>(num_splits.data_ptr()),
num_sm_parts,
};
const std::size_t smem = sizeof(int) * static_cast<std::size_t>(b * 5 + 1 + num_sm_parts * kMetaInts);
RuntimeCheck(smem <= 48 * 1024, "schedule does not fit in shared memory: ", smem, " bytes");
LaunchKernel(1, kBlockSize, device_.unwrap(), smem)(flashmla_sched_meta_kernel, p);
}
} // namespace sglang
@@ -0,0 +1,249 @@
/// \file small_gemm_bf16.cuh
/// \brief Small bf16 GEMMs for two fixed shapes, `out[m, n] = sum_k a[m, k] * b[n, k]`
/// with N = 128, K = 512 (one warp per output column) and N = 32, K = 5120 (one CTA
/// per output column).
///
/// The thread-to-K mapping and reduction order are those of tiny_gemm's N-variant,
/// so results are row-invariant across M and bitwise equal to tiny_gemm where both
/// apply; cuBLAS uses a different reduction order.
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/tile.cuh>
#include <sgl_kernel/type.cuh>
#include <sgl_kernel/utils.cuh>
#include <sgl_kernel/vec.cuh>
#include <sgl_kernel/warp.cuh>
#include <sgl_kernel/gemm/dot_product.cuh>
#include <tvm/ffi/container/tensor.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <utility>
namespace sglang {
struct SmallGemmParams {
bf16_t* __restrict__ out;
const bf16_t* __restrict__ a;
const bf16_t* __restrict__ b;
int64_t stride_a; // row stride of `a`, in elements
uint32_t m; // batch size
};
template <uint32_t M_SPLIT_>
struct N128K512Trait {
static constexpr uint32_t N = 128;
static constexpr uint32_t K = 512;
static constexpr uint32_t kMaxMSplit = 8;
static constexpr uint32_t M_SPLIT = M_SPLIT_; // rows of `a` one warp handles
static_assert(M_SPLIT >= 1 && M_SPLIT <= kMaxMSplit, "M_SPLIT must be in [1, 8]");
static constexpr uint32_t kVecSize = device::kMaxVecBytes / sizeof(bf16_t);
static constexpr uint32_t kNumVecs = K / (kVecSize * device::kWarpThreads);
static_assert(K % (kVecSize * device::kWarpThreads) == 0, "K must be a whole number of warp-wide vectors");
using vec_t = device::AlignedVector<bf16x2_t, kVecSize / 2>;
static constexpr uint32_t kBlockSize = 128;
static constexpr uint32_t kNumWarps = kBlockSize / device::kWarpThreads;
static_assert(N % kNumWarps == 0, "every warp of a block owns one output column");
static constexpr uint32_t kGridX = N / kNumWarps;
};
template <typename T, bool kUsePDL>
__global__ __launch_bounds__(T::kBlockSize, 1) void n128k512_kernel(const SmallGemmParams params) {
using namespace device;
constexpr uint32_t kNumVecs = T::kNumVecs;
constexpr uint32_t M_SPLIT = T::M_SPLIT;
constexpr uint32_t K = T::K;
constexpr uint32_t N = T::N;
using vec_t = typename T::vec_t;
const uint32_t M = params.m;
const uint32_t warp_id = threadIdx.x / kWarpThreads;
const uint32_t n = blockIdx.x * T::kNumWarps + warp_id;
const uint32_t m_start = blockIdx.y * M_SPLIT;
const auto gmem = tile::Memory<vec_t>::warp();
// The weight row does not depend on the previous kernel: load it before the PDL wait.
vec_t b[kNumVecs];
#pragma unroll
for (uint32_t j = 0; j < kNumVecs; ++j) {
b[j] = gmem.load(params.b + n * K, j);
}
PDLWaitPrimary<kUsePDL>();
// Clamp padded loads to the last valid row to keep vector loads branch-free;
// only stores are guarded.
vec_t a[M_SPLIT][kNumVecs];
#pragma unroll
for (uint32_t i = 0; i < M_SPLIT; ++i) {
const uint32_t m = min(m_start + i, M - 1);
#pragma unroll
for (uint32_t j = 0; j < kNumVecs; ++j) {
a[i][j] = gmem.load(params.a + m * params.stride_a, j);
}
}
#pragma unroll
for (uint32_t i = 0; i < M_SPLIT; ++i) {
float acc = 0.0f;
#pragma unroll
for (uint32_t j = 0; j < kNumVecs; ++j) {
dot_product_vec(a[i][j], b[j], acc);
}
acc = warp::reduce_sum(acc);
const uint32_t m = m_start + i;
if (m < M) {
params.out[m * N + n] = cast<bf16_t>(acc);
}
}
PDLTriggerSecondary<kUsePDL>();
}
template <uint32_t M_SPLIT_>
struct N32K5120Trait {
static constexpr uint32_t N = 32;
static constexpr uint32_t K = 5120;
static constexpr uint32_t kMaxMSplit = 8;
static constexpr uint32_t M_SPLIT = M_SPLIT_; // rows of `a` one CTA handles
static_assert(M_SPLIT >= 1 && M_SPLIT <= kMaxMSplit, "M_SPLIT must be in [1, 8]");
static constexpr uint32_t kVecSize = device::kMaxVecBytes / sizeof(bf16_t);
// Ten warps preserve tiny_gemm's mapping: thread t owns [16t, 16t + 16).
static constexpr uint32_t kBlockSize = 320;
static constexpr uint32_t kNumWarps = kBlockSize / device::kWarpThreads;
static constexpr uint32_t kNumVecs = K / (kVecSize * kBlockSize); // vectors per thread
static_assert(K % (kVecSize * kBlockSize) == 0, "K must be a whole number of CTA-wide vectors");
static_assert(kBlockSize % device::kWarpThreads == 0, "the reduction needs whole warps");
static_assert(M_SPLIT <= kBlockSize, "one thread finishes one row");
using vec_t = device::AlignedVector<bf16x2_t, kVecSize / 2>;
static constexpr uint32_t kGridX = N; // one CTA per output column
};
template <typename T, bool kUsePDL>
__global__ __launch_bounds__(T::kBlockSize, 1) void n32k5120_kernel(const SmallGemmParams params) {
using namespace device;
constexpr uint32_t kNumVecs = T::kNumVecs;
constexpr uint32_t kNumWarps = T::kNumWarps;
constexpr uint32_t M_SPLIT = T::M_SPLIT;
constexpr uint32_t K = T::K;
constexpr uint32_t N = T::N;
using vec_t = typename T::vec_t;
const uint32_t M = params.m;
const uint32_t tx = threadIdx.x;
const uint32_t warp_id = tx / kWarpThreads;
const uint32_t n = blockIdx.x;
const uint32_t m_start = blockIdx.y * M_SPLIT;
const auto gmem = tile::Memory<vec_t>::cta(T::kBlockSize);
// The weight column does not depend on the previous kernel: load it before the PDL wait.
vec_t b[kNumVecs];
#pragma unroll
for (uint32_t j = 0; j < kNumVecs; ++j) {
b[j] = gmem.load(params.b + n * K, j);
}
PDLWaitPrimary<kUsePDL>();
// Clamp padded loads to the last valid row to keep vector loads branch-free;
// only stores are guarded.
vec_t a[M_SPLIT][kNumVecs];
#pragma unroll
for (uint32_t i = 0; i < M_SPLIT; ++i) {
const uint32_t m = min(m_start + i, M - 1);
#pragma unroll
for (uint32_t j = 0; j < kNumVecs; ++j) {
a[i][j] = gmem.load(params.a + m * params.stride_a, j);
}
}
__shared__ float s_acc[kNumWarps][M_SPLIT];
#pragma unroll
for (uint32_t i = 0; i < M_SPLIT; ++i) {
float acc = 0.0f;
#pragma unroll
for (uint32_t j = 0; j < kNumVecs; ++j) {
dot_product_vec(a[i][j], b[j], acc);
}
acc = warp::reduce_sum(acc);
if (tx % kWarpThreads == 0) s_acc[warp_id][i] = acc;
}
PDLTriggerSecondary<kUsePDL>();
__syncthreads();
// One thread per row sums the warps in a fixed order, so a row's result does
// not depend on which rows share its CTA.
if (tx < M_SPLIT) {
float acc = s_acc[0][tx];
#pragma unroll
for (uint32_t w = 1; w < kNumWarps; ++w) {
acc += s_acc[w][tx];
}
const uint32_t m = m_start + tx;
if (m < M) {
params.out[m * N + n] = cast<bf16_t>(acc);
}
}
}
// Which kernel serves a trait family; the launcher below is shared.
template <typename T, bool kUsePDL>
struct SmallGemmLaunch;
template <uint32_t M_SPLIT, bool kUsePDL>
struct SmallGemmLaunch<N128K512Trait<M_SPLIT>, kUsePDL> {
static constexpr void (*kFn)(SmallGemmParams) = n128k512_kernel<N128K512Trait<M_SPLIT>, kUsePDL>;
};
template <uint32_t M_SPLIT, bool kUsePDL>
struct SmallGemmLaunch<N32K5120Trait<M_SPLIT>, kUsePDL> {
static constexpr void (*kFn)(SmallGemmParams) = n32k5120_kernel<N32K5120Trait<M_SPLIT>, kUsePDL>;
};
template <template <uint32_t> class Trait, bool kUsePDL>
struct SmallGemmKernel {
using Trait1 = Trait<1>;
static constexpr uint32_t kMaxMSplit = Trait1::kMaxMSplit;
using KernelFn = void (*)(SmallGemmParams);
template <std::size_t... I>
static constexpr auto make_table(std::index_sequence<I...>) {
return std::array<KernelFn, kMaxMSplit + 1>{nullptr, SmallGemmLaunch<Trait<I + 1>, kUsePDL>::kFn...};
}
static constexpr auto kTable = make_table(std::make_index_sequence<kMaxMSplit>{});
static void run(const tvm::ffi::TensorView a, const tvm::ffi::TensorView b, const tvm::ffi::TensorView out) {
using namespace host;
auto M = SymbolicSize{"num_tokens"};
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
TensorMatcher({M, Trait1::K}).with_strides({-1, 1}).with_dtype<bf16_t>().with_device(device).verify(a);
TensorMatcher({Trait1::N, Trait1::K}).with_dtype<bf16_t>().with_device(device).verify(b);
TensorMatcher({M, Trait1::N}).with_dtype<bf16_t>().with_device(device).verify(out);
const auto m = static_cast<uint32_t>(M.unwrap());
if (m == 0) return;
// Rows are loaded as whole vectors, so a row-sliced view must keep its row starts vector-aligned.
CHECK_HOST(a.stride(0) % Trait1::kVecSize == 0)
<< "a rows must stay aligned to the vector width, got stride " << a.stride(0);
// Spread the rows evenly over as few y-blocks as kMaxMSplit rows each allow.
const uint32_t grid_y = div_ceil(m, kMaxMSplit);
const uint32_t m_split = div_ceil(m, grid_y);
const auto params = SmallGemmParams{
.out = static_cast<bf16_t*>(out.data_ptr()),
.a = static_cast<const bf16_t*>(a.data_ptr()),
.b = static_cast<const bf16_t*>(b.data_ptr()),
.stride_a = static_cast<int64_t>(a.stride(0)),
.m = m,
};
LaunchKernel(dim3(Trait1::kGridX, grid_y), dim3(Trait1::kBlockSize), device.unwrap())
.enable_pdl(kUsePDL)(kTable[m_split], params);
}
};
} // namespace sglang
@@ -6,6 +6,8 @@
#include <sgl_kernel/vec.cuh>
#include <sgl_kernel/warp.cuh>
#include <sgl_kernel/gemm/dot_product.cuh>
#include <tvm/ffi/container/tensor.h>
#include <array>
@@ -66,23 +68,6 @@ struct TinyGEMMParams {
int64_t stride_x;
};
template <std::size_t N>
SGL_DEVICE void dot_product(device::AlignedVector<bf16x2_t, N> a, device::AlignedVector<bf16x2_t, N> b, float& acc) {
using namespace device;
#pragma unroll
for (uint32_t i = 0; i < N; ++i) {
#if SGL_ARCH_BLACKWELL_OR_GREATER
acc = device::math::fma_f32_bf16(a[i].x, b[i].x, acc);
acc = device::math::fma_f32_bf16(a[i].y, b[i].y, acc);
#else
const auto [a0, a1] = cast<fp32x2_t>(a[i]);
const auto [b0, b1] = cast<fp32x2_t>(b[i]);
acc += a0 * b0;
acc += a1 * b1;
#endif
}
}
template <typename Trait, uint32_t M, typename Out, bool kUsePDL>
TINY_GEMM_KERNEL void tiny_n_gemm_kernel(const TinyGEMMParams params) {
using namespace device;
@@ -130,7 +115,7 @@ TINY_GEMM_KERNEL void tiny_n_gemm_kernel(const TinyGEMMParams params) {
float acc = 0.0f;
#pragma unroll
for (uint32_t u = 0; u < kUnroll; ++u) {
dot_product(xv[m][u], wv[n][u], acc);
dot_product_vec(xv[m][u], wv[n][u], acc);
}
s_acc[warp_id][m * N_SPLIT + n] = warp::reduce_sum(acc);
}
@@ -185,7 +170,7 @@ TINY_GEMM_KERNEL void tiny_k_gemm_kernel(const TinyGEMMParams params) {
#pragma unroll
for (uint32_t m = 0; m < M; ++m) {
float acc = 0.0f;
dot_product(xv[m], wv, acc);
dot_product_vec(xv[m], wv, acc);
// Broadcast store: every lane of the group holds the reduced sum.
const auto sum = warp::reduce_sum<kNumKLanes>(acc);
static_cast<Out*>(params.out)[m * N + n_idx] = cast<Out>(sum);
@@ -0,0 +1,40 @@
/// \file dot_product.cuh
/// \brief bf16 x bf16 -> fp32 dot product of two packed vectors, shared by the
/// small-GEMM kernels so that they accumulate in the same order and agree
/// bitwise where both apply.
#pragma once
#include <sgl_kernel/math.cuh>
#include <sgl_kernel/type.cuh>
#include <sgl_kernel/utils.cuh>
#include <sgl_kernel/vec.cuh>
#include <cstddef>
#include <cstdint>
namespace sglang {
namespace device {
/// Accumulate `sum_i a[i] * b[i]` into `acc`, in element order, one fp32 fma per
/// product. On Blackwell the mixed-precision fma consumes bf16 directly; the
/// fallback converts first, which is exact, so both round once per product.
template <std::size_t N>
SGL_DEVICE void dot_product_vec(AlignedVector<bf16x2_t, N> a, AlignedVector<bf16x2_t, N> b, float& acc) {
#pragma unroll
for (uint32_t i = 0; i < N; ++i) {
#if SGL_ARCH_BLACKWELL_OR_GREATER
acc = math::fma_f32_bf16(a[i].x, b[i].x, acc);
acc = math::fma_f32_bf16(a[i].y, b[i].y, acc);
#else
const auto [a0, a1] = cast<fp32x2_t>(a[i]);
const auto [b0, b1] = cast<fp32x2_t>(b[i]);
acc += a0 * b0;
acc += a1 * b1;
#endif
}
}
} // namespace device
} // namespace sglang
@@ -0,0 +1,207 @@
"""Candidate-block scores and visibility masking for paged indexer logits."""
import torch
import triton
import triton.language as tl
from sglang.kernels.jit.utils import is_arch_support_pdl
@triton.jit
def _maximum_with_nan(a, b):
return tl.maximum(a, b, propagate_nan=tl.PropagateNan.ALL)
@triton.jit
def _candidate_scores_kernel(
X,
LENS,
OUT,
SCORES,
WIDTH: tl.constexpr,
STRIDE: tl.constexpr,
BLOCKS: tl.constexpr,
GROUP: tl.constexpr,
GROUP_PAD: tl.constexpr,
TILE: tl.constexpr,
):
row = tl.program_id(0).to(tl.int64)
blocks = tl.program_id(1) * TILE + tl.arange(0, TILE)
offsets = tl.arange(0, GROUP_PAD)
cols = blocks[:, None] * GROUP + offsets[None, :]
length = tl.load(LENS + row)
in_bounds = (cols < WIDTH) & (offsets[None, :] < GROUP)
values = tl.load(
X + row * STRIDE + cols, in_bounds & (cols < length), other=-float("inf")
).to(tl.float32)
tl.store(OUT + row * WIDTH + cols, values, in_bounds)
scores = tl.reduce(values, axis=1, combine_fn=_maximum_with_nan)
scores = tl.where(
(length > 0) & (blocks == (length - 1) // GROUP), float("inf"), scores
)
tl.store(SCORES + row * BLOCKS + blocks, scores, blocks < BLOCKS)
@triton.jit
def _candidate_mask_kernel(
X,
LENS,
KEEP,
OUT,
WIDTH: tl.constexpr,
STRIDE: tl.constexpr,
KEEP_STRIDE: tl.constexpr,
KEEP_COL_STRIDE: tl.constexpr,
TILE: tl.constexpr,
):
row = tl.program_id(0).to(tl.int64)
cols = tl.program_id(1) * TILE + tl.arange(0, TILE)
visible = (cols < WIDTH) & (cols < tl.load(LENS + row))
keep = tl.load(KEEP + row * KEEP_STRIDE + cols * KEEP_COL_STRIDE, visible, other=0)
values = tl.load(X + row * STRIDE + cols, visible & keep, other=-float("inf")).to(
tl.float32
)
tl.store(OUT + row * WIDTH + cols, values, cols < WIDTH)
@triton.jit
def _publish_candidate_mask_kernel(
INDICES,
VALUES,
KEEP,
WIDTH: tl.constexpr,
GROUP: tl.constexpr,
TOPK: tl.constexpr,
TILE: tl.constexpr,
):
row = tl.program_id(0).to(tl.int64)
i = tl.program_id(1) * TILE + tl.arange(0, TILE)
selected = tl.load(INDICES + row * TOPK + i // GROUP, i < TOPK * GROUP, 0)
score = tl.load(VALUES + row * TOPK + i // GROUP, i < TOPK * GROUP, -float("inf"))
cols = selected * GROUP + i % GROUP
# torch.topk returns unique block indices: each output position has one writer.
tl.store(
KEEP + row * WIDTH + cols,
score > -float("inf"),
(i < TOPK * GROUP) & (cols < WIDTH),
)
def candidate_block_logits(
logits: torch.Tensor,
seq_lens: torch.Tensor,
*,
topk_blocks: int,
block_size: int,
published: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
"""Keep torch.topk's block selection, including its tie behavior.
Without ``published`` (a source) mask the unread tail while scoring blocks;
with it (a consumer) apply visibility and the published mask in one pass.
"""
rows, width = logits.shape
output = torch.empty((rows, width), dtype=torch.float32, device=logits.device)
if published is not None:
_candidate_mask_kernel[(rows, triton.cdiv(width, 4096))](
logits,
seq_lens,
published,
output,
width,
logits.stride(0),
published.stride(0),
published.stride(1),
4096,
)
return output, None
blocks = triton.cdiv(width, block_size)
scores = torch.empty((rows, blocks), dtype=torch.float32, device=logits.device)
group_pad = triton.next_power_of_2(block_size)
tile = max(1, 1024 // group_pad)
_candidate_scores_kernel[(rows, triton.cdiv(blocks, tile))](
logits,
seq_lens,
output,
scores,
width,
logits.stride(0),
blocks,
block_size,
group_pad,
tile,
)
# Publication only needs membership; sorting the selected pairs is unused.
top = scores.topk(min(topk_blocks, blocks), dim=-1, sorted=False)
keep = torch.zeros((rows, width), dtype=torch.bool, device=logits.device)
_publish_candidate_mask_kernel[
(rows, triton.cdiv(top.indices.shape[1] * block_size, 256))
](
top.indices,
top.values,
keep,
width,
block_size,
top.indices.shape[1],
256,
num_warps=4,
)
return output, keep
@triton.jit
def _candidate_row_lens_kernel(
LENS,
NBLOCKS,
VALID,
ROWS,
TOPK: tl.constexpr,
BLOCK: tl.constexpr,
TILE: tl.constexpr,
USE_PDL: tl.constexpr,
):
rows = tl.program_id(0) * TILE + tl.arange(0, TILE)
mask = rows < ROWS
if USE_PDL:
tl.extra.cuda.gdc_wait() # LENS is the previous kernel's output
length = tl.load(LENS + rows, mask, 0).to(tl.int32)
if USE_PDL:
tl.extra.cuda.gdc_launch_dependents()
nblocks = (length + (BLOCK - 1)) // BLOCK
kept = tl.minimum(nblocks, TOPK)
# the kept blocks laid out back to back, the newest one possibly partial
valid = BLOCK * (kept - 1) + (length - 1) % BLOCK + 1
valid = tl.where(length > 0, valid, 0)
tl.store(NBLOCKS + rows, nblocks, mask)
tl.store(VALID + rows, valid, mask)
def candidate_row_lens(
seq_lens: torch.Tensor, topk_blocks: int, block_size: int = 8
) -> tuple[torch.Tensor, torch.Tensor]:
"""Per row: its number of blocks ``ceil(seq_len / block_size)`` and the
length of its sparse logits row once the ``min(topk_blocks, blocks)`` kept
blocks are laid out back to back (the newest block possibly partial):
``block_size * (kept - 1) + (seq_len - 1) % block_size + 1``. Both int32
``[rows]``; a zero-length row gets 0 for both."""
assert seq_lens.dim() == 1 and seq_lens.is_contiguous()
rows = seq_lens.numel()
nblocks = torch.empty(rows, dtype=torch.int32, device=seq_lens.device)
valid = torch.empty_like(nblocks)
tile = 256
use_pdl = is_arch_support_pdl()
pdl_kwargs = {"launch_pdl": True} if use_pdl else {}
_candidate_row_lens_kernel[(triton.cdiv(rows, tile),)](
seq_lens,
nblocks,
valid,
rows,
topk_blocks,
block_size,
tile,
use_pdl,
num_warps=4,
**pdl_kwargs,
)
return nblocks, valid
@@ -0,0 +1,69 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import torch
from sglang.kernels.jit.utils import cache_once, load_jit
from .utils import make_name
if TYPE_CHECKING:
from tvm_ffi.module import Module
# sizeof(DecodingSchedMeta) / 4, fixed by FlashMLA's params.h.
META_INTS = 8
@cache_once
def _jit_flashmla_sched_meta_module() -> Module:
return load_jit(
make_name("flashmla_sched_meta"),
cuda_files=["deepseek_v4/flashmla_sched_meta.cuh"],
cuda_wrappers=[("flashmla_sched_meta", "flashmla_sched_meta")],
)
def flashmla_sched_meta(
tile_scheduler_metadata: torch.Tensor,
num_splits: torch.Tensor,
*,
topk_length: Optional[torch.Tensor] = None,
extra_topk_length: Optional[torch.Tensor] = None,
seqlens_k: Optional[torch.Tensor] = None,
block_size_n: int,
fixed_overhead_num_blocks: int,
topk: int,
extra_topk: int = 0,
) -> None:
"""Fill FlashMLA's split-KV tile-scheduler metadata in place.
Produces the schedule FlashMLA computes for itself when handed no metadata,
so passing the filled tensors as the cached ``tile_scheduler_metadata`` /
``num_splits`` lets it skip its own kernel. The schedule has to fit in 48 KB
of shared memory, ``4 * (5 * batch_size + 1 + 8 * num_sm_parts)`` bytes, so
a few thousand requests at most.
Args:
tile_scheduler_metadata: ``[num_sm_parts, 8]`` int32, written.
num_splits: ``[batch_size + 1]`` int32, written.
topk_length: ``[batch_size]`` int32 per-request candidate count, or None
to use ``topk`` for every request.
extra_topk_length: the same for the extra cache, when ``extra_topk``.
seqlens_k: ``[batch_size]`` int32, required only for a dense schedule.
block_size_n: the kernel's KV block size.
fixed_overhead_num_blocks: the implementation's per-request overhead.
topk: the sparse top-k, or -1 for a dense model.
extra_topk: the extra cache's top-k, 0 when there is none.
"""
_jit_flashmla_sched_meta_module().flashmla_sched_meta(
tile_scheduler_metadata,
num_splits,
topk_length,
extra_topk_length,
seqlens_k,
block_size_n,
fixed_overhead_num_blocks,
topk,
extra_topk,
)
@@ -0,0 +1,74 @@
"""Filter selected indexer scores and map logical positions to KV slots."""
import torch
import triton
import triton.language as tl
@triton.jit
def _filter_topk_pages(
SCORES,
INDICES,
PAGES,
OUT,
RAW,
WIDTH: tl.constexpr,
TOPK: tl.constexpr,
PAGE_SIZE: tl.constexpr,
SS: tl.constexpr,
SI: tl.constexpr,
SP: tl.constexpr,
SO: tl.constexpr,
SR: tl.constexpr,
WRITE_RAW: tl.constexpr,
BLOCK: tl.constexpr,
):
row = tl.program_id(0).to(tl.int64)
col = tl.program_id(1) * BLOCK + tl.arange(0, BLOCK)
index = tl.load(INDICES + row * SI + col, col < TOPK, -1).to(tl.int64)
in_bounds = (index >= 0) & (index < WIDTH) & (col < TOPK)
score = tl.load(SCORES + row * SS + index, in_bounds, -float("inf"))
# This comparison also rejects NaN; +inf remains a valid score.
valid = in_bounds & (score > -float("inf"))
page = tl.load(PAGES + row * SP + index // PAGE_SIZE, valid, 0)
slot = (page * PAGE_SIZE).to(tl.int64) + index % PAGE_SIZE
tl.store(OUT + row * SO + col, tl.where(valid, slot, -1), col < TOPK)
if WRITE_RAW:
tl.store(RAW + row * SR + col, tl.where(valid, index, -1), col < TOPK)
def filter_topk_pages(
scores: torch.Tensor,
indices: torch.Tensor,
page_table: torch.Tensor,
page_indices: torch.Tensor,
page_size: int,
raw_indices: torch.Tensor | None = None,
) -> None:
"""Preserve top-k order, write -1 for invalid scores, and map valid slots."""
rows, topk = indices.shape
assert scores.ndim == page_table.ndim == page_indices.ndim == 2
assert scores.shape[0] == page_table.shape[0] == page_indices.shape[0] == rows
assert page_indices.shape[1] == topk and scores.shape[1] > 0
assert page_table.shape[1] * page_size >= scores.shape[1]
assert all(t.stride(1) == 1 for t in (scores, indices, page_table, page_indices))
if raw_indices is not None:
assert raw_indices.shape == indices.shape and raw_indices.stride(1) == 1
_filter_topk_pages[(rows, triton.cdiv(topk, 256))](
scores,
indices,
page_table,
page_indices,
raw_indices,
scores.shape[1],
topk,
page_size,
scores.stride(0),
indices.stride(0),
page_table.stride(0),
page_indices.stride(0),
raw_indices.stride(0) if raw_indices is not None else 0,
raw_indices is not None,
256,
num_warps=4,
)
@@ -0,0 +1,107 @@
"""Rotate Q while writing the padded buffer consumed by sparse attention."""
import torch
import triton
import triton.language as tl
from sglang.kernels.jit.utils import is_arch_support_pdl
@triton.jit
def _q_rope_store(
X, Y, F, POS, SX: tl.constexpr, SY: tl.constexpr, USE_GDC: tl.constexpr = False
):
row, head = tl.program_id(0), tl.program_id(1)
r = tl.arange(0, 512)
if USE_GDC:
tl.extra.cuda.gdc_wait() # X is the wq_b GEMM output; POS may be in-graph
value = tl.load(X + row * SX + head * 512 + r).to(tl.float32)
partner = tl.gather(value, r ^ 1, 0)
position = tl.load(POS + row)
cos = tl.load(F + position * 64 + (r - 448) // 2 * 2, r >= 448, 0)
sin = tl.load(F + position * 64 + (r - 448) // 2 * 2 + 1, r >= 448, 0)
# Match the operation order of deepseek_rope_kernel before BF16 rounding.
even = tl.fma(value, cos, -partner * sin)
odd = tl.fma(partner, sin, value * cos)
rotated = tl.where((r & 1) == 0, even, odd)
tl.store(Y + row * SY + head * 512 + r, tl.where(r >= 448, rotated, value))
if USE_GDC:
tl.extra.cuda.gdc_launch_dependents()
@triton.jit
def _q_rope_store_prefill(
X,
Y,
F,
POS,
M,
H: tl.constexpr,
SX: tl.constexpr,
SY: tl.constexpr,
BLOCK_HEADS: tl.constexpr,
):
# Keep token count dynamic to avoid compiling every prefill batch length.
heads = tl.program_id(0) * BLOCK_HEADS + tl.arange(0, BLOCK_HEADS)
row, head = heads // H, heads % H
r = tl.arange(0, 512)
# The padded output can exceed 2 GiB at the 64K prefill ceiling.
x_offset = row[:, None].to(tl.int64) * SX + head[:, None] * 512 + r[None, :]
value = tl.load(X + x_offset, row[:, None] < M, 0).to(tl.float32)
partner = tl.gather(value, tl.broadcast_to((r ^ 1)[None, :], (BLOCK_HEADS, 512)), 1)
position = tl.load(POS + row, row < M, 0)
freq_offset = position[:, None].to(tl.int64) * 64 + (r[None, :] - 448) // 2 * 2
rope_mask = (row[:, None] < M) & (r[None, :] >= 448)
cos = tl.load(F + freq_offset, rope_mask, 0)
sin = tl.load(F + freq_offset + 1, rope_mask, 0)
# Keep the same arithmetic and BF16 rounding as the decode kernel.
even = tl.fma(value, cos, -partner * sin)
odd = tl.fma(partner, sin, value * cos)
rotated = tl.where((r[None, :] & 1) == 0, even, odd)
y_offset = row[:, None].to(tl.int64) * SY + head[:, None] * 512 + r[None, :]
tl.store(
Y + y_offset,
tl.where(r[None, :] >= 448, rotated, value),
row[:, None] < M,
)
def q_rope_store(
q: torch.Tensor,
output: torch.Tensor,
freqs_cis: torch.Tensor,
positions: torch.Tensor,
) -> None:
"""Apply 64-wide forward RoPE to 512-wide heads without changing Q padding."""
assert q.shape == output.shape and q.ndim == 3 and q.shape[2] == 512
assert q.dtype == output.dtype == torch.bfloat16
assert q.stride(2) == output.stride(2) == 1
assert q.stride(1) == output.stride(1) == 512
assert freqs_cis.dtype == torch.complex64 and freqs_cis.is_contiguous()
assert freqs_cis.shape[1] == 32 and positions.shape == (q.shape[0],)
assert positions.dtype in (torch.int32, torch.int64) and positions.is_contiguous()
if q.shape[0] >= 4096 and q.shape[1] == 16:
_q_rope_store_prefill[(triton.cdiv(q.shape[0] * q.shape[1], 4),)](
q,
output,
torch.view_as_real(freqs_cis),
positions,
q.shape[0],
q.shape[1],
q.stride(0),
output.stride(0),
BLOCK_HEADS=4,
num_warps=4,
)
return
pdl_kwargs = {"USE_GDC": True, "launch_pdl": True} if is_arch_support_pdl() else {}
_q_rope_store[(q.shape[0], q.shape[1])](
q,
output,
torch.view_as_real(freqs_cis),
positions,
q.stride(0),
output.stride(0),
num_warps=4,
**pdl_kwargs,
)
@@ -0,0 +1,138 @@
"""Grouped BF16 WO-A projections for decode: a single-token GEMV, a split-K
small-batch GEMM, and the small-batch variant with a fused MXFP8 epilogue."""
import torch
import triton
import triton.language as tl
from sglang.kernels.ops.layernorm.mxfp8_epilogue import ue8m0_scale
@triton.jit
def _wo_a_bf16_gemv_kernel(X, W, Y, R: tl.constexpr, D: tl.constexpr, BN: tl.constexpr):
group = tl.program_id(1)
rows = tl.program_id(0) * BN + tl.arange(0, BN)
columns = tl.arange(0, D)
x = tl.load(X + group * D + columns).to(tl.float32)
w = tl.load(
W + (group * R + rows[:, None]) * D + columns[None, :],
rows[:, None] < R,
0,
).to(tl.float32)
result = tl.sum(w * x[None, :], axis=1)
tl.store(Y + group * R + rows, result, rows < R)
def wo_a_bf16_gemv(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
"""Compute ``einsum('tgd,grd->tgr', x, weight)`` for one token."""
assert x.shape[0] == 1 and x.ndim == weight.ndim == 3
assert x.dtype == weight.dtype == torch.bfloat16
assert x.is_cuda and x.device == weight.device
assert x.is_contiguous() and weight.is_contiguous()
groups, rows, dim = weight.shape
assert x.shape[1:] == (groups, dim) and dim == triton.next_power_of_2(dim)
result = torch.empty((1, groups, rows), dtype=x.dtype, device=x.device)
# One output row per CTA keeps register use low and exposes enough
# independent weight loads for single-token decode.
_wo_a_bf16_gemv_kernel[(rows, groups)](
x,
weight,
result,
rows,
dim,
1,
num_warps=4,
enable_fp_fusion=False,
)
return result
@triton.jit
def _wo_a_partial(X, W, P, M: tl.constexpr, SX: tl.constexpr):
tile, group, split = tl.program_id(0), tl.program_id(1), tl.program_id(2)
m = tl.arange(0, 16)
n = tile * 64 + tl.arange(0, 64)
k = split * 512 + tl.arange(0, 128)
acc = tl.zeros((16, 64), tl.float32)
for i in range(4):
offsets = k + i * 128
x = tl.load(
X + m[:, None] * SX + group * 4096 + offsets[None, :], m[:, None] < M, 0
)
w = tl.load(W + (group * 1024 + n[None, :]) * 4096 + offsets[:, None])
acc += tl.dot(x, w)
tl.store(
P + ((split * M + m[:, None]) * 2 + group) * 1024 + n[None, :],
acc,
m[:, None] < M,
)
@triton.jit
def _wo_a_reduce(P, Y, E: tl.constexpr):
i = tl.program_id(0) * 256 + tl.arange(0, 256)
split = tl.arange(0, 8)
values = tl.load(P + split[:, None] * E + i[None, :], i[None, :] < E, 0)
tl.store(Y + i, tl.sum(values, 0), i < E)
def wo_a_bf16_small_batch(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
"""Compute ``einsum('tgd,grd->tgr', x, weight)`` for the TP4 WO-A shape.
Partial sums stay in FP32 until the final BF16, token-major store."""
m = x.shape[0]
assert 2 <= m <= 8 and x.shape[1:] == (2, 4096)
assert weight.shape == (2, 1024, 4096) and weight.is_contiguous()
assert x.dtype == weight.dtype == torch.bfloat16
assert x.is_cuda and x.device == weight.device
assert x.stride(2) == 1 and x.stride(1) == 4096 and x.stride(0) >= 8192
result = torch.empty((m, 2, 1024), dtype=x.dtype, device=x.device)
partial = torch.empty((8, m, 2, 1024), dtype=torch.float32, device=x.device)
_wo_a_partial[(16, 2, 8)](
x, weight, partial, m, x.stride(0), num_warps=4, num_stages=3
)
_wo_a_reduce[(triton.cdiv(m * 2048, 256),)](partial, result, m * 2048, num_warps=4)
return result
@triton.jit
def _wo_a_reduce_quant(P, Q, S, M: tl.constexpr):
row, tile = tl.program_id(0), tl.program_id(1)
i = tile * 256 + tl.arange(0, 256)
split = tl.arange(0, 8)
v = tl.load(P + split[:, None] * (M * 2048) + row * 2048 + i[None, :])
y = tl.sum(v, 0).to(tl.bfloat16).to(tl.float32).reshape((8, 32))
amax = tl.max(tl.abs(y), 1)
sf, inv = ue8m0_scale(amax)
quant = tl.minimum(tl.maximum(y * inv[:, None], -448.0), 448.0).to(tl.float8e4nv)
tl.store(Q + row * 2048 + i, quant.reshape((256,)))
col = tile * 8 + tl.arange(0, 8)
off = (col // 4) * 512 + row * 16 + col % 4
tl.store(S + off, sf.to(tl.uint8))
# Zero only padding rows; valid scale bytes have disjoint writers above.
for z in tl.static_range(triton.cdiv(8192, M * 8 * 256)):
s = (row * 8 + tile) * 256 + tl.arange(0, 256) + z * (M * 8 * 256)
sr = (s % 512) // 16 + ((s % 16) // 4) * 32
tl.store(S + s, 0, (s < 8192) & (sr >= M))
def _quantize_partial(p):
m = p.shape[1]
q = torch.empty((m, 2048), device=p.device, dtype=torch.float8_e4m3fn)
s = torch.empty(8192, device=p.device, dtype=torch.uint8)
_wo_a_reduce_quant[(m, 8)](p, q, s, m, num_warps=4)
return q, s
def wo_a_bf16_small_batch_mxfp8(x: torch.Tensor, weight: torch.Tensor):
"""WO-A with BF16 rounding followed by FlashInfer-compatible MXFP8 quantization."""
m = x.shape[0]
assert 2 <= m <= 8 and x.shape[1:] == (2, 4096)
assert x.dtype == weight.dtype == torch.bfloat16
assert weight.shape == (2, 1024, 4096) and weight.is_contiguous()
assert x.is_cuda and x.device == weight.device
assert x.stride(2) == 1 and x.stride(1) == 4096 and x.stride(0) >= 8192
partial = torch.empty((8, m, 2, 1024), dtype=torch.float32, device=x.device)
_wo_a_partial[(16, 2, 8)](
x, weight, partial, m, x.stride(0), num_warps=4, num_stages=3
)
return _quantize_partial(partial)
@@ -14,4 +14,44 @@ register_kernel(
)
)
register_kernel(
KernelSpec(
op="embeddings.engram_gather",
backend=KernelBackend.TRITON,
target="sglang.kernels.ops.embeddings.engram_gather:engram_gather",
)
)
register_kernel(
KernelSpec(
op="embeddings.engram_hash_ids",
backend=KernelBackend.TRITON,
target="sglang.kernels.ops.embeddings.engram_hash:engram_hash_ids",
)
)
register_kernel(
KernelSpec(
op="embeddings.engram_hash_ids_and_commit",
backend=KernelBackend.TRITON,
target="sglang.kernels.ops.embeddings.engram_hash:engram_hash_ids_and_commit",
)
)
register_kernel(
KernelSpec(
op="embeddings.engram_commit_history",
backend=KernelBackend.TRITON,
target="sglang.kernels.ops.embeddings.engram_hash:engram_commit_history",
)
)
register_kernel(
KernelSpec(
op="embeddings.fused_engram_gate",
backend=KernelBackend.TRITON,
target="sglang.kernels.ops.embeddings.engram_gate:fused_engram_gate",
)
)
__all__ = []
@@ -0,0 +1,76 @@
"""Fused FP32 Engram gate with a single final cast to the activation dtype."""
import torch
import triton
import triton.language as tl
from triton.language.extra import libdevice
@triton.jit
def _engram_gate_kernel(
X,
KV,
QW,
KW,
O,
D: tl.constexpr,
HC: tl.constexpr,
EPS: tl.constexpr,
CLAMP: tl.constexpr,
B: tl.constexpr,
):
row = tl.program_id(0)
token = row // HC
hc = row % HC
col = tl.arange(0, B)
mask = col < D
x = tl.load(X + row * D + col, mask, 0).to(tl.float32)
key = tl.load(KV + token * (HC + 1) * D + hc * D + col, mask, 0).to(tl.float32)
q_weight = tl.load(QW + hc * D + col, mask, 0).to(tl.float32)
k_weight = tl.load(KW + hc * D + col, mask, 0).to(tl.float32)
weight = q_weight * k_weight
rstd = tl.rsqrt(tl.sum(x * x, 0) / D + EPS) * tl.rsqrt(
tl.sum(key * key, 0) / D + EPS
)
dot = tl.sum((x * weight) * key, 0) * rstd * (D**-0.5)
gate = tl.sigmoid(libdevice.copysign(tl.sqrt(tl.maximum(tl.abs(dot), CLAMP)), dot))
value = tl.load(KV + token * (HC + 1) * D + HC * D + col, mask, 0).to(tl.float32)
tl.store(O + row * D + col, x + gate * value, mask)
def fused_engram_gate(
x: torch.Tensor,
kv: torch.Tensor,
q_weight: torch.Tensor,
k_weight: torch.Tensor,
eps: float,
clamp_value: float,
) -> torch.Tensor:
assert x.ndim == 3 and kv.ndim == 2
t, hc, d = x.shape
assert kv.shape == (t, (hc + 1) * d)
assert q_weight.shape == k_weight.shape == (hc, d)
assert all(
a.is_cuda and a.is_contiguous() and a.device == x.device
for a in (x, kv, q_weight, k_weight)
)
assert all(
a.dtype in (torch.bfloat16, torch.float32) for a in (x, kv, q_weight, k_weight)
)
out = torch.empty_like(x)
if t:
_engram_gate_kernel[(t * hc,)](
x,
kv,
q_weight,
k_weight,
out,
d,
hc,
eps,
clamp_value,
triton.next_power_of_2(d),
num_warps=4,
enable_fp_fusion=False,
)
return out
@@ -0,0 +1,85 @@
"""Triton gather of DeepSeek-V4.1 engram rows: fp8 e4m3 payload, e8m0 block scales.
The table pointers arrive as raw addresses so one kernel serves a device table, a
pinned host table, or (on Grace-Blackwell, through ATS) a plain host mapping. The
output is bf16 computed as fp32(row) * 2**(exp - 127) then rounded once, which is
the arithmetic of the torch lookup it replaces.
"""
import torch
import triton
import triton.language as tl
# e8m0 has no zero: the exponent byte 0 encodes 2**-127.
_E8M0_ZERO = 2.0**-127
@triton.jit
def _engram_gather_kernel(
w_ptr,
s_ptr,
ids_ptr,
out_ptr,
row_lo,
row_hi,
DIM: tl.constexpr,
BLK: tl.constexpr,
E8M0_ZERO: tl.constexpr,
):
row = tl.program_id(0).to(tl.int64)
idx = tl.load(ids_ptr + row).to(tl.int64)
# The table holds rows [row_lo, row_hi); an id outside it is not read and
# comes out as zeros, which is what the sharded all-reduce sums.
owned = (idx >= row_lo) & (idx < row_hi)
local = tl.where(owned, idx - row_lo, 0)
w = w_ptr.to(tl.int64).to(tl.pointer_type(tl.float8e4nv))
s = s_ptr.to(tl.int64).to(tl.pointer_type(tl.uint8))
offs = tl.arange(0, DIM)
vals = tl.load(w + local * DIM + offs, mask=owned, other=0.0).to(tl.float32)
exps = tl.load(s + local * (DIM // BLK) + offs // BLK, mask=owned, other=0).to(
tl.int32
)
# 2**(e - 127) from the exponent bits: exact, no exp2 rounding or denormal flush.
scale = (exps << 23).to(tl.float32, bitcast=True)
scale = tl.where(exps == 0, E8M0_ZERO, scale)
out = tl.where(owned, vals * scale, 0.0)
tl.store(out_ptr + row * DIM + offs, out.to(tl.bfloat16))
def engram_gather(
weight_ptr: int,
scale_ptr: int,
ids: torch.Tensor,
out: torch.Tensor,
dim: int,
block_size: int,
row_lo: int = 0,
row_hi: int = 2**62,
) -> torch.Tensor:
"""Gather rows ``ids`` ([N] int) into ``out`` ([N, dim] bf16, contiguous).
``weight_ptr`` addresses [rows, dim] fp8 e4m3 bytes and ``scale_ptr``
[rows, dim // block_size] e8m0 bytes for global rows [row_lo, row_hi); both
may live in device or host memory. Ids outside the range produce zero rows.
"""
assert dim > 0 and dim & (dim - 1) == 0 and block_size > 0, (dim, block_size)
assert dim % block_size == 0, (dim, block_size)
assert (
ids.is_cuda and ids.is_contiguous() and ids.dtype in (torch.int32, torch.int64)
)
assert out.is_contiguous() and out.dtype == torch.bfloat16
assert out.device == ids.device and out.shape == (ids.numel(), dim), out.shape
n = ids.numel()
if n:
_engram_gather_kernel[(n,)](
weight_ptr,
scale_ptr,
ids,
out,
row_lo,
row_hi,
DIM=dim,
BLK=block_size,
E8M0_ZERO=_E8M0_ZERO,
)
return out
@@ -0,0 +1,363 @@
"""Triton n-gram hash ids for the DeepSeek-V4.1 engram, one launch per forward.
Each token t needs its n predecessors: shift 0 is the token itself, shift s an
earlier token of the same request. Shifts that reach past the request's first token
of this forward come from a per-request history row (oldest first). A shift that runs
off the sequence start, or (with vision) reaches an image token, is PAD, and so is
every older shift. The compressed ids are multiplied per (layer, shift), XOR-folded
one shift at a time and bucketed by that n-gram size's per-head primes.
The arithmetic must remain bit-identical to EngramHasher's torch path
in sglang.srt.layers.engram.
"""
from typing import Optional
import torch
import triton
import triton.language as tl
MODE_DECODE = 0
MODE_VERIFY = 1
MODE_EXTEND = 2
@triton.jit
def _engram_commit_history_kernel(
history_ptr,
tokens_ptr,
slots_ptr,
commit_ptr,
HISTORY_STRIDE: tl.constexpr,
HISTORY_WIDTH: tl.constexpr,
TOKEN_STRIDE: tl.constexpr,
BLOCK: tl.constexpr,
):
row = tl.program_id(0)
slot = tl.load(slots_ptr + row).to(tl.int64)
commit = tl.load(commit_ptr + row)
col = tl.arange(0, BLOCK)
source_col = commit + col
valid = col < HISTORY_WIDTH
old = tl.load(
history_ptr + slot * HISTORY_STRIDE + source_col,
mask=valid & (source_col < HISTORY_WIDTH),
other=0,
)
new = tl.load(
tokens_ptr + row * TOKEN_STRIDE + source_col - HISTORY_WIDTH,
mask=valid & (source_col >= HISTORY_WIDTH),
other=0,
)
values = tl.where(source_col < HISTORY_WIDTH, old, new)
# Every lane must finish reading the old row before any lane overwrites it.
tl.debug_barrier()
tl.store(history_ptr + slot * HISTORY_STRIDE + col, values, mask=valid)
def engram_commit_history(
history: torch.Tensor,
verify_ids: torch.Tensor,
req_slots: torch.Tensor,
commit_lens: torch.Tensor,
) -> None:
"""Update distinct live request slots with anchor + correct drafts, not bonus."""
bs = req_slots.numel()
width = history.shape[1]
if bs == 0 or width == 0:
return
assert history.stride(1) == 1 and verify_ids.stride(1) == 1
assert req_slots.stride(0) == 1 and commit_lens.stride(0) == 1
_engram_commit_history_kernel[(bs,)](
history,
verify_ids,
req_slots,
commit_lens,
HISTORY_STRIDE=history.stride(0),
HISTORY_WIDTH=width,
TOKEN_STRIDE=verify_ids.stride(0),
BLOCK=triton.next_power_of_2(width),
num_warps=4,
)
@triton.jit
def _engram_hash_kernel(
ids_ptr,
pos_ptr,
row_ptr,
starts_ptr,
slots_ptr,
hist_ptr,
token_map_ptr,
mult_ptr,
primes_ptr,
offsets_ptr,
out_loc_ptr,
tokens_out_ptr,
out_ptr,
num_tokens,
num_real,
pad_id,
image_token_id,
mm_pad_shift,
MODE: tl.constexpr,
BLOCK: tl.constexpr,
HIST_VIA_SLOTS: tl.constexpr,
HAS_IMAGE: tl.constexpr,
COMMIT: tl.constexpr,
WRITE_TOKENS: tl.constexpr,
N: tl.constexpr,
L: tl.constexpr,
H: tl.constexpr,
BLOCK_T: tl.constexpr,
):
COLS: tl.constexpr = (N - 1) * H
t = tl.program_id(0) * BLOCK_T + tl.arange(0, BLOCK_T)
tmask = t < num_tokens
real = t < num_real
# Request row r and the token's offset inside its run for this forward.
if MODE == 0:
r = t.to(tl.int64)
off = t * 0
elif MODE == 1:
r = (t // BLOCK).to(tl.int64)
off = t - (t // BLOCK) * BLOCK
else:
r = tl.load(row_ptr + t, mask=real, other=0).to(tl.int64)
off = t - tl.load(starts_ptr + r, mask=real, other=0).to(tl.int32)
if HIST_VIA_SLOTS:
hrow = tl.load(slots_ptr + r, mask=real, other=0).to(tl.int64)
else:
hrow = r
pos = tl.load(pos_ptr + t, mask=tmask, other=0).to(tl.int32)
t2 = t[:, None]
s = tl.arange(0, N)[None, :]
tmask2 = tmask[:, None] & (s >= 0)
real2 = real[:, None] & (s >= 0)
off2 = off[:, None]
# Predecessor at shift s: an earlier token of the same run when s <= off, else
# history[row, n - 2 - (s - off - 1)]; the history is oldest first.
from_batch = tl.load(ids_ptr + tl.maximum(t2 - s, 0), mask=tmask2, other=0).to(
tl.int64
)
hcol = tl.minimum(tl.maximum(N - 2 - (s - off2 - 1), 0), N - 2)
from_hist = tl.load(
hist_ptr + hrow[:, None] * (N - 1) + hcol, mask=tmask2, other=0
).to(tl.int64)
tok = tl.where(s <= off2, from_batch, from_hist)
tok = tl.where(real2, tok, 0)
blk = (pos[:, None] < s) | (real2 == 0)
if HAS_IMAGE:
# Scheduler-provided history still carries the multimodal pad ids.
tok = tl.where(tok >= mm_pad_shift, image_token_id, tok)
blk = blk | (tok == image_token_id)
# Once a shift is blocked every older shift is too (cummax along s).
blocked = tl.cumsum(blk.to(tl.int32), axis=1) > 0
if WRITE_TOKENS:
tl.store(tokens_out_ptr + t2 * N + s, tok.to(tl.int32), mask=tmask2)
if COMMIT:
# Decode: the token and its n - 2 newest predecessors become the request's
# history, oldest first. Graph-padded rows (out_cache_loc 0) write nothing.
live = tl.load(out_loc_ptr + t, mask=real, other=0) != 0
cmask = real2 & live[:, None] & (s <= N - 2)
tl.store(
hist_ptr + hrow[:, None] * (N - 1) + (N - 2 - s),
tok.to(tl.int32),
mask=cmask,
)
mapped = tl.load(token_map_ptr + tok, mask=tmask2, other=0).to(tl.int64)
comp = tl.where(blocked, pad_id, mapped)
h = tl.arange(0, H)[None, :]
omask = tmask[:, None] & (h >= 0)
for l in tl.static_range(L):
mult = tl.load(mult_ptr + l * N + s)
prod = comp * mult
for i in tl.static_range(1, N):
# (i + 1)-gram hash: XOR of the first i + 1 shifts' products.
rolling = tl.xor_sum(tl.where(s <= i, prod, 0), axis=1)
primes = tl.load(primes_ptr + l * COLS + (i - 1) * H + h)
offsets = tl.load(offsets_ptr + l * COLS + (i - 1) * H + h)
val = rolling[:, None] % primes + offsets
tl.store(
out_ptr + t2 * (L * COLS) + l * COLS + (i - 1) * H + h, val, mask=omask
)
def _launch_hash_kernel(
input_ids: torch.Tensor,
positions: torch.Tensor,
*,
mode: int,
history: torch.Tensor,
token_map: torch.Tensor,
multipliers: torch.Tensor,
primes: torch.Tensor,
offsets: torch.Tensor,
pad_id: int,
num_real: Optional[int],
req_slots: Optional[torch.Tensor],
block: int,
row: Optional[torch.Tensor],
starts: Optional[torch.Tensor],
image_token_id: Optional[int],
mm_pad_shift: int,
out_cache_loc: Optional[torch.Tensor],
write_tokens: bool,
block_t: int,
) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
num_tokens = input_ids.shape[0]
L, N = multipliers.shape
H = primes.shape[-1]
assert N & (N - 1) == 0 and H & (H - 1) == 0, (N, H)
assert primes.shape == (L, N - 1, H) and offsets.shape == (L, (N - 1) * H)
assert history.dim() == 2 and history.shape[1] == N - 1, history.shape
if mode == MODE_EXTEND:
assert row is not None and starts is not None
if out_cache_loc is not None:
assert mode == MODE_DECODE and req_slots is not None, "commit is decode-only"
assert out_cache_loc.shape[0] == num_tokens, out_cache_loc.shape
if num_real is None:
num_real = num_tokens
device = input_ids.device
out = torch.empty(num_tokens, L, (N - 1) * H, dtype=torch.int64, device=device)
tokens = (
torch.empty(num_tokens, N, dtype=torch.int32, device=device)
if write_tokens
else None
)
if num_tokens == 0:
return out, tokens
dummy = out # unused pointer slots; never dereferenced under their constexprs
_engram_hash_kernel[(triton.cdiv(num_tokens, block_t),)](
input_ids,
positions,
row if row is not None else dummy,
starts if starts is not None else dummy,
req_slots if req_slots is not None else dummy,
history,
token_map,
multipliers,
primes,
offsets,
out_cache_loc if out_cache_loc is not None else dummy,
tokens if tokens is not None else dummy,
out,
num_tokens,
num_real,
pad_id,
image_token_id if image_token_id is not None else -1,
mm_pad_shift,
MODE=mode,
BLOCK=block,
HIST_VIA_SLOTS=req_slots is not None,
HAS_IMAGE=image_token_id is not None,
COMMIT=out_cache_loc is not None,
WRITE_TOKENS=write_tokens,
N=N,
L=L,
H=H,
BLOCK_T=block_t,
num_warps=4,
)
return out, tokens
def engram_hash_ids(
input_ids: torch.Tensor,
positions: torch.Tensor,
*,
mode: int,
history: torch.Tensor,
token_map: torch.Tensor,
multipliers: torch.Tensor,
primes: torch.Tensor,
offsets: torch.Tensor,
pad_id: int,
num_real: Optional[int] = None,
req_slots: Optional[torch.Tensor] = None,
block: int = 1,
row: Optional[torch.Tensor] = None,
starts: Optional[torch.Tensor] = None,
image_token_id: Optional[int] = None,
mm_pad_shift: int = 0,
block_t: int = 32,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Hash ids [T, L, (n - 1) * heads] int64 and the predecessor table [T, n] int32.
Reads ``history`` and never writes it.
``mode``: MODE_DECODE (row = t, offset 0), MODE_VERIFY (row = t // block),
MODE_EXTEND (``row`` [num_real] and ``starts`` [bs] give each token's request and
the run's first token). ``history`` is [rows, n - 1] oldest first; with
``req_slots`` given it is indexed by ``req_slots[row]``, else by ``row``.
Tokens at or past ``num_real`` are padding: PAD ids, zero predecessors.
"""
out, tokens = _launch_hash_kernel(
input_ids,
positions,
mode=mode,
history=history,
token_map=token_map,
multipliers=multipliers,
primes=primes,
offsets=offsets,
pad_id=pad_id,
num_real=num_real,
req_slots=req_slots,
block=block,
row=row,
starts=starts,
image_token_id=image_token_id,
mm_pad_shift=mm_pad_shift,
out_cache_loc=None,
write_tokens=True,
block_t=block_t,
)
return out, tokens
def engram_hash_ids_and_commit(
input_ids: torch.Tensor,
positions: torch.Tensor,
*,
history: torch.Tensor,
req_slots: torch.Tensor,
out_cache_loc: torch.Tensor,
token_map: torch.Tensor,
multipliers: torch.Tensor,
primes: torch.Tensor,
offsets: torch.Tensor,
pad_id: int,
image_token_id: Optional[int] = None,
mm_pad_shift: int = 0,
block_t: int = 32,
) -> torch.Tensor:
"""One decode step: hash ids [T, L, (n - 1) * heads] for the T = bs tokens, and
``history[req_slots[t]]`` advanced in place to the token and its n - 2 newest
predecessors (oldest first). Rows whose ``out_cache_loc`` is 0 are CUDA-graph
padding and leave the table untouched.
"""
out, _ = _launch_hash_kernel(
input_ids,
positions,
mode=MODE_DECODE,
history=history,
token_map=token_map,
multipliers=multipliers,
primes=primes,
offsets=offsets,
pad_id=pad_id,
num_real=None,
req_slots=req_slots,
block=1,
row=None,
starts=None,
image_token_id=image_token_id,
mm_pad_shift=mm_pad_shift,
out_cache_loc=out_cache_loc,
write_tokens=False,
block_t=block_t,
)
return out
@@ -215,6 +215,32 @@ register_kernel(
description="Tiny bf16 GEMM (sglang.kernels.jit, JIT-only).",
)
)
register_kernel(
KernelSpec(
op="gemm.n128k512",
backend=KernelBackend.JIT,
target="sglang.kernels.ops.gemm.small_gemm_bf16:n128k512_gemm_bf16",
capabilities=_CUDA,
format_signature=FormatSignature(
supported_dtypes=("bfloat16",),
description="[m, 512] @ [128, 512].T for decode batches m <= 32",
),
description="bf16 GEMM specialised for N = 128, K = 512 (sglang.kernels.jit, JIT-only).",
)
)
register_kernel(
KernelSpec(
op="gemm.n32k5120",
backend=KernelBackend.JIT,
target="sglang.kernels.ops.gemm.small_gemm_bf16:n32k5120_gemm_bf16",
capabilities=_CUDA,
format_signature=FormatSignature(
supported_dtypes=("bfloat16",),
description="[m, 5120] @ [32, 5120].T for decode batches m <= 32",
),
description="bf16 GEMM specialised for N = 32, K = 5120 (sglang.kernels.jit, JIT-only).",
)
)
register_kernel(
KernelSpec(
op="gemm.qwen3x_nvfp4",
@@ -298,6 +324,26 @@ def tiny_gemm_bf16(
return impl(x, w, out, out_dtype=out_dtype, max_m=max_m)
def n128k512_gemm_bf16(
x: torch.Tensor,
w: torch.Tensor,
out: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""bf16 GEMM ``x[m, 512] @ w[128, 512].T`` for decode batches (m <= 32)."""
impl = get_kernel("gemm.n128k512", KernelBackend.JIT)
return impl(x, w, out)
def n32k5120_gemm_bf16(
x: torch.Tensor,
w: torch.Tensor,
out: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""bf16 GEMM ``x[m, 5120] @ w[32, 5120].T`` for decode batches (m <= 32)."""
impl = get_kernel("gemm.n32k5120", KernelBackend.JIT)
return impl(x, w, out)
def try_qwen3x_nvfp4_gemm(
input: torch.Tensor,
weight: torch.Tensor,
@@ -335,6 +381,8 @@ __all__ = [
"bmm_fp8",
"dsv3_fused_a_gemm",
"fp8_scaled_mm",
"n128k512_gemm_bf16",
"n32k5120_gemm_bf16",
"tiny_gemm_bf16",
"try_qwen3x_nvfp4_gemm",
"try_sm120_fp8_linear",
@@ -0,0 +1,123 @@
"""Decode-batch bf16 GEMMs for two fixed shapes: ``x[m, 512] @ w[128, 512].T`` and
``x[m, 5120] @ w[32, 5120].T``.
Shares dot_product_vec's reduction order with tiny_gemm_bf16 for bitwise parity;
a row's result does not depend on the batch composition.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
import torch
from sglang.kernels.jit.utils import (
cache_once,
get_jit_cuda_arch,
is_arch_support_pdl,
is_hip_runtime,
load_jit,
make_cpp_args,
)
from sglang.kernels.kernel_api_logging import debug_kernel_api
from sglang.srt.utils.custom_op import register_custom_op
if TYPE_CHECKING:
from tvm_ffi.module import Module
# Decode batches only: above this the caller keeps the general GEMM. The kernels
# accept any m (rows are split over blockIdx.y), this is a policy cap.
MAX_M: int = 32
@cache_once
def _jit_small_gemm_module(trait: str) -> Module:
args = make_cpp_args(is_arch_support_pdl())
return load_jit(
f"small_gemm_bf16_{trait}",
*args,
cuda_files=["gemm/small_gemm_bf16.cuh"],
cuda_wrappers=[("run", f"SmallGemmKernel<{trait}, {args}>::run")],
extra_cuda_cflags=["-O3"],
)
@register_custom_op(op_name="n128k512_gemm_bf16", mutates_args=["out"])
def _n128k512_gemm_custom_op(
x: torch.Tensor, w: torch.Tensor, out: torch.Tensor
) -> None:
_jit_small_gemm_module("N128K512Trait").run(x, w, out)
@register_custom_op(op_name="n32k5120_gemm_bf16", mutates_args=["out"])
def _n32k5120_gemm_custom_op(
x: torch.Tensor, w: torch.Tensor, out: torch.Tensor
) -> None:
_jit_small_gemm_module("N32K5120Trait").run(x, w, out)
@cache_once
def _arch_supported() -> bool:
return not is_hip_runtime() and get_jit_cuda_arch().major >= 9
def can_use_n128k512_gemm(n: int, k: int, m: int, max_m: int = MAX_M) -> bool:
"""Whether :func:`n128k512_gemm_bf16` serves ``[m, k] @ [n, k].T``.
Callers fall back to a general GEMM when this is False."""
return n == 128 and k == 512 and 1 <= m <= max_m and _arch_supported()
@debug_kernel_api
def n128k512_gemm_bf16(
x: torch.Tensor,
w: torch.Tensor,
out: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""Equal to ``torch.nn.functional.linear(x, w)`` for ``w`` of shape [128, 512].
Call :func:`can_use_n128k512_gemm` first: other shapes raise rather than fall
back. ``x`` may be a row-sliced view (``x.stride(1) == 1``), but each row must
start on a 32-byte boundary because rows are loaded as whole vectors.
:param x: Shape [m, 512], bf16.
:param w: Shape [128, 512], bf16, contiguous.
:param out: Optional [m, 128] bf16 buffer to write into.
"""
m = x.shape[0]
if out is None:
out = torch.empty((m, 128), dtype=torch.bfloat16, device=x.device)
if m == 0:
return out
_n128k512_gemm_custom_op(x, w, out)
return out
def can_use_n32k5120_gemm(n: int, k: int, m: int, max_m: int = MAX_M) -> bool:
"""Whether :func:`n32k5120_gemm_bf16` serves ``[m, k] @ [n, k].T``.
Callers fall back to a general GEMM when this is False."""
return n == 32 and k == 5120 and 1 <= m <= max_m and _arch_supported()
@debug_kernel_api
def n32k5120_gemm_bf16(
x: torch.Tensor,
w: torch.Tensor,
out: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""Equal to ``torch.nn.functional.linear(x, w)`` for ``w`` of shape [32, 5120].
Call :func:`can_use_n32k5120_gemm` first: other shapes raise rather than fall
back. ``x`` may be a row-sliced view (``x.stride(1) == 1``), but each row must
start on a 32-byte boundary because rows are loaded as whole vectors.
:param x: Shape [m, 5120], bf16.
:param w: Shape [32, 5120], bf16, contiguous.
:param out: Optional [m, 32] bf16 buffer to write into.
"""
m = x.shape[0]
if out is None:
out = torch.empty((m, 32), dtype=torch.bfloat16, device=x.device)
if m == 0:
return out
_n32k5120_gemm_custom_op(x, w, out)
return out
@@ -527,6 +527,23 @@ for _mod, _fn, _bk in _PHASE25_KERNELS:
)
del _mod, _fn, _bk
# Fused hyper-connection combine / norm kernels for small speculative batches.
_HC_NORM_KERNELS = [
("hc_combine_norm", "hc_combine_norm"),
("mhc_post_split_h", "mhc_post_split_h"),
("hc_combine_norm", "hc_combine_norm_mxfp8"),
("mxfp8_epilogue", "rmsnorm_mxfp8"),
]
for _mod, _fn in _HC_NORM_KERNELS:
register_kernel(
KernelSpec(
op=f"layernorm.{_fn}",
backend=KernelBackend.TRITON,
target=f"sglang.kernels.ops.layernorm.{_mod}:{_fn}",
)
)
del _mod, _fn
# The fused-rmsnorm variants physically live in the shared fused-pointwise
# collection (sglang.kernels.ops.elementwise.elementwise) but stay layernorm ops.
for _fn in ("fused_dual_residual_rmsnorm", "fused_rmsnorm"):
@@ -0,0 +1,146 @@
"""Collapse the hyper-connection streams and normalize the sublayer input."""
import torch
import triton
import triton.language as tl
from sglang.kernels.ops.layernorm.mxfp8_epilogue import mxfp8_epilogue
@triton.jit
def _hc_combine_norm(X, P, W, Y, SX: tl.constexpr, SP: tl.constexpr, EPS: tl.constexpr):
row, part = tl.program_id(0), tl.program_id(1)
h = tl.arange(0, 8192)
value = tl.full((8192,), 0, tl.float32)
for c in tl.static_range(4):
pre = tl.load(P + row * SP + c).to(tl.float32)
x = tl.load(X + row * SX + c * 5120 + h, h < 5120, 0).to(tl.float32)
value += x * pre
# The unfused combine stores BF16 before RMSNorm reads it.
value = value.to(tl.bfloat16).to(tl.float32)
inv_rms = tl.rsqrt(tl.sum(value * value, 0) / 5120 + EPS)
mask = (h >= part * 1280) & (h < (part + 1) * 1280)
weight = tl.load(W + h, mask, 0).to(tl.float32)
tl.store(Y + row * 5120 + h, value * inv_rms * weight, mask)
@triton.jit
def _hc_combine_norm_prefill(
X, P, W, Y, SX: tl.constexpr, SP: tl.constexpr, EPS: tl.constexpr
):
# Large batches have enough rows to use one CTA per row without repeating
# the combine and RMS reduction for each output partition.
row = tl.program_id(0).to(tl.int64)
h = tl.arange(0, 8192)
value = tl.full((8192,), 0, tl.float32)
for c in tl.static_range(4):
pre = tl.load(P + row * SP + c).to(tl.float32)
x = tl.load(X + row * SX + c * 5120 + h, h < 5120, 0).to(tl.float32)
value += x * pre
value = value.to(tl.bfloat16).to(tl.float32)
inv_rms = tl.rsqrt(tl.sum(value * value, 0) / 5120 + EPS)
weight = tl.load(W + h, h < 5120, 0).to(tl.float32)
tl.store(Y + row * 5120 + h, value * inv_rms * weight, h < 5120)
def hc_combine_norm(
x: torch.Tensor, pre: torch.Tensor, weight: torch.Tensor, eps: float
) -> torch.Tensor:
"""Fuse four-stream combine and RMSNorm for BF16 batches of width 5120."""
m = x.shape[0]
assert (0 < m <= 8 or 4096 <= m <= 65536) and x.shape == (m, 20480)
assert pre.shape == (m, 4) and pre.stride(1) == 1
assert weight.shape == (5120,) and weight.is_contiguous()
assert x.dtype == weight.dtype == torch.bfloat16 and x.stride(1) == 1
y = torch.empty((m, 5120), dtype=x.dtype, device=x.device)
if m >= 4096:
_hc_combine_norm_prefill[(m,)](
x, pre, weight, y, x.stride(0), pre.stride(0), eps, num_warps=4
)
return y
# Four CTAs per row trade redundant statistics for more concurrent loads
# when only a few speculative tokens are being processed.
_hc_combine_norm[(m, 4)](
x, pre, weight, y, x.stride(0), pre.stride(0), eps, num_warps=8
)
return y
@triton.jit
def _hc_combine_norm_mxfp8_kernel(
X,
P,
W,
Y,
Q,
S,
SX: tl.constexpr,
SP: tl.constexpr,
EPS: tl.constexpr,
K: tl.constexpr,
BLOCK: tl.constexpr,
GROUPS: tl.constexpr,
SLICE: tl.constexpr,
):
row, part = tl.program_id(0), tl.program_id(1)
h = tl.arange(0, BLOCK)
m = h < K
value = tl.full((BLOCK,), 0, tl.float32)
for c in tl.static_range(4):
pre = tl.load(P + row * SP + c).to(tl.float32)
x = tl.load(X + row * SX + c * K + h, m, 0).to(tl.float32)
value += x * pre
# The unfused combine stores BF16 before RMSNorm reads it.
value = value.to(tl.bfloat16).to(tl.float32)
inv_rms = tl.rsqrt(tl.sum(value * value, 0) / K + EPS)
weight = tl.load(W + h, m, 0).to(tl.float32)
y = (value * inv_rms * weight).to(tl.bfloat16)
tl.store(Y + row * K + h, y, m & (h >= part * SLICE) & (h < (part + 1) * SLICE))
mxfp8_epilogue(
y, row, Q, S, K, BLOCK, GROUPS, part * (SLICE // 32), (part + 1) * (SLICE // 32)
)
def _parts_for(k: int) -> int:
# Row splits: recomputing the statistic beats running 6 CTAs on 148 SMs.
parts = 4
while parts > 1 and (k % (parts * 32)):
parts //= 2
return parts
def _alloc(m, k, device):
q = torch.empty((m, k), dtype=torch.float8_e4m3fn, device=device)
s = torch.zeros(
(k // 32) * (triton.cdiv(m, 128) * 128), dtype=torch.uint8, device=device
)
return q, s
def hc_combine_norm_mxfp8(
x: torch.Tensor, pre: torch.Tensor, weight: torch.Tensor, eps: float
):
"""Four-stream combine + RMSNorm returning ``(y_bf16, y_q, y_sf)``."""
m = x.shape[0]
assert 0 < m <= 8, "the fused MXFP8 epilogue only supports small decode/verify"
k = x.shape[1] // 4
y = torch.empty((m, k), dtype=x.dtype, device=x.device)
q, s = _alloc(m, k, x.device)
parts = _parts_for(k)
_hc_combine_norm_mxfp8_kernel[(m, parts)](
x,
pre,
weight,
y,
q,
s,
SX=x.stride(0),
SP=pre.stride(0),
EPS=eps,
K=k,
BLOCK=triton.next_power_of_2(k),
GROUPS=k // 32,
SLICE=k // parts,
num_warps=8,
)
return y, q, s
@@ -0,0 +1,49 @@
"""Small-batch HC=4 post-mix with independent hidden-dimension CTAs."""
import torch
import triton
import triton.language as tl
@triton.jit
def _mhc_post_split_h_kernel(X, R, P, C, Y, H: tl.constexpr, B: tl.constexpr):
token = tl.program_id(0)
h = tl.program_id(1) * B + tl.arange(0, B)
channel = tl.arange(0, 4)
x = tl.load(X + token * H + h, h < H, 0).to(tl.float32)
post = tl.load(P + token * 4 + channel)
residual0 = tl.load(R + token * 4 * H + h, h < H, 0).to(tl.float32)
comb0 = tl.load(C + token * 16 + channel)
# Match NVCC's contraction in mhc_post_tilelang: round comb[0]*residual[0]
# first, then FMA post*x into it. Reversing these two terms changes BF16
# rounding on rare ties even though the symbolic expression is the same.
acc = tl.fma(post[:, None], x[None, :], comb0[:, None] * residual0[None, :])
for i in tl.static_range(1, 4):
residual = tl.load(R + (token * 4 + i) * H + h, h < H, 0).to(tl.float32)
comb = tl.load(C + token * 16 + i * 4 + channel)
acc = tl.fma(comb[:, None], residual[None, :], acc)
tl.store(Y + (token * 4 + channel[:, None]) * H + h[None, :], acc, h[None, :] < H)
def mhc_post_split_h(
x: torch.Tensor, residual: torch.Tensor, post: torch.Tensor, comb: torch.Tensor
) -> torch.Tensor:
"""Same result as the TileLang post kernel for contiguous BF16 HC=4 inputs."""
assert x.dtype == residual.dtype == torch.bfloat16
assert post.dtype == comb.dtype == torch.float32
assert residual.shape == (x.shape[0], 4, x.shape[1])
assert all(t.is_contiguous() for t in (x, residual, post, comb))
output = torch.empty_like(residual)
block = 128 if x.shape[0] <= 8 else 1024
_mhc_post_split_h_kernel[(x.shape[0], triton.cdiv(x.shape[1], block))](
x,
residual,
post,
comb,
output,
H=x.shape[1],
B=block,
num_warps=4,
enable_fp_fusion=False,
)
return output
@@ -0,0 +1,111 @@
"""MXFP8 quantization written as an epilogue of the kernel that produces the row.
At the speculative BS=1 shapes (<=8 rows) the standalone FlashInfer
``mxfp8_quantize`` launch costs about as much as the norm it follows, even
though it only re-reads what that norm just wrote. These kernels do the norm
and the quantization in one pass; the quantized values come from the same BF16
rounding the standalone pair produces, so both outputs are bitwise identical to
``rmsnorm`` followed by ``mxfp8_quantize(..., is_sf_swizzled_layout=True)``.
The scale factors use FlashInfer's 128x4 swizzle:
``off = (g // 4) * 512 + ((r % 32) * 4 + ((r // 32) % 4)) * 4 + (g % 4)``
over a row count padded to a multiple of 128, with the UE8M0 conversion
(positive rounding, subnormals included) that ``mxfp8_quantize`` uses.
"""
from __future__ import annotations
import torch
import triton
import triton.language as tl
# FlashInfer's positive-rounding UE8M0 conversion of a per-group amax, subnormals
# included: the scale byte and the multiplier that maps the group into e4m3 range.
@triton.jit
def ue8m0_scale(amax):
normalized = amax * (1.0 / 448.0)
bits = normalized.to(tl.int32, bitcast=True)
exponent = (bits >> 23) & 255
mantissa = bits & 0x7FFFFF
bump = (mantissa != 0) & ~((exponent == 0) & (mantissa <= 0x400000))
sf = tl.where(normalized <= 0, 0, tl.minimum(exponent + bump.to(tl.int32), 254))
inv = tl.where(sf == 0, 0, ((254 - sf) << 23)).to(tl.float32, bitcast=True)
return sf, inv
@triton.jit
def mxfp8_epilogue(
y, row, Q, S, K: tl.constexpr, BLOCK: tl.constexpr, GROUPS: tl.constexpr, g_lo, g_hi
):
# Stores only groups in [g_lo, g_hi) so a row can be split across CTAs.
GP: tl.constexpr = BLOCK // 32
g = tl.arange(0, GP)
gmask = (g < GROUPS) & (g >= g_lo) & (g < g_hi)
e = tl.arange(0, 32)
idx = g[:, None] * 32 + e[None, :]
v = tl.reshape(y.to(tl.float32), (GP, 32))
amax = tl.max(tl.abs(v), 1)
sf, scale = ue8m0_scale(amax)
q = tl.minimum(tl.maximum(v * scale[:, None], -448.0), 448.0).to(tl.float8e4nv)
tl.store(Q + row * K + idx, q, gmask[:, None])
off = (g // 4) * 512 + ((row % 32) * 4 + ((row // 32) % 4)) * 4 + (g % 4)
tl.store(S + off, sf.to(tl.uint8), gmask)
@triton.jit
def _rmsnorm_mxfp8_kernel(
X,
W,
Y,
Q,
S,
SX: tl.constexpr,
M: tl.constexpr,
K: tl.constexpr,
EPS: tl.constexpr,
BLOCK: tl.constexpr,
GROUPS: tl.constexpr,
):
row = tl.program_id(0)
if row < M:
h = tl.arange(0, BLOCK)
v = tl.load(X + row * SX + h, h < K, 0).to(tl.float32)
weight = tl.load(W + h, h < K, 0).to(tl.float32)
inv = tl.rsqrt(tl.sum(v * v, 0) / K + EPS)
y = (v * inv * weight).to(tl.bfloat16)
tl.store(Y + row * K + h, y, h < K)
mxfp8_epilogue(y, row, Q, S, K, BLOCK, GROUPS, 0, GROUPS)
else:
# Padding scale entries are disjoint from the live rows above.
off = (row - M) * 512 + tl.arange(0, 512)
pad_row = ((off // 4) % 4) * 32 + ((off // 16) % 32)
tl.store(S + off, 0, (off < GROUPS * 128) & (pad_row >= M))
def rmsnorm_mxfp8(
x: torch.Tensor, weight: torch.Tensor, eps: float
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""RMSNorm returning ``(y_bf16, y_q, y_sf)`` with the same MXFP8 epilogue."""
m, k = x.shape
assert 0 < m <= 8 and k % 32 == 0
assert x.dtype == weight.dtype == torch.bfloat16 and x.stride(1) == 1
y = torch.empty_like(x, memory_format=torch.contiguous_format)
q = torch.empty((m, k), dtype=torch.float8_e4m3fn, device=x.device)
s = torch.empty((k // 32) * 128, dtype=torch.uint8, device=x.device)
_rmsnorm_mxfp8_kernel[(m + triton.cdiv(s.numel(), 512),)](
x,
weight,
y,
q,
s,
SX=x.stride(0),
M=m,
K=k,
EPS=eps,
BLOCK=triton.next_power_of_2(k),
GROUPS=k // 32,
num_warps=8,
enable_fp_fusion=False,
)
return y, q, s
@@ -0,0 +1,39 @@
"""Out-of-place RMSNorm with FP32 statistics and weight multiplication."""
import torch
import triton
import triton.language as tl
@triton.jit
def _rmsnorm_fp32_kernel(X, W, Y, D: tl.constexpr, EPS: tl.constexpr, B: tl.constexpr):
row = tl.program_id(0)
col = tl.arange(0, B)
x = tl.load(X + row * D + col, col < D, 0).to(tl.float32)
weight = tl.load(W + col, col < D, 0).to(tl.float32)
inv_rms = tl.rsqrt(tl.sum(x * x, axis=0) / D + EPS)
normalized = x * inv_rms
tl.store(Y + row * D + col, normalized * weight, col < D)
def rmsnorm_fp32(x: torch.Tensor, weight: torch.Tensor, eps: float) -> torch.Tensor:
"""Match ``weight * (x.float() * rsqrt(mean(x.float()**2) + eps))``."""
assert x.is_cuda and x.is_contiguous() and weight.is_contiguous()
assert x.dtype in (torch.bfloat16, torch.float32)
assert weight.dtype in (torch.bfloat16, torch.float32)
assert x.device == weight.device and weight.shape == (x.shape[-1],)
output = torch.empty_like(x)
dim = x.shape[-1]
rows = x.numel() // dim
if rows:
_rmsnorm_fp32_kernel[(rows,)](
x,
weight,
output,
dim,
eps,
triton.next_power_of_2(dim),
num_warps=4,
enable_fp_fusion=False,
)
return output
@@ -22,6 +22,7 @@ _TRITON_KERNELS = [
("ragged_verify_kernels", "pad_verify_lens_to_bucket"),
("ragged_verify_kernels", "build_qo_indptr"),
("reject_sampling", "chain_speculative_sampling_triton"),
("row_argmax", "row_argmax"),
]
for _mod, _fn in _TRITON_KERNELS:
register_kernel(
@@ -0,0 +1,65 @@
"""Row-wise argmax for the tall-and-thin speculative verify logits.
``torch.argmax`` reduces each row in a single block, which leaves most of the
machine idle for a handful of very wide rows; a flat two-stage split over the
vocabulary saturates it instead.
Ties resolve to the lowest index, matching ``ArgMaxOps``' strict ``>``.
"""
import torch
import triton
import triton.language as tl
@triton.jit
def _argmax_partial_kernel(
X, OUTV, OUTI, N, SX, SPLITS: tl.constexpr, BLOCK: tl.constexpr
):
row = tl.program_id(0)
part = tl.program_id(1)
per = tl.cdiv(N, SPLITS)
start = part * per
best_v = float("-inf")
best_i = N
for off in tl.range(start, tl.minimum(start + per, N), BLOCK):
idx = off + tl.arange(0, BLOCK)
m = idx < tl.minimum(start + per, N)
v = tl.load(X + row * SX + idx, m, float("-inf"))
cur_v = tl.max(v, 0)
# lowest index among the maxima of this tile
cur_i = tl.min(tl.where(v == cur_v, idx, N), 0)
take = (cur_v > best_v) | ((cur_v == best_v) & (cur_i < best_i))
best_i = tl.where(take, cur_i, best_i)
best_v = tl.where(take, cur_v, best_v)
tl.store(OUTV + row * SPLITS + part, best_v)
tl.store(OUTI + row * SPLITS + part, best_i)
@triton.jit
def _argmax_final_kernel(INV, INI, OUT, SPLITS: tl.constexpr, BLOCK: tl.constexpr):
row = tl.program_id(0)
o = tl.arange(0, BLOCK)
m = o < SPLITS
v = tl.load(INV + row * SPLITS + o, m, float("-inf"))
i = tl.load(INI + row * SPLITS + o, m, 0x7FFFFFFF)
best_v = tl.max(v, 0)
best_i = tl.min(tl.where(v == best_v, i, 0x7FFFFFFF), 0)
tl.store(OUT + row, best_i.to(tl.int64))
_SPLITS = 64
def row_argmax(x: torch.Tensor) -> torch.Tensor:
"""``x.argmax(dim=-1)`` for a 2D FP32 tensor with few rows and a wide vocab."""
assert x.dim() == 2 and x.dtype == torch.float32 and x.stride(1) == 1
rows, n = x.shape
out = torch.empty((rows,), dtype=torch.int64, device=x.device)
pv = torch.empty((rows, _SPLITS), dtype=torch.float32, device=x.device)
pi = torch.empty((rows, _SPLITS), dtype=torch.int32, device=x.device)
_argmax_partial_kernel[(rows, _SPLITS)](
x, pv, pi, n, x.stride(0), SPLITS=_SPLITS, BLOCK=2048, num_warps=8
)
_argmax_final_kernel[(rows,)](pv, pi, out, SPLITS=_SPLITS, BLOCK=64, num_warps=2)
return out