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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,522 @@
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/runtime.cuh>
#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/deepseek_v4/compress.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/object.h>
#include <cstdint>
namespace {
using Plan128 = device::compress::PrefillPlan;
using IndiceT = int32_t;
/// \brief Each thread will handle this many elements (split along head_dim)
constexpr int32_t kTileElements = 2;
/// \brief Each warp will handle this many elements (split along 128)
constexpr int32_t kElementsPerWarp = 8;
constexpr uint32_t kNumWarps = 128 / kElementsPerWarp;
constexpr uint32_t kBlockSize = device::kWarpThreads * kNumWarps;
/// \brief Need to reduce register usage to increase occupancy
#define C128_KERNEL __global__ __launch_bounds__(kBlockSize, 2)
struct Compress128DecodeParams {
/**
* \brief Shape: `[num_indices, 128, head_dim * 2]` \n
* last dimension layout:
* | kv current | score current |
*/
void* __restrict__ kv_score_buffer;
/** \brief Shape: `[batch_size, head_dim * 2]` */
const void* __restrict__ kv_score_input;
/** \brief Shape: `[batch_size, head_dim]` */
void* __restrict__ kv_compressed_output;
/** \brief Shape: `[128, head_dim]` (called `ape`) */
const void* __restrict__ score_bias;
/** \brief Shape: `[batch_size, ]`*/
const IndiceT* __restrict__ indices;
/** \brief Shape: `[batch_size, ]` */
const IndiceT* __restrict__ seq_lens;
/** \NOTE: `batch_size` <= `num_indices` */
uint32_t batch_size;
};
struct Compress128PrefillParams {
/**
* \brief Shape: `[num_indices, 128, head_dim * 2]` \n
* last dimension layout:
* | kv current | score current |
*/
void* __restrict__ kv_score_buffer;
/** \brief Shape: `[batch_size, head_dim * 2]` */
const void* __restrict__ kv_score_input;
/** \brief Shape: `[batch_size, head_dim]` */
void* __restrict__ kv_compressed_output;
/** \brief Shape: `[128, head_dim]` (called `ape`) */
const void* __restrict__ score_bias;
/** \brief Shape: `[batch_size, ]`*/
const IndiceT* __restrict__ indices;
/** \brief Shape: `[batch_size, ]`*/
const int32_t* __restrict__ load_indices;
/** \brief The following part is plan info. */
const Plan128* __restrict__ compress_plan;
const Plan128* __restrict__ write_plan;
uint32_t num_compress;
uint32_t num_write;
};
struct Compress128SharedBuffer {
using Storage = device::AlignedVector<float, kTileElements>;
Storage data[kNumWarps][device::kWarpThreads + 1]; // padding to avoid bank conflict
SGL_DEVICE Storage& operator()(uint32_t warp_id, uint32_t lane_id) {
return data[warp_id][lane_id];
}
SGL_DEVICE float& operator()(uint32_t warp_id, uint32_t lane_id, uint32_t tile_id) {
return data[warp_id][lane_id][tile_id];
}
};
template <typename T>
SGL_DEVICE void c128_write(
T* kv_score_buf, //
const T* kv_score_src,
const int64_t head_dim,
const int32_t write_pos,
const uint32_t lane_id) {
using namespace device;
using Storage = AlignedVector<T, kTileElements>;
const auto element_size = head_dim * 2;
const auto gmem = tile::Memory<Storage>{lane_id, kWarpThreads};
kv_score_buf += write_pos * element_size;
/// NOTE: Layout | [0] = kv | [1] = score |
Storage kv_score[2];
#pragma unroll
for (int32_t i = 0; i < 2; ++i) {
kv_score[i] = gmem.load(kv_score_src + head_dim * i);
}
#pragma unroll
for (int32_t i = 0; i < 2; ++i) {
gmem.store(kv_score_buf + head_dim * i, kv_score[i]);
}
}
template <typename InFloat, typename OutFloat>
SGL_DEVICE void c128_forward(
const InFloat* kv_score_buf,
const InFloat* kv_score_src,
OutFloat* kv_out,
const InFloat* score_bias,
const int64_t head_dim,
const int32_t window_len,
const uint32_t warp_id,
const uint32_t lane_id) {
using namespace device;
const auto element_size = head_dim * 2;
const auto score_offset = head_dim;
/// NOTE: part 1: load kv + score
using StorageIn = AlignedVector<InFloat, kTileElements>;
const auto gmem_in = tile::Memory<StorageIn>{lane_id, kWarpThreads};
StorageIn kv[kElementsPerWarp];
StorageIn score[kElementsPerWarp];
StorageIn bias[kElementsPerWarp];
const int32_t warp_offset = warp_id * kElementsPerWarp;
#pragma unroll
for (int32_t i = 0; i < 8; ++i) {
const int32_t j = i + warp_offset;
bias[i] = gmem_in.load(score_bias + j * head_dim);
}
#pragma unroll
for (int32_t i = 0; i < kElementsPerWarp; ++i) {
const int32_t j = i + warp_offset;
const InFloat* src;
__builtin_assume(j < 128);
if (j < window_len) {
src = kv_score_buf + j * element_size;
} else {
/// NOTE: k in [-127, 0]. We'll load from the ragged `kv_score_src`
const int32_t k = j - 127;
src = kv_score_src + k * element_size;
}
kv[i] = gmem_in.load(src);
score[i] = gmem_in.load(src + score_offset);
}
/// NOTE: part 2: safe online softmax + weighted sum
using TmpStorage = typename Compress128SharedBuffer::Storage;
__shared__ Compress128SharedBuffer s_local_val_max;
__shared__ Compress128SharedBuffer s_local_exp_sum;
__shared__ Compress128SharedBuffer s_local_product;
TmpStorage tmp_val_max;
TmpStorage tmp_exp_sum;
TmpStorage tmp_product;
#pragma unroll
for (int32_t i = 0; i < kTileElements; ++i) {
float score_fp32[kElementsPerWarp];
#pragma unroll
for (int32_t j = 0; j < kElementsPerWarp; ++j) {
score_fp32[j] = cast<float>(score[j][i]) + cast<float>(bias[j][i]);
}
float max_value = score_fp32[0];
float sum_exp_value = 0.0f;
#pragma unroll
for (int32_t j = 1; j < kElementsPerWarp; ++j) {
const auto fp32_score = score_fp32[j];
max_value = fmaxf(max_value, fp32_score);
}
float sum_product = 0.0f;
#pragma unroll
for (int32_t j = 0; j < 8; ++j) {
const auto fp32_score = score_fp32[j];
const auto exp_score = expf(fp32_score - max_value);
sum_product += cast<float>(kv[j][i]) * exp_score;
sum_exp_value += exp_score;
}
tmp_val_max[i] = max_value;
tmp_exp_sum[i] = sum_exp_value;
tmp_product[i] = sum_product;
}
// naturally aligned, so no bank conflict
s_local_val_max(warp_id, lane_id) = tmp_val_max;
s_local_exp_sum(warp_id, lane_id) = tmp_exp_sum;
s_local_product(warp_id, lane_id) = tmp_product;
__syncthreads();
/// NOTE: part 3: online softmax
/// NOTE: We have `kTileElements * kWarpThreads * kNumWarps` values to reduce
/// each reduce will consume `kNumWarps` threads (use partial warp reduction)
constexpr uint32_t kReductionCount = kTileElements * kWarpThreads * kNumWarps;
constexpr uint32_t kIteration = kReductionCount / kBlockSize;
#pragma unroll
for (uint32_t i = 0; i < kIteration; ++i) {
/// NOTE: Range `[0, kTileElements * kWarpThreads * kNumWarps)`
const uint32_t j = i * kBlockSize + warp_id * kWarpThreads + lane_id;
/// NOTE: Range `[0, kNumWarps)`
const uint32_t local_warp_id = j % kNumWarps;
/// NOTE: Range `[0, kTileElements * kWarpThreads)`
const uint32_t local_elem_id = j / kNumWarps;
/// NOTE: Range `[0, kTileElements)`
const uint32_t local_tile_id = local_elem_id % kTileElements;
/// NOTE: Range `[0, kWarpThreads)`
const uint32_t local_lane_id = local_elem_id / kTileElements;
/// NOTE: each warp will access the whole tile (all `kTileElements`)
/// and for different lanes, the memory access only differ in `local_warp_id`
/// so there's no bank conflict in shared memory access.
static_assert(kTileElements * kNumWarps == kWarpThreads, "TODO: support other configs");
const auto local_val_max = s_local_val_max(local_warp_id, local_lane_id, local_tile_id);
const auto local_exp_sum = s_local_exp_sum(local_warp_id, local_lane_id, local_tile_id);
const auto local_product = s_local_product(local_warp_id, local_lane_id, local_tile_id);
const auto global_val_max = warp::reduce_max<kNumWarps>(local_val_max);
const auto rescale = expf(local_val_max - global_val_max);
const auto global_exp_sum = warp::reduce_sum<kNumWarps>(local_exp_sum * rescale);
const auto final_scale = rescale / global_exp_sum;
const auto global_product = warp::reduce_sum<kNumWarps>(local_product * final_scale);
kv_out[local_elem_id] = cast<OutFloat>(global_product);
}
}
template <int64_t kHeadDim, typename InFloat, typename OutFloat, bool kUsePDL>
C128_KERNEL void flash_c128_decode(const __grid_constant__ Compress128DecodeParams params) {
using namespace device;
constexpr int64_t kTileDim = kTileElements * kWarpThreads; // 64
constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
constexpr int64_t kElementSize = kHeadDim * 2;
static_assert(kHeadDim % kTileDim == 0, "Head dim must be multiple of tile dim");
const auto& [
_kv_score_buffer, _kv_score_input, _kv_compressed_output, _score_bias, // kv score
indices, seq_lens, batch_size // decode info
] = params;
const uint32_t warp_id = threadIdx.x / kWarpThreads;
const uint32_t lane_id = threadIdx.x % kWarpThreads;
const uint32_t global_bid = blockIdx.x / kNumSplit; // batch id
const uint32_t global_sid = blockIdx.x % kNumSplit; // split id
if (global_bid >= batch_size) return;
const int32_t index = indices[global_bid];
const int32_t seq_len = seq_lens[global_bid];
const int64_t split_offset = global_sid * kTileDim;
// kv score
const auto kv_score_buffer = static_cast<InFloat*>(_kv_score_buffer);
const auto kv_buf = kv_score_buffer + index * (kElementSize * 128) + split_offset;
// kv input
const auto kv_score_input = static_cast<const InFloat*>(_kv_score_input);
const auto kv_src = kv_score_input + global_bid * kElementSize + split_offset;
// kv output
const auto kv_compressed_output = static_cast<OutFloat*>(_kv_compressed_output);
const auto kv_out = kv_compressed_output + global_bid * kHeadDim + split_offset;
// score bias (ape)
const auto score_bias = static_cast<const InFloat*>(_score_bias) + split_offset;
PDLWaitPrimary<kUsePDL>();
/// NOTE: the write must be visible to the subsequent c128_forward,
/// so only the last warp can write to HBM
/// In addition, `position` = `seq_len - 1`. To avoid underflow, we use `seq_len + 127`
if (warp_id == kNumWarps - 1) {
c128_write(kv_buf, kv_src, kHeadDim, /*write_pos=*/(seq_len + 127) % 128, lane_id);
}
if (seq_len % 128 == 0) {
c128_forward(kv_buf, kv_src, kv_out, score_bias, kHeadDim, /*window_len=*/128, warp_id, lane_id);
}
PDLTriggerSecondary<kUsePDL>();
}
// compress kernel
template <int64_t kHeadDim, typename InFloat, typename OutFloat, bool kWrite, bool kUsePDL>
C128_KERNEL void flash_c128_prefill(const __grid_constant__ Compress128PrefillParams params) {
using namespace device;
constexpr int64_t kTileDim = kTileElements * kWarpThreads; // 64
constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
constexpr int64_t kElementSize = kHeadDim * 2;
static_assert(kHeadDim % kTileDim == 0, "Head dim must be multiple of tile dim");
const auto& [
_kv_score_buffer, _kv_score_input, _kv_compressed_output, _score_bias, // kv score
indices, load_indices, compress_plan, write_plan, num_compress, num_write // prefill plan
] = params;
const uint32_t warp_id = threadIdx.x / kWarpThreads;
const uint32_t lane_id = threadIdx.x % kWarpThreads;
uint32_t global_id;
if constexpr (kWrite) {
// for write kernel, we use global warp_id to dispatch work
global_id = (blockIdx.x * blockDim.x + threadIdx.x) / kWarpThreads;
} else {
// for compress kernel, we use block id to dispatch work
global_id = blockIdx.x; // block id
}
const uint32_t global_pid = global_id / kNumSplit; // plan id
const uint32_t global_sid = global_id % kNumSplit; // split id
/// NOTE: compiler can optimize this if-else at compile time
const auto num_plans = kWrite ? num_write : num_compress;
const auto plan_ptr = kWrite ? write_plan : compress_plan;
if (global_pid >= num_plans) return;
const auto& [ragged_id, global_bid, position, window_len] = plan_ptr[global_pid];
const auto indices_ptr = kWrite ? indices : load_indices;
const int64_t split_offset = global_sid * kTileDim;
// kv input
const auto kv_score_input = static_cast<const InFloat*>(_kv_score_input);
const auto kv_src = kv_score_input + ragged_id * kElementSize + split_offset;
// kv output
const auto kv_compressed_output = static_cast<OutFloat*>(_kv_compressed_output);
const auto kv_out = kv_compressed_output + ragged_id * kHeadDim + split_offset;
// score bias (ape)
const auto score_bias = static_cast<const InFloat*>(_score_bias) + split_offset;
if (ragged_id == 0xFFFFFFFF) [[unlikely]]
return;
const int32_t index = indices_ptr[global_bid];
// kv score
const auto kv_score_buffer = static_cast<InFloat*>(_kv_score_buffer);
const auto kv_buf = kv_score_buffer + index * (kElementSize * 128) + split_offset;
PDLWaitPrimary<kUsePDL>();
// only responsible for the compress part
if constexpr (kWrite) {
c128_write(kv_buf, kv_src, kHeadDim, /*write_pos=*/position % 128, lane_id);
} else {
c128_forward(kv_buf, kv_src, kv_out, score_bias, kHeadDim, window_len, warp_id, lane_id);
}
PDLTriggerSecondary<kUsePDL>();
}
template <int64_t kHeadDim, typename InFloat, typename OutFloat, bool kUsePDL>
struct FlashCompress128Kernel {
static constexpr auto decode_kernel = flash_c128_decode<kHeadDim, InFloat, OutFloat, kUsePDL>;
template <bool kWrite>
static constexpr auto prefill_kernel = flash_c128_prefill<kHeadDim, InFloat, OutFloat, kWrite, kUsePDL>;
static constexpr auto prefill_c_kernel = prefill_kernel</*kWrite=*/false>;
static constexpr auto prefill_w_kernel = prefill_kernel</*kWrite=*/true>;
static constexpr int64_t kTileDim = kTileElements * device::kWarpThreads; // 64
static constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
static constexpr uint32_t kWriteBlockSize = 128;
static constexpr uint32_t kWarpsPerWriteBlock = kWriteBlockSize / device::kWarpThreads;
static void run_decode(
const tvm::ffi::TensorView kv_score_buffer,
const tvm::ffi::TensorView kv_score_input,
const tvm::ffi::TensorView kv_compressed_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView indices,
const tvm::ffi::TensorView seq_lens,
const tvm::ffi::Optional<tvm::ffi::TensorView> /* UNUSED */) {
using namespace host;
// this should not happen in practice
auto B = SymbolicSize{"batch_size"};
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
TensorMatcher({-1, 128, kHeadDim * 2}) // kv score
.with_dtype<InFloat>()
.with_device(device)
.verify(kv_score_buffer);
TensorMatcher({B, kHeadDim * 2}) // kv score input
.with_dtype<InFloat>()
.with_device(device)
.verify(kv_score_input);
TensorMatcher({B, kHeadDim}) // kv compressed output
.with_dtype<OutFloat>()
.with_device(device)
.verify(kv_compressed_output);
TensorMatcher({128, kHeadDim}) // ape
.with_dtype<InFloat>()
.with_device(device)
.verify(ape);
TensorMatcher({B}) // indices
.with_dtype<IndiceT>()
.with_device(device)
.verify(indices);
TensorMatcher({B}) // seq lens
.with_dtype<IndiceT>()
.with_device(device)
.verify(seq_lens);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto params = Compress128DecodeParams{
.kv_score_buffer = kv_score_buffer.data_ptr(),
.kv_score_input = kv_score_input.data_ptr(),
.kv_compressed_output = kv_compressed_output.data_ptr(),
.score_bias = ape.data_ptr(),
.indices = static_cast<const IndiceT*>(indices.data_ptr()),
.seq_lens = static_cast<const IndiceT*>(seq_lens.data_ptr()),
.batch_size = batch_size,
};
const uint32_t num_blocks = batch_size * kNumSplit;
LaunchKernel(num_blocks, kBlockSize, device.unwrap()) //
.enable_pdl(kUsePDL)(decode_kernel, params);
}
static void run_prefill(
const tvm::ffi::TensorView kv_score_buffer,
const tvm::ffi::TensorView kv_score_input,
const tvm::ffi::TensorView kv_compressed_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView indices,
const tvm::ffi::TensorView compress_plan,
const tvm::ffi::TensorView write_plan,
const tvm::ffi::Optional<tvm::ffi::TensorView> extra) {
using namespace host;
auto B = SymbolicSize{"batch_size"};
auto N = SymbolicSize{"num_q_tokens"};
auto X = SymbolicSize{"compress_tokens"};
auto Y = SymbolicSize{"write_tokens"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({-1, 128, kHeadDim * 2}) // kv score
.with_dtype<InFloat>()
.with_device(device_)
.verify(kv_score_buffer);
TensorMatcher({N, kHeadDim * 2}) // kv score input
.with_dtype<InFloat>()
.with_device(device_)
.verify(kv_score_input);
TensorMatcher({N, kHeadDim}) // kv compressed output
.with_dtype<OutFloat>()
.with_device(device_)
.verify(kv_compressed_output);
TensorMatcher({128, kHeadDim}) // ape
.with_dtype<InFloat>()
.with_device(device_)
.verify(ape);
TensorMatcher({B}) // indices
.with_dtype<IndiceT>()
.with_device(device_)
.verify(indices);
TensorMatcher({X, compress::kPrefillPlanDim}) // compress plan
.with_dtype<compress::PrefillPlanTensorDtype>()
.with_device(device_)
.verify(compress_plan);
TensorMatcher({Y, compress::kPrefillPlanDim}) // write plan
.with_dtype<compress::PrefillPlanTensorDtype>()
.with_device(device_)
.verify(write_plan);
// might be needed for prefill write
const auto load_indices = extra.value_or(indices);
TensorMatcher({B}) // [read_positions]
.with_dtype<IndiceT>()
.with_device(device_)
.verify(load_indices);
const auto device = device_.unwrap();
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto num_q_tokens = static_cast<uint32_t>(N.unwrap());
const auto num_c = static_cast<uint32_t>(X.unwrap());
const auto num_w = static_cast<uint32_t>(Y.unwrap());
const auto params = Compress128PrefillParams{
.kv_score_buffer = kv_score_buffer.data_ptr(),
.kv_score_input = kv_score_input.data_ptr(),
.kv_compressed_output = kv_compressed_output.data_ptr(),
.score_bias = ape.data_ptr(),
.indices = static_cast<const IndiceT*>(indices.data_ptr()),
.load_indices = static_cast<const IndiceT*>(load_indices.data_ptr()),
.compress_plan = static_cast<const Plan128*>(compress_plan.data_ptr()),
.write_plan = static_cast<const Plan128*>(write_plan.data_ptr()),
.num_compress = num_c,
.num_write = num_w,
};
RuntimeCheck(num_q_tokens >= batch_size, "num_q_tokens must be >= batch_size");
RuntimeCheck(num_q_tokens >= std::max(num_c, num_w), "invalid prefill plan");
constexpr auto kBlockSize_C = kBlockSize;
constexpr auto kBlockSize_W = kWriteBlockSize;
if (const auto num_c_blocks = num_c * kNumSplit) {
LaunchKernel(num_c_blocks, kBlockSize_C, device) //
.enable_pdl(kUsePDL)(prefill_c_kernel, params);
}
if (const auto num_w_blocks = div_ceil(num_w * kNumSplit, kWarpsPerWriteBlock)) {
LaunchKernel(num_w_blocks, kBlockSize_W, device) //
.enable_pdl(kUsePDL)(prefill_w_kernel, params);
}
}
};
} // namespace
@@ -0,0 +1,726 @@
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/runtime.cuh>
#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/deepseek_v4/compress.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/container/tuple.h>
#include <tvm/ffi/object.h>
#include <algorithm>
#include <cfloat>
#include <cstdint>
namespace device::compress {
/// \brief Plan entry for online compress 128 prefill.
/// Each entry describes a contiguous segment of tokens that lies inside a
/// single 128-chunk. Multiple segments can map to the same batch id when the
/// extend tokens span chunk boundaries.
///
/// **Layout compatibility:** the field order/types match `PrefillPlan` so that
/// downstream kernels (e.g. `fused_norm_rope` in `CompressExtend` mode) can
/// consume the compress_plan tensor as-if it were a `PrefillPlan` tensor --
/// they only read `ragged_id` and `position`, both of which carry identical
/// semantics here (the LAST token of the segment in q-ragged and global
/// coordinates respectively).
///
/// Note that `window_len` here means "number of real tokens in this segment"
/// (1..128), which differs from `PrefillPlan::window_len`. Downstream kernels
/// that share the tensor MUST NOT read it under that name.
struct alignas(16) OnlinePrefillPlan {
/// \brief Ragged-q position of the LAST token in this segment.
/// Equal to `segment_start_ragged + window_len - 1`.
uint32_t ragged_id;
/// \brief Index into the `indices` / `load_indices` arrays.
uint32_t batch_id;
/// \brief Global position of the LAST token in this segment.
/// For compress plans, `position % 128 == 127` (chunk-closing); for write
/// plans, `position % 128 < 127`.
uint32_t position;
/// \brief Number of real tokens in this segment (1..128).
/// The first segment token sits at `position - window_len + 1` (global) and
/// at `ragged_id - window_len + 1` (ragged).
uint32_t window_len;
};
static_assert(alignof(OnlinePrefillPlan) == alignof(PrefillPlan));
static_assert(sizeof(OnlinePrefillPlan) == sizeof(PrefillPlan));
} // namespace device::compress
namespace host::compress {
using device::compress::OnlinePrefillPlan;
using OnlinePrefillPlanTensorDtype = uint8_t;
inline constexpr int64_t kOnlinePrefillPlanDim = 16;
static_assert(alignof(OnlinePrefillPlan) == sizeof(OnlinePrefillPlan));
static_assert(sizeof(OnlinePrefillPlan) == kOnlinePrefillPlanDim * sizeof(OnlinePrefillPlanTensorDtype));
} // namespace host::compress
namespace {
using OnlinePlan = device::compress::OnlinePrefillPlan;
using IndiceT = int32_t;
/// \brief Need to reduce register usage to increase occupancy
struct Compress128OnlineDecodeParams {
/** \brief Shape: `[num_indices, 1, head_dim * 3 (max, sum, kv) ]` \n */
void* __restrict__ kv_score_buffer;
/** \brief Shape: `[batch_size, head_dim * 2]` */
const void* __restrict__ kv_score_input;
/** \brief Shape: `[batch_size, head_dim]` */
void* __restrict__ kv_compressed_output;
/** \brief Shape: `[128, head_dim]` (called `ape`) */
const void* __restrict__ score_bias;
/** \brief Shape: `[batch_size, ]`*/
const IndiceT* __restrict__ indices;
/** \brief Shape: `[batch_size, ]` */
const IndiceT* __restrict__ seq_lens;
/** \NOTE: `batch_size` <= `num_indices` */
uint32_t batch_size;
};
/// \brief Need to reduce register usage to increase occupancy
struct Compress128OnlinePrefillParams {
/** \brief Shape: `[num_indices, 1, head_dim * 3 (max, sum, kv) ]` \n */
void* __restrict__ kv_score_buffer;
/** \brief Shape: `[num_q_tokens, head_dim * 2]` */
const void* __restrict__ kv_score_input;
/** \brief Shape: `[num_q_tokens, head_dim]` */
void* __restrict__ kv_compressed_output;
/** \brief Shape: `[128, head_dim]` (called `ape`) */
const void* __restrict__ score_bias;
/** \brief Shape: `[batch_size, ]`*/
const IndiceT* __restrict__ indices;
/** \brief Shape: `[batch_size, ]`*/
const IndiceT* __restrict__ load_indices;
/// \brief Plan for segments that close a chunk (write to `kv_compressed_output`).
/// Shape: `[num_compress, 16]` (uint8).
const OnlinePlan* __restrict__ compress_plan;
/// \brief Plan for the trailing partial segment of each batch (write back to
/// `kv_score_buffer`). Shape: `[num_write, 16]` (uint8).
const OnlinePlan* __restrict__ write_plan;
uint32_t num_compress;
uint32_t num_write;
};
// 4 elements per thread, kHeadDim / 4 threads per block
template <int64_t kHeadDim, bool kUsePDL>
__global__ void flash_c128_online_decode(const __grid_constant__ Compress128OnlineDecodeParams params) {
using namespace device;
constexpr uint32_t kVecSize = 4;
constexpr uint32_t kBlockSize = kHeadDim / kVecSize;
using Vec = AlignedVector<float, kVecSize>;
const auto gmem = tile::Memory<Vec>::cta(kBlockSize);
const auto batch_id = blockIdx.x;
const auto index = params.indices[batch_id];
const auto seq_len = params.seq_lens[batch_id];
const auto kv_score_buffer = static_cast<float*>(params.kv_score_buffer);
const auto kv_buf = kv_score_buffer + index * (kHeadDim * 3);
const auto kv_score_input = static_cast<const float*>(params.kv_score_input);
const auto kv_src = kv_score_input + batch_id * (kHeadDim * 2);
/// NOTE: kv_score_buffer layout is [max, sum, kv] (slot 0 / 1 / 2). Reads,
/// writes, and the prefill kernel must all agree on this order.
const auto max_score_vec = gmem.load(kv_buf, 0);
const auto sum_score_vec = gmem.load(kv_buf, 1);
const auto old_kv_vec = gmem.load(kv_buf, 2);
/// NOTE: kv_score_input layout is | kv | score | (head_dim each), matching
/// the offline c128 kernel and the online prefill kernel.
const auto new_kv_vec = gmem.load(kv_src, 0);
const auto new_score_raw_vec = gmem.load(kv_src, 1);
/// NOTE: the new token sits at global position `seq_len - 1`, so its
/// position inside the 128-chunk is `(seq_len - 1) % 128`. The previous
/// `seq_len % 128` was off by one (`bias[127]` vs `bias[0]`, etc.).
const auto pos_in_chunk = (seq_len - 1) % 128;
const auto bias_vec = gmem.load(params.score_bias, pos_in_chunk);
Vec out_kv_vec;
Vec out_max_vec;
Vec out_sum_vec;
if (pos_in_chunk != 0) {
// Mid-chunk: combine prior partial state with the new token via online softmax.
#pragma unroll
for (uint32_t i = 0; i < 4; ++i) {
const auto old_max = max_score_vec[i];
const auto old_kv = old_kv_vec[i];
const auto new_score = new_score_raw_vec[i] + bias_vec[i];
const auto new_kv = new_kv_vec[i];
const auto new_max = fmax(old_max, new_score);
const auto old_sum = sum_score_vec[i] * expf(old_max - new_max);
const auto new_exp = expf(new_score - new_max);
const auto new_sum = old_sum + new_exp;
out_kv_vec[i] = (old_kv * old_sum + new_kv * new_exp) / new_sum;
out_max_vec[i] = new_max;
out_sum_vec[i] = new_sum;
}
} else {
// First token of a new 128-chunk: initialize state with this token alone.
#pragma unroll
for (uint32_t i = 0; i < 4; ++i) {
out_kv_vec[i] = new_kv_vec[i];
out_max_vec[i] = new_score_raw_vec[i] + bias_vec[i];
out_sum_vec[i] = 1.0f; // exp(score - max) with max == score
}
}
if (pos_in_chunk == 127) {
// Chunk just closed: emit the compressed kv. No need to update the buffer
// -- the next chunk's first token will overwrite it.
const auto kv_out = static_cast<float*>(params.kv_compressed_output) + batch_id * kHeadDim;
gmem.store(kv_out, out_kv_vec);
} else {
// Otherwise persist the running [max, sum, kv] state for the next step.
gmem.store(kv_buf, out_max_vec, 0);
gmem.store(kv_buf, out_sum_vec, 1);
gmem.store(kv_buf, out_kv_vec, 2);
}
}
constexpr int32_t kTileElements = 2; // split (along head-dim)
/// \brief Each warp will handle this many elements (split along softmax-128)
constexpr int32_t kElementsPerWarp = 8;
constexpr uint32_t kNumWarps = 128 / kElementsPerWarp;
constexpr uint32_t kPrefillBlockSize = device::kWarpThreads * kNumWarps;
using PrefillStorage = device::AlignedVector<float, kTileElements>;
struct Compress128SharedBuffer {
using Storage = device::AlignedVector<float, 4>;
Storage data[kNumWarps][device::kWarpThreads + 1]; // padding to avoid bank conflict
SGL_DEVICE Storage& operator()(uint32_t warp_id, uint32_t lane_id) {
return data[warp_id][lane_id];
}
SGL_DEVICE float& operator()(uint32_t warp_id, uint32_t lane_id, uint32_t tile_id) {
return data[warp_id][lane_id][tile_id];
}
};
template <bool kNeedData>
SGL_DEVICE void c128_prefill_forward(
const PrefillStorage (&kv)[kElementsPerWarp],
const PrefillStorage (&score)[kElementsPerWarp],
float* kv_out,
float* max_out,
float* sum_out,
const uint32_t warp_id,
const uint32_t lane_id) {
using namespace device;
/// NOTE: part 2: safe online softmax + weighted sum
using TmpStorage = typename Compress128SharedBuffer::Storage;
__shared__ Compress128SharedBuffer s_local_val_max;
__shared__ Compress128SharedBuffer s_local_exp_sum;
__shared__ Compress128SharedBuffer s_local_product;
TmpStorage tmp_val_max;
TmpStorage tmp_exp_sum;
TmpStorage tmp_product;
#pragma unroll
for (int32_t i = 0; i < kTileElements; ++i) {
float score_fp32[kElementsPerWarp];
#pragma unroll
for (int32_t j = 0; j < kElementsPerWarp; ++j) {
score_fp32[j] = score[j][i];
}
float max_value = score_fp32[0];
float sum_exp_value = 0.0f;
#pragma unroll
for (int32_t j = 1; j < kElementsPerWarp; ++j) {
const auto fp32_score = score_fp32[j];
max_value = fmaxf(max_value, fp32_score);
}
float sum_product = 0.0f;
#pragma unroll
for (int32_t j = 0; j < 8; ++j) {
const auto fp32_score = score_fp32[j];
const auto exp_score = expf(fp32_score - max_value);
sum_product += cast<float>(kv[j][i]) * exp_score;
sum_exp_value += exp_score;
}
tmp_val_max[i] = max_value;
tmp_exp_sum[i] = sum_exp_value;
tmp_product[i] = sum_product;
}
// naturally aligned, so no bank conflict
s_local_val_max(warp_id, lane_id) = tmp_val_max;
s_local_exp_sum(warp_id, lane_id) = tmp_exp_sum;
s_local_product(warp_id, lane_id) = tmp_product;
__syncthreads();
/// NOTE: part 3: online softmax
/// NOTE: We have `kTileElements * kWarpThreads * kNumWarps` values to reduce
/// each reduce will consume `kNumWarps` threads (use partial warp reduction)
constexpr uint32_t kReductionCount = kTileElements * kWarpThreads * kNumWarps;
constexpr uint32_t kIteration = kReductionCount / kPrefillBlockSize;
#pragma unroll
for (uint32_t i = 0; i < kIteration; ++i) {
/// NOTE: Range `[0, kTileElements * kWarpThreads * kNumWarps)`
const uint32_t j = i * kPrefillBlockSize + warp_id * kWarpThreads + lane_id;
/// NOTE: Range `[0, kNumWarps)`
const uint32_t local_warp_id = j % kNumWarps;
/// NOTE: Range `[0, kTileElements * kWarpThreads)`
const uint32_t local_elem_id = j / kNumWarps;
/// NOTE: Range `[0, kTileElements)`
const uint32_t local_tile_id = local_elem_id % kTileElements;
/// NOTE: Range `[0, kWarpThreads)`
const uint32_t local_lane_id = local_elem_id / kTileElements;
/// NOTE: each warp will access the whole tile (all `kTileElements`)
/// and for different lanes, the memory access only differ in `local_warp_id`
/// so there's no bank conflict in shared memory access.
static_assert(kTileElements * kNumWarps == kWarpThreads, "TODO: support other configs");
const auto local_val_max = s_local_val_max(local_warp_id, local_lane_id, local_tile_id);
const auto local_exp_sum = s_local_exp_sum(local_warp_id, local_lane_id, local_tile_id);
const auto local_product = s_local_product(local_warp_id, local_lane_id, local_tile_id);
const auto global_val_max = warp::reduce_max<kNumWarps>(local_val_max);
const auto rescale = expf(local_val_max - global_val_max);
const auto global_exp_sum = warp::reduce_sum<kNumWarps>(local_exp_sum * rescale);
const auto final_scale = rescale / global_exp_sum;
const auto global_product = warp::reduce_sum<kNumWarps>(local_product * final_scale);
kv_out[local_elem_id] = global_product;
if constexpr (kNeedData) {
max_out[local_elem_id] = global_val_max;
sum_out[local_elem_id] = global_exp_sum;
}
}
if constexpr (kNeedData) __syncthreads();
}
/// \brief Sentinel score for padded positions in a 128-segment.
/// Must be finite so that `score - max` never produces NaN even when an
/// entire warp has only padded positions.
constexpr float kPadScore = -FLT_MAX;
/// \brief Online compress 128 prefill. Two passes share this body:
/// - `kWrite=false` (compress pass): handles segments that close a chunk.
/// May load prior partial state from the buffer, but never writes to it,
/// so concurrent blocks can read the same slot without racing.
/// - `kWrite=true` (write pass): handles the trailing partial segment of each
/// batch. Each batch contributes at most one such plan, so concurrent blocks
/// touch disjoint buffer slots.
///
/// The two passes MUST run as separate kernel launches (in stream order) so
/// that all reads in pass 1 finish before any writes in pass 2 start.
template <int64_t kHeadDim, bool kWrite, bool kUsePDL>
__global__ __launch_bounds__(kPrefillBlockSize, 2) //
void flash_c128_online_prefill(const __grid_constant__ Compress128OnlinePrefillParams params) {
using namespace device;
constexpr int64_t kTileDim = kTileElements * kWarpThreads; // 64
constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
static_assert(kHeadDim % kTileDim == 0, "Head dim must be multiple of tile dim");
/// NOTE: the compiler folds the if-else at compile time.
const auto num_plans = kWrite ? params.num_write : params.num_compress;
const auto plan_ptr = kWrite ? params.write_plan : params.compress_plan;
const uint32_t global_id = blockIdx.x;
const uint32_t global_pid = global_id / kNumSplit; // plan id
const uint32_t global_sid = global_id % kNumSplit; // split id
if (global_pid >= num_plans) return;
const auto [ragged_id, batch_id, position, window_len] = plan_ptr[global_pid];
if (ragged_id == 0xFFFFFFFFu) [[unlikely]]
return;
const uint32_t warp_id = threadIdx.x / kWarpThreads;
const uint32_t lane_id = threadIdx.x % kWarpThreads;
const int32_t split_offset = global_sid * kTileDim; // int32 is enough
const auto kv_score_buffer = static_cast<float*>(params.kv_score_buffer);
const auto kv_score_input = static_cast<const float*>(params.kv_score_input);
const auto kv_compressed_output = static_cast<float*>(params.kv_compressed_output);
const auto score_bias_base = static_cast<const float*>(params.score_bias);
constexpr int64_t kElementSize = kHeadDim * 2; // | kv | score |
const uint32_t chunk_offset = (position % 128u) + 1u - window_len;
const uint32_t window_end = chunk_offset + window_len; // exclusive, in [1, 128]
const int32_t segment_start = ragged_id - (position % 128u); // can be negative, but safe
const int32_t load_index = chunk_offset != 0 ? params.load_indices[batch_id] : -1;
const int32_t store_index = kWrite ? params.indices[batch_id] : -1;
PDLWaitPrimary<kUsePDL>();
// 2 * 8 = 16 register per elem. in theory we should consume 48 register here
PrefillStorage kv[kElementsPerWarp];
PrefillStorage score[kElementsPerWarp];
PrefillStorage bias[kElementsPerWarp];
const auto warp_offset = warp_id * kElementsPerWarp;
#pragma unroll
for (uint32_t i = 0; i < kElementsPerWarp; ++i) {
const uint32_t j = i + warp_offset;
if (j >= chunk_offset && j < window_end) {
const auto kv_src_ptr = kv_score_input + (segment_start + j) * kElementSize + split_offset;
const auto score_src_ptr = kv_src_ptr + kHeadDim;
const auto bias_src_ptr = score_bias_base + j * kHeadDim + split_offset;
kv[i].load(kv_src_ptr, lane_id);
score[i].load(score_src_ptr, lane_id);
bias[i].load(bias_src_ptr, lane_id);
}
}
#pragma unroll
for (uint32_t i = 0; i < kElementsPerWarp; ++i) {
const uint32_t j = i + warp_offset;
const bool is_valid = (j >= chunk_offset && j < window_end);
#pragma unroll
for (uint32_t ii = 0; ii < kTileElements; ++ii) {
score[i][ii] = is_valid ? score[i][ii] + bias[i][ii] : kPadScore;
/// NOTE: must zero out kv on padded slots -- `c128_prefill_forward`
/// computes `kv * exp_score` where `exp_score = expf(-FLT_MAX - max) ??? 0`,
/// and IEEE-754 makes `NaN * 0 = NaN` / `+-inf * 0 = NaN`. An
/// uninitialized register can hold a NaN/inf bit pattern, so without
/// this reset a single padded warp can poison the whole softmax.
kv[i][ii] = is_valid ? kv[i][ii] : 0.0f;
}
}
__shared__ alignas(16) float seg_kv[kTileDim];
__shared__ alignas(16) float seg_max[kTileDim];
__shared__ alignas(16) float seg_sum[kTileDim];
c128_prefill_forward<true>(kv, score, seg_kv, seg_max, seg_sum, warp_id, lane_id);
PDLTriggerSecondary<kUsePDL>();
if (warp_id == 0) {
PrefillStorage out_kv_vec, out_max_vec, out_sum_vec;
out_kv_vec.load(seg_kv, lane_id);
out_max_vec.load(seg_max, lane_id);
out_sum_vec.load(seg_sum, lane_id);
if (chunk_offset != 0) {
/// NOTE: load (max, sum, kv) of the in-progress chunk for this index.
/// `load_indices` may differ from `indices` when the prior partial state
/// lives on a different slot than the slot we ultimately write to.
const auto buf_load = kv_score_buffer + load_index * (kHeadDim * 3) + split_offset;
PrefillStorage buf_max_vec, buf_sum_vec, buf_kv_vec;
buf_max_vec.load(buf_load + 0 * kHeadDim, lane_id);
buf_sum_vec.load(buf_load + 1 * kHeadDim, lane_id);
buf_kv_vec.load(buf_load + 2 * kHeadDim, lane_id);
#pragma unroll
for (uint32_t ii = 0; ii < kTileElements; ++ii) {
const float m1 = buf_max_vec[ii];
const float s1 = buf_sum_vec[ii];
const float k1 = buf_kv_vec[ii];
const float m2 = out_max_vec[ii];
const float s2 = out_sum_vec[ii];
const float k2 = out_kv_vec[ii];
const float new_max = fmaxf(m1, m2);
const float new_s1 = s1 * expf(m1 - new_max);
const float new_s2 = s2 * expf(m2 - new_max);
const float new_sum = new_s1 + new_s2;
const float new_kv = (k1 * new_s1 + k2 * new_s2) / new_sum;
out_max_vec[ii] = new_max;
out_sum_vec[ii] = new_sum;
out_kv_vec[ii] = new_kv;
}
}
if constexpr (kWrite) {
const auto buf_store = kv_score_buffer + store_index * (kHeadDim * 3) + split_offset;
reinterpret_cast<PrefillStorage*>(buf_store + 0 * kHeadDim)[lane_id] = out_max_vec;
reinterpret_cast<PrefillStorage*>(buf_store + 1 * kHeadDim)[lane_id] = out_sum_vec;
reinterpret_cast<PrefillStorage*>(buf_store + 2 * kHeadDim)[lane_id] = out_kv_vec;
} else {
const auto out_ptr = kv_compressed_output + ragged_id * kHeadDim + split_offset;
reinterpret_cast<PrefillStorage*>(out_ptr)[lane_id] = out_kv_vec;
}
}
}
template <int64_t kHeadDim, bool kUsePDL>
struct FlashCompress128OnlineKernel {
static constexpr auto decode_kernel = flash_c128_online_decode<kHeadDim, kUsePDL>;
template <bool kWrite>
static constexpr auto prefill_kernel = flash_c128_online_prefill<kHeadDim, kWrite, kUsePDL>;
static constexpr auto prefill_c_kernel = prefill_kernel</*kWrite=*/false>;
static constexpr auto prefill_w_kernel = prefill_kernel</*kWrite=*/true>;
static constexpr int64_t kTileDim = kTileElements * device::kWarpThreads; // 64
static constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
static constexpr uint32_t kDecodeBlockSize = kHeadDim / 4;
static void run_decode(
const tvm::ffi::TensorView kv_score_buffer,
const tvm::ffi::TensorView kv_score_input,
const tvm::ffi::TensorView kv_compressed_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView indices,
const tvm::ffi::TensorView seq_lens,
const tvm::ffi::Optional<tvm::ffi::TensorView> /* UNUSED */) {
using namespace host;
auto B = SymbolicSize{"batch_size"};
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
TensorMatcher({-1, 1, kHeadDim * 3}) // kv score buffer (max, sum, kv)
.with_dtype<float>()
.with_device(device)
.verify(kv_score_buffer);
TensorMatcher({B, kHeadDim * 2}) // kv score input
.with_dtype<float>()
.with_device(device)
.verify(kv_score_input);
TensorMatcher({B, kHeadDim}) // kv compressed output
.with_dtype<float>()
.with_device(device)
.verify(kv_compressed_output);
TensorMatcher({128, kHeadDim}) // ape
.with_dtype<float>()
.with_device(device)
.verify(ape);
TensorMatcher({B}).with_dtype<IndiceT>().with_device(device).verify(indices);
TensorMatcher({B}).with_dtype<IndiceT>().with_device(device).verify(seq_lens);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto params = Compress128OnlineDecodeParams{
.kv_score_buffer = kv_score_buffer.data_ptr(),
.kv_score_input = kv_score_input.data_ptr(),
.kv_compressed_output = kv_compressed_output.data_ptr(),
.score_bias = ape.data_ptr(),
.indices = static_cast<const IndiceT*>(indices.data_ptr()),
.seq_lens = static_cast<const IndiceT*>(seq_lens.data_ptr()),
.batch_size = batch_size,
};
LaunchKernel(batch_size, kDecodeBlockSize, device.unwrap()) //
.enable_pdl(kUsePDL)(decode_kernel, params);
}
static void run_prefill(
const tvm::ffi::TensorView kv_score_buffer,
const tvm::ffi::TensorView kv_score_input,
const tvm::ffi::TensorView kv_compressed_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView indices,
const tvm::ffi::TensorView compress_plan,
const tvm::ffi::TensorView write_plan,
const tvm::ffi::Optional<tvm::ffi::TensorView> extra) {
using namespace host;
using host::compress::kOnlinePrefillPlanDim;
using host::compress::OnlinePrefillPlanTensorDtype;
auto B = SymbolicSize{"batch_size"};
auto N = SymbolicSize{"num_q_tokens"};
auto X = SymbolicSize{"compress_tokens"};
auto Y = SymbolicSize{"write_tokens"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({-1, 1, kHeadDim * 3}) // kv score buffer (max, sum, kv) ??? 2D
.with_dtype<float>()
.with_device(device_)
.verify(kv_score_buffer);
TensorMatcher({N, kHeadDim * 2}) // kv score input
.with_dtype<float>()
.with_device(device_)
.verify(kv_score_input);
TensorMatcher({N, kHeadDim}) // kv compressed output
.with_dtype<float>()
.with_device(device_)
.verify(kv_compressed_output);
TensorMatcher({128, kHeadDim}) // ape
.with_dtype<float>()
.with_device(device_)
.verify(ape);
TensorMatcher({B}) // indices
.with_dtype<IndiceT>()
.with_device(device_)
.verify(indices);
TensorMatcher({X, kOnlinePrefillPlanDim}) // compress plan
.with_dtype<OnlinePrefillPlanTensorDtype>()
.with_device(device_)
.verify(compress_plan);
TensorMatcher({Y, kOnlinePrefillPlanDim}) // write plan
.with_dtype<OnlinePrefillPlanTensorDtype>()
.with_device(device_)
.verify(write_plan);
/// NOTE: `extra` is `load_indices`. When the previous partial state lives
/// on a slot different from the destination slot (e.g. paged buffers), the
/// caller must supply this; otherwise it defaults to `indices`.
const auto load_indices = extra.value_or(indices);
TensorMatcher({B}).with_dtype<IndiceT>().with_device(device_).verify(load_indices);
const auto device = device_.unwrap();
const auto num_c = static_cast<uint32_t>(X.unwrap());
const auto num_w = static_cast<uint32_t>(Y.unwrap());
const auto params = Compress128OnlinePrefillParams{
.kv_score_buffer = kv_score_buffer.data_ptr(),
.kv_score_input = kv_score_input.data_ptr(),
.kv_compressed_output = kv_compressed_output.data_ptr(),
.score_bias = ape.data_ptr(),
.indices = static_cast<const IndiceT*>(indices.data_ptr()),
.load_indices = static_cast<const IndiceT*>(load_indices.data_ptr()),
.compress_plan = static_cast<const OnlinePlan*>(compress_plan.data_ptr()),
.write_plan = static_cast<const OnlinePlan*>(write_plan.data_ptr()),
.num_compress = num_c,
.num_write = num_w,
};
/// NOTE: pass 1 reads the buffer (for the first segment of each batch
/// that started mid-chunk) and writes only to `kv_compressed_output`.
/// Pass 2 then writes the trailing partial state of each batch back to
/// the buffer. Stream serialization between the two launches enforces
/// read-before-write on shared buffer slots.
if (const auto num_c_blocks = num_c * kNumSplit) {
LaunchKernel(num_c_blocks, kPrefillBlockSize, device) //
.enable_pdl(kUsePDL)(prefill_c_kernel, params);
}
if (const auto num_w_blocks = num_w * kNumSplit) {
LaunchKernel(num_w_blocks, kPrefillBlockSize, device) //
.enable_pdl(kUsePDL)(prefill_w_kernel, params);
}
}
};
} // namespace
namespace host::compress {
using OnlinePlanResult = tvm::ffi::Tuple<uint32_t, uint32_t>;
struct OnlinePrefillCompressParams {
OnlinePrefillPlan* __restrict__ compress_plan;
OnlinePrefillPlan* __restrict__ write_plan;
const int64_t* __restrict__ seq_lens;
const int64_t* __restrict__ extend_lens;
uint32_t batch_size;
uint32_t num_tokens;
};
/// \brief Build the compress + write plans for online compress 128 prefill.
///
/// Each batch's `[prefix_len, prefix_len + extend_len)` range is split at
/// 128-aligned boundaries. Every resulting segment falls into one of:
/// - **compress**: closes a 128-chunk (`chunk_offset + window_len == 128`).
/// These plans only read the buffer (when starting mid-chunk) and write the
/// compressed kv to `kv_compressed_output`.
/// - **write**: trailing partial of the batch (`chunk_offset + window_len < 128`).
/// May read the buffer and always writes the new partial state back to it.
/// Each batch produces at most one such plan.
///
/// The two plans MUST be dispatched as separate kernel launches in stream
/// order so that pass-1 reads of a buffer slot complete before any pass-2
/// write of the same slot.
inline OnlinePlanResult plan_online_prefill_host(const OnlinePrefillCompressParams& params, const bool use_cuda_graph) {
const auto& [compress_plan, write_plan, seq_lens, extend_lens, batch_size, num_tokens] = params;
uint32_t counter = 0;
uint32_t compress_count = 0;
uint32_t write_count = 0;
for (const auto i : irange(batch_size)) {
const uint32_t seq_len = static_cast<uint32_t>(seq_lens[i]);
const uint32_t extend_len = static_cast<uint32_t>(extend_lens[i]);
RuntimeCheck(0 < extend_len && extend_len <= seq_len);
const uint32_t prefix_len = seq_len - extend_len;
const uint32_t end_pos = prefix_len + extend_len;
/// NOTE: split the extend range into per-128-chunk segments. Each segment
/// stays inside one chunk, so the kernel can decide load/store from
/// `chunk_offset` and `window_len` alone.
uint32_t pos = prefix_len;
while (pos < end_pos) {
const uint32_t chunk_start = (pos / 128u) * 128u;
const uint32_t seg_end = std::min(end_pos, chunk_start + 128u); // exclusive
const uint32_t seg_len = seg_end - pos;
const uint32_t chunk_off = pos - chunk_start;
/// NOTE: store last-token coordinates so that downstream consumers
/// (e.g. `fused_norm_rope`) can read `ragged_id` and `position` with the
/// same semantics as `PrefillPlan`. The segment start is recoverable as
/// `ragged_id - window_len + 1` and `position - window_len + 1`.
const uint32_t last_pos = seg_end - 1;
const uint32_t last_ragged = counter + (last_pos - prefix_len);
const auto plan = OnlinePrefillPlan{
.ragged_id = last_ragged,
.batch_id = i,
.position = last_pos,
.window_len = seg_len,
};
if (chunk_off + seg_len == 128u) {
// full chunk, must be complete, maybe read the buffer, no write
RuntimeCheck(compress_count < num_tokens);
compress_plan[compress_count++] = plan;
} else {
// last chunk, must be incomplete, maybe read the buffer, must write
RuntimeCheck(write_count < num_tokens);
write_plan[write_count++] = plan;
}
pos = seg_end;
}
counter += extend_len;
}
RuntimeCheck(counter == num_tokens, "input size ", counter, " != num_q_tokens ", num_tokens);
if (!use_cuda_graph) return OnlinePlanResult{compress_count, write_count};
/// NOTE: pad both plans with sentinel entries so cuda-graph runs always see
/// the same number of blocks. The kernel skips plans whose `ragged_id` is -1.
constexpr auto kInvalid = static_cast<uint32_t>(-1);
constexpr auto kInvalidPlan = OnlinePrefillPlan{kInvalid, kInvalid, kInvalid, kInvalid};
for (const auto i : irange(compress_count, num_tokens)) {
compress_plan[i] = kInvalidPlan;
}
for (const auto i : irange(write_count, num_tokens)) {
write_plan[i] = kInvalidPlan;
}
return OnlinePlanResult{num_tokens, num_tokens};
}
inline OnlinePlanResult plan_online_prefill(
const tvm::ffi::TensorView extend_lens,
const tvm::ffi::TensorView seq_lens,
const tvm::ffi::TensorView compress_plan,
const tvm::ffi::TensorView write_plan,
const bool use_cuda_graph) {
auto N = SymbolicSize{"batch_size"};
auto M = SymbolicSize{"num_tokens"};
auto device = SymbolicDevice{};
/// NOTE: only host (CPU/cuda-host) planning is implemented for now. The
device.set_options<kDLCPU, kDLCUDAHost>();
TensorMatcher({N}) //
.with_dtype<int64_t>()
.with_device(device)
.verify(extend_lens)
.verify(seq_lens);
TensorMatcher({M, kOnlinePrefillPlanDim}) //
.with_dtype<OnlinePrefillPlanTensorDtype>()
.with_device(device)
.verify(compress_plan)
.verify(write_plan);
const auto params = OnlinePrefillCompressParams{
.compress_plan = static_cast<OnlinePrefillPlan*>(compress_plan.data_ptr()),
.write_plan = static_cast<OnlinePrefillPlan*>(write_plan.data_ptr()),
.seq_lens = static_cast<const int64_t*>(seq_lens.data_ptr()),
.extend_lens = static_cast<const int64_t*>(extend_lens.data_ptr()),
.batch_size = static_cast<uint32_t>(N.unwrap()),
.num_tokens = static_cast<uint32_t>(M.unwrap()),
};
return plan_online_prefill_host(params, use_cuda_graph);
}
} // namespace host::compress
namespace {
[[maybe_unused]]
constexpr auto& plan_compress_online_prefill = host::compress::plan_online_prefill;
} // namespace
@@ -0,0 +1,876 @@
#include <sgl_kernel/ffi.h>
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/runtime.cuh>
#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/deepseek_v4/compress_v2.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/container/tuple.h>
#include <algorithm>
#include <cfloat>
#include <cstdint>
#include <cstdlib>
#include <cstring>
namespace {
using PlanD = device::compress::DecodePlan;
using PlanC = device::compress::CompressPlan;
// ---------------------------------------------------------------------------
// Decode kernel: 1 token / batch. Each block handles one batch.
// 4 elements per thread -> kBlockSize = head_dim / 4.
// ---------------------------------------------------------------------------
struct Compress128OnlineDecodeParams {
void* __restrict__ kv_score_buffer; // [num_slots, 1, head_dim * 3]
const void* __restrict__ kv_score_input; // [batch_size, head_dim * 2]
void* __restrict__ kv_compressed_output; // [batch_size, head_dim]
const void* __restrict__ score_bias; // [128, head_dim]
const PlanD* __restrict__ plan_d;
uint32_t batch_size;
};
template <int64_t kHeadDim, bool kUsePDL>
__global__ void flash_c128_online_decode_v2(const __grid_constant__ Compress128OnlineDecodeParams params) {
using namespace device;
constexpr uint32_t kVecSize = 4;
constexpr uint32_t kBlockSize = kHeadDim / kVecSize;
using Vec = AlignedVector<float, kVecSize>;
const auto gmem = tile::Memory<Vec>::cta(kBlockSize);
const auto batch_id = blockIdx.x;
if (batch_id >= params.batch_size) return;
// Wait for the plan-finalize kernel to publish `plan.read_page_0 / write_loc`
// before reading the plan. The plan kernel runs on the same stream and does
// NOT issue a PDL trigger, so launching this kernel with PDL means our
// pre-wait global reads can race with the plan kernel's writes.
PDLWaitPrimary<kUsePDL>();
const auto plan = params.plan_d[batch_id];
const auto pos_in_chunk = (plan.seq_len - 1) % 128;
const auto kv_score_buffer = static_cast<float*>(params.kv_score_buffer);
const auto kv_score_input = static_cast<const float*>(params.kv_score_input);
const auto kv_load_buf = kv_score_buffer + plan.read_page_0 * (kHeadDim * 3);
const auto kv_store_buf = kv_score_buffer + plan.write_loc * (kHeadDim * 3);
const auto kv_src = kv_score_input + batch_id * (kHeadDim * 2);
// Buffer layout: [max | sum | kv] (slot 0 / 1 / 2 of the head_dim*3 row).
const auto new_kv_vec = gmem.load(kv_src, 0);
const auto new_score_raw_vec = gmem.load(kv_src, 1);
const auto bias_vec = gmem.load(params.score_bias, pos_in_chunk);
Vec out_kv_vec;
Vec out_max_vec;
Vec out_sum_vec;
if (pos_in_chunk != 0) {
// Mid-chunk: combine prior partial state with the new token.
const auto max_score_vec = gmem.load(kv_load_buf, 0);
const auto sum_score_vec = gmem.load(kv_load_buf, 1);
const auto old_kv_vec = gmem.load(kv_load_buf, 2);
#pragma unroll
for (uint32_t i = 0; i < kVecSize; ++i) {
const auto old_max = max_score_vec[i];
const auto old_kv = old_kv_vec[i];
const auto new_score = new_score_raw_vec[i] + bias_vec[i];
const auto new_kv = new_kv_vec[i];
const auto new_max = fmaxf(old_max, new_score);
const auto old_sum = sum_score_vec[i] * expf(old_max - new_max);
const auto new_exp = expf(new_score - new_max);
const auto new_sum = old_sum + new_exp;
out_kv_vec[i] = (old_kv * old_sum + new_kv * new_exp) / new_sum;
out_max_vec[i] = new_max;
out_sum_vec[i] = new_sum;
}
} else {
// First token of a new chunk: state == this token alone.
#pragma unroll
for (uint32_t i = 0; i < kVecSize; ++i) {
out_kv_vec[i] = new_kv_vec[i];
out_max_vec[i] = new_score_raw_vec[i] + bias_vec[i];
out_sum_vec[i] = 1.0f;
}
}
if (pos_in_chunk == 127) {
// Chunk just closed: emit compressed kv, no buffer update.
const auto kv_out = static_cast<float*>(params.kv_compressed_output) + batch_id * kHeadDim;
gmem.store(kv_out, out_kv_vec);
} else {
gmem.store(kv_store_buf, out_max_vec, 0);
gmem.store(kv_store_buf, out_sum_vec, 1);
gmem.store(kv_store_buf, out_kv_vec, 2);
}
}
// ---------------------------------------------------------------------------
// Prefill kernel: 1 segment / block. Two passes (compress + write) share the
// kernel template, parameterized by `kWrite`.
// 16 warps per block; each warp handles 8 of the 128 chunk positions.
// ---------------------------------------------------------------------------
constexpr int32_t kTileElements = 2; // split along head-dim
constexpr int32_t kElementsPerWarp = 8; // split along the 128-chunk
constexpr uint32_t kNumWarps = 128 / kElementsPerWarp;
constexpr uint32_t kPrefillBlockSize = device::kWarpThreads * kNumWarps;
using PrefillStorage = device::AlignedVector<float, kTileElements>;
struct Compress128OnlinePrefillParams {
void* __restrict__ kv_score_buffer; // [num_slots, 1, head_dim * 3]
const void* __restrict__ kv_score_input; // [num_q_tokens, head_dim * 2]
void* __restrict__ kv_compressed_output; // [num_compress, head_dim]
const void* __restrict__ score_bias; // [128, head_dim]
const PlanC* __restrict__ plan_c; // close-chunk segments
const PlanC* __restrict__ plan_w; // trailing partial segments
uint32_t num_compress;
uint32_t num_write;
};
struct Compress128SharedBuffer {
using Storage = device::AlignedVector<float, 4>;
Storage data[kNumWarps][device::kWarpThreads + 1]; // +1 to avoid bank conflict
SGL_DEVICE Storage& operator()(uint32_t warp_id, uint32_t lane_id) {
return data[warp_id][lane_id];
}
SGL_DEVICE float& operator()(uint32_t warp_id, uint32_t lane_id, uint32_t tile_id) {
return data[warp_id][lane_id][tile_id];
}
};
/// \brief Sentinel score for padded positions in a 128-segment.
constexpr float kPadScore = -FLT_MAX;
[[maybe_unused]]
SGL_DEVICE void c128_prefill_segment_softmax(
const PrefillStorage (&kv)[kElementsPerWarp],
const PrefillStorage (&score)[kElementsPerWarp],
float* seg_kv,
float* seg_max,
float* seg_sum,
const uint32_t warp_id,
const uint32_t lane_id) {
using namespace device;
// Per-warp running state (max, sum, kv) for kTileElements head-dim slots.
using TmpStorage = typename Compress128SharedBuffer::Storage;
__shared__ Compress128SharedBuffer s_local_val_max;
__shared__ Compress128SharedBuffer s_local_exp_sum;
__shared__ Compress128SharedBuffer s_local_product;
TmpStorage tmp_val_max;
TmpStorage tmp_exp_sum;
TmpStorage tmp_product;
#pragma unroll
for (int32_t i = 0; i < kTileElements; ++i) {
float score_fp32[kElementsPerWarp];
#pragma unroll
for (int32_t j = 0; j < kElementsPerWarp; ++j) {
score_fp32[j] = score[j][i];
}
float max_value = score_fp32[0];
#pragma unroll
for (int32_t j = 1; j < kElementsPerWarp; ++j) {
max_value = fmaxf(max_value, score_fp32[j]);
}
float sum_exp_value = 0.0f;
float sum_product = 0.0f;
#pragma unroll
for (int32_t j = 0; j < kElementsPerWarp; ++j) {
const auto exp_score = expf(score_fp32[j] - max_value);
sum_product += kv[j][i] * exp_score;
sum_exp_value += exp_score;
}
tmp_val_max[i] = max_value;
tmp_exp_sum[i] = sum_exp_value;
tmp_product[i] = sum_product;
}
// Aligned writes (no bank conflict thanks to `+1` padding).
s_local_val_max(warp_id, lane_id) = tmp_val_max;
s_local_exp_sum(warp_id, lane_id) = tmp_exp_sum;
s_local_product(warp_id, lane_id) = tmp_product;
__syncthreads();
// Cross-warp reduction. Same recipe as c128_online.cuh: each block-thread
// pair reduces a (tile_id, lane_id) slot using a kNumWarps-wide warp shuffle.
constexpr uint32_t kReductionCount = kTileElements * kWarpThreads * kNumWarps;
constexpr uint32_t kIteration = kReductionCount / kPrefillBlockSize;
static_assert(kTileElements * kNumWarps == kWarpThreads, "TODO: support other configs");
#pragma unroll
for (uint32_t i = 0; i < kIteration; ++i) {
const uint32_t j = i * kPrefillBlockSize + warp_id * kWarpThreads + lane_id;
const uint32_t local_warp_id = j % kNumWarps;
const uint32_t local_elem_id = j / kNumWarps;
const uint32_t local_tile_id = local_elem_id % kTileElements;
const uint32_t local_lane_id = local_elem_id / kTileElements;
const auto local_val_max = s_local_val_max(local_warp_id, local_lane_id, local_tile_id);
const auto local_exp_sum = s_local_exp_sum(local_warp_id, local_lane_id, local_tile_id);
const auto local_product = s_local_product(local_warp_id, local_lane_id, local_tile_id);
const auto global_val_max = warp::reduce_max<kNumWarps>(local_val_max);
const auto rescale = expf(local_val_max - global_val_max);
const auto global_exp_sum = warp::reduce_sum<kNumWarps>(local_exp_sum * rescale);
const auto final_scale = rescale / global_exp_sum;
const auto global_product = warp::reduce_sum<kNumWarps>(local_product * final_scale);
seg_kv[local_elem_id] = global_product;
seg_max[local_elem_id] = global_val_max;
seg_sum[local_elem_id] = global_exp_sum;
}
__syncthreads();
}
/// \brief Online compress 128 prefill v2.
///
/// `kWrite=false` (compress pass): handles segments that close a 128-chunk.
/// Reads optional prior state from `read_page_0` (-1 = none), emits compressed
/// kv to `kv_compressed_output[plan_id]` (compact).
/// `kWrite=true` (write pass) : handles trailing partial segments.
/// Reads optional prior state from `read_page_1` (-1 = fallback to
/// `read_page_0`), writes new running state to `read_page_0`.
template <int64_t kHeadDim, bool kWrite, bool kUsePDL>
__global__ __launch_bounds__(kPrefillBlockSize, 2) //
void flash_c128_online_prefill_v2(const __grid_constant__ Compress128OnlinePrefillParams params) {
using namespace device;
constexpr int64_t kTileDim = kTileElements * kWarpThreads; // 64
constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
static_assert(kHeadDim % kTileDim == 0);
// Compile-time fold to the right plan list.
const auto num_plans = kWrite ? params.num_write : params.num_compress;
const auto plan_ptr = kWrite ? params.plan_w : params.plan_c;
const uint32_t global_id = blockIdx.x;
const uint32_t global_pid = global_id / kNumSplit;
const uint32_t global_sid = global_id % kNumSplit;
if (global_pid >= num_plans) return;
const uint32_t warp_id = threadIdx.x / kWarpThreads;
const uint32_t lane_id = threadIdx.x % kWarpThreads;
const int32_t split_offset = global_sid * kTileDim;
// The previous kernel (plan-finalize stage 1) does NOT issue a PDL trigger,
// so PDLWaitPrimary effectively waits for stage 1 to complete. Read the plan
// AFTER the wait so the freshly-written `read_page_0` (= state-pool slot) is
// visible. Reading it before the wait is a real race -- with PDL enabled the
// kernel can begin executing before stage 1's stores propagate, and we'd see
// the stage-0 batch_id placeholder in `read_page_0` instead of the slot.
PDLWaitPrimary<kUsePDL>();
const auto plan = plan_ptr[global_pid];
if (plan.is_invalid()) [[unlikely]]
return;
const auto kv_score_buffer = static_cast<float*>(params.kv_score_buffer);
const auto kv_score_input = static_cast<const float*>(params.kv_score_input);
const auto kv_compressed_output = static_cast<float*>(params.kv_compressed_output);
const auto score_bias_base = static_cast<const float*>(params.score_bias);
constexpr int64_t kElementSize = kHeadDim * 2; // | kv | score |
// `j` below is a chunk-local offset. Convert it to the ragged-input row by
// anchoring on the last token in this segment: ragged_id - pos_in_chunk_end + 1 + j.
const uint32_t window_len = plan.buffer_len;
const uint32_t position = plan.seq_len - 1;
const uint32_t pos_in_chunk_end = (position % 128u) + 1u; // exclusive, in [1, 128]
const uint32_t chunk_offset = pos_in_chunk_end - window_len; // in [0, 127]
const int32_t chunk_start_ragged = static_cast<int32_t>(plan.ragged_id) - static_cast<int32_t>(pos_in_chunk_end) + 1;
// --- Stage 1: load kv / score / bias for this warp's 8 chunk positions.
PrefillStorage kv[kElementsPerWarp];
PrefillStorage score[kElementsPerWarp];
PrefillStorage bias[kElementsPerWarp];
const uint32_t warp_offset = warp_id * kElementsPerWarp;
#pragma unroll
for (uint32_t i = 0; i < kElementsPerWarp; ++i) {
const uint32_t j = i + warp_offset;
if (j >= chunk_offset && j < pos_in_chunk_end) {
const int32_t ragged_id = chunk_start_ragged + static_cast<int32_t>(j);
const auto kv_src_ptr = kv_score_input + ragged_id * kElementSize + split_offset;
const auto score_src_ptr = kv_src_ptr + kHeadDim;
const auto bias_src_ptr = score_bias_base + j * kHeadDim + split_offset;
kv[i].load(kv_src_ptr, lane_id);
score[i].load(score_src_ptr, lane_id);
bias[i].load(bias_src_ptr, lane_id);
}
}
// --- Stage 2: pad invalid positions. score = -FLT_MAX, kv = 0 (so that
// kv * exp(score-max) ??? 0 / 0 cleanly without producing NaN/inf).
#pragma unroll
for (uint32_t i = 0; i < kElementsPerWarp; ++i) {
const uint32_t j = i + warp_offset;
const bool is_valid = (j >= chunk_offset && j < pos_in_chunk_end);
#pragma unroll
for (uint32_t ii = 0; ii < kTileElements; ++ii) {
score[i][ii] = is_valid ? score[i][ii] + bias[i][ii] : kPadScore;
kv[i][ii] = is_valid ? kv[i][ii] : 0.0f;
}
}
// --- Stage 3: warp-tile online softmax over the 128-position chunk.
__shared__ alignas(16) float seg_kv[kTileDim];
__shared__ alignas(16) float seg_max[kTileDim];
__shared__ alignas(16) float seg_sum[kTileDim];
c128_prefill_segment_softmax(kv, score, seg_kv, seg_max, seg_sum, warp_id, lane_id);
PDLTriggerSecondary<kUsePDL>();
// --- Stage 4: warp 0 folds with prior partial state (if any) and writes.
if (warp_id == 0) {
PrefillStorage out_kv_vec, out_max_vec, out_sum_vec;
out_kv_vec.load(seg_kv, lane_id);
out_max_vec.load(seg_max, lane_id);
out_sum_vec.load(seg_sum, lane_id);
const int32_t read_page = plan.read_page_1 >= 0 ? plan.read_page_1 : plan.read_page_0;
if (chunk_offset != 0 && read_page >= 0) {
// Combine with prior partial state for this slot.
const auto buf_load = kv_score_buffer + read_page * (kHeadDim * 3) + split_offset;
PrefillStorage buf_max_vec, buf_sum_vec, buf_kv_vec;
buf_max_vec.load(buf_load + 0 * kHeadDim, lane_id);
buf_sum_vec.load(buf_load + 1 * kHeadDim, lane_id);
buf_kv_vec.load(buf_load + 2 * kHeadDim, lane_id);
#pragma unroll
for (uint32_t ii = 0; ii < kTileElements; ++ii) {
const float m1 = buf_max_vec[ii];
const float s1 = buf_sum_vec[ii];
const float k1 = buf_kv_vec[ii];
const float m2 = out_max_vec[ii];
const float s2 = out_sum_vec[ii];
const float k2 = out_kv_vec[ii];
const float new_max = fmaxf(m1, m2);
const float new_s1 = s1 * expf(m1 - new_max);
const float new_s2 = s2 * expf(m2 - new_max);
const float new_sum = new_s1 + new_s2;
const float new_kv = (k1 * new_s1 + k2 * new_s2) / new_sum;
out_max_vec[ii] = new_max;
out_sum_vec[ii] = new_sum;
out_kv_vec[ii] = new_kv;
}
}
if constexpr (kWrite) {
// For trailing-partial segments the load and store slots collapse to the
// segment's own chunk slot (the request keeps a single in-progress
// chunk's running state at any time), so we reuse `read_page_0`.
const auto buf_store = kv_score_buffer + plan.read_page_0 * (kHeadDim * 3) + split_offset;
reinterpret_cast<PrefillStorage*>(buf_store + 0 * kHeadDim)[lane_id] = out_max_vec;
reinterpret_cast<PrefillStorage*>(buf_store + 1 * kHeadDim)[lane_id] = out_sum_vec;
reinterpret_cast<PrefillStorage*>(buf_store + 2 * kHeadDim)[lane_id] = out_kv_vec;
} else {
// Compact output: one row per compress plan, indexed by `global_pid`.
const auto out_ptr = kv_compressed_output + global_pid * kHeadDim + split_offset;
reinterpret_cast<PrefillStorage*>(out_ptr)[lane_id] = out_kv_vec;
}
}
}
// ---------------------------------------------------------------------------
// Host wrapper: matches the c128_v2 / c4_v2 host API style (run_decode /
// run_prefill methods on a kernel-class template). We only expose `kHeadDim`
// + `kUsePDL`; the dtype is fixed to fp32 for the online state pool.
// ---------------------------------------------------------------------------
template <int64_t kHeadDim, bool kUsePDL>
struct FlashCompress128OnlineKernel {
static constexpr auto decode_kernel = flash_c128_online_decode_v2<kHeadDim, kUsePDL>;
template <bool kWrite>
static constexpr auto prefill_kernel = flash_c128_online_prefill_v2<kHeadDim, kWrite, kUsePDL>;
static constexpr int64_t kTileDim = kTileElements * device::kWarpThreads; // 64
static constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
static constexpr uint32_t kDecodeBlockSize = kHeadDim / 4;
static void run_decode(
const tvm::ffi::TensorView kv_score_buffer,
const tvm::ffi::TensorView kv_score_input,
const tvm::ffi::TensorView kv_compressed_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView plan_d_) {
using namespace host;
auto B = SymbolicSize{"batch_size"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({-1, 1, kHeadDim * 3}) // kv score buffer (max, sum, kv)
.with_dtype<float>()
.with_device(device_)
.verify(kv_score_buffer);
TensorMatcher({B, kHeadDim * 2}) // kv score input
.with_dtype<float>()
.with_device(device_)
.verify(kv_score_input);
TensorMatcher({B, kHeadDim}) // kv compressed output (sparse by batch_id)
.with_dtype<float>()
.with_device(device_)
.verify(kv_compressed_output);
TensorMatcher({128, kHeadDim}) // ape
.with_dtype<float>()
.with_device(device_)
.verify(ape);
const auto plan_d = compress::verify_plan_d(plan_d_, B, device_);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
if (batch_size == 0) return;
const auto params = Compress128OnlineDecodeParams{
.kv_score_buffer = kv_score_buffer.data_ptr(),
.kv_score_input = kv_score_input.data_ptr(),
.kv_compressed_output = kv_compressed_output.data_ptr(),
.score_bias = ape.data_ptr(),
.plan_d = plan_d,
.batch_size = batch_size,
};
LaunchKernel(batch_size, kDecodeBlockSize, device_.unwrap()) //
.enable_pdl(kUsePDL)(decode_kernel, params);
}
static void run_prefill(
const tvm::ffi::TensorView kv_score_buffer,
const tvm::ffi::TensorView kv_score_input,
const tvm::ffi::TensorView kv_compressed_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView plan_c_,
const tvm::ffi::TensorView plan_w_) {
using namespace host;
auto N = SymbolicSize{"num_q_tokens"};
auto C = SymbolicSize{"num_c_plans"};
auto W = SymbolicSize{"num_w_plans"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({-1, 1, kHeadDim * 3}) // kv score buffer
.with_dtype<float>()
.with_device(device_)
.verify(kv_score_buffer);
TensorMatcher({N, kHeadDim * 2}) // kv score input (ragged)
.with_dtype<float>()
.with_device(device_)
.verify(kv_score_input);
TensorMatcher({C, kHeadDim}) // kv compressed output (compact, by plan_c index)
.with_dtype<float>()
.with_device(device_)
.verify(kv_compressed_output);
TensorMatcher({128, kHeadDim}) // ape
.with_dtype<float>()
.with_device(device_)
.verify(ape);
// Both compress and write segments use PlanC layout. Stage 1 stores the
// committed-bank load slot in read_page_1 and the write slot in read_page_0.
const auto plan_c = compress::verify_plan_c(plan_c_, C, device_);
const auto plan_w = compress::verify_plan_c(plan_w_, W, device_);
const auto device = device_.unwrap();
const auto num_q_tokens = static_cast<uint32_t>(N.unwrap());
const auto num_c = static_cast<uint32_t>(C.unwrap());
const auto num_w = static_cast<uint32_t>(W.unwrap());
RuntimeCheck(num_q_tokens >= num_w, "invalid prefill plan: num_q < num_w");
const auto params = Compress128OnlinePrefillParams{
.kv_score_buffer = kv_score_buffer.data_ptr(),
.kv_score_input = kv_score_input.data_ptr(),
.kv_compressed_output = kv_compressed_output.data_ptr(),
.score_bias = ape.data_ptr(),
.plan_c = plan_c,
.plan_w = plan_w,
.num_compress = num_c,
.num_write = num_w,
};
// The two passes MUST be serialized in stream order: pass 1 reads slots
// that pass 2 may write to; running them in parallel would race.
if (const auto num_c_blocks = num_c * kNumSplit) {
LaunchKernel(num_c_blocks, kPrefillBlockSize, device) //
.enable_pdl(kUsePDL)(prefill_kernel</*kWrite=*/false>, params);
}
if (const auto num_w_blocks = num_w * kNumSplit) {
LaunchKernel(num_w_blocks, kPrefillBlockSize, device) //
.enable_pdl(kUsePDL)(prefill_kernel</*kWrite=*/true>, params);
}
}
};
} // namespace
// ===========================================================================
// Plan builders. Mirrors the offline v2 pattern (`c_plan.cuh`):
// - Decode: a single GPU kernel reads seq_lens / req_to_token /
// req_pool_indices on device and emits the final PlanD tensor in one go.
// - Prefill: stage 0 (host, on CPU pinned memory) splits each batch's
// extend range into per-chunk segments and emits PlanC entries with the
// batch_id stashed in `read_page_0` as a placeholder. Stage 1 is a tiny
// GPU kernel that finalizes `read_page_0` to `req_to_token[rid][chunk_start]`,
// so the slot tensors never leave GPU memory. The online state pool keeps
// a single in-progress chunk per request, so each segment's load and
// store slot collapse to one value (the slot for the segment's own chunk).
// For online-c128 MTP, stage 1 keeps that write slot in `read_page_0` and
// stores the committed-bank load slot in `read_page_1`.
// ===========================================================================
namespace host::compress {
using device::compress::CompressPlan;
using device::compress::DecodePlan;
// ---------------------------------------------------------------------------
// Decode plan builder.
// ---------------------------------------------------------------------------
struct OnlineDecodePlanParams {
DecodePlan* __restrict__ plan_d;
const int64_t* __restrict__ seq_lens;
const int64_t* __restrict__ req_pool_indices;
const int32_t* __restrict__ req_to_token;
int64_t stride_r2t;
int32_t state_slot_offset;
uint32_t batch_size;
};
__global__ void plan_c128_online_decode_kernel(const OnlineDecodePlanParams params) {
const uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= params.batch_size) return;
const auto seq_len = static_cast<uint32_t>(params.seq_lens[idx]);
const auto rid = params.req_pool_indices[idx];
const int32_t slot = static_cast<int32_t>(rid) + params.state_slot_offset;
params.plan_d[idx] = DecodePlan{
.seq_len = seq_len,
.write_loc = slot,
.read_page_0 = slot,
.read_page_1 = -1,
};
}
/// \brief Build the decode plan tensor. Caller (Python) pre-allocates
/// `plan_d_dev` as a `(batch_size, 16)` device uint8 tensor; this routine
/// only fills it. See `plan_online_prefill` for the rationale (avoid
/// `ffi::empty` + dlpack roundtrip / PyTorch caching-allocator stream
/// tracking issue that surfaces as IMA in unrelated downstream kernels).
inline void plan_online_decode(
const tvm::ffi::TensorView seq_lens,
const tvm::ffi::TensorView req_pool_indices,
const tvm::ffi::TensorView req_to_token,
const tvm::ffi::TensorView plan_d_dev_,
const int32_t state_slot_offset) {
auto B = SymbolicSize{"batch_size"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
auto seq_dtype = SymbolicDType{};
TensorMatcher({B}) //
.with_dtype<int64_t>(seq_dtype)
.with_device(device_)
.verify(seq_lens);
TensorMatcher({B}) //
.with_dtype<int64_t>()
.with_device(device_)
.verify(req_pool_indices);
TensorMatcher({-1, -1}) //
.with_dtype<int32_t>()
.with_device(device_)
.verify(req_to_token);
TensorMatcher({B, sizeof(DecodePlan)}) //
.with_dtype<uint8_t>()
.with_device(device_)
.verify(plan_d_dev_);
RuntimeCheck(state_slot_offset >= 0);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
if (batch_size == 0) return;
const auto device = device_.unwrap();
constexpr uint32_t kBlockSize = 256;
const uint32_t num_blocks = host::div_ceil(batch_size, kBlockSize);
const auto stride_r2t = req_to_token.stride(0);
const auto params = OnlineDecodePlanParams{
.plan_d = static_cast<DecodePlan*>(plan_d_dev_.data_ptr()),
.seq_lens = static_cast<const int64_t*>(seq_lens.data_ptr()),
.req_pool_indices = static_cast<const int64_t*>(req_pool_indices.data_ptr()),
.req_to_token = static_cast<const int32_t*>(req_to_token.data_ptr()),
.stride_r2t = stride_r2t,
.state_slot_offset = state_slot_offset,
.batch_size = batch_size,
};
LaunchKernel(num_blocks, kBlockSize, device)(plan_c128_online_decode_kernel, params);
}
// ---------------------------------------------------------------------------
// Prefill plan builder: host stage 0 + GPU stage 1.
// ---------------------------------------------------------------------------
struct OnlinePrefillStage0Params {
CompressPlan* __restrict__ plan_c;
CompressPlan* __restrict__ plan_w;
const int64_t* __restrict__ seq_lens;
const int64_t* __restrict__ extend_lens;
uint32_t batch_size;
uint32_t num_q_tokens;
};
inline std::tuple<uint32_t, uint32_t> _plan_prefill_partial(const OnlinePrefillStage0Params& p) {
uint32_t counter = 0;
uint32_t compress_count = 0;
uint32_t write_count = 0;
for (const auto i : irange(p.batch_size)) {
const uint32_t seq_len = static_cast<uint32_t>(p.seq_lens[i]);
const uint32_t extend_len = static_cast<uint32_t>(p.extend_lens[i]);
RuntimeCheck(0 < extend_len && extend_len <= seq_len);
const uint32_t prefix_len = seq_len - extend_len;
const uint32_t end_pos = prefix_len + extend_len;
uint32_t pos = prefix_len;
while (pos < end_pos) {
const uint32_t chunk_start = (pos / 128u) * 128u;
const uint32_t seg_end = std::min(end_pos, chunk_start + 128u); // exclusive
const uint32_t seg_len = seg_end - pos;
const uint32_t chunk_off = pos - chunk_start;
const uint32_t last_pos = seg_end - 1;
const uint32_t last_ragged = counter + (last_pos - prefix_len);
RuntimeCheck(last_ragged < (1u << 16), "PlanC.ragged_id is uint16; ragged ", last_ragged, " overflows");
RuntimeCheck(seg_len <= 128u);
// Stash batch_id in `read_page_0` for stage 1 to translate. A
// chunk-aligned segment never loads, so we still need stage 1 to fill
// a slot in -- the kernel keys the load on `chunk_offset != 0`.
const auto plan = CompressPlan{
.seq_len = last_pos + 1u,
.ragged_id = static_cast<uint16_t>(last_ragged),
.buffer_len = static_cast<uint16_t>(seg_len),
.read_page_0 = static_cast<int32_t>(i), // batch_id placeholder
.read_page_1 = -1, // filled by stage 1 with committed-bank slot
};
if (chunk_off + seg_len == 128u) {
// close-chunk segment
RuntimeCheck(compress_count < p.num_q_tokens);
p.plan_c[compress_count++] = plan;
} else {
// trailing partial segment
RuntimeCheck(write_count < p.num_q_tokens);
p.plan_w[write_count++] = plan;
}
pos = seg_end;
}
counter += extend_len;
}
RuntimeCheck(counter == p.num_q_tokens, "input size ", counter, " != num_q_tokens ", p.num_q_tokens);
return std::tuple<uint32_t, uint32_t>{compress_count, write_count};
}
struct OnlinePrefillStage1Params {
CompressPlan* __restrict__ plan_c;
CompressPlan* __restrict__ plan_w;
const int64_t* __restrict__ req_pool_indices; // (batch_size,)
const int32_t* __restrict__ req_to_token; // (num_reqs, max_tokens)
int64_t stride_r2t;
int32_t state_slot_offset;
uint32_t num_c;
uint32_t num_w;
};
__global__ void plan_c128_online_prefill_kernel(const OnlinePrefillStage1Params params) {
const uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x;
const uint32_t total = params.num_c + params.num_w;
if (idx >= total) return;
const bool is_compress = idx < params.num_c;
CompressPlan* const plan_ptr = is_compress ? &params.plan_c[idx] : &params.plan_w[idx - params.num_c];
auto plan = *plan_ptr;
if (plan.is_invalid()) return;
const auto batch_id = plan.read_page_0;
const auto rid = params.req_pool_indices[batch_id];
const int32_t main_slot = static_cast<int32_t>(rid);
plan.read_page_0 = main_slot + params.state_slot_offset;
plan.read_page_1 = main_slot;
*plan_ptr = plan;
}
using OnlinePrefillPlan = tvm::ffi::Tuple<uint32_t, uint32_t>;
inline OnlinePrefillPlan plan_online_prefill(
const tvm::ffi::TensorView seq_lens,
const tvm::ffi::TensorView extend_lens,
const tvm::ffi::TensorView req_pool_indices,
const tvm::ffi::TensorView req_to_token,
const tvm::ffi::TensorView plan_c_pin,
const tvm::ffi::TensorView plan_w_pin,
const tvm::ffi::TensorView plan_c_dev_,
const tvm::ffi::TensorView plan_w_dev_,
const int32_t state_slot_offset,
const bool use_cuda_graph) {
auto B = SymbolicSize{"batch_size"};
auto N = SymbolicSize{"num_q_tokens"};
auto cpu = SymbolicDevice{};
auto device_ = SymbolicDevice{};
cpu.set_options<kDLCPU, kDLCUDAHost>();
device_.set_options<kDLCUDA>();
TensorMatcher({B}) //
.with_dtype<int64_t>()
.with_device(cpu)
.verify(seq_lens)
.verify(extend_lens);
TensorMatcher({B}) //
.with_dtype<int64_t>()
.with_device(device_)
.verify(req_pool_indices);
TensorMatcher({-1, -1}) //
.with_dtype<int32_t>()
.with_device(device_)
.verify(req_to_token);
TensorMatcher({N, sizeof(CompressPlan)}) //
.with_dtype<uint8_t>()
.with_device(cpu)
.verify(plan_c_pin)
.verify(plan_w_pin);
TensorMatcher({N, sizeof(CompressPlan)}) //
.with_dtype<uint8_t>()
.with_device(device_)
.verify(plan_c_dev_)
.verify(plan_w_dev_);
RuntimeCheck(state_slot_offset >= 0);
const auto stage0_params = OnlinePrefillStage0Params{
.plan_c = static_cast<CompressPlan*>(plan_c_pin.data_ptr()),
.plan_w = static_cast<CompressPlan*>(plan_w_pin.data_ptr()),
.seq_lens = static_cast<const int64_t*>(seq_lens.data_ptr()),
.extend_lens = static_cast<const int64_t*>(extend_lens.data_ptr()),
.batch_size = static_cast<uint32_t>(B.unwrap()),
.num_q_tokens = static_cast<uint32_t>(N.unwrap()),
};
// Debug instrumentation: SGLANG_DEBUG_C128_ONLINE_GUARD=1 wraps stage 0
// with redzone + post-write magic-check on the pin buffers, plus a strict
// upper-bound check on `batch_size` and `num_q_tokens`. If stage 0 has a
// CPU OOB this trips a clear panic at the offending byte instead of a
// delayed CUDA IMA from corrupted heap memory.
static const bool kGuard = []() {
const char* v = std::getenv("SGLANG_DEBUG_C128_ONLINE_GUARD");
return v != nullptr && v[0] == '1';
}();
if (kGuard) {
RuntimeCheck(stage0_params.batch_size <= 65536u, "batch_size out of bound: ", stage0_params.batch_size);
RuntimeCheck(stage0_params.num_q_tokens <= 65536u, "num_q_tokens out of bound: ", stage0_params.num_q_tokens);
// Stamp the pin buffers with 0xAB so we can detect any byte still 0xAB
// beyond what stage 0 should have written (= OOB never reached, that's fine)
// or any byte BEYOND num_q_tokens*16 written to (= true OOB into
// adjacent allocation).
auto* pc = static_cast<uint8_t*>(plan_c_pin.data_ptr());
auto* pw = static_cast<uint8_t*>(plan_w_pin.data_ptr());
const auto bytes = static_cast<size_t>(N.unwrap()) * sizeof(CompressPlan);
std::memset(pc, 0xAB, bytes);
std::memset(pw, 0xAB, bytes);
}
const auto [num_c, num_w] = _plan_prefill_partial(stage0_params);
const auto num_c_padded = use_cuda_graph ? static_cast<uint32_t>(N.unwrap()) : num_c;
const auto num_w_padded = use_cuda_graph ? static_cast<uint32_t>(N.unwrap()) : num_w;
if (kGuard) {
// Verify stage 0 wrote ONLY to the [0, num_c*16) and [0, num_w*16) prefix.
auto* pc = static_cast<const uint8_t*>(plan_c_pin.data_ptr());
auto* pw = static_cast<const uint8_t*>(plan_w_pin.data_ptr());
const auto end_c = static_cast<size_t>(num_c) * sizeof(CompressPlan);
const auto end_w = static_cast<size_t>(num_w) * sizeof(CompressPlan);
const auto pin_bytes = static_cast<size_t>(N.unwrap()) * sizeof(CompressPlan);
for (size_t k = end_c; k < pin_bytes; ++k) {
RuntimeCheck(
pc[k] == 0xAB,
"GUARD: plan_c_pin OOB write at byte ",
k,
" (num_c=",
num_c,
", num_q_tokens=",
N.unwrap(),
")");
}
for (size_t k = end_w; k < pin_bytes; ++k) {
RuntimeCheck(
pw[k] == 0xAB,
"GUARD: plan_w_pin OOB write at byte ",
k,
" (num_w=",
num_w,
", num_q_tokens=",
N.unwrap(),
")");
}
}
const auto device = device_.unwrap();
// Out-params pre-allocated by Python. Cast to typed pointers for use.
auto* const plan_c_dev_ptr = static_cast<CompressPlan*>(plan_c_dev_.data_ptr());
auto* const plan_w_dev_ptr = static_cast<CompressPlan*>(plan_w_dev_.data_ptr());
if (use_cuda_graph) {
const auto kInvalidPlan = CompressPlan::invalid();
auto* const plan_c_pin_ptr = static_cast<CompressPlan*>(plan_c_pin.data_ptr());
auto* const plan_w_pin_ptr = static_cast<CompressPlan*>(plan_w_pin.data_ptr());
for (const auto i : irange(num_c, num_c_padded)) {
plan_c_pin_ptr[i] = kInvalidPlan;
}
for (const auto i : irange(num_w, num_w_padded)) {
plan_w_pin_ptr[i] = kInvalidPlan;
}
}
if (const auto total = num_c_padded + num_w_padded) {
const auto stream = LaunchKernel::resolve_device(device);
// SGLANG_DEBUG_C128_ONLINE_SYNC_H2D=1 forces a synchronous H2D copy.
static const bool kSyncH2D = []() {
const char* v = std::getenv("SGLANG_DEBUG_C128_ONLINE_SYNC_H2D");
return v != nullptr && v[0] == '1';
}();
// SGLANG_DEBUG_C128_ONLINE_NO_H2D=1 skips the H2D copy entirely (debug only).
static const bool kNoH2D = []() {
const char* v = std::getenv("SGLANG_DEBUG_C128_ONLINE_NO_H2D");
return v != nullptr && v[0] == '1';
}();
const auto copy_to_device = [stream](void* dst, void* src, int64_t count) {
if (kNoH2D) return;
const auto bytes = count * sizeof(CompressPlan);
if (kSyncH2D) {
RuntimeDeviceCheck(::cudaMemcpy(dst, src, bytes, ::cudaMemcpyHostToDevice));
} else {
RuntimeDeviceCheck(::cudaMemcpyAsync(dst, src, bytes, ::cudaMemcpyHostToDevice, stream));
}
};
if (num_c_padded) copy_to_device(plan_c_dev_ptr, plan_c_pin.data_ptr(), num_c_padded);
if (num_w_padded) copy_to_device(plan_w_dev_ptr, plan_w_pin.data_ptr(), num_w_padded);
const auto stage1_params = OnlinePrefillStage1Params{
.plan_c = plan_c_dev_ptr,
.plan_w = plan_w_dev_ptr,
.req_pool_indices = static_cast<const int64_t*>(req_pool_indices.data_ptr()),
.req_to_token = static_cast<const int32_t*>(req_to_token.data_ptr()),
.stride_r2t = req_to_token.stride(0),
.state_slot_offset = state_slot_offset,
.num_c = num_c_padded,
.num_w = num_w_padded,
};
constexpr uint32_t kBlockSize = 128;
const auto num_blocks = host::div_ceil(total, kBlockSize);
LaunchKernel(num_blocks, kBlockSize, device)(plan_c128_online_prefill_kernel, stage1_params);
}
return OnlinePrefillPlan{num_c_padded, num_w_padded};
}
} // namespace host::compress
namespace {
[[maybe_unused]]
constexpr auto& plan_compress_128_online_decode = host::compress::plan_online_decode;
[[maybe_unused]]
constexpr auto& plan_compress_128_online_prefill = host::compress::plan_online_prefill;
} // namespace
@@ -0,0 +1,512 @@
/**
* \brief Here's some dimension info for the main buffer used in C128 prefill and decode.
*
* kv_buffer: [num_indices, 128, head_dim * 2]
* - last dimension layout: | kv | score |
* kv_input: [batch_size, head_dim * 2]
* kv_output: [batch_size, head_dim]
* score_bias (ape): [128, head_dim]
* plan_c/plan_w: [variable length]
*
* For prefill, batch_size = num_q_tokens
*/
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/runtime.cuh>
#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/deepseek_v4/compress_v2.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/object.h>
#include <cfloat>
#include <cstdint>
#include <type_traits>
namespace {
using PlanD = device::compress::DecodePlan;
using PlanC = device::compress::CompressPlan;
using PlanW = device::compress::WritePlan;
/// \brief Each thread will handle this many elements (split along head_dim)
constexpr int32_t kTileElements = 2;
/// \brief Each warp will handle this many elements (split along 128)
constexpr int32_t kElementsPerWarp = 8;
constexpr uint32_t kNumWarps = 128 / kElementsPerWarp;
constexpr uint32_t kBlockSize = device::kWarpThreads * kNumWarps;
constexpr uint32_t kWriteBlockSize = 128; // one warp per write
/// \brief Need to reduce register usage to increase occupancy
#define C128_KERNEL __global__ __launch_bounds__(kBlockSize, 2)
#define WRITE_KERNEL __global__ __launch_bounds__(kWriteBlockSize, 16)
struct Compress128DecodeParams {
void* __restrict__ kv_buffer;
const void* __restrict__ kv_input;
void* __restrict__ kv_output;
const void* __restrict__ score_bias;
const PlanD* __restrict__ plan_d;
uint32_t batch_size;
};
struct Compress128PrefillParams {
void* __restrict__ kv_buffer;
const void* __restrict__ kv_input;
void* __restrict__ kv_output;
const void* __restrict__ score_bias;
const PlanC* __restrict__ plan_c;
const PlanW* __restrict__ plan_w;
uint32_t num_compress;
uint32_t num_write;
};
struct Compress128SharedBuffer {
using Storage = device::AlignedVector<float, kTileElements>;
Storage data[kNumWarps][device::kWarpThreads + 1]; // padding to avoid bank conflict
SGL_DEVICE Storage& operator()(uint32_t warp_id, uint32_t lane_id) {
return data[warp_id][lane_id];
}
SGL_DEVICE float& operator()(uint32_t warp_id, uint32_t lane_id, uint32_t tile_id) {
return data[warp_id][lane_id][tile_id];
}
};
template <int64_t kHeadDim_>
struct C128Trait {
static constexpr int64_t kTileDim = kTileElements * device::kWarpThreads; // 64
static constexpr int64_t kHeadDim = kHeadDim_;
static constexpr int64_t kScoreOffset = kHeadDim;
static constexpr int64_t kElementSize = kHeadDim * 2;
static constexpr int64_t kPageElementSize = 128 * kElementSize; // page size = 128
static constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
static_assert(kHeadDim % kTileDim == 0);
};
template <typename Trait, bool kUsePDL, typename BufferFloat, typename InputFloat, typename OutFloat>
SGL_DEVICE void c128_forward(
const BufferFloat* kv_buf, // [128n, 128n + 127]
const InputFloat* kv_src, // ragged pointer at position = 128n + 127
OutFloat* kv_out,
const InputFloat* score_bias,
const int32_t buffer_len) {
using namespace device;
const auto warp_id = threadIdx.x / kWarpThreads;
const auto lane_id = threadIdx.x % kWarpThreads;
/// NOTE: part 1: load kv + score
using StorageIn = AlignedVector<InputFloat, kTileElements>;
const auto gmem_in = tile::Memory<StorageIn>{lane_id, kWarpThreads};
StorageIn kv[kElementsPerWarp];
StorageIn score[kElementsPerWarp];
StorageIn bias[kElementsPerWarp];
const int32_t warp_offset = warp_id * kElementsPerWarp;
#pragma unroll
for (int32_t i = 0; i < 8; ++i) {
const int32_t j = i + warp_offset;
bias[i] = gmem_in.load(score_bias + j * Trait::kHeadDim);
}
const auto kv_start = kv_src - 127 * Trait::kElementSize; // point to start
if constexpr (std::is_same_v<BufferFloat, InputFloat>) {
#pragma unroll
for (int32_t i = 0; i < kElementsPerWarp; ++i) {
const int32_t j = i + warp_offset;
__builtin_assume(j < 128);
const auto src = j < buffer_len ? kv_buf : kv_start;
kv[i] = gmem_in.load(src + j * Trait::kElementSize);
score[i] = gmem_in.load(src + j * Trait::kElementSize + Trait::kScoreOffset);
}
} else { // mixed dtype
using StorageBuffer = AlignedVector<BufferFloat, kTileElements>;
const auto gmem_buffer = tile::Memory<StorageBuffer>{lane_id, kWarpThreads};
#pragma unroll
for (int32_t i = 0; i < kElementsPerWarp; ++i) {
const int32_t j = i + warp_offset;
__builtin_assume(j < 128);
if (j < buffer_len) {
const auto src = kv_buf + j * Trait::kElementSize;
const auto kv_tmp = gmem_buffer.load(src);
const auto score_tmp = gmem_buffer.load(src + Trait::kScoreOffset);
#pragma unroll
for (int32_t k = 0; k < kTileElements; ++k) {
kv[i][k] = cast<InputFloat>(kv_tmp[k]);
score[i][k] = cast<InputFloat>(score_tmp[k]);
}
} else {
const auto src = kv_start + j * Trait::kElementSize;
kv[i] = gmem_in.load(src);
score[i] = gmem_in.load(src + Trait::kScoreOffset);
}
}
}
/// NOTE: part 2: safe online softmax + weighted sum
using TmpStorage = typename Compress128SharedBuffer::Storage;
__shared__ Compress128SharedBuffer s_local_val_max;
__shared__ Compress128SharedBuffer s_local_exp_sum;
__shared__ Compress128SharedBuffer s_local_product;
TmpStorage tmp_val_max;
TmpStorage tmp_exp_sum;
TmpStorage tmp_product;
float score_fp32[kTileElements][kElementsPerWarp];
// convert to fp32 and apply bias first
#pragma unroll
for (int32_t i = 0; i < kTileElements; ++i) {
#pragma unroll
for (int32_t j = 0; j < kElementsPerWarp; ++j) {
score_fp32[i][j] = cast<float>(score[j][i]) + cast<float>(bias[j][i]);
}
}
#pragma unroll
for (int32_t i = 0; i < kTileElements; ++i) {
const auto& score = score_fp32[i];
float max_value = score[0];
float sum_exp_value = 0.0f;
#pragma unroll
for (int32_t j = 1; j < kElementsPerWarp; ++j) {
const auto fp32_score = score[j];
max_value = fmaxf(max_value, fp32_score);
}
float sum_product = 0.0f;
#pragma unroll
for (int32_t j = 0; j < 8; ++j) {
const auto fp32_score = score[j];
const auto exp_score = expf(fp32_score - max_value);
sum_product += cast<float>(kv[j][i]) * exp_score;
sum_exp_value += exp_score;
}
tmp_val_max[i] = max_value;
tmp_exp_sum[i] = sum_exp_value;
tmp_product[i] = sum_product;
}
// naturally aligned, so no bank conflict
s_local_val_max(warp_id, lane_id) = tmp_val_max;
s_local_exp_sum(warp_id, lane_id) = tmp_exp_sum;
s_local_product(warp_id, lane_id) = tmp_product;
__syncthreads();
/// NOTE: part 3: online softmax
/// NOTE: We have `kTileElements * kWarpThreads * kNumWarps` values to reduce
/// each reduce will consume `kNumWarps` threads (use partial warp reduction)
constexpr uint32_t kReductionCount = kTileElements * kWarpThreads * kNumWarps;
constexpr uint32_t kIteration = kReductionCount / kBlockSize;
PDLTriggerSecondary<kUsePDL>();
#pragma unroll
for (uint32_t i = 0; i < kIteration; ++i) {
/// NOTE: Range `[0, kTileElements * kWarpThreads * kNumWarps)`
const uint32_t j = i * kBlockSize + warp_id * kWarpThreads + lane_id;
/// NOTE: Range `[0, kNumWarps)`
const uint32_t local_warp_id = j % kNumWarps;
/// NOTE: Range `[0, kTileElements * kWarpThreads)`
const uint32_t local_elem_id = j / kNumWarps;
/// NOTE: Range `[0, kTileElements)`
const uint32_t local_tile_id = local_elem_id % kTileElements;
/// NOTE: Range `[0, kWarpThreads)`
const uint32_t local_lane_id = local_elem_id / kTileElements;
/// NOTE: each warp will access the whole tile (all `kTileElements`)
/// and for different lanes, the memory access only differ in `local_warp_id`
/// so there's no bank conflict in shared memory access.
static_assert(kTileElements * kNumWarps == kWarpThreads, "TODO: support other configs");
const auto local_val_max = s_local_val_max(local_warp_id, local_lane_id, local_tile_id);
const auto local_exp_sum = s_local_exp_sum(local_warp_id, local_lane_id, local_tile_id);
const auto local_product = s_local_product(local_warp_id, local_lane_id, local_tile_id);
const auto global_val_max = warp::reduce_max<kNumWarps>(local_val_max);
const auto rescale = expf(local_val_max - global_val_max);
const auto global_exp_sum = warp::reduce_sum<kNumWarps>(local_exp_sum * rescale);
const auto final_scale = rescale / global_exp_sum;
const auto global_product = warp::reduce_sum<kNumWarps>(local_product * final_scale);
kv_out[local_elem_id] = cast<OutFloat>(global_product);
}
}
template <typename Trait, typename BufferFloat, typename InputFloat>
SGL_DEVICE void c128_write_decode(BufferFloat* kv_buf, const InputFloat* kv_src) {
using namespace device;
using StorageInput = AlignedVector<InputFloat, kTileElements>;
const auto gmem_input = tile::Memory<StorageInput>::warp();
StorageInput data[2];
#pragma unroll
for (int32_t i = 0; i < 2; ++i) {
data[i] = gmem_input.load(kv_src + Trait::kHeadDim * i);
}
if constexpr (std::is_same_v<BufferFloat, InputFloat>) {
#pragma unroll
for (int32_t i = 0; i < 2; ++i) {
gmem_input.store(kv_buf + Trait::kHeadDim * i, data[i]);
}
} else {
using StorageBuffer = AlignedVector<BufferFloat, kTileElements>;
const auto gmem_buffer = tile::Memory<StorageBuffer>::warp();
StorageBuffer data_cast[2];
#pragma unroll
for (int32_t i = 0; i < 2; ++i) {
#pragma unroll
for (int32_t j = 0; j < kTileElements; ++j) {
data_cast[i][j] = cast<BufferFloat>(data[i][j]);
}
gmem_buffer.store(kv_buf + Trait::kHeadDim * i, data_cast[i]);
}
}
}
template <int64_t kHeadDim, typename BufferFloat, typename InputFloat, typename OutFloat, bool kUsePDL>
C128_KERNEL void flash_c128_decode(const __grid_constant__ Compress128DecodeParams params) {
using namespace device;
using Trait = C128Trait<kHeadDim>;
const uint32_t warp_id = threadIdx.x / kWarpThreads;
const uint32_t global_bid = blockIdx.x / Trait::kNumSplit; // batch id
const uint32_t global_sid = blockIdx.x % Trait::kNumSplit; // split id
const int64_t split_offset = global_sid * Trait::kTileDim;
if (global_bid >= params.batch_size) return;
const auto plan = params.plan_d[global_bid];
const auto kv_input = static_cast<const InputFloat*>(params.kv_input) + split_offset;
const auto kv_output = static_cast<OutFloat*>(params.kv_output) + split_offset;
const auto kv_buffer = static_cast<BufferFloat*>(params.kv_buffer) + split_offset;
const auto score_bias = static_cast<const InputFloat*>(params.score_bias) + split_offset;
const auto kv_src = kv_input + global_bid * Trait::kElementSize;
const auto kv_out = kv_output + global_bid * Trait::kHeadDim;
const auto kv_buf = kv_buffer + plan.read_page_1 * Trait::kPageElementSize;
const auto kv_dst = kv_buffer + plan.write_loc * Trait::kElementSize;
PDLWaitPrimary<kUsePDL>();
// the write warp must match the load warp in the following `c128_forward`
if (warp_id == kNumWarps - 1) {
c128_write_decode<Trait, BufferFloat, InputFloat>(kv_dst, kv_src);
}
if (plan.write_loc % 128 == 127) {
c128_forward<Trait, kUsePDL, BufferFloat, InputFloat, OutFloat>(kv_buf, kv_src, kv_out, score_bias, 128);
}
}
// compress kernel
template <int64_t kHeadDim, typename BufferFloat, typename InputFloat, typename OutFloat, bool kUsePDL>
C128_KERNEL void flash_c128_prefill(const __grid_constant__ Compress128PrefillParams params) {
using namespace device;
using Trait = C128Trait<kHeadDim>;
const uint32_t global_pid = blockIdx.x / Trait::kNumSplit; // plan id
const uint32_t global_sid = blockIdx.x % Trait::kNumSplit; // split id
const int64_t split_offset = global_sid * Trait::kTileDim;
if (global_pid >= params.num_compress) return;
const auto plan = params.plan_c[global_pid];
const auto kv_input = static_cast<const InputFloat*>(params.kv_input) + split_offset;
const auto kv_output = static_cast<OutFloat*>(params.kv_output) + split_offset;
const auto kv_buffer = static_cast<BufferFloat*>(params.kv_buffer) + split_offset;
const auto score_bias = static_cast<const InputFloat*>(params.score_bias) + split_offset;
if (plan.is_invalid()) return;
const auto kv_src = kv_input + plan.ragged_id * Trait::kElementSize;
// Compact output: one row per compress plan, indexed by `global_pid`.
const auto kv_out = kv_output + global_pid * Trait::kHeadDim;
const auto kv_buf = kv_buffer + plan.read_page_1 * Trait::kPageElementSize;
PDLWaitPrimary<kUsePDL>();
c128_forward<Trait, kUsePDL, BufferFloat, InputFloat, OutFloat>(kv_buf, kv_src, kv_out, score_bias, plan.buffer_len);
}
template <int64_t kHeadDim, typename BufferFloat, typename InputFloat, typename OutFloat, bool kUsePDL>
WRITE_KERNEL void write_c128_prefill(const __grid_constant__ Compress128PrefillParams params) {
using namespace device;
using Trait = C128Trait<kHeadDim>;
using StorageInput = AlignedVector<InputFloat, kTileElements>;
const uint32_t global_tid = blockIdx.x * blockDim.x + threadIdx.x;
const uint32_t global_wid = global_tid / kWarpThreads; // warp id
const uint32_t global_pid = global_wid / Trait::kNumSplit; // plan id
const uint32_t global_sid = global_wid % Trait::kNumSplit; // split id
// split the contiguous `kHeadDim * 2` into `kNumSplit` tiles
// each warp handles 1 contiguous tile (in contrast, decode handle the strided head_dim)
const int64_t split_offset = global_sid * (Trait::kTileDim * 2);
if (global_pid >= params.num_write) return;
const auto plan = params.plan_w[global_pid];
const auto kv_input = static_cast<const InputFloat*>(params.kv_input) + split_offset;
const auto kv_buffer = static_cast<BufferFloat*>(params.kv_buffer) + split_offset;
if (plan.is_invalid()) return;
// each warp will handle a contiguous region
const auto kv_src = kv_input + plan.ragged_id * Trait::kElementSize;
const auto kv_buf = kv_buffer + plan.write_loc * Trait::kElementSize;
const auto gmem_input = tile::Memory<StorageInput>::warp();
PDLWaitPrimary<kUsePDL>();
StorageInput data[2];
#pragma unroll
for (int32_t i = 0; i < 2; ++i) {
data[i] = gmem_input.load(kv_src, i);
}
if constexpr (std::is_same_v<BufferFloat, InputFloat>) {
PDLTriggerSecondary<kUsePDL>();
#pragma unroll
for (int32_t i = 0; i < 2; ++i) {
gmem_input.store(kv_buf, data[i], i);
}
} else {
using StorageBuffer = AlignedVector<BufferFloat, kTileElements>;
const auto gmem_buffer = tile::Memory<StorageBuffer>::warp();
StorageBuffer data_cast[2];
#pragma unroll
for (int32_t i = 0; i < 2; ++i) {
#pragma unroll
for (int32_t j = 0; j < kTileElements; ++j) {
data_cast[i][j] = cast<BufferFloat>(data[i][j]);
}
}
PDLTriggerSecondary<kUsePDL>();
#pragma unroll
for (int32_t i = 0; i < 2; ++i) {
gmem_buffer.store(kv_buf, data_cast[i], i);
}
}
}
template <int64_t kHeadDim, typename BufferFloat, typename InputFloat, typename OutFloat, bool kUsePDL>
struct FlashCompress128Kernel {
static constexpr auto decode_kernel = flash_c128_decode<kHeadDim, BufferFloat, InputFloat, OutFloat, kUsePDL>;
static constexpr auto prefill_c_kernel = flash_c128_prefill<kHeadDim, BufferFloat, InputFloat, OutFloat, kUsePDL>;
static constexpr auto prefill_w_kernel = write_c128_prefill<kHeadDim, BufferFloat, InputFloat, OutFloat, kUsePDL>;
static constexpr int64_t kTileDim = kTileElements * device::kWarpThreads; // 64
static constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
using Trait = C128Trait<kHeadDim>;
static void run_decode(
const tvm::ffi::TensorView kv_buffer,
const tvm::ffi::TensorView kv_input,
const tvm::ffi::TensorView kv_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView plan_d_) {
using namespace host;
auto N = SymbolicSize{"batch_size"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLGPU>();
TensorMatcher({-1, 128, Trait::kElementSize}) // kv score
.with_dtype<BufferFloat>()
.with_device(device_)
.verify(kv_buffer);
TensorMatcher({N, Trait::kElementSize}) // kv score input
.with_dtype<InputFloat>()
.with_device(device_)
.verify(kv_input);
TensorMatcher({N, kHeadDim}) // kv compressed output
.with_dtype<OutFloat>()
.with_device(device_)
.verify(kv_output);
TensorMatcher({128, kHeadDim}) // ape
.with_dtype<InputFloat>()
.with_device(device_)
.verify(ape);
const auto plan_d = compress::verify_plan_d(plan_d_, N, device_);
const auto batch_size = static_cast<uint32_t>(N.unwrap());
const auto params = Compress128DecodeParams{
.kv_buffer = kv_buffer.data_ptr(),
.kv_input = kv_input.data_ptr(),
.kv_output = kv_output.data_ptr(),
.score_bias = ape.data_ptr(),
.plan_d = plan_d,
.batch_size = batch_size,
};
const uint32_t num_blocks = batch_size * kNumSplit;
LaunchKernel(num_blocks, kBlockSize, device_.unwrap()) //
.enable_pdl(kUsePDL)(decode_kernel, params);
}
static void run_prefill(
const tvm::ffi::TensorView kv_buffer,
const tvm::ffi::TensorView kv_input,
const tvm::ffi::TensorView kv_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView plan_c_,
const tvm::ffi::TensorView plan_w_) {
using namespace host;
auto N = SymbolicSize{"num_q_tokens"};
auto C = SymbolicSize{"num_c_plans"};
auto W = SymbolicSize{"num_w_plans"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLGPU>();
TensorMatcher({-1, 128, Trait::kElementSize}) // kv score
.with_dtype<BufferFloat>()
.with_device(device_)
.verify(kv_buffer);
TensorMatcher({N, Trait::kElementSize}) // kv score input (ragged)
.with_dtype<InputFloat>()
.with_device(device_)
.verify(kv_input);
TensorMatcher({C, kHeadDim}) // kv compressed output (compact)
.with_dtype<OutFloat>()
.with_device(device_)
.verify(kv_output);
TensorMatcher({128, kHeadDim}) // ape
.with_dtype<InputFloat>()
.with_device(device_)
.verify(ape);
const auto plan_c = compress::verify_plan_c(plan_c_, C, device_);
const auto plan_w = compress::verify_plan_w(plan_w_, W, device_);
const auto device = device_.unwrap();
const auto num_q_tokens = static_cast<uint32_t>(N.unwrap());
const auto num_c = static_cast<uint32_t>(C.unwrap());
const auto num_w = static_cast<uint32_t>(W.unwrap());
const auto params = Compress128PrefillParams{
.kv_buffer = kv_buffer.data_ptr(),
.kv_input = kv_input.data_ptr(),
.kv_output = kv_output.data_ptr(),
.score_bias = ape.data_ptr(),
.plan_c = plan_c,
.plan_w = plan_w,
.num_compress = num_c,
.num_write = num_w,
};
RuntimeCheck(num_q_tokens >= num_w, "invalid prefill plan: num_q < num_w");
if (const auto num_c_blocks = num_c * kNumSplit) {
constexpr auto kBlockSize_C = kBlockSize;
LaunchKernel(num_c_blocks, kBlockSize_C, device) //
.enable_pdl(kUsePDL)(prefill_c_kernel, params);
}
constexpr uint32_t kWarpsPerWriteBlock = kWriteBlockSize / device::kWarpThreads;
if (const auto num_w_blocks = div_ceil(num_w * kNumSplit, kWarpsPerWriteBlock)) {
constexpr auto kBlockSize_W = kWriteBlockSize;
LaunchKernel(num_w_blocks, kBlockSize_W, device) //
.enable_pdl(kUsePDL)(prefill_w_kernel, params);
}
}
};
} // namespace
@@ -0,0 +1,549 @@
#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/deepseek_v4/compress.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/object.h>
#include <cstdint>
namespace {
using Plan4 = device::compress::PrefillPlan;
using IndiceT = int32_t;
/// \brief Each thread will handle this many elements (split along head_dim)
constexpr int kTileElements = 4;
/// \brief Need to improve register usage to reduce latency
#define C4_KERNEL __global__ __launch_bounds__(128, 4)
enum class PageMode {
RingBuffer = 8,
Page4Align = 4,
};
struct alignas(16) C4IndexBundle {
int32_t load_first_page;
int32_t load_second_page;
int32_t write_first_page;
int32_t last_position;
};
struct Compress4DecodeParams {
/**
* \brief Shape: `[num_indices, 8, head_dim * 4]` \n
* last dimension layout:
* | kv overlap | kv | score overlap | score |
*/
void* __restrict__ kv_score_buffer;
/** \brief Shape: `[batch_size, head_dim * 4]` */
const void* __restrict__ kv_score_input;
/** \brief Shape: `[batch_size, head_dim]` */
void* __restrict__ kv_compressed_output;
/** \brief Shape: `[8, head_dim]` (called `ape`) */
const void* __restrict__ score_bias;
/** \brief Shape: `[batch_size, ]`*/
const IndiceT* __restrict__ indices;
/** \brief Shape: `[batch_size, ]` */
const IndiceT* __restrict__ seq_lens;
/** \brief Shape: `[batch_size, 1]` */
const int32_t* __restrict__ extra;
/** \NOTE: `batch_size` <= `num_indices` */
uint32_t batch_size;
};
struct Compress4PrefillParams {
/**
* \brief Shape: `[num_indices, 8, head_dim * 4]` \n
* last dimension layout:
* | kv overlap | kv | score overlap | score |
*/
void* __restrict__ kv_score_buffer;
/** \brief Shape: `[num_q_tokens, head_dim * 4]` */
const void* __restrict__ kv_score_input;
/** \brief Shape: `[num_q_tokens, head_dim]` */
void* __restrict__ kv_compressed_output;
/** \brief Shape: `[8, head_dim]` (called `ape`) */
const void* __restrict__ score_bias;
/** \brief Shape: `[batch_size, ]`*/
const IndiceT* __restrict__ indices;
/** \brief Shape: `[batch_size, 4]` */
const C4IndexBundle* __restrict__ extra;
/** \brief The following part is plan info. */
const Plan4* __restrict__ compress_plan;
const Plan4* __restrict__ write_plan;
uint32_t num_compress;
uint32_t num_write;
};
template <typename T>
SGL_DEVICE void c4_write(
T* kv_score_buf, //
const T* kv_score_src,
const int64_t head_dim,
const int32_t write_pos) {
using namespace device;
using Storage = AlignedVector<T, kTileElements>;
const auto element_size = head_dim * 4;
const auto gmem = tile::Memory<Storage>::warp();
kv_score_buf += write_pos * element_size;
/// NOTE: Layout | [0] = kv overlap | [1] = kv | [2] = score overlap | [3] = score |
Storage kv_score[4];
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
kv_score[i] = gmem.load(kv_score_src + head_dim * i);
}
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
gmem.store(kv_score_buf + head_dim * i, kv_score[i]);
}
}
template <bool kPaged, typename InFloat, typename OutFloat>
SGL_DEVICE void c4_forward(
const InFloat* kv_score_buf,
const InFloat* kv_score_src,
OutFloat* kv_out,
const InFloat* score_bias,
const int64_t head_dim,
const int32_t seq_len,
const int32_t window_len,
[[maybe_unused]] const InFloat* kv_score_overlap_buf = nullptr) {
using namespace device;
const auto element_size = head_dim * 4;
const auto score_offset = head_dim * 2;
const auto overlap_stride = head_dim;
/// NOTE: part 1: load kv + score
using StorageIn = AlignedVector<InFloat, kTileElements>;
const auto gmem_in = tile::Memory<StorageIn>::warp();
StorageIn kv[8];
StorageIn score[8];
StorageIn bias[8];
#pragma unroll
for (int32_t i = 0; i < 8; ++i) {
bias[i] = gmem_in.load(score_bias + i * head_dim);
}
#pragma unroll
for (int32_t i = 0; i < 8; ++i) {
const bool is_overlap = i < 4;
const InFloat* src;
if (i < window_len) {
/// NOTE: `seq_len` must be a multiple of 4 here
if constexpr (kPaged) {
const auto kv_score_ptr = is_overlap ? kv_score_overlap_buf : kv_score_buf;
const int32_t k = i % 4;
src = kv_score_ptr + k * element_size;
} else {
const int32_t k = (seq_len + i) % 8;
src = kv_score_buf + k * element_size;
}
} else {
/// NOTE: k in [-7, 0]. We'll load from the ragged `kv_score_src`
const int32_t k = i - 7;
src = kv_score_src + k * element_size;
}
src += (is_overlap ? 0 : overlap_stride);
kv[i] = gmem_in.load(src);
score[i] = gmem_in.load(src + score_offset);
}
if (seq_len == 4) {
[[unlikely]];
constexpr float kFloatNegInf = -1e9f;
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
kv[i].fill(cast<InFloat>(0.0f));
score[i].fill(cast<InFloat>(kFloatNegInf));
}
}
/// NOTE: part 2: safe online softmax + weighted sum
using StorageOut = AlignedVector<OutFloat, kTileElements>;
const auto gmem_out = tile::Memory<StorageOut>::warp();
StorageOut result;
#pragma unroll
for (int32_t i = 0; i < kTileElements; ++i) {
float score_fp32[8];
#pragma unroll
for (int32_t j = 0; j < 8; ++j) {
score_fp32[j] = cast<float>(score[j][i]) + cast<float>(bias[j][i]);
}
float max_value = score_fp32[0];
float sum_exp_value = 0.0f;
#pragma unroll
for (int32_t j = 1; j < 8; ++j) {
const auto fp32_score = score_fp32[j];
max_value = fmaxf(max_value, fp32_score);
}
float sum_product = 0.0f;
#pragma unroll
for (int32_t j = 0; j < 8; ++j) {
const auto fp32_score = score_fp32[j];
const auto exp_score = expf(fp32_score - max_value);
sum_product += cast<float>(kv[j][i]) * exp_score;
sum_exp_value += exp_score;
}
result[i] = cast<OutFloat>(sum_product / sum_exp_value);
}
gmem_out.store(kv_out, result);
}
template <int64_t kHeadDim, typename InFloat, typename OutFloat, PageMode kMode, bool kUsePDL>
C4_KERNEL void flash_c4_decode(const __grid_constant__ Compress4DecodeParams params) {
using namespace device;
constexpr int64_t kTileDim = kTileElements * kWarpThreads; // 128
constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
constexpr int64_t kElementSize = kHeadDim * 4; // `* 4` due to overlap transform + score
static_assert(kHeadDim % kTileDim == 0, "Head dim must be multiple of tile dim");
const auto& [
_kv_score_buffer, _kv_score_input, _kv_compressed_output, _score_bias, // kv score
indices, seq_lens, extra, batch_size // decode info
] = params;
const uint32_t global_tid = blockIdx.x * blockDim.x + threadIdx.x;
const uint32_t global_wid = global_tid / kWarpThreads; // warp id
const uint32_t global_bid = global_wid / kNumSplit; // batch id
const uint32_t global_sid = global_wid % kNumSplit; // split id
if (global_bid >= batch_size) return;
const int32_t index = indices[global_bid];
const int32_t seq_len = seq_lens[global_bid];
const int64_t split_offset = global_sid * kTileDim;
// kv score
const auto kv_score_buffer = static_cast<InFloat*>(_kv_score_buffer);
// kv input
const auto kv_score_input = static_cast<const InFloat*>(_kv_score_input);
const auto kv_src = kv_score_input + global_bid * kElementSize + split_offset;
// kv output
const auto kv_compressed_output = static_cast<OutFloat*>(_kv_compressed_output);
const auto kv_out = kv_compressed_output + global_bid * kHeadDim + split_offset;
// score bias (ape)
const auto score_bias = static_cast<const InFloat*>(_score_bias) + split_offset;
PDLWaitPrimary<kUsePDL>();
/// NOTE: `position` = `seq_len - 1`. To avoid underflow, we use `seq_len + page_size - 1`
if constexpr (kMode == PageMode::Page4Align) {
const auto index_prev = extra[global_bid];
const auto kv_buf = kv_score_buffer + index * (kElementSize * 4) + split_offset;
c4_write(kv_buf, kv_src, kHeadDim, /*write_pos=*/(seq_len + 3) % 4);
if (seq_len % 4 == 0) {
const auto kv_overlap = kv_buf + (index_prev - index) * (kElementSize * 4);
c4_forward<true>(kv_buf, kv_src, kv_out, score_bias, kHeadDim, seq_len, 8, kv_overlap);
}
} else {
static_assert(kMode == PageMode::RingBuffer, "Unsupported PageMode");
const auto kv_buf = kv_score_buffer + index * (kElementSize * 8) + split_offset;
c4_write(kv_buf, kv_src, kHeadDim, /*write_pos=*/(seq_len + 7) % 8);
if (seq_len % 4 == 0) {
c4_forward<false>(kv_buf, kv_src, kv_out, score_bias, kHeadDim, seq_len, /*window_size=*/8);
}
}
PDLTriggerSecondary<kUsePDL>();
}
template <int64_t kHeadDim, typename InFloat, typename OutFloat, PageMode kMode, bool kWrite, bool kUsePDL>
C4_KERNEL void flash_c4_prefill(const __grid_constant__ Compress4PrefillParams params) {
using namespace device;
constexpr int64_t kTileDim = kTileElements * kWarpThreads; // 128
constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
constexpr int64_t kElementSize = kHeadDim * 4; // `* 4` due to overlap transform + score
static_assert(kHeadDim % kTileDim == 0, "Head dim must be multiple of tile dim");
const auto& [
_kv_score_buffer, _kv_score_input, _kv_compressed_output, _score_bias, // kv score
indices, extra, compress_plan, write_plan, num_compress, num_write // prefill plan
] = params;
const uint32_t global_tid = blockIdx.x * blockDim.x + threadIdx.x;
const uint32_t global_wid = global_tid / kWarpThreads; // warp id
const uint32_t global_pid = global_wid / kNumSplit; // plan id
const uint32_t global_sid = global_wid % kNumSplit; // split id
/// NOTE: compiler can optimize this if-else at compile time
const auto num_plans = kWrite ? num_write : num_compress;
const auto plan_ptr = kWrite ? write_plan : compress_plan;
if (global_pid >= num_plans) return;
const auto& [ragged_id, global_bid, position, window_len] = plan_ptr[global_pid];
const int64_t split_offset = global_sid * kTileDim;
// kv score
const auto kv_score_buffer = static_cast<InFloat*>(_kv_score_buffer);
// kv input
const auto kv_score_input = static_cast<const InFloat*>(_kv_score_input);
const auto kv_src = kv_score_input + ragged_id * kElementSize + split_offset;
// kv output
const auto kv_compressed_output = static_cast<OutFloat*>(_kv_compressed_output);
const auto kv_out = kv_compressed_output + ragged_id * kHeadDim + split_offset;
if (ragged_id == 0xFFFFFFFF) [[unlikely]]
return;
// score bias (ape)
const auto score_bias = static_cast<const InFloat*>(_score_bias) + split_offset;
const auto seq_len = position + 1;
const int32_t index = indices[global_bid];
PDLWaitPrimary<kUsePDL>();
if constexpr (kMode == PageMode::Page4Align) {
const auto write_second_page = index;
const auto [load_first_page, load_second_page, write_first_page, last_pos] = extra[global_bid];
if constexpr (kWrite) {
int32_t index;
if (position < static_cast<uint32_t>(last_pos)) {
index = write_first_page;
} else {
index = write_second_page;
}
const auto kv_buf = kv_score_buffer + index * (kElementSize * 4) + split_offset;
c4_write(kv_buf, kv_src, kHeadDim, /*write_pos=*/position % 4);
} else {
int32_t index_overlap, index_normal;
if (window_len <= 4) {
index_overlap = load_second_page;
index_normal = load_second_page; // not used
} else {
index_overlap = load_first_page;
index_normal = load_second_page;
}
const auto kv_buf = kv_score_buffer + index_normal * (kElementSize * 4) + split_offset;
const auto kv_overlap = kv_score_buffer + index_overlap * (kElementSize * 4) + split_offset;
c4_forward<true>(kv_buf, kv_src, kv_out, score_bias, kHeadDim, seq_len, window_len, kv_overlap);
}
} else {
static_assert(kMode == PageMode::RingBuffer, "Unsupported PageMode");
const auto kv_buf = kv_score_buffer + index * (kElementSize * 8) + split_offset;
if constexpr (kWrite) {
c4_write(kv_buf, kv_src, kHeadDim, /*write_pos=*/position % 8);
} else {
c4_forward<false>(kv_buf, kv_src, kv_out, score_bias, kHeadDim, seq_len, window_len);
}
}
PDLTriggerSecondary<kUsePDL>();
}
template <int64_t kHeadDim, typename InFloat, typename OutFloat, bool kUsePDL>
struct FlashCompress4Kernel {
template <PageMode kMode>
static constexpr auto decode_kernel = flash_c4_decode<kHeadDim, InFloat, OutFloat, kMode, kUsePDL>;
template <PageMode kMode, bool kWrite>
static constexpr auto prefill_kernel = flash_c4_prefill<kHeadDim, InFloat, OutFloat, kMode, kWrite, kUsePDL>;
template <PageMode kMode>
static constexpr auto prefill_c_kernel = prefill_kernel<kMode, /*kWrite=*/false>;
template <PageMode kMode>
static constexpr auto prefill_w_kernel = prefill_kernel<kMode, /*kWrite=*/true>;
static constexpr uint32_t kBlockSize = 128;
static constexpr uint32_t kTileDim = kTileElements * device::kWarpThreads;
static constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
static constexpr uint32_t kWarpsPerBlock = kBlockSize / device::kWarpThreads;
using Self = FlashCompress4Kernel;
static void run_decode(
const tvm::ffi::TensorView kv_score_buffer,
const tvm::ffi::TensorView kv_score_input,
const tvm::ffi::TensorView kv_compressed_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView indices,
const tvm::ffi::TensorView seq_lens,
const tvm::ffi::Optional<tvm::ffi::TensorView> extra) {
using namespace host;
// this should not happen in practice
auto B = SymbolicSize{"batch_size"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
const auto extra_ptr = _get_extra_pointer(B, device_, extra);
const auto page_size = extra_ptr != nullptr ? 4 : 8;
TensorMatcher({-1, page_size, kHeadDim * 4}) // kv score
.with_dtype<InFloat>()
.with_device(device_)
.verify(kv_score_buffer);
TensorMatcher({B, kHeadDim * 4}) // kv score input
.with_dtype<InFloat>()
.with_device(device_)
.verify(kv_score_input);
TensorMatcher({B, kHeadDim}) // kv compressed output
.with_dtype<OutFloat>()
.with_device(device_)
.verify(kv_compressed_output);
TensorMatcher({8, kHeadDim}) // ape
.with_dtype<InFloat>()
.with_device(device_)
.verify(ape);
TensorMatcher({B}) // indices
.with_dtype<IndiceT>()
.with_device(device_)
.verify(indices);
TensorMatcher({B}) // seq lens
.with_dtype<IndiceT>()
.with_device(device_)
.verify(seq_lens);
const auto device = device_.unwrap();
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto params = Compress4DecodeParams{
.kv_score_buffer = kv_score_buffer.data_ptr(),
.kv_score_input = kv_score_input.data_ptr(),
.kv_compressed_output = kv_compressed_output.data_ptr(),
.score_bias = ape.data_ptr(),
.indices = static_cast<const IndiceT*>(indices.data_ptr()),
.seq_lens = static_cast<const IndiceT*>(seq_lens.data_ptr()),
.extra = static_cast<const int32_t*>(extra_ptr),
.batch_size = batch_size,
};
const auto kernel = extra_ptr != nullptr ? decode_kernel<PageMode::Page4Align> //
: decode_kernel<PageMode::RingBuffer>;
const uint32_t num_blocks = div_ceil(batch_size * kNumSplit, kWarpsPerBlock);
LaunchKernel(num_blocks, kBlockSize, device) //
.enable_pdl(kUsePDL)(kernel, params);
}
static void run_prefill(
const tvm::ffi::TensorView kv_score_buffer,
const tvm::ffi::TensorView kv_score_input,
const tvm::ffi::TensorView kv_compressed_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView indices,
const tvm::ffi::TensorView compress_plan,
const tvm::ffi::TensorView write_plan,
const tvm::ffi::Optional<tvm::ffi::TensorView> extra) {
using namespace host;
auto B = SymbolicSize{"batch_size"};
auto N = SymbolicSize{"num_q_tokens"};
auto X = SymbolicSize{"compress_tokens"};
auto Y = SymbolicSize{"write_tokens"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
const auto extra_ptr = _get_extra_pointer(B, device_, extra, /*is_prefill=*/true);
const auto page_size = extra_ptr != nullptr ? 4 : 8;
TensorMatcher({-1, page_size, kHeadDim * 4}) // kv score
.with_dtype<InFloat>()
.with_device(device_)
.verify(kv_score_buffer);
TensorMatcher({N, kHeadDim * 4}) // kv score input
.with_dtype<InFloat>()
.with_device(device_)
.verify(kv_score_input);
TensorMatcher({N, kHeadDim}) // kv compressed output
.with_dtype<OutFloat>()
.with_device(device_)
.verify(kv_compressed_output);
TensorMatcher({8, kHeadDim}) // ape
.with_dtype<InFloat>()
.with_device(device_)
.verify(ape);
TensorMatcher({B}) // indices
.with_dtype<IndiceT>()
.with_device(device_)
.verify(indices);
TensorMatcher({X, compress::kPrefillPlanDim}) // compress plan
.with_dtype<compress::PrefillPlanTensorDtype>()
.with_device(device_)
.verify(compress_plan);
TensorMatcher({Y, compress::kPrefillPlanDim}) // write plan
.with_dtype<compress::PrefillPlanTensorDtype>()
.with_device(device_)
.verify(write_plan);
const auto device = device_.unwrap();
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto num_q_tokens = static_cast<uint32_t>(N.unwrap());
const auto num_c = static_cast<uint32_t>(X.unwrap());
const auto num_w = static_cast<uint32_t>(Y.unwrap());
const auto params = Compress4PrefillParams{
.kv_score_buffer = kv_score_buffer.data_ptr(),
.kv_score_input = kv_score_input.data_ptr(),
.kv_compressed_output = kv_compressed_output.data_ptr(),
.score_bias = ape.data_ptr(),
.indices = static_cast<const IndiceT*>(indices.data_ptr()),
.extra = static_cast<const C4IndexBundle*>(extra_ptr),
.compress_plan = static_cast<const Plan4*>(compress_plan.data_ptr()),
.write_plan = static_cast<const Plan4*>(write_plan.data_ptr()),
.num_compress = num_c,
.num_write = num_w,
};
RuntimeCheck(num_q_tokens >= batch_size, "num_q_tokens must be >= batch_size");
RuntimeCheck(num_q_tokens >= std::max(num_c, num_w), "invalid prefill plan");
if (const auto num_c_blocks = div_ceil(num_c * kNumSplit, kWarpsPerBlock)) {
const auto c_kernel = extra_ptr != nullptr ? prefill_c_kernel<PageMode::Page4Align> //
: prefill_c_kernel<PageMode::RingBuffer>;
LaunchKernel(num_c_blocks, kBlockSize, device) //
.enable_pdl(kUsePDL)(c_kernel, params);
}
if (const auto num_w_blocks = div_ceil(num_w * kNumSplit, kWarpsPerBlock)) {
const auto w_kernel = extra_ptr != nullptr ? prefill_w_kernel<PageMode::Page4Align> //
: prefill_w_kernel<PageMode::RingBuffer>;
LaunchKernel(num_w_blocks, kBlockSize, device) //
.enable_pdl(kUsePDL)(w_kernel, params);
}
}
// some auxiliary functions
private:
static const void* _get_extra_pointer(
host::SymbolicSize& B, // batch_size
host::SymbolicDevice& device,
const tvm::ffi::Optional<tvm::ffi::TensorView>& extra,
bool is_prefill = false) {
// only have value when using page-aligned mode
if (!extra.has_value()) return nullptr;
const auto& extra_tensor = extra.value();
/// NOTE: the metadata layout is different for prefill and decode:
/// for prefill, last 4 are:
/// load overlap | load normal | write overlap | last written page
/// for decode, last 1 is the write (also load) overlap
host::TensorMatcher({B, is_prefill ? 4 : 1}) // extra tensor
.with_dtype<int32_t>()
.with_device(device)
.verify(extra_tensor);
const auto data_ptr = extra_tensor.data_ptr();
host::RuntimeCheck(data_ptr != nullptr, "extra tensor data ptr is null");
if (is_prefill) {
static_assert(alignof(C4IndexBundle) == 16);
host::RuntimeCheck(std::bit_cast<uintptr_t>(data_ptr) % 16 == 0, "extra tensor is not properly aligned");
}
return data_ptr;
}
};
} // namespace
@@ -0,0 +1,491 @@
/**
* \brief Here's some dimension info for the main buffer used in C4 prefill and decode.
*
* kv_buffer: [num_indices, 8, head_dim * 4]
* - last dimension layout: | kv overlap | kv | score overlap | score |
* kv_input: [batch_size, head_dim * 4]
* kv_output: [batch_size, head_dim]
* score_bias (ape): [8, head_dim]
* plan_c/plan_w: [variable length]
*
* For prefill, batch_size = num_q_tokens
*/
#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/deepseek_v4/compress_v2.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <tvm/ffi/object.h>
#include <cfloat>
#include <cstdint>
#include <type_traits>
namespace {
using PlanD = device::compress::DecodePlan;
using PlanC = device::compress::CompressPlan;
using PlanW = device::compress::WritePlan;
/// \brief Each thread will handle this many elements (split along head_dim)
constexpr int32_t kTileElements = 4;
/// \brief Need to improve register usage to reduce latency
#define C4_KERNEL __global__ __launch_bounds__(128, 4)
#define WRITE_KERNEL __global__ __launch_bounds__(128, 16)
struct Compress4DecodeParams {
void* __restrict__ kv_buffer;
const void* __restrict__ kv_input;
void* __restrict__ kv_output;
const void* __restrict__ score_bias;
const PlanD* __restrict__ plan_d;
uint32_t batch_size;
};
struct Compress4PrefillParams {
void* __restrict__ kv_buffer;
const void* __restrict__ kv_input;
void* __restrict__ kv_output;
const void* __restrict__ score_bias;
const PlanC* __restrict__ plan_c;
const PlanW* __restrict__ plan_w;
uint32_t num_compress;
uint32_t num_write;
};
template <int64_t kHeadDim_>
struct C4Trait {
static constexpr int64_t kTileDim = kTileElements * device::kWarpThreads; // 128
static constexpr int64_t kHeadDim = kHeadDim_;
static constexpr int64_t kOverlapOffset = kHeadDim;
static constexpr int64_t kScoreOffset = kHeadDim * 2;
static constexpr int64_t kElementSize = kHeadDim * 4;
static constexpr int64_t kPageElementSize = 4 * kElementSize; // page size = 4
static constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
static_assert(kHeadDim % kTileDim == 0);
};
template <typename Trait, bool kUsePDL, typename BufferFloat, typename InputFloat, typename OutFloat>
SGL_DEVICE void c4_forward(
const BufferFloat* kv_buf_0, // overlap [4n - 4, 4n - 1]
const BufferFloat* kv_buf_1, // normal [4n + 0, 4n + 3]
const InputFloat* kv_src, // ragged pointer at position = 4n + 3
OutFloat* kv_out,
const InputFloat* score_bias,
const bool should_overlap,
const int32_t buffer_len) {
using namespace device;
using StorageIn = AlignedVector<InputFloat, kTileElements>;
const auto gmem_in = tile::Memory<StorageIn>::warp();
StorageIn kv[8];
StorageIn score[8];
StorageIn bias[8];
#pragma unroll
for (int32_t i = 0; i < 8; ++i) {
bias[i] = gmem_in.load(score_bias + i * Trait::kHeadDim);
}
if constexpr (std::is_same_v<BufferFloat, InputFloat>) {
if (should_overlap) {
const auto kv_start = kv_src - 7 * Trait::kElementSize; // point to start
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
const auto src = i < buffer_len ? kv_buf_0 : kv_start;
const auto base = src + i * Trait::kElementSize;
kv[i] = gmem_in.load(base);
score[i] = gmem_in.load(base + Trait::kScoreOffset);
}
} else {
[[unlikely]];
constexpr float kFloatNegInf = -FLT_MAX;
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
kv[i].fill(cast<InputFloat>(0.0f));
score[i].fill(cast<InputFloat>(kFloatNegInf));
}
}
const auto kv_start = kv_src - 3 * Trait::kElementSize; // point to start
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
const auto src = i + 4 < buffer_len ? kv_buf_1 : kv_start;
const auto base = src + i * Trait::kElementSize + Trait::kOverlapOffset;
kv[i + 4] = gmem_in.load(base);
score[i + 4] = gmem_in.load(base + Trait::kScoreOffset);
}
} else { // mixed dtype
using StorageBuffer = AlignedVector<BufferFloat, kTileElements>;
const auto gmem_buffer = tile::Memory<StorageBuffer>::warp();
const auto kv_start_0 = kv_src - 7 * Trait::kElementSize; // point to start
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
if (should_overlap && i < buffer_len) {
const auto base = kv_buf_0 + i * Trait::kElementSize;
const auto kv_tmp = gmem_buffer.load(base);
const auto score_tmp = gmem_buffer.load(base + Trait::kScoreOffset);
#pragma unroll
for (int32_t j = 0; j < kTileElements; ++j) {
kv[i][j] = cast<InputFloat>(kv_tmp[j]);
score[i][j] = cast<InputFloat>(score_tmp[j]);
}
} else if (should_overlap) {
const auto base = kv_start_0 + i * Trait::kElementSize;
kv[i] = gmem_in.load(base);
score[i] = gmem_in.load(base + Trait::kScoreOffset);
} else {
[[unlikely]];
constexpr float kFloatNegInf = -FLT_MAX;
kv[i].fill(cast<InputFloat>(0.0f));
score[i].fill(cast<InputFloat>(kFloatNegInf));
}
}
const auto kv_start = kv_src - 3 * Trait::kElementSize; // point to start
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
if (i + 4 < buffer_len) {
const auto base = kv_buf_1 + i * Trait::kElementSize + Trait::kOverlapOffset;
const auto kv_tmp = gmem_buffer.load(base);
const auto score_tmp = gmem_buffer.load(base + Trait::kScoreOffset);
#pragma unroll
for (int32_t j = 0; j < kTileElements; ++j) {
kv[i + 4][j] = cast<InputFloat>(kv_tmp[j]);
score[i + 4][j] = cast<InputFloat>(score_tmp[j]);
}
} else {
const auto base = kv_start + i * Trait::kElementSize + Trait::kOverlapOffset;
kv[i + 4] = gmem_in.load(base);
score[i + 4] = gmem_in.load(base + Trait::kScoreOffset);
}
}
}
/// NOTE: part 2: safe online softmax + weighted sum
using StorageOut = AlignedVector<OutFloat, kTileElements>;
const auto gmem_out = tile::Memory<StorageOut>::warp();
StorageOut result;
// consume 32 fp registers
float score_fp32[kTileElements][8];
// convert to fp32 and apply bias first
#pragma unroll
for (int32_t i = 0; i < kTileElements; ++i) {
#pragma unroll
for (int32_t j = 0; j < 8; ++j) {
score_fp32[i][j] = cast<float>(score[j][i]) + cast<float>(bias[j][i]);
}
}
#pragma unroll
for (int32_t i = 0; i < kTileElements; ++i) {
const auto& score = score_fp32[i];
float max_value = score[0];
float sum_exp_value = 0.0f;
#pragma unroll
for (int32_t j = 1; j < 8; ++j) {
const auto fp32_score = score[j];
max_value = fmaxf(max_value, fp32_score);
}
float sum_product = 0.0f;
#pragma unroll
for (int32_t j = 0; j < 8; ++j) {
const auto fp32_score = score[j];
const auto exp_score = expf(fp32_score - max_value);
sum_product += cast<float>(kv[j][i]) * exp_score;
sum_exp_value += exp_score;
}
result[i] = cast<OutFloat>(sum_product / sum_exp_value);
}
// overlap the store with the next iteration's load
PDLTriggerSecondary<kUsePDL>();
gmem_out.store(kv_out, result);
}
template <typename Trait, typename BufferFloat, typename InputFloat>
SGL_DEVICE void c4_write_decode(BufferFloat* kv_buf, const InputFloat* kv_src) {
using namespace device;
using StorageInput = AlignedVector<InputFloat, kTileElements>;
const auto gmem_input = tile::Memory<StorageInput>::warp();
StorageInput data[4];
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
data[i] = gmem_input.load(kv_src + Trait::kHeadDim * i);
}
if constexpr (std::is_same_v<BufferFloat, InputFloat>) {
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
gmem_input.store(kv_buf + Trait::kHeadDim * i, data[i]);
}
} else {
using StorageBuffer = AlignedVector<BufferFloat, kTileElements>;
const auto gmem_buffer = tile::Memory<StorageBuffer>::warp();
StorageBuffer data_cast[4];
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
#pragma unroll
for (int32_t j = 0; j < kTileElements; ++j) {
data_cast[i][j] = cast<BufferFloat>(data[i][j]);
}
gmem_buffer.store(kv_buf + Trait::kHeadDim * i, data_cast[i]);
}
}
}
template <int64_t kHeadDim, typename BufferFloat, typename InputFloat, typename OutFloat, bool kUsePDL>
C4_KERNEL void flash_c4_decode(const __grid_constant__ Compress4DecodeParams params) {
using namespace device;
using Trait = C4Trait<kHeadDim>;
const uint32_t global_tid = blockIdx.x * blockDim.x + threadIdx.x;
const uint32_t global_wid = global_tid / kWarpThreads; // warp id
const uint32_t global_bid = global_wid / Trait::kNumSplit; // batch id
const uint32_t global_sid = global_wid % Trait::kNumSplit; // split id
const int64_t split_offset = global_sid * Trait::kTileDim;
if (global_bid >= params.batch_size) return;
const auto plan = params.plan_d[global_bid];
const auto kv_input = static_cast<const InputFloat*>(params.kv_input) + split_offset;
const auto kv_output = static_cast<OutFloat*>(params.kv_output) + split_offset;
const auto kv_buffer = static_cast<BufferFloat*>(params.kv_buffer) + split_offset;
const auto score_bias = static_cast<const InputFloat*>(params.score_bias) + split_offset;
const auto kv_src = kv_input + global_bid * Trait::kElementSize;
const auto kv_out = kv_output + global_bid * Trait::kHeadDim;
const auto kv_buf_0 = kv_buffer + plan.read_page_0 * Trait::kPageElementSize;
const auto kv_buf_1 = kv_buffer + plan.read_page_1 * Trait::kPageElementSize;
const auto kv_dst = kv_buffer + plan.write_loc * Trait::kElementSize;
PDLWaitPrimary<kUsePDL>();
c4_write_decode<Trait, BufferFloat, InputFloat>(kv_dst, kv_src);
if (plan.seq_len % 4 == 0) {
const auto need_overlap = plan.seq_len > 4;
c4_forward<Trait, kUsePDL, BufferFloat, InputFloat, OutFloat>(
kv_buf_0, kv_buf_1, kv_src, kv_out, score_bias, need_overlap, 8);
}
}
template <int64_t kHeadDim, typename BufferFloat, typename InputFloat, typename OutFloat, bool kUsePDL>
C4_KERNEL void flash_c4_prefill(const __grid_constant__ Compress4PrefillParams params) {
using namespace device;
using Trait = C4Trait<kHeadDim>;
const uint32_t global_tid = blockIdx.x * blockDim.x + threadIdx.x;
const uint32_t global_wid = global_tid / kWarpThreads; // warp id
const uint32_t global_pid = global_wid / Trait::kNumSplit; // plan id
const uint32_t global_sid = global_wid % Trait::kNumSplit; // split id
const int64_t split_offset = global_sid * Trait::kTileDim;
if (global_pid >= params.num_compress) return;
const auto plan = params.plan_c[global_pid];
const auto kv_input = static_cast<const InputFloat*>(params.kv_input) + split_offset;
const auto kv_output = static_cast<OutFloat*>(params.kv_output) + split_offset;
const auto kv_buffer = static_cast<BufferFloat*>(params.kv_buffer) + split_offset;
const auto score_bias = static_cast<const InputFloat*>(params.score_bias) + split_offset;
if (plan.is_invalid()) return;
const auto kv_src = kv_input + plan.ragged_id * Trait::kElementSize;
// Compact output: one row per compress plan, indexed by `global_pid`.
const auto kv_out = kv_output + global_pid * Trait::kHeadDim;
const auto kv_buf_0 = kv_buffer + plan.read_page_0 * Trait::kPageElementSize;
const auto kv_buf_1 = kv_buffer + plan.read_page_1 * Trait::kPageElementSize;
const bool need_overlap = plan.seq_len > 4;
PDLWaitPrimary<kUsePDL>();
c4_forward<Trait, kUsePDL, BufferFloat, InputFloat, OutFloat>(
kv_buf_0, kv_buf_1, kv_src, kv_out, score_bias, need_overlap, plan.buffer_len);
}
template <int64_t kHeadDim, typename BufferFloat, typename InputFloat, typename OutFloat, bool kUsePDL>
WRITE_KERNEL void write_c4_prefill(const __grid_constant__ Compress4PrefillParams params) {
using namespace device;
using Trait = C4Trait<kHeadDim>;
using StorageInput = AlignedVector<InputFloat, kTileElements>;
const uint32_t global_tid = blockIdx.x * blockDim.x + threadIdx.x;
const uint32_t global_wid = global_tid / kWarpThreads; // warp id
const uint32_t global_pid = global_wid / Trait::kNumSplit; // plan id
const uint32_t global_sid = global_wid % Trait::kNumSplit; // split id
// split the contiguous `kHeadDim * 4` into `kNumSplit` tiles
// each warp handles 1 contiguous tile (in contrast, decode handle the strided head_dim)
const int64_t split_offset = global_sid * (Trait::kTileDim * 4);
if (global_pid >= params.num_write) return;
const auto plan = params.plan_w[global_pid];
const auto kv_input = static_cast<const InputFloat*>(params.kv_input) + split_offset;
const auto kv_buffer = static_cast<BufferFloat*>(params.kv_buffer) + split_offset;
if (plan.is_invalid()) return;
// each warp will handle a contiguous region
const auto kv_src = kv_input + plan.ragged_id * Trait::kElementSize;
const auto kv_buf = kv_buffer + plan.write_loc * Trait::kElementSize;
const auto gmem_input = tile::Memory<StorageInput>::warp();
PDLWaitPrimary<kUsePDL>();
StorageInput data[4];
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
data[i] = gmem_input.load(kv_src, i);
}
if constexpr (std::is_same_v<BufferFloat, InputFloat>) {
PDLTriggerSecondary<kUsePDL>();
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
gmem_input.store(kv_buf, data[i], i);
}
} else {
using StorageBuffer = AlignedVector<BufferFloat, kTileElements>;
const auto gmem_buffer = tile::Memory<StorageBuffer>::warp();
StorageBuffer data_cast[4];
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
#pragma unroll
for (int32_t j = 0; j < kTileElements; ++j) {
data_cast[i][j] = cast<BufferFloat>(data[i][j]);
}
}
PDLTriggerSecondary<kUsePDL>();
#pragma unroll
for (int32_t i = 0; i < 4; ++i) {
gmem_buffer.store(kv_buf, data_cast[i], i);
}
}
}
template <int64_t kHeadDim, typename BufferFloat, typename InputFloat, typename OutFloat, bool kUsePDL>
struct FlashCompress4Kernel {
static constexpr auto decode_kernel = flash_c4_decode<kHeadDim, BufferFloat, InputFloat, OutFloat, kUsePDL>;
static constexpr auto prefill_c_kernel = flash_c4_prefill<kHeadDim, BufferFloat, InputFloat, OutFloat, kUsePDL>;
static constexpr auto prefill_w_kernel = write_c4_prefill<kHeadDim, BufferFloat, InputFloat, OutFloat, kUsePDL>;
static constexpr uint32_t kBlockSize = 128;
static constexpr uint32_t kTileDim = kTileElements * device::kWarpThreads;
static constexpr uint32_t kNumSplit = kHeadDim / kTileDim;
static constexpr uint32_t kWarpsPerBlock = kBlockSize / device::kWarpThreads;
using Trait = C4Trait<kHeadDim>;
static void run_decode(
const tvm::ffi::TensorView kv_buffer,
const tvm::ffi::TensorView kv_input,
const tvm::ffi::TensorView kv_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView plan_d_) {
using namespace host;
auto N = SymbolicSize{"batch_size"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLGPU>();
TensorMatcher({-1, 4, Trait::kElementSize}) // kv score
.with_dtype<BufferFloat>()
.with_device(device_)
.verify(kv_buffer);
TensorMatcher({N, Trait::kElementSize}) // kv score input
.with_dtype<InputFloat>()
.with_device(device_)
.verify(kv_input);
TensorMatcher({N, kHeadDim}) // kv compressed output
.with_dtype<OutFloat>()
.with_device(device_)
.verify(kv_output);
TensorMatcher({8, kHeadDim}) // ape
.with_dtype<InputFloat>()
.with_device(device_)
.verify(ape);
const auto plan_d = compress::verify_plan_d(plan_d_, N, device_);
const auto batch_size = static_cast<uint32_t>(N.unwrap());
const auto params = Compress4DecodeParams{
.kv_buffer = kv_buffer.data_ptr(),
.kv_input = kv_input.data_ptr(),
.kv_output = kv_output.data_ptr(),
.score_bias = ape.data_ptr(),
.plan_d = plan_d,
.batch_size = batch_size,
};
const uint32_t num_blocks = div_ceil(batch_size * kNumSplit, kWarpsPerBlock);
LaunchKernel(num_blocks, kBlockSize, device_.unwrap()) //
.enable_pdl(kUsePDL)(decode_kernel, params);
}
static void run_prefill(
const tvm::ffi::TensorView kv_buffer,
const tvm::ffi::TensorView kv_input,
const tvm::ffi::TensorView kv_output,
const tvm::ffi::TensorView ape,
const tvm::ffi::TensorView plan_c_,
const tvm::ffi::TensorView plan_w_) {
using namespace host;
auto N = SymbolicSize{"num_q_tokens"};
auto C = SymbolicSize{"num_c_plans"};
auto W = SymbolicSize{"num_w_plans"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLGPU>();
TensorMatcher({-1, 4, Trait::kElementSize}) // kv score
.with_dtype<BufferFloat>()
.with_device(device_)
.verify(kv_buffer);
TensorMatcher({N, Trait::kElementSize}) // kv score input (ragged)
.with_dtype<InputFloat>()
.with_device(device_)
.verify(kv_input);
TensorMatcher({C, kHeadDim}) // kv compressed output (compact)
.with_dtype<OutFloat>()
.with_device(device_)
.verify(kv_output);
TensorMatcher({8, kHeadDim}) // ape
.with_dtype<InputFloat>()
.with_device(device_)
.verify(ape);
const auto plan_c = compress::verify_plan_c(plan_c_, C, device_);
const auto plan_w = compress::verify_plan_w(plan_w_, W, device_);
const auto device = device_.unwrap();
const auto num_q_tokens = static_cast<uint32_t>(N.unwrap());
const auto num_c = static_cast<uint32_t>(C.unwrap());
const auto num_w = static_cast<uint32_t>(W.unwrap());
const auto params = Compress4PrefillParams{
.kv_buffer = kv_buffer.data_ptr(),
.kv_input = kv_input.data_ptr(),
.kv_output = kv_output.data_ptr(),
.score_bias = ape.data_ptr(),
.plan_c = plan_c,
.plan_w = plan_w,
.num_compress = num_c,
.num_write = num_w,
};
RuntimeCheck(num_q_tokens >= num_w, "invalid prefill plan: num_q < num_w");
if (const auto num_c_blocks = div_ceil(num_c * kNumSplit, kWarpsPerBlock)) {
LaunchKernel(num_c_blocks, kBlockSize, device) //
.enable_pdl(kUsePDL)(prefill_c_kernel, params);
}
if (const auto num_w_blocks = div_ceil(num_w * kNumSplit, kWarpsPerBlock)) {
LaunchKernel(num_w_blocks, kBlockSize, device) //
.enable_pdl(kUsePDL)(prefill_w_kernel, params);
}
}
};
} // namespace
@@ -0,0 +1,868 @@
#include <sgl_kernel/ffi.h>
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/utils.cuh>
#include <sgl_kernel/warp.cuh>
#include <sgl_kernel/deepseek_v4/compress_v2.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tuple.h>
#include <cstdint>
#include <limits>
namespace host::compress {
constexpr auto kDLUInt8 = DLDataType{.code = kDLUInt, .bits = 8, .lanes = 1};
using PlanC = CompressPlan;
using PlanW = WritePlan;
using PlanD = DecodePlan;
using RID_T = int64_t;
using R2T_T = int32_t;
using F2S_T = int64_t;
using IDX_T = int64_t;
/// NOTE: for the internal use, we pack the ragged and batch id, since both not exceed 65536
SGL_DEVICE __host__ PlanW pack_w(uint32_t ragged_id, uint32_t batch_id, int32_t seq_len) {
return {static_cast<uint32_t>(ragged_id | batch_id << 16), seq_len};
}
/// NOTE: for the internal use, we pack the ragged and batch id, since both not exceed 65536
SGL_DEVICE uint2 unpack_w(PlanW plan) {
return {static_cast<uint16_t>(plan.ragged_id), static_cast<uint16_t>(plan.ragged_id >> 16)};
}
struct Prefill0Params {
PlanC* plan_c;
PlanW* plan_w;
const IDX_T* seq_lens_ptr; // [batch_size]
const IDX_T* extend_lens_ptr; // [batch_size]
uint32_t batch_size;
uint32_t num_q_tokens;
int32_t compress_ratio;
int32_t swa_page_size;
int32_t mtp_pad;
};
struct Prefill1Params {
PlanC* plan_c;
PlanW* plan_w;
const RID_T* rid_ptr; // [batch_size]
const R2T_T* r2t_ptr; // [num_reqs, stride_r2t]
const F2S_T* f2s_ptr; // [num_full_slots], full_loc -> swa_loc
int64_t stride_r2t;
uint32_t num_c;
uint32_t num_w;
uint32_t num_c_padded;
uint32_t num_w_padded;
uint32_t num_work;
int32_t swa_page_size;
int32_t ring_size;
int32_t compress_ratio;
};
struct DecodeParams {
PlanD* plan_d;
const RID_T* rid_ptr; // [batch_size]
const R2T_T* r2t_ptr; // [num_reqs, stride_r2t]
const F2S_T* f2s_ptr; // [num_full_slots], full_loc -> swa_loc
const IDX_T* seq_ptr; // [batch_size]
int64_t stride_r2t;
uint32_t batch_size;
int32_t swa_page_size;
int32_t ring_size;
int32_t compress_ratio;
};
struct Prefill1ParamsLegacy {
PlanC* plan_c;
PlanW* plan_w;
const RID_T* rid_ptr; // [batch_size]
uint32_t num_c;
uint32_t num_w;
uint32_t num_c_padded;
uint32_t num_w_padded;
uint32_t num_work;
int32_t compress_ratio;
};
struct DecodeParamsLegacy {
PlanD* plan_d;
const RID_T* rid_ptr; // [batch_size]
const IDX_T* seq_ptr; // [batch_size]
uint32_t batch_size;
int32_t compress_ratio;
};
inline constexpr uint32_t kMaxPrefillBatchSize = 1024;
SGL_DEVICE uint32_t warp_inclusive_sum(uint32_t lane_id, uint32_t val) {
static_assert(device::kWarpThreads == 32);
#pragma unroll
for (uint32_t offset = 1; offset < 32; offset *= 2) {
#ifndef USE_ROCM
uint32_t n = __shfl_up_sync(device::kFullMask, val, offset);
#else
uint32_t n = __shfl_up(val, offset, 32);
#endif
if (lane_id >= offset) val += n;
}
return val;
}
/// Warp-wide max/min for integer types. `device::warp::reduce_max` routes through
/// `dtype_trait<T>::max` which is only specialized for FP types.
SGL_DEVICE uint32_t warp_reduce_max_u32(uint32_t val) {
#pragma unroll
for (uint32_t mask = 16; mask > 0; mask >>= 1) {
#ifndef USE_ROCM
val = max(val, __shfl_xor_sync(device::kFullMask, val, mask, 32));
#else
val = max(val, __shfl_xor(val, mask, 32));
#endif
}
return val;
}
SGL_DEVICE uint32_t warp_reduce_min_u32(uint32_t val) {
#pragma unroll
for (uint32_t mask = 16; mask > 0; mask >>= 1) {
#ifndef USE_ROCM
val = min(val, __shfl_xor_sync(device::kFullMask, val, mask, 32));
#else
val = min(val, __shfl_xor(val, mask, 32));
#endif
}
return val;
}
__global__ __launch_bounds__(1024, 1) //
void plan_compress_prefill_kernel0(const Prefill0Params params) {
using namespace device;
const auto tx = threadIdx.x;
const auto block_size = kMaxPrefillBatchSize;
constexpr auto kNumWarps = kMaxPrefillBatchSize / kWarpThreads;
const auto cr = params.compress_ratio;
const auto sps = params.swa_page_size;
const bool is_overlap = (cr == 4);
const int32_t window_size = cr * (is_overlap ? 2 : 1);
alignas(128) __shared__ uint32_t counter_c;
alignas(128) __shared__ uint32_t counter_w;
__shared__ int32_t s_seq_len[kMaxPrefillBatchSize];
__shared__ int32_t s_prefix_len[kMaxPrefillBatchSize];
__shared__ uint32_t warp_max[kNumWarps];
__shared__ uint32_t warp_min[kNumWarps];
__shared__ uint32_t s_max_extend;
__shared__ uint32_t s_min_extend;
const auto lane_id = tx % kWarpThreads;
const auto warp_id = tx / kWarpThreads;
// === Stage A: load per-batch fields, init shared scratch ===
int32_t seq_len = 0, extend_len = 0, prefix_len = 0;
if (tx < params.batch_size) {
seq_len = static_cast<int32_t>(params.seq_lens_ptr[tx]);
extend_len = static_cast<int32_t>(params.extend_lens_ptr[tx]);
prefix_len = seq_len - extend_len;
s_seq_len[tx] = seq_len;
s_prefix_len[tx] = prefix_len;
}
if (tx == 0) {
counter_c = 0;
counter_w = 0;
}
if (tx < kNumWarps) {
warp_max[tx] = 0;
warp_min[tx] = 0xFFFFFFFFu;
}
// === Stage B: min/max(extend_len) for MTP-uniform detection ===
// For min, treat threads outside `batch_size` as +inf so they don't pull the min down.
const uint32_t e_for_max = static_cast<uint32_t>(extend_len);
const uint32_t e_for_min = (tx < params.batch_size) ? e_for_max : 0xFFFFFFFFu;
warp_max[warp_id] = warp_reduce_max_u32(e_for_max);
warp_min[warp_id] = warp_reduce_min_u32(e_for_min);
__syncthreads();
if (warp_id == 0) {
s_max_extend = warp_reduce_max_u32(warp_max[lane_id]);
s_min_extend = warp_reduce_min_u32(warp_min[lane_id]);
}
__syncthreads();
const auto num_q = params.num_q_tokens;
// MTP-uniform: every batch shares the same small extend_len `E`, so we can decompose
// a global token id `k` into (batch_id, j) = (k / E, k % E) and skip the per-batch loop.
const bool is_mtp_extend = (s_min_extend == s_max_extend) && (s_max_extend > 0) && (s_max_extend <= 32);
// === Stage C: emit valid plans, slot allocation via shared-mem atomicAdd ===
if (is_mtp_extend) {
// Path 1: token-driven. Each global token id maps to exactly one (batch_id, j).
const uint32_t E = s_max_extend;
// num_q is the padded buffer size (graph bucket), not the work size: cap the
// loop at the real token count so batch_id = k / E stays < batch_size on an
// underfilled replay; Stage D pads [counter, num_q) with invalid.
const uint32_t num_real_q = params.batch_size * E;
for (uint32_t k = tx; k < num_real_q; k += block_size) {
const uint32_t batch_id = k / E;
const uint32_t j = k % E;
const int32_t pl = s_prefix_len[batch_id];
const int32_t sl = s_seq_len[batch_id];
const int32_t position = pl + static_cast<int32_t>(j);
const uint32_t ragged_id = k;
if ((position + 1) % cr == 0) {
const int32_t buffer_len = window_size - min(static_cast<int32_t>(j) + 1, window_size);
const uint32_t out_idx = atomicAdd(&counter_c, 1u);
params.plan_c[out_idx] = {
.seq_len = static_cast<uint32_t>(position + 1),
.ragged_id = static_cast<uint16_t>(ragged_id),
.buffer_len = static_cast<uint16_t>(buffer_len),
.read_page_0 = -1,
.read_page_1 = static_cast<int32_t>(batch_id),
};
}
const int32_t last_c_pos = (sl / cr) * cr;
const int32_t first_w_pos = min(last_c_pos - (is_overlap ? cr : 0), sl - params.mtp_pad);
bool do_write = position >= first_w_pos;
if (!do_write && is_overlap) do_write = (position % sps) >= (sps - cr);
if (do_write) {
const uint32_t out_idx = atomicAdd(&counter_w, 1u);
params.plan_w[out_idx] = pack_w(ragged_id, batch_id, position + 1);
}
}
} else {
// Path 2: general prefill (long extend_len). Iterate batches in an outer loop;
// the whole block sweeps each batch's tokens in parallel.
uint32_t base_e = 0;
for (uint32_t batch_id = 0; batch_id < params.batch_size; ++batch_id) {
const int32_t pl = s_prefix_len[batch_id];
const int32_t sl = s_seq_len[batch_id];
const int32_t el = sl - pl;
const int32_t last_c_pos = (sl / cr) * cr;
const int32_t first_w_pos = min(last_c_pos - (is_overlap ? cr : 0), sl - params.mtp_pad);
for (int32_t j = static_cast<int32_t>(tx); j < el; j += static_cast<int32_t>(block_size)) {
const int32_t position = pl + j;
const uint32_t ragged_id = base_e + static_cast<uint32_t>(j);
if ((position + 1) % cr == 0) {
const int32_t buffer_len = window_size - min(j + 1, window_size);
const uint32_t out_idx = atomicAdd(&counter_c, 1u);
params.plan_c[out_idx] = {
.seq_len = static_cast<uint32_t>(position + 1),
.ragged_id = static_cast<uint16_t>(ragged_id),
.buffer_len = static_cast<uint16_t>(buffer_len),
.read_page_0 = -1,
.read_page_1 = static_cast<int32_t>(batch_id),
};
}
bool do_write = position >= first_w_pos;
if (!do_write && is_overlap) do_write = (position % sps) >= (sps - cr);
if (do_write) {
const uint32_t out_idx = atomicAdd(&counter_w, 1u);
params.plan_w[out_idx] = pack_w(ragged_id, static_cast<uint32_t>(batch_id), position + 1);
}
}
base_e += static_cast<uint32_t>(el);
}
}
__syncthreads();
// === Stage D: pad [counter_c, num_q) / [counter_w, num_q) with invalid ===
const auto total_c = counter_c;
const auto total_w = counter_w;
for (uint32_t k = total_c + tx; k < num_q; k += block_size) {
params.plan_c[k] = PlanC::invalid();
}
for (uint32_t k = total_w + tx; k < num_q; k += block_size) {
params.plan_w[k] = PlanW::invalid();
}
}
/// NOTE: stage 1
__global__ void plan_compress_prefill_kernel_1(const Prefill1Params params) {
const auto idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= params.num_work) return;
auto plan_c = idx < params.num_c ? params.plan_c[idx] : PlanC::invalid();
auto plan_w = idx < params.num_w ? params.plan_w[idx] : PlanW::invalid();
const auto compute_loc = [&](int32_t swa_loc) {
const auto swa_page = swa_loc / params.swa_page_size;
const auto ring_offset = swa_loc % params.ring_size;
return swa_page * params.ring_size + ring_offset;
};
const auto compute_c128_loc = [&](int64_t rid, int32_t position) {
return static_cast<int32_t>(rid * params.ring_size + position % params.ring_size);
};
if (!plan_c.is_invalid()) { // 1. in bound. 2. not masked
if (plan_c.buffer_len > 0) {
const auto batch_id = plan_c.read_page_1;
const auto rid = params.rid_ptr[batch_id];
const auto mapping = params.r2t_ptr + rid * params.stride_r2t;
// `seq_len` should be ratio-aligned here
const auto position_1 = static_cast<int32_t>(plan_c.seq_len - 1);
// only used for c4, harmless for c128
const auto position_0 = max(position_1 - params.compress_ratio, 0);
if (params.compress_ratio == 128) {
plan_c.read_page_0 = compute_c128_loc(rid, position_0) / 128;
plan_c.read_page_1 = compute_c128_loc(rid, position_1) / 128;
} else {
const auto raw_loc_0 = mapping[position_0];
const auto raw_loc_1 = mapping[position_1];
const auto state_loc_0 = params.f2s_ptr[raw_loc_0];
const auto state_loc_1 = params.f2s_ptr[raw_loc_1];
plan_c.read_page_0 = compute_loc(state_loc_0) / params.compress_ratio;
plan_c.read_page_1 = compute_loc(state_loc_1) / params.compress_ratio;
}
params.plan_c[idx] = plan_c;
}
} else if (idx < params.num_c_padded) {
params.plan_c[idx] = PlanC::invalid();
}
if (!plan_w.is_invalid()) { // 1. in bound. 2. not masked
const auto [ragged_id, batch_id] = unpack_w(plan_w);
const auto rid = params.rid_ptr[batch_id];
const auto mapping = params.r2t_ptr + rid * params.stride_r2t;
// `seq_len` (`write_loc`) may not be aligned here
const auto position = static_cast<int32_t>(plan_w.write_loc - 1);
plan_w.ragged_id = ragged_id;
if (params.compress_ratio == 128) {
plan_w.write_loc = compute_c128_loc(rid, position);
} else {
const auto raw_loc = mapping[position];
plan_w.write_loc = compute_loc(params.f2s_ptr[raw_loc]);
}
params.plan_w[idx] = plan_w;
} else if (idx < params.num_w_padded) {
params.plan_w[idx] = PlanW::invalid();
}
}
__global__ void plan_compress_decode_kernel(const DecodeParams params) {
const auto idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= params.batch_size) return;
const auto rid = params.rid_ptr[idx];
const auto mapping = params.r2t_ptr + rid * params.stride_r2t;
const auto compute_loc = [&](int32_t swa_loc) {
const auto swa_page = swa_loc / params.swa_page_size;
const auto ring_offset = swa_loc % params.ring_size;
return swa_page * params.ring_size + ring_offset;
};
const auto compute_c128_loc = [&](int64_t rid, int32_t position) {
return static_cast<int32_t>(rid * params.ring_size + position % params.ring_size);
};
const auto seq_len = static_cast<int32_t>(params.seq_ptr[idx]);
const auto position_1 = static_cast<int32_t>(seq_len - 1);
const auto position_0 = max(position_1 - params.compress_ratio, 0);
int32_t write_loc;
int32_t read_page_0;
int32_t read_page_1;
if (params.compress_ratio == 128) {
write_loc = compute_c128_loc(rid, position_1);
read_page_0 = compute_c128_loc(rid, position_0) / 128;
read_page_1 = compute_c128_loc(rid, position_1) / 128;
} else {
const auto raw_loc_0 = mapping[position_0];
const auto raw_loc_1 = mapping[position_1];
const auto state_loc_0 = params.f2s_ptr[raw_loc_0];
const auto state_loc_1 = params.f2s_ptr[raw_loc_1];
write_loc = static_cast<int32_t>(compute_loc(state_loc_1));
read_page_0 = static_cast<int32_t>(compute_loc(state_loc_0) / params.compress_ratio);
read_page_1 = static_cast<int32_t>(write_loc / params.compress_ratio);
}
params.plan_d[idx] = {
.seq_len = static_cast<uint32_t>(seq_len),
.write_loc = write_loc,
.read_page_0 = read_page_0,
.read_page_1 = read_page_1,
};
}
__global__ void plan_compress_prefill_legacy_kernel(const Prefill1ParamsLegacy params) {
const auto idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= params.num_work) return;
auto plan_c = idx < params.num_c ? params.plan_c[idx] : PlanC::invalid();
auto plan_w = idx < params.num_w ? params.plan_w[idx] : PlanW::invalid();
/// Per-request ring buffer slot translation:
/// - c4: page = rid * 2 + (position / 4) % 2; slot = page * 4 + position % 4
/// - c128: page = rid; slot = rid * 128 + position % 128
const auto legacy_compute_page = [&](int32_t rid, int32_t position) {
if (params.compress_ratio == 4) return rid * 2 + ((position / 4) & 1);
return rid; // c128
};
const auto legacy_compute_loc = [&](int32_t rid, int32_t position) {
const auto remainder = position % params.compress_ratio;
return legacy_compute_page(rid, position) * params.compress_ratio + remainder;
};
if (!plan_c.is_invalid()) {
const auto batch_id = plan_c.read_page_1;
const auto rid = static_cast<int32_t>(params.rid_ptr[batch_id]);
// `seq_len` is ratio-aligned for compress events
const auto position_1 = static_cast<int32_t>(plan_c.seq_len) - 1;
const auto position_0 = max(position_1 - params.compress_ratio, 0);
plan_c.read_page_0 = legacy_compute_page(rid, position_0);
plan_c.read_page_1 = legacy_compute_page(rid, position_1);
params.plan_c[idx] = plan_c;
} else if (idx < params.num_c_padded) {
params.plan_c[idx] = PlanC::invalid();
}
if (!plan_w.is_invalid()) {
const auto [ragged_id, batch_id] = unpack_w(plan_w);
const auto rid = static_cast<int32_t>(params.rid_ptr[batch_id]);
// `write_loc` carries (position + 1) at this stage; may not be ratio-aligned
const auto position = static_cast<int32_t>(plan_w.write_loc) - 1;
plan_w.ragged_id = ragged_id;
plan_w.write_loc = legacy_compute_loc(rid, position);
params.plan_w[idx] = plan_w;
} else if (idx < params.num_w_padded) {
params.plan_w[idx] = PlanW::invalid();
}
}
__global__ void plan_compress_decode_legacy_kernel(const DecodeParamsLegacy params) {
const auto idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= params.batch_size) return;
/// Per-request ring buffer slot translation:
/// - c4: page = rid * 2 + (position / 4) % 2; slot = page * 4 + position % 4
/// - c128: page = rid; slot = rid * 128 + position % 128
const auto legacy_compute_page = [&](int32_t rid, int32_t position) {
if (params.compress_ratio == 4) return rid * 2 + ((position / 4) & 1);
return rid; // c128
};
const auto legacy_compute_loc = [&](int32_t rid, int32_t position) {
const auto remainder = position % params.compress_ratio;
return legacy_compute_page(rid, position) * params.compress_ratio + remainder;
};
const auto rid = static_cast<int32_t>(params.rid_ptr[idx]);
const auto seq_len = static_cast<int32_t>(params.seq_ptr[idx]);
const auto position_1 = seq_len - 1;
const auto position_0 = max(position_1 - params.compress_ratio, 0);
const int32_t write_loc = legacy_compute_loc(rid, position_1);
const int32_t read_page_0 = legacy_compute_page(rid, position_0);
const int32_t read_page_1 = legacy_compute_page(rid, position_1);
params.plan_d[idx] = {
.seq_len = static_cast<uint32_t>(seq_len),
.write_loc = write_loc,
.read_page_0 = read_page_0,
.read_page_1 = read_page_1,
};
}
using PrefillPlan = tvm::ffi::Tuple<tvm::ffi::Tensor, tvm::ffi::Tensor>;
/**
* \brief Build c4/c128 prefill plan tensors. CPU-resident.
* Inputs (all CPU-resident):
* @param req_pool_indices `[batch_size]` int64_t
* @param req_to_token `[num_reqs, max_tokens_per_req]` int64_t
* @param full_to_state `[full_cache_size]` int64_t. For c4 this maps
* full loc -> SWA loc; ignored for c128, whose
* state slot is request-scoped.
* @param seq_lens `[batch_size]` int64
* @param extend_lens `[batch_size]` int64
* @param compress_plan `[num_q_tokens, 16]` uint8 (output)
* @param write_plan `[num_q_tokens, 8]` uint8 (output)
* @param compress_ratio 4 for c4, 128 for c128
* @param use_cuda_graph Whether the plans will be used with cuda graph (affects padding)
* @return (compress plan tensor, write plan tensor)
*/
inline PrefillPlan plan_compress_prefill(
const tvm::ffi::TensorView req_pool_indices, // GPU
const tvm::ffi::TensorView req_to_token, // GPU
const tvm::ffi::TensorView full_to_state, // GPU
const tvm::ffi::TensorView seq_lens, // CPU/GPU
const tvm::ffi::TensorView extend_lens, // CPU/GPU
const tvm::ffi::TensorView pin_buffer, // CPU
const uint32_t num_q_tokens,
const int32_t compress_ratio,
const int32_t swa_page_size,
const int32_t ring_size,
const bool use_cuda_graph) {
auto B = SymbolicSize{"batch_size"};
auto N = SymbolicSize{"num_q_tokens"};
auto cpu_or_gpu = SymbolicDevice{};
auto device_ = SymbolicDevice{};
cpu_or_gpu.set_options<kDLCPU, kDLGPU>();
device_.set_options<kDLGPU>();
TensorMatcher({B}) //
.with_dtype<RID_T>()
.with_device(device_)
.verify(req_pool_indices);
TensorMatcher({-1, -1}) //
.with_dtype<R2T_T>()
.with_device(device_)
.verify(req_to_token);
TensorMatcher({-1}) //
.with_dtype<F2S_T>()
.with_device(device_)
.verify(full_to_state);
TensorMatcher({B}) //
.with_dtype<IDX_T>()
.with_device(cpu_or_gpu)
.verify(seq_lens)
.verify(extend_lens);
TensorMatcher({-1}) //
.with_dtype<uint8_t>()
.with_device<kDLCPU>()
.verify(pin_buffer);
const bool is_overlap = (compress_ratio == 4);
const int32_t window_size = compress_ratio * (is_overlap ? 2 : 1);
const auto seq_ptr = static_cast<const IDX_T*>(seq_lens.data_ptr());
const auto ext_ptr = static_cast<const IDX_T*>(extend_lens.data_ptr());
const auto rid_ptr = static_cast<const RID_T*>(req_pool_indices.data_ptr());
const auto r2t_ptr = static_cast<const R2T_T*>(req_to_token.data_ptr());
const auto f2s_ptr = static_cast<const F2S_T*>(full_to_state.data_ptr());
const auto batch_size = static_cast<uint32_t>(B.unwrap());
constexpr auto kMaxTokens = static_cast<uint32_t>(std::numeric_limits<uint16_t>::max());
RuntimeCheck(compress_ratio == 4 || compress_ratio == 128);
RuntimeCheck(batch_size <= num_q_tokens && num_q_tokens <= kMaxTokens);
// `swa_page_size` >= `ring_size` >= `compress_ratio`
RuntimeCheck(swa_page_size % ring_size == 0 && ring_size % compress_ratio == 0);
const auto device = device_.unwrap();
const auto stream = LaunchKernel::resolve_device(device);
constexpr int32_t kMaxMTPDraftTokens = 4;
const auto mtp_pad = std::min(ring_size - compress_ratio, kMaxMTPDraftTokens);
if (cpu_or_gpu.unwrap().device_type == kDLGPU) {
// GPU input path: kernel0 builds the (CPU-loop-equivalent) plan metadata directly
// on device, padding to num_q_tokens with invalid; kernel_1 then finalizes the
// SWA-translated read/write locations. Used for MTP / cuda-graph capture where
// a host sync would be expensive.
RuntimeCheck(batch_size <= kMaxPrefillBatchSize, "GPU plan only support batch size up to ", kMaxPrefillBatchSize);
auto C = ffi::empty({num_q_tokens, sizeof(PlanC)}, kDLUInt8, device);
auto W = ffi::empty({num_q_tokens, sizeof(PlanW)}, kDLUInt8, device);
const auto params0 = Prefill0Params{
.plan_c = static_cast<PlanC*>(C.data_ptr()),
.plan_w = static_cast<PlanW*>(W.data_ptr()),
.seq_lens_ptr = seq_ptr,
.extend_lens_ptr = ext_ptr,
.batch_size = batch_size,
.num_q_tokens = num_q_tokens,
.compress_ratio = compress_ratio,
.swa_page_size = swa_page_size,
.mtp_pad = mtp_pad,
};
LaunchKernel(1, kMaxPrefillBatchSize, device)(plan_compress_prefill_kernel0, params0);
// kernel_1 sees the already-padded buffers, so num_c == num_w == num_padded == num_q_tokens.
const auto params1 = Prefill1Params{
.plan_c = static_cast<PlanC*>(C.data_ptr()),
.plan_w = static_cast<PlanW*>(W.data_ptr()),
.rid_ptr = rid_ptr,
.r2t_ptr = r2t_ptr,
.f2s_ptr = f2s_ptr,
.stride_r2t = req_to_token.stride(0),
.num_c = num_q_tokens,
.num_w = num_q_tokens,
.num_c_padded = num_q_tokens,
.num_w_padded = num_q_tokens,
.num_work = num_q_tokens,
.swa_page_size = swa_page_size,
.ring_size = ring_size,
.compress_ratio = compress_ratio,
};
const auto block_size_1 = 256;
const auto num_blocks_1 = div_ceil(params1.num_work, block_size_1);
LaunchKernel(num_blocks_1, block_size_1, device)(plan_compress_prefill_kernel_1, params1);
return PrefillPlan{std::move(C), std::move(W)};
}
// CPU input path: only here do we need the pinned scratch buffer.
const auto pin_buffer_bytes = static_cast<size_t>(pin_buffer.numel()) * sizeof(uint8_t);
RuntimeCheck(pin_buffer_bytes >= num_q_tokens * (sizeof(PlanC) + sizeof(PlanW)));
const auto plan_c_ptr = reinterpret_cast<PlanC*>(pin_buffer.data_ptr());
const auto plan_w_ptr = reinterpret_cast<PlanW*>(plan_c_ptr + num_q_tokens);
uint32_t counter = 0;
uint32_t counter_c = 0;
uint32_t counter_w = 0;
const auto should_compress = [=](int32_t position) { return (position + 1) % compress_ratio == 0; };
for (const auto i : irange(batch_size)) {
const int32_t seq_len = seq_ptr[i];
const int32_t extend_len = ext_ptr[i];
const int32_t prefix_len = seq_len - extend_len;
const int32_t last_c_pos = seq_len / compress_ratio * compress_ratio;
const int32_t first_w_pos = last_c_pos - (is_overlap ? compress_ratio : 0);
RuntimeCheck(0 < extend_len && extend_len <= seq_len);
const auto should_write = [=](int32_t position) {
if (position >= first_w_pos) return true;
return is_overlap && position % swa_page_size >= (swa_page_size - compress_ratio);
};
for (const auto j : irange(extend_len)) {
const int32_t position = prefix_len + j;
const int32_t ragged_id = counter + j;
if (should_compress(position)) {
const auto buffer_len = window_size - std::min(j + 1, window_size);
plan_c_ptr[counter_c++] = {
.seq_len = static_cast<uint32_t>(position + 1),
.ragged_id = static_cast<uint16_t>(ragged_id),
.buffer_len = static_cast<uint16_t>(buffer_len),
// to be filled by kernel
.read_page_0 = -1,
.read_page_1 = static_cast<int32_t>(i),
};
}
if (should_write(position)) {
plan_w_ptr[counter_w++] = pack_w(ragged_id, i, position + 1);
}
}
counter += extend_len;
}
RuntimeCheck(counter == num_q_tokens);
const auto copy_to_device = [stream](void* cuda_ptr, auto* host_ptr, size_t count) {
const auto size_bytes = count * sizeof(*host_ptr);
RuntimeDeviceCheck(cudaMemcpyAsync(cuda_ptr, host_ptr, size_bytes, cudaMemcpyHostToDevice, stream));
};
const auto num_c_padded = use_cuda_graph ? num_q_tokens : counter_c;
const auto num_w_padded = use_cuda_graph ? num_q_tokens : counter_w;
auto C = ffi::empty({num_c_padded, sizeof(PlanC)}, kDLUInt8, device);
auto W = ffi::empty({num_w_padded, sizeof(PlanW)}, kDLUInt8, device);
copy_to_device(C.data_ptr(), plan_c_ptr, counter_c);
copy_to_device(W.data_ptr(), plan_w_ptr, counter_w);
const auto params = Prefill1Params{
.plan_c = static_cast<PlanC*>(C.data_ptr()),
.plan_w = static_cast<PlanW*>(W.data_ptr()),
.rid_ptr = rid_ptr,
.r2t_ptr = r2t_ptr,
.f2s_ptr = f2s_ptr,
.stride_r2t = req_to_token.size(1),
.num_c = counter_c,
.num_w = counter_w,
.num_c_padded = num_c_padded,
.num_w_padded = num_w_padded,
.num_work = std::max(num_c_padded, num_w_padded),
.swa_page_size = swa_page_size,
.ring_size = ring_size,
.compress_ratio = compress_ratio,
};
const auto block_size = 256;
const auto num_blocks = div_ceil(params.num_work, block_size);
LaunchKernel(num_blocks, block_size, device)(plan_compress_prefill_kernel_1, params);
return PrefillPlan{std::move(C), std::move(W)};
}
inline tvm::ffi::Tensor plan_compress_decode(
const tvm::ffi::TensorView req_pool_indices, // GPU
const tvm::ffi::TensorView req_to_token, // GPU
const tvm::ffi::TensorView full_to_state, // GPU
const tvm::ffi::TensorView seq_lens, // CPU/GPU
const int32_t compress_ratio,
const int32_t swa_page_size,
const int32_t ring_size) {
auto B = SymbolicSize{"batch_size"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLGPU>();
TensorMatcher({B}) //
.with_dtype<RID_T>()
.with_device(device_)
.verify(req_pool_indices);
TensorMatcher({-1, -1}) //
.with_dtype<R2T_T>()
.with_device(device_)
.verify(req_to_token);
TensorMatcher({-1}) //
.with_dtype<F2S_T>()
.with_device(device_)
.verify(full_to_state);
TensorMatcher({B}) //
.with_dtype<IDX_T>()
.with_device(device_)
.verify(seq_lens);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto device = device_.unwrap();
auto D = ffi::empty({batch_size, sizeof(PlanD)}, kDLUInt8, device);
const auto params = DecodeParams{
.plan_d = static_cast<PlanD*>(D.data_ptr()),
.rid_ptr = static_cast<const RID_T*>(req_pool_indices.data_ptr()),
.r2t_ptr = static_cast<const R2T_T*>(req_to_token.data_ptr()),
.f2s_ptr = static_cast<const F2S_T*>(full_to_state.data_ptr()),
.seq_ptr = static_cast<const IDX_T*>(seq_lens.data_ptr()),
.stride_r2t = req_to_token.size(1),
.batch_size = batch_size,
.swa_page_size = swa_page_size,
.ring_size = ring_size,
.compress_ratio = compress_ratio,
};
const auto block_size = 256;
const auto num_blocks = div_ceil(batch_size, block_size);
LaunchKernel(num_blocks, block_size, device)(plan_compress_decode_kernel, params);
return D;
}
/**
* \brief Build c4/c128 prefill plan tensors for the legacy non-paged ring
* buffer. Uses only `req_pool_indices` to derive ring slots:
* - c4 (overlap): each request occupies 2 contiguous pages (8 token slots)
* - c128: each request occupies 1 page (128 token slots)
*
* Inputs:
* @param req_pool_indices `[batch_size]` int64 (GPU)
* @param seq_lens `[batch_size]` int64 (CPU)
* @param extend_lens `[batch_size]` int64 (CPU)
* @param pin_buffer pinned scratch (CPU uint8)
* @return (compress plan tensor, write plan tensor)
*/
inline PrefillPlan plan_compress_prefill_legacy(
const tvm::ffi::TensorView req_pool_indices, // GPU
const tvm::ffi::TensorView seq_lens, // CPU
const tvm::ffi::TensorView extend_lens, // CPU
const tvm::ffi::TensorView pin_buffer, // CPU
const uint32_t num_q_tokens,
const int32_t compress_ratio,
const bool use_cuda_graph) {
auto B = SymbolicSize{"batch_size"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLGPU>();
TensorMatcher({B}) //
.with_dtype<RID_T>()
.with_device(device_)
.verify(req_pool_indices);
TensorMatcher({B}) //
.with_dtype<IDX_T>()
.with_device<kDLCPU>()
.verify(seq_lens)
.verify(extend_lens);
TensorMatcher({-1}) //
.with_dtype<uint8_t>()
.with_device<kDLCPU>()
.verify(pin_buffer);
const auto pin_buffer_bytes = static_cast<size_t>(pin_buffer.numel()) * sizeof(uint8_t);
RuntimeCheck(pin_buffer_bytes >= num_q_tokens * (sizeof(PlanC) + sizeof(PlanW)));
const auto plan_c_ptr = reinterpret_cast<PlanC*>(pin_buffer.data_ptr());
const auto plan_w_ptr = reinterpret_cast<PlanW*>(plan_c_ptr + num_q_tokens);
const bool is_overlap = (compress_ratio == 4);
const auto seq_ptr = static_cast<const IDX_T*>(seq_lens.data_ptr());
const auto ext_ptr = static_cast<const IDX_T*>(extend_lens.data_ptr());
const auto rid_ptr = static_cast<const RID_T*>(req_pool_indices.data_ptr());
const auto window_size = compress_ratio * (is_overlap ? 2 : 1);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
constexpr auto kMaxTokens = static_cast<uint32_t>(std::numeric_limits<uint16_t>::max());
RuntimeCheck(compress_ratio == 4 || compress_ratio == 128);
RuntimeCheck(batch_size <= num_q_tokens && num_q_tokens <= kMaxTokens);
uint32_t counter = 0;
uint32_t counter_c = 0;
uint32_t counter_w = 0;
const auto should_compress = [=](int32_t position) { return (position + 1) % compress_ratio == 0; };
for (const auto i : irange(batch_size)) {
const int32_t seq_len = seq_ptr[i];
const int32_t extend_len = ext_ptr[i];
const int32_t prefix_len = seq_len - extend_len;
const int32_t last_c_pos = seq_len / compress_ratio * compress_ratio;
const int32_t first_w_pos = last_c_pos - (is_overlap ? compress_ratio : 0);
RuntimeCheck(0 < extend_len && extend_len <= seq_len);
const auto should_write = [=](int32_t position) { return position >= first_w_pos; };
for (const auto j : irange(extend_len)) {
const int32_t position = prefix_len + j;
const int32_t ragged_id = counter + j;
if (should_compress(position)) {
const auto buffer_len = window_size - std::min(j + 1, window_size);
plan_c_ptr[counter_c++] = {
.seq_len = static_cast<uint32_t>(position + 1),
.ragged_id = static_cast<uint16_t>(ragged_id),
.buffer_len = static_cast<uint16_t>(buffer_len),
// to be filled by kernel
.read_page_0 = -1,
.read_page_1 = static_cast<int32_t>(i),
};
}
if (should_write(position)) {
plan_w_ptr[counter_w++] = pack_w(ragged_id, i, position + 1);
}
}
counter += extend_len;
}
RuntimeCheck(counter == num_q_tokens);
const auto device = device_.unwrap();
const auto stream = LaunchKernel::resolve_device(device);
const auto copy_to_device = [stream](void* cuda_ptr, auto* host_ptr, size_t count) {
const auto size_bytes = count * sizeof(*host_ptr);
RuntimeDeviceCheck(cudaMemcpyAsync(cuda_ptr, host_ptr, size_bytes, cudaMemcpyHostToDevice, stream));
};
const auto num_c_padded = use_cuda_graph ? num_q_tokens : counter_c;
const auto num_w_padded = use_cuda_graph ? num_q_tokens : counter_w;
auto C = ffi::empty({num_c_padded, sizeof(PlanC)}, kDLUInt8, device);
auto W = ffi::empty({num_w_padded, sizeof(PlanW)}, kDLUInt8, device);
copy_to_device(C.data_ptr(), plan_c_ptr, counter_c);
copy_to_device(W.data_ptr(), plan_w_ptr, counter_w);
const auto params = Prefill1ParamsLegacy{
.plan_c = static_cast<PlanC*>(C.data_ptr()),
.plan_w = static_cast<PlanW*>(W.data_ptr()),
.rid_ptr = rid_ptr,
.num_c = counter_c,
.num_w = counter_w,
.num_c_padded = num_c_padded,
.num_w_padded = num_w_padded,
.num_work = std::max(num_c_padded, num_w_padded),
.compress_ratio = compress_ratio,
};
const auto block_size = 256;
const auto num_blocks = div_ceil(params.num_work, block_size);
if (num_blocks > 0) {
LaunchKernel(num_blocks, block_size, device)(plan_compress_prefill_legacy_kernel, params);
}
return PrefillPlan{std::move(C), std::move(W)};
}
inline tvm::ffi::Tensor plan_compress_decode_legacy(
const tvm::ffi::TensorView req_pool_indices, // GPU
const tvm::ffi::TensorView seq_lens, // GPU
const int32_t compress_ratio) {
auto B = SymbolicSize{"batch_size"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLGPU>();
TensorMatcher({B}) //
.with_dtype<RID_T>()
.with_device(device_)
.verify(req_pool_indices);
TensorMatcher({B}) //
.with_dtype<IDX_T>()
.with_device(device_)
.verify(seq_lens);
RuntimeCheck(compress_ratio == 4 || compress_ratio == 128);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto device = device_.unwrap();
auto D = ffi::empty({batch_size, sizeof(PlanD)}, kDLUInt8, device);
const auto params = DecodeParamsLegacy{
.plan_d = static_cast<PlanD*>(D.data_ptr()),
.rid_ptr = static_cast<const RID_T*>(req_pool_indices.data_ptr()),
.seq_ptr = static_cast<const IDX_T*>(seq_lens.data_ptr()),
.batch_size = batch_size,
.compress_ratio = compress_ratio,
};
const auto block_size = 256;
const auto num_blocks = div_ceil(batch_size, block_size);
LaunchKernel(num_blocks, block_size, device)(plan_compress_decode_legacy_kernel, params);
return D;
}
} // namespace host::compress
using namespace host::compress; // expose binding
@@ -0,0 +1,208 @@
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/deepseek_v4/compress.cuh>
#include <dlpack/dlpack.h>
namespace host::compress {
using PlanResult = tvm::ffi::Tuple<uint32_t, uint32_t>;
struct CompressParams {
PrefillPlan* __restrict__ compress_plan;
PrefillPlan* __restrict__ write_plan;
const int64_t* __restrict__ seq_lens;
const int64_t* __restrict__ extend_lens;
uint32_t batch_size;
uint32_t num_tokens;
uint32_t compress_ratio;
bool is_overlap;
};
inline constexpr uint32_t kBlockSize = 1024;
#define PLAN_KERNEL __global__ __launch_bounds__(kBlockSize, 1) inline
PLAN_KERNEL void plan_prefill_cuda(const __grid_constant__ CompressParams params) {
const auto &[
compress_plan, write_plan, seq_lens, extend_lens, // pointers
batch_size, num_tokens, compress_ratio, is_overlap // values
] = params;
__shared__ uint32_t compress_counter;
__shared__ uint32_t write_counter;
uint32_t batch_id = 0;
uint32_t counter = 0;
uint32_t extend_len = extend_lens[0];
const auto tid = threadIdx.x;
if (tid == 0) {
compress_counter = 0;
write_counter = 0;
}
__syncthreads();
for (uint32_t i = tid; i < num_tokens; i += blockDim.x) {
const uint32_t ragged_id = i;
uint32_t j = ragged_id - counter;
while (j >= extend_len) {
j -= extend_len;
batch_id += 1;
if (batch_id >= batch_size) [[unlikely]]
break;
counter += extend_len;
extend_len = extend_lens[batch_id];
}
if (batch_id >= batch_size) [[unlikely]]
break;
const uint32_t seq_len = seq_lens[batch_id];
const uint32_t extend_len = extend_lens[batch_id];
const uint32_t prefix_len = seq_len - extend_len;
const uint32_t ratio = compress_ratio * (1 + is_overlap);
const uint32_t window_len = j + 1 < ratio ? ratio - (j + 1) : 0;
const uint32_t position = prefix_len + j;
const auto plan = PrefillPlan{
.ragged_id = ragged_id,
.batch_id = batch_id,
.position = position,
.window_len = window_len,
};
const uint32_t start_write_pos = [seq_len, compress_ratio, is_overlap] {
const uint32_t pos = seq_len / compress_ratio * compress_ratio;
if (!is_overlap) return pos;
return pos >= compress_ratio ? pos - compress_ratio : 0;
}();
if ((position + 1) % compress_ratio == 0) {
const auto write_pos = atomicAdd(&compress_counter, 1);
compress_plan[write_pos] = plan;
}
if (position >= start_write_pos) {
const auto write_pos = atomicAdd(&write_counter, 1);
write_plan[write_pos] = plan;
}
}
__syncthreads();
constexpr auto kInvalid = static_cast<uint32_t>(-1);
const auto kInvalidPlan = PrefillPlan{kInvalid, kInvalid, kInvalid, kInvalid};
const auto compress_count = compress_counter;
const auto write_count = write_counter;
for (uint32_t i = compress_count + tid; i < num_tokens; i += blockDim.x) {
compress_plan[i] = kInvalidPlan;
}
for (uint32_t i = write_count + tid; i < num_tokens; i += blockDim.x) {
write_plan[i] = kInvalidPlan;
}
}
inline PlanResult plan_prefill_host(const CompressParams& params, const bool use_cuda_graph) {
const auto &[
compress_ptr, write_ptr, seq_lens_ptr, extend_lens_ptr, // pointers
batch_size, num_tokens, compress_ratio, is_overlap // values
] = params;
uint32_t counter = 0;
uint32_t compress_counter = 0;
uint32_t write_counter = 0;
const auto ratio = compress_ratio * (1 + is_overlap);
for (const auto i : irange(batch_size)) {
const uint32_t seq_len = seq_lens_ptr[i];
const uint32_t extend_len = extend_lens_ptr[i];
const uint32_t prefix_len = seq_len - extend_len;
RuntimeCheck(0 < extend_len && extend_len <= seq_len);
/// NOTE: `start_write_pos` must be a multiple of `compress_ratio`
const uint32_t start_write_pos = [seq_len, compress_ratio, is_overlap] {
const uint32_t pos = seq_len / compress_ratio * compress_ratio;
if (!is_overlap) return pos;
/// NOTE: to avoid unsigned integer underflow, don't use `pos - compress_ratio`
return pos >= compress_ratio ? pos - compress_ratio : 0;
}();
/// NOTE: `position` is within [prefix_len, seq_len)
for (const auto j : irange(extend_len)) {
const uint32_t position = prefix_len + j;
const auto plan = PrefillPlan{
.ragged_id = counter + j,
.batch_id = i,
.position = position,
.window_len = ratio - std::min(j + 1, ratio),
};
RuntimeCheck(plan.is_valid(compress_ratio, is_overlap), "Internal error!");
if ((position + 1) % compress_ratio == 0) {
compress_ptr[compress_counter++] = plan;
}
if (position >= start_write_pos) {
write_ptr[write_counter++] = plan;
}
}
counter += extend_len;
}
RuntimeCheck(counter == num_tokens, "input size ", counter, " != num_q_tokens ", num_tokens);
if (!use_cuda_graph) return PlanResult{compress_counter, write_counter};
constexpr auto kInvalid = static_cast<uint32_t>(-1);
constexpr auto kInvalidPlan = PrefillPlan{kInvalid, kInvalid, kInvalid, kInvalid};
for (const auto i : irange(compress_counter, num_tokens)) {
compress_ptr[i] = kInvalidPlan;
}
for (const auto i : irange(write_counter, num_tokens)) {
write_ptr[i] = kInvalidPlan;
}
return PlanResult{num_tokens, num_tokens};
}
inline PlanResult plan_prefill(
const tvm::ffi::TensorView extend_lens,
const tvm::ffi::TensorView seq_lens,
const tvm::ffi::TensorView compress_plan,
const tvm::ffi::TensorView write_plan,
const uint32_t compress_ratio,
const bool is_overlap, // for overlap transform, we have to keep 1 more extra window
const bool use_cuda_graph) {
auto N = SymbolicSize{"batch_size"};
auto M = SymbolicSize{"num_tokens"};
auto device = SymbolicDevice{};
const bool is_cuda = [&] {
if (extend_lens.device().device_type == kDLCUDA) {
device.set_options<kDLCUDA>();
return true;
} else {
device.set_options<kDLCPU, kDLCUDAHost>();
return false;
}
}();
TensorMatcher({N}) // extend_lens and seq_lens
.with_dtype<int64_t>()
.with_device(device)
.verify(extend_lens)
.verify(seq_lens);
TensorMatcher({M, kPrefillPlanDim}) // compress_plan and write_plan
.with_dtype<PrefillPlanTensorDtype>()
.with_device(device)
.verify(compress_plan)
.verify(write_plan);
const auto params = CompressParams{
.compress_plan = static_cast<PrefillPlan*>(compress_plan.data_ptr()),
.write_plan = static_cast<PrefillPlan*>(write_plan.data_ptr()),
.seq_lens = static_cast<const int64_t*>(seq_lens.data_ptr()),
.extend_lens = static_cast<const int64_t*>(extend_lens.data_ptr()),
.batch_size = static_cast<uint32_t>(N.unwrap()),
.num_tokens = static_cast<uint32_t>(M.unwrap()),
.compress_ratio = compress_ratio,
.is_overlap = is_overlap,
};
if (!is_cuda) return plan_prefill_host(params, use_cuda_graph);
/// NOTE: cuda kernel plan is naturally compatible with cuda graph
LaunchKernel(1, kBlockSize, device.unwrap())(plan_prefill_cuda, params);
return PlanResult{params.num_tokens, params.num_tokens};
}
} // namespace host::compress
namespace {
[[maybe_unused]]
constexpr auto& plan_compress_prefill = host::compress::plan_prefill;
} // namespace
@@ -0,0 +1,169 @@
// DeepSeek-V4 wo_a activation quantization for DeepGEMM fp8_einsum.
//
// This is intentionally narrower than the generic per_token_group_quant_8bit_v2
// kernel: input is a [T, G, D] view with contiguous hidden groups, output_q is
// contiguous [T, G, D], group_size is fixed to 128, scales are fp32 UE8M0
// power-of-two values, and output_s is a logical [T, G, D/128] view backed by
// group-major [G, T, D/128] storage.
//
// The generic kernel cannot read the strided DSV4 view while producing
// contiguous [T, G, D] codes and group-major scales without an extra full-tensor
// copy.
#include <sgl_kernel/tensor.h> // TensorMatcher, SymbolicSize/Device
#include <sgl_kernel/utils.h> // RuntimeCheck
#include <sgl_kernel/utils.cuh> // fp8 aliases, PDL helpers
#include <sgl_kernel/warp.cuh> // warp::reduce_max
#include <sgl_kernel/deepseek_v4/fp8_utils.cuh> // UE8M0 and FP8 helpers
#include <tvm/ffi/container/tensor.h> // tvm::ffi::TensorView
#include <cstdint>
#include <cuda_fp8.h>
namespace {
using deepseek_v4::fp8::cast_to_ue8m0;
using deepseek_v4::fp8::inv_scale_ue8m0;
using deepseek_v4::fp8::pack_fp8;
constexpr float LOCAL_ABSMAX_ABS = 1e-10f;
constexpr uint32_t GROUP_SIZE = 128;
constexpr uint32_t THREADS_PER_GROUP = 8;
constexpr uint32_t SUBWARPS_PER_BLOCK = 16;
constexpr uint32_t INPUT_VEC_NUM_BYTES = 32;
constexpr uint32_t INPUT_INT4_SIZE = INPUT_VEC_NUM_BYTES / sizeof(int4);
template <int THREADS_PER_SUBWARP>
SGL_DEVICE float GroupReduceMax(float val) {
static_assert(
(THREADS_PER_SUBWARP & (THREADS_PER_SUBWARP - 1)) == 0 && THREADS_PER_SUBWARP <= 16 && THREADS_PER_SUBWARP >= 1,
"THREADS_PER_SUBWARP must be 1, 2, 4, 8, or 16");
// Tail subwarps can be inactive at the bounds check, so reduce with only the
// current subgroup's lanes rather than a full-warp mask.
constexpr device::warp::mask_t kSub = (device::warp::mask_t{1} << THREADS_PER_SUBWARP) - 1;
const device::warp::mask_t mask = kSub << (THREADS_PER_SUBWARP * ((threadIdx.x % 32) / THREADS_PER_SUBWARP));
return device::warp::reduce_max<THREADS_PER_SUBWARP>(val, mask);
}
template <typename T, bool kUsePDL>
__global__ void fp8_wo_a_group_major_quant_ue8m0_kernel(
const T* __restrict__ input,
fp8_e4m3_t* __restrict__ output_q,
float* __restrict__ output_s,
int64_t total_scale_groups,
int64_t num_tokens,
int hidden_dim_groups,
int num_outer_groups,
int64_t input_stride_t) {
device::PDLWaitPrimary<kUsePDL>();
const int64_t subwarp_id = threadIdx.x / THREADS_PER_GROUP;
const int lane_id = threadIdx.x % THREADS_PER_GROUP;
const int64_t group_id = static_cast<int64_t>(blockIdx.x) * SUBWARPS_PER_BLOCK + subwarp_id;
if (group_id < total_scale_groups) {
const int hidden_group = group_id % hidden_dim_groups;
const int64_t token_outer = group_id / hidden_dim_groups;
const int outer_idx = token_outer % num_outer_groups;
const int64_t token_idx = token_outer / num_outer_groups;
constexpr uint32_t INPUT_VEC_SIZE = INPUT_VEC_NUM_BYTES / sizeof(T);
static_assert(INPUT_VEC_SIZE * THREADS_PER_GROUP == GROUP_SIZE);
const int64_t input_group_start_offset =
token_idx * input_stride_t + outer_idx * GROUP_SIZE * hidden_dim_groups + hidden_group * GROUP_SIZE;
const int64_t output_group_start_offset = group_id * GROUP_SIZE;
int4 input_int4[INPUT_INT4_SIZE];
T* input_vec = reinterpret_cast<T*>(input_int4);
#pragma unroll
for (uint32_t j = 0; j < INPUT_INT4_SIZE; ++j) {
input_int4[j] = reinterpret_cast<const int4*>(input + input_group_start_offset + lane_id * INPUT_VEC_SIZE)[j];
}
float local_absmax = LOCAL_ABSMAX_ABS;
#pragma unroll
for (uint32_t j = 0; j < INPUT_VEC_SIZE; ++j) {
const float val = static_cast<float>(input_vec[j]);
local_absmax = fmaxf(local_absmax, fabsf(val));
}
local_absmax = GroupReduceMax<THREADS_PER_GROUP>(local_absmax);
constexpr float kFp8MaxInv = 1.0f / kFP8E4M3Max;
const int32_t scale_ue8m0 = cast_to_ue8m0(local_absmax * kFp8MaxInv);
const float y_scale = inv_scale_ue8m0(scale_ue8m0);
const float y_scale_inv = __uint_as_float(static_cast<uint32_t>(scale_ue8m0) << 23);
int4 output_buf;
auto* output_buf_ptr = reinterpret_cast<fp8x2_e4m3_t*>(&output_buf);
#pragma unroll
for (uint32_t j = 0; j < INPUT_VEC_SIZE; j += 2) {
output_buf_ptr[j / 2] =
pack_fp8(static_cast<float>(input_vec[j]) * y_scale, static_cast<float>(input_vec[j + 1]) * y_scale);
}
*reinterpret_cast<int4*>(output_q + output_group_start_offset + lane_id * INPUT_VEC_SIZE) = output_buf;
if (lane_id == 0) {
output_s[(outer_idx * num_tokens + token_idx) * hidden_dim_groups + hidden_group] = y_scale_inv;
}
}
device::PDLTriggerSecondary<kUsePDL>();
}
template <typename T, bool kUsePDL>
struct FP8WoAGroupMajorQuantUE8M0Kernel {
static void run(tvm::ffi::TensorView input, tvm::ffi::TensorView output_q, tvm::ffi::TensorView output_s) {
using namespace host;
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
auto TSize = SymbolicSize{"num_tokens"};
auto GSize = SymbolicSize{"num_outer_groups"};
auto DSize = SymbolicSize{"hidden_dim"};
auto SSize = SymbolicSize{"hidden_dim_groups"};
TensorMatcher({TSize, GSize, DSize}).with_strides({-1, DSize, 1}).with_dtype<T>().with_device(device).verify(input);
TensorMatcher({TSize, GSize, DSize}).with_dtype<fp8_e4m3_t>().with_device(device).verify(output_q);
TensorMatcher({GSize, TSize, SSize}).with_dtype<float>().with_device(device).verify(output_s);
const auto num_tokens = TSize.unwrap();
const auto num_outer_groups = GSize.unwrap();
const auto hidden_dim = DSize.unwrap();
const auto hidden_dim_groups = SSize.unwrap();
const auto input_stride_t = input.stride(0);
constexpr int64_t kInputAlignElements = sizeof(int4) / sizeof(T);
RuntimeCheck(hidden_dim % GROUP_SIZE == 0, "hidden_dim must be divisible by 128");
RuntimeCheck(hidden_dim_groups == hidden_dim / GROUP_SIZE, "output_s hidden dim mismatch");
RuntimeCheck(
reinterpret_cast<uintptr_t>(input.data_ptr()) % sizeof(int4) == 0,
"input base pointer must be 16-byte aligned");
RuntimeCheck(
num_tokens <= 1 || input_stride_t % kInputAlignElements == 0,
"input token stride must preserve 16-byte vector-load alignment");
const int64_t total_scale_groups = num_tokens * num_outer_groups * hidden_dim_groups;
if (total_scale_groups == 0) return;
const auto grid = dim3((total_scale_groups + SUBWARPS_PER_BLOCK - 1) / SUBWARPS_PER_BLOCK);
const auto block = dim3(SUBWARPS_PER_BLOCK * THREADS_PER_GROUP);
host::LaunchKernel(grid, block, device.unwrap())
.enable_pdl(kUsePDL)(
fp8_wo_a_group_major_quant_ue8m0_kernel<T, kUsePDL>,
static_cast<const T*>(input.data_ptr()),
static_cast<fp8_e4m3_t*>(output_q.data_ptr()),
static_cast<float*>(output_s.data_ptr()),
total_scale_groups,
static_cast<int64_t>(num_tokens),
static_cast<int>(hidden_dim_groups),
static_cast<int>(num_outer_groups),
static_cast<int64_t>(input_stride_t));
}
};
} // namespace
@@ -0,0 +1,254 @@
#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/deepseek_v4/compress.cuh>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
#include <type_traits>
namespace {
using Plan = device::compress::PrefillPlan;
/// \brief common block size for memory-bound kernel
constexpr uint32_t kBlockSize = 128;
constexpr uint32_t kNumWarps = kBlockSize / device::kWarpThreads;
struct FusedNormRopeParams {
void* __restrict__ input;
const void* __restrict__ weight;
float eps;
uint32_t num_works;
const void* __restrict__ handle;
const float* __restrict__ freqs_cis;
uint32_t compress_ratio;
};
enum class ForwardMode {
CompressExtend = 0,
CompressDecode = 1,
DefaultForward = 2,
};
template <typename DType, int64_t kHeadDim, int64_t kRopeDim, ForwardMode kMode, bool kUsePDL>
__global__ void fused_norm_rope(const __grid_constant__ FusedNormRopeParams params) {
using namespace device;
using enum ForwardMode;
constexpr int64_t kMaxVecSize = 16 / sizeof(DType);
constexpr int64_t kVecSize = std::min(kMaxVecSize, kHeadDim / kWarpThreads);
constexpr int64_t kLocalSize = kHeadDim / (kWarpThreads * kVecSize);
constexpr int64_t kRopeVecSize = kRopeDim / (kWarpThreads * 2);
constexpr uint32_t kRopeSize = kRopeDim / kVecSize;
static_assert(kHeadDim % (kWarpThreads * kVecSize) == 0);
static_assert(kLocalSize * kVecSize * kWarpThreads == kHeadDim);
static_assert(kRopeDim % (kWarpThreads * 2) == 0);
static_assert(kRopeDim % (kVecSize * kLocalSize) == 0);
static_assert(kRopeSize <= kWarpThreads);
static_assert(kRopeVecSize == 1, "only support rope dim = 64");
const auto& [
_input, _weight, eps, num_works, // norm
handle, freqs_cis, compress_ratio // rope
] = params;
const auto warp_id = threadIdx.x / kWarpThreads;
const auto lane_id = threadIdx.x % kWarpThreads;
const auto work_id = blockIdx.x * kNumWarps + warp_id;
if (work_id >= num_works) return;
DType* input;
int32_t position;
if constexpr (kMode == CompressExtend) {
const auto plan = static_cast<const Plan*>(handle)[work_id];
input = static_cast<DType*>(_input) + plan.ragged_id * kHeadDim;
position = plan.position + 1 - compress_ratio;
if (plan.ragged_id == 0xFFFFFFFF) [[unlikely]]
return;
} else if constexpr (kMode == CompressDecode) {
input = static_cast<DType*>(_input) + work_id * kHeadDim;
const auto seq_len = static_cast<const int32_t*>(handle)[work_id];
if (seq_len % compress_ratio != 0) return;
position = seq_len - compress_ratio;
} else if constexpr (kMode == DefaultForward) {
input = static_cast<DType*>(_input) + work_id * kHeadDim;
position = static_cast<const int64_t*>(handle)[work_id];
} else {
static_assert(host::dependent_false_v<DType>, "Unsupported Mode");
}
using Storage = AlignedVector<DType, kVecSize>;
__shared__ Storage s_rope_input[kNumWarps][kRopeSize];
// prefetch freq
const auto mem_freq = tile::Memory<fp32x2_t>::warp();
const auto freq = mem_freq.load(freqs_cis + position * kRopeDim);
PDLWaitPrimary<kUsePDL>();
// part 1: norm
{
const auto gmem = tile::Memory<Storage>::warp();
Storage input_vec[kLocalSize];
Storage weight_vec[kLocalSize];
#pragma unroll
for (int i = 0; i < kLocalSize; ++i) {
input_vec[i] = gmem.load(input, i);
}
#pragma unroll
for (int i = 0; i < kLocalSize; ++i) {
weight_vec[i] = gmem.load(_weight, i);
}
float sum_of_squares = 0.0f;
#pragma unroll
for (int i = 0; i < kLocalSize; ++i) {
#pragma unroll
for (int j = 0; j < kVecSize; ++j) {
const auto fp32_input = cast<float>(input_vec[i][j]);
sum_of_squares += fp32_input * fp32_input;
}
}
sum_of_squares = warp::reduce_sum(sum_of_squares);
const auto norm_factor = math::rsqrt(sum_of_squares / kHeadDim + eps);
#pragma unroll
for (int i = 0; i < kLocalSize; ++i) {
#pragma unroll
for (int j = 0; j < kVecSize; ++j) {
const auto fp32_input = cast<float>(input_vec[i][j]);
const auto fp32_weight = cast<float>(weight_vec[i][j]);
input_vec[i][j] = cast<DType>(fp32_input * norm_factor * fp32_weight);
}
}
const bool is_rope_lane = lane_id >= kWarpThreads - kRopeSize;
#pragma unroll
for (int i = 0; i < kLocalSize; ++i) {
if (i == kLocalSize - 1 && is_rope_lane) {
const auto rope_id = lane_id - (kWarpThreads - kRopeSize);
s_rope_input[warp_id][rope_id] = input_vec[i];
} else {
gmem.store(input, input_vec[i], i);
}
}
__syncwarp();
}
// part 2: rope
{
// mem elem = DType x 2
using DTypex2_t = packed_t<DType>;
const auto mem_elem = tile::Memory<DTypex2_t>::warp();
const auto elem = mem_elem.load(s_rope_input[warp_id]);
const auto [x_real, x_imag] = cast<fp32x2_t>(elem);
const auto [freq_real, freq_imag] = freq;
const fp32x2_t output = {
x_real * freq_real - x_imag * freq_imag,
x_real * freq_imag + x_imag * freq_real,
};
mem_elem.store(input + (kHeadDim - kRopeDim), cast<DTypex2_t>(output));
}
PDLTriggerSecondary<kUsePDL>();
}
template <typename DType, int64_t kHeadDim, int64_t kRopeDim, bool kUsePDL>
struct FusedNormRopeKernel {
template <ForwardMode kMode>
static constexpr auto fused_kernel = fused_norm_rope<DType, kHeadDim, kRopeDim, kMode, kUsePDL>;
static void forward(
const tvm::ffi::TensorView input,
const tvm::ffi::TensorView weight,
const tvm::ffi::TensorView handle,
const tvm::ffi::TensorView freqs_cis,
int32_t _mode,
float eps,
uint32_t compress_ratio) {
using namespace host;
using enum ForwardMode;
const auto mode = static_cast<ForwardMode>(_mode);
auto B = SymbolicSize{"num_q_tokens"};
auto N = SymbolicSize{"num_compress_tokens"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({B, kHeadDim}) // input
.with_dtype<DType>()
.with_device(device_)
.verify(input);
TensorMatcher({kHeadDim}) // weight
.with_dtype<DType>()
.with_device(device_)
.verify(weight);
TensorMatcher({-1, kRopeDim}) // freqs_cis
.with_dtype<float>()
.with_device(device_)
.verify(freqs_cis);
switch (mode) {
case CompressExtend:
TensorMatcher({N, compress::kPrefillPlanDim}) // plan
.with_dtype<compress::PrefillPlanTensorDtype>()
.with_device(device_)
.verify(handle);
RuntimeCheck(compress_ratio > 0);
break;
case CompressDecode:
TensorMatcher({N}) // seq_len
.with_dtype<int32_t>()
.with_device(device_)
.verify(handle);
RuntimeCheck(compress_ratio > 0);
break;
case DefaultForward:
TensorMatcher({N}) // position
.with_dtype<int64_t>()
.with_device(device_)
.verify(handle);
RuntimeCheck(compress_ratio == 0);
break;
default:
Panic("unsupported forward mode: ", static_cast<int>(mode));
}
// launch kernel
const auto num_compress_tokens = static_cast<uint32_t>(N.unwrap());
if (num_compress_tokens == 0) return;
const auto params = FusedNormRopeParams{
.input = input.data_ptr(),
.weight = weight.data_ptr(),
.eps = eps,
.num_works = num_compress_tokens,
.handle = handle.data_ptr(),
.freqs_cis = static_cast<const float*>(freqs_cis.data_ptr()),
.compress_ratio = compress_ratio,
};
const auto num_blocks = div_ceil(num_compress_tokens, kNumWarps);
using KernelType = std::decay_t<decltype(fused_norm_rope<DType, kHeadDim, kRopeDim, CompressExtend, kUsePDL>)>;
static constexpr KernelType kernel_table[3] = {
[static_cast<int>(CompressExtend)] = fused_kernel<CompressExtend>,
[static_cast<int>(CompressDecode)] = fused_kernel<CompressDecode>,
[static_cast<int>(DefaultForward)] = fused_kernel<DefaultForward>,
};
const auto kernel = kernel_table[static_cast<int>(mode)];
LaunchKernel(num_blocks, kBlockSize, device_.unwrap()).enable_pdl(kUsePDL)(kernel, params);
}
};
} // namespace
@@ -0,0 +1,663 @@
#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/deepseek_v4/compress_v2.cuh>
#include <sgl_kernel/deepseek_v4/fp8_utils.cuh>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
namespace {
using PlanC = device::compress::CompressPlan;
using PlanD = device::compress::DecodePlan;
using deepseek_v4::fp8::cast_to_ue8m0;
using deepseek_v4::fp8::inv_scale_ue8m0;
using deepseek_v4::fp8::pack_fp8;
SGL_DEVICE uint8_t quant_fp4_e2m1(float x) {
const float ax = fminf(fabsf(x), 6.0f);
uint8_t idx = 0;
idx += ax > 0.25f;
idx += ax > 0.75f;
idx += ax > 1.25f;
idx += ax > 1.75f;
idx += ax > 2.5f;
idx += ax > 3.5f;
idx += ax > 5.0f;
if (x < 0.0f && idx != 0) idx |= 0x8;
return idx;
}
constexpr uint32_t kBlockSize = 256;
constexpr uint32_t kNumWarps = kBlockSize / device::kWarpThreads;
struct FusedNormRopeStoreParams {
void* __restrict__ input;
const void* __restrict__ handle; // plan decode / compress
const void* __restrict__ weight;
const float* __restrict__ freqs_cis;
const int64_t* __restrict__ out_loc;
uint8_t* __restrict__ kvcache;
float eps;
uint32_t compress_ratio;
uint32_t num_tokens;
};
enum class ForwardMode : bool {
CompressExtend = 0,
CompressDecode = 1,
};
#define INDEXER_KERNEL __global__ __launch_bounds__(kBlockSize, 8)
#define FLASHMLA_KERNEL __global__ __launch_bounds__(kBlockSize, 8)
// ----------------------------------------------------------------------------
// Indexer variant: kHeadDim = 128, 1 token per *warp* (8 tokens per block).
// Each warp's 32 lanes cover the full 128-elem head_dim (kVecSize = 4 each).
// Cache layout: 132 bytes/token (128 fp8 nope + 4 fp32 scale).
// ----------------------------------------------------------------------------
template <typename DType, ForwardMode kMode, int32_t kPageBits, bool kUsePDL>
INDEXER_KERNEL void fused_norm_rope_indexer(const __grid_constant__ FusedNormRopeStoreParams params) {
using namespace device;
using enum ForwardMode;
constexpr int64_t kHeadDim = 128;
constexpr int64_t kRopeDim = 64;
constexpr int64_t kVecSize = 4;
constexpr uint32_t kRopeSize = kRopeDim / kVecSize;
constexpr int64_t kPageBytes = 132ll << kPageBits;
static_assert(kHeadDim == kWarpThreads * kVecSize);
static_assert(kRopeDim == kWarpThreads * 2);
static_assert(kRopeSize <= kWarpThreads);
using Storage = AlignedVector<DType, kVecSize>;
using Float4 = AlignedVector<float, kVecSize>;
const auto warp_id = threadIdx.x / kWarpThreads;
const auto lane_id = threadIdx.x % kWarpThreads;
const auto work_id = blockIdx.x * kNumWarps + warp_id;
// Lanes whose 4-elem pack lies in the rope tail (= last `kRopeSize` packs).
const bool is_rope_lane = lane_id >= kWarpThreads - kRopeSize;
if (work_id >= params.num_tokens) return;
const auto input = static_cast<DType*>(params.input) + work_id * kHeadDim;
int32_t position;
int64_t out_loc;
if constexpr (kMode == CompressExtend) {
const auto plan = static_cast<const PlanC*>(params.handle)[work_id];
if (plan.is_invalid()) return;
position = plan.seq_len - params.compress_ratio;
out_loc = params.out_loc[plan.ragged_id];
} else if constexpr (kMode == CompressDecode) {
const auto plan = static_cast<const PlanD*>(params.handle)[work_id];
if (plan.seq_len % params.compress_ratio != 0) return;
position = plan.seq_len - params.compress_ratio;
out_loc = params.out_loc[work_id];
} else {
static_assert(host::dependent_false_v<DType>, "Unsupported Mode");
}
const auto freqs_cis = params.freqs_cis + position * kRopeDim;
PDLWaitPrimary<kUsePDL>();
Float4 data, freq;
// part 1: norm
{
Storage input_vec, weight_vec;
input_vec.load(input, lane_id);
weight_vec.load(params.weight, lane_id);
if (is_rope_lane) freq.load(freqs_cis, lane_id - (kWarpThreads - kRopeSize));
float sum_of_squares = 0.0f;
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
const auto fp32_input = cast<float>(input_vec[i]);
sum_of_squares += fp32_input * fp32_input;
}
sum_of_squares = warp::reduce_sum(sum_of_squares);
const auto norm_factor = math::rsqrt(sum_of_squares / kHeadDim + params.eps);
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
const auto fp32_input = cast<float>(input_vec[i]);
const auto fp32_weight = cast<float>(weight_vec[i]);
data[i] = fp32_input * norm_factor * fp32_weight;
}
}
// part 2: rope (rope-lane only, 4 elems per lane = 2 (real, imag) pairs)
if (is_rope_lane) {
const auto x_real = data[0];
const auto x_imag = data[1];
const auto y_real = data[2];
const auto y_imag = data[3];
const auto freq_x_real = freq[0];
const auto freq_x_imag = freq[1];
const auto freq_y_real = freq[2];
const auto freq_y_imag = freq[3];
data[0] = x_real * freq_x_real - x_imag * freq_x_imag;
data[1] = x_real * freq_x_imag + x_imag * freq_x_real;
data[2] = y_real * freq_y_real - y_imag * freq_y_imag;
data[3] = y_real * freq_y_imag + y_imag * freq_y_real;
}
// part 3: hadamard transform
{
// Stage 1: butterfly (data[0], data[1]) and (data[2], data[3]).
{
const float a0 = data[0], a1 = data[1], a2 = data[2], a3 = data[3];
data[0] = a0 + a1;
data[1] = a0 - a1;
data[2] = a2 + a3;
data[3] = a2 - a3;
}
// Stage 2: butterfly (data[0], data[2]) and (data[1], data[3]).
{
const float a0 = data[0], a1 = data[1], a2 = data[2], a3 = data[3];
data[0] = a0 + a2;
data[1] = a1 + a3;
data[2] = a0 - a2;
data[3] = a1 - a3;
}
// Stages 3..7: cross-lane butterflies. Lower-lane (mask bit clear) keeps
// the sum, upper-lane (mask bit set) keeps the difference. shfl_xor is
// unsynchronized across early-returned lanes, but invalid-plan returns
// happen above for *all* lanes of a warp (work_id is warp-uniform), so
// the warp is intact here.
#pragma unroll
for (uint32_t mask = 1; mask < kWarpThreads; mask <<= 1) {
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
#ifndef USE_ROCM
const float other = __shfl_xor_sync(kFullMask, data[i], mask, kWarpThreads);
#else
const float other = __shfl_xor(data[i], mask, kWarpThreads);
#endif
data[i] = (lane_id & mask) ? (other - data[i]) : (data[i] + other);
}
}
const float kHadamardScale = math::rsqrt(static_cast<float>(kHeadDim));
#pragma unroll
for (int i = 0; i < kVecSize; ++i)
data[i] *= kHadamardScale;
}
// part 4: per-warp UE8M0 quant + store. The whole warp emits one fp8 group
// (= 128 elements) plus a single fp32 scale, matching the indexer cache
// layout (`fused_store_indexer_cache`).
{
using OutStorage = AlignedVector<fp8x2_e4m3_t, 2>;
float local_max = math::abs(data[0]);
#pragma unroll
for (int i = 1; i < kVecSize; ++i) {
local_max = math::max(local_max, math::abs(data[i]));
}
const auto abs_max = warp::reduce_max(local_max);
const auto scale = fmaxf(1e-4f, abs_max) / kFP8E4M3Max;
const auto inv_scale = 1.0f / scale;
const int64_t page = out_loc >> kPageBits;
const int64_t offset = out_loc & ((1 << kPageBits) - 1);
const auto page_ptr = params.kvcache + page * kPageBytes;
const auto value_ptr = page_ptr + offset * 128;
const auto scale_ptr = page_ptr + (128 << kPageBits) + offset * 4;
OutStorage result;
result[0] = pack_fp8(data[0] * inv_scale, data[1] * inv_scale);
result[1] = pack_fp8(data[2] * inv_scale, data[3] * inv_scale);
PDLTriggerSecondary<kUsePDL>();
result.store(value_ptr, lane_id);
// The single fp32 scale is identical across all lanes -- write from any lane.
if (lane_id == 0) reinterpret_cast<float*>(scale_ptr)[0] = scale;
}
}
template <typename DType, ForwardMode kMode, int32_t kPageBits, bool kUsePDL>
INDEXER_KERNEL void fused_norm_rope_indexer_fp4(const __grid_constant__ FusedNormRopeStoreParams params) {
using namespace device;
using enum ForwardMode;
constexpr int64_t kHeadDim = 128;
constexpr int64_t kRopeDim = 64;
constexpr int64_t kVecSize = 4;
constexpr uint32_t kRopeSize = kRopeDim / kVecSize;
constexpr int64_t kPageBytes = 68ll << kPageBits;
static_assert(kHeadDim == kWarpThreads * kVecSize);
static_assert(kRopeDim == kWarpThreads * 2);
static_assert(kRopeSize <= kWarpThreads);
using Storage = AlignedVector<DType, kVecSize>;
using Float4 = AlignedVector<float, kVecSize>;
const auto warp_id = threadIdx.x / kWarpThreads;
const auto lane_id = threadIdx.x % kWarpThreads;
const auto work_id = blockIdx.x * kNumWarps + warp_id;
const bool is_rope_lane = lane_id >= kWarpThreads - kRopeSize;
if (work_id >= params.num_tokens) return;
const auto input = static_cast<DType*>(params.input) + work_id * kHeadDim;
int32_t position;
int64_t out_loc;
if constexpr (kMode == CompressExtend) {
const auto plan = static_cast<const PlanC*>(params.handle)[work_id];
if (plan.is_invalid()) return;
position = plan.seq_len - params.compress_ratio;
out_loc = params.out_loc[plan.ragged_id];
} else if constexpr (kMode == CompressDecode) {
const auto plan = static_cast<const PlanD*>(params.handle)[work_id];
if (plan.seq_len % params.compress_ratio != 0) return;
position = plan.seq_len - params.compress_ratio;
out_loc = params.out_loc[work_id];
} else {
static_assert(host::dependent_false_v<DType>, "Unsupported Mode");
}
const auto freqs_cis = params.freqs_cis + position * kRopeDim;
PDLWaitPrimary<kUsePDL>();
Float4 data, freq;
{
Storage input_vec, weight_vec;
input_vec.load(input, lane_id);
weight_vec.load(params.weight, lane_id);
if (is_rope_lane) freq.load(freqs_cis, lane_id - (kWarpThreads - kRopeSize));
float sum_of_squares = 0.0f;
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
const auto fp32_input = cast<float>(input_vec[i]);
sum_of_squares += fp32_input * fp32_input;
}
sum_of_squares = warp::reduce_sum(sum_of_squares);
const auto norm_factor = math::rsqrt(sum_of_squares / kHeadDim + params.eps);
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
const auto fp32_input = cast<float>(input_vec[i]);
const auto fp32_weight = cast<float>(weight_vec[i]);
data[i] = fp32_input * norm_factor * fp32_weight;
}
}
if (is_rope_lane) {
const auto x_real = data[0];
const auto x_imag = data[1];
const auto y_real = data[2];
const auto y_imag = data[3];
const auto freq_x_real = freq[0];
const auto freq_x_imag = freq[1];
const auto freq_y_real = freq[2];
const auto freq_y_imag = freq[3];
data[0] = x_real * freq_x_real - x_imag * freq_x_imag;
data[1] = x_real * freq_x_imag + x_imag * freq_x_real;
data[2] = y_real * freq_y_real - y_imag * freq_y_imag;
data[3] = y_real * freq_y_imag + y_imag * freq_y_real;
}
{
{
const float a0 = data[0], a1 = data[1], a2 = data[2], a3 = data[3];
data[0] = a0 + a1;
data[1] = a0 - a1;
data[2] = a2 + a3;
data[3] = a2 - a3;
}
{
const float a0 = data[0], a1 = data[1], a2 = data[2], a3 = data[3];
data[0] = a0 + a2;
data[1] = a1 + a3;
data[2] = a0 - a2;
data[3] = a1 - a3;
}
#pragma unroll
for (uint32_t mask = 1; mask < kWarpThreads; mask <<= 1) {
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
#ifndef USE_ROCM
const float other = __shfl_xor_sync(kFullMask, data[i], mask, kWarpThreads);
#else
const float other = __shfl_xor(data[i], mask, kWarpThreads);
#endif
data[i] = (lane_id & mask) ? (other - data[i]) : (data[i] + other);
}
}
const float kHadamardScale = math::rsqrt(static_cast<float>(kHeadDim));
#pragma unroll
for (int i = 0; i < kVecSize; ++i)
data[i] *= kHadamardScale;
}
{
float local_max = math::abs(data[0]);
#pragma unroll
for (int i = 1; i < kVecSize; ++i) {
local_max = math::max(local_max, math::abs(data[i]));
}
local_max = warp::reduce_max<8>(local_max);
const auto scale_raw = fmaxf(1e-4f, local_max) / 6.0f;
const auto scale_ue8m0 = static_cast<uint8_t>(cast_to_ue8m0(scale_raw));
const auto inv_scale = inv_scale_ue8m0(scale_ue8m0);
const uint8_t packed0 = quant_fp4_e2m1(data[0] * inv_scale) | (quant_fp4_e2m1(data[1] * inv_scale) << 4);
const uint8_t packed1 = quant_fp4_e2m1(data[2] * inv_scale) | (quant_fp4_e2m1(data[3] * inv_scale) << 4);
const uint16_t packed = static_cast<uint16_t>(packed0) | (static_cast<uint16_t>(packed1) << 8);
const int64_t page = out_loc >> kPageBits;
const int64_t offset = out_loc & ((1 << kPageBits) - 1);
const auto page_ptr = params.kvcache + page * kPageBytes;
const auto value_ptr = page_ptr + offset * 64;
const auto scale_ptr = page_ptr + (64 << kPageBits) + offset * 4;
PDLTriggerSecondary<kUsePDL>();
reinterpret_cast<uint16_t*>(value_ptr)[lane_id] = packed;
if ((lane_id & 7) == 0) static_cast<uint8_t*>(scale_ptr)[lane_id >> 3] = scale_ue8m0;
}
}
// ----------------------------------------------------------------------------
// FlashMLA variant: kHeadDim = 512, 1 token per *block* (256 threads).
// Each thread loads kVecSize=2 BF16, so 256 threads cover the full 512 elems.
// Cache layout: 584 bytes/token = 448 fp8 nope + 64 (=32 bf16x2) rope + 8 scale.
// ----------------------------------------------------------------------------
template <typename DType, ForwardMode kMode, int32_t kPageBits, bool kUsePDL, bool kBf16Store = false>
FLASHMLA_KERNEL void fused_norm_rope_flashmla(const __grid_constant__ FusedNormRopeStoreParams params) {
using namespace device;
using enum ForwardMode;
constexpr int64_t kHeadDim = 512;
constexpr int64_t kRopeDim = 64;
constexpr int64_t kVecSize = 2;
// Last warp owns the rope tail. The remaining 7 warps each emit one
// 64-element fp8 group (own UE8M0 scale).
constexpr uint32_t kRopeWarp = kNumWarps - 1;
// kBf16Store: write the whole head_dim as plain BF16 (no fp8 / no scale) into a
// [num_slots, head_dim] bf16 cache (page_size==1) at row out_loc
constexpr int64_t kPageBytes =
kBf16Store ? ((kHeadDim * 2ll) << kPageBits) : host::div_ceil(584ll << kPageBits, 576) * 576;
static_assert(kHeadDim == kBlockSize * kVecSize);
static_assert(kRopeDim == kWarpThreads * kVecSize);
static_assert(kHeadDim - kRopeDim == kRopeWarp * kWarpThreads * kVecSize);
using Storage = AlignedVector<DType, kVecSize>;
using Float2 = AlignedVector<float, kVecSize>;
const auto tx = threadIdx.x;
const auto warp_id = tx / kWarpThreads;
const auto lane_id = tx % kWarpThreads;
const auto work_id = blockIdx.x;
if (work_id >= params.num_tokens) return;
const auto input = static_cast<DType*>(params.input) + work_id * kHeadDim;
int32_t position;
int64_t out_loc;
if constexpr (kMode == CompressExtend) {
const auto plan = static_cast<const PlanC*>(params.handle)[work_id];
if (plan.is_invalid()) return;
position = plan.seq_len - params.compress_ratio;
out_loc = params.out_loc[plan.ragged_id];
} else if constexpr (kMode == CompressDecode) {
const auto plan = static_cast<const PlanD*>(params.handle)[work_id];
if (plan.seq_len % params.compress_ratio != 0) return;
position = plan.seq_len - params.compress_ratio;
out_loc = params.out_loc[work_id];
} else {
static_assert(host::dependent_false_v<DType>, "Unsupported Mode");
}
const auto freqs_cis = params.freqs_cis + position * kRopeDim;
PDLWaitPrimary<kUsePDL>();
Float2 data, freq;
// part 1: norm. Each thread owns one 2-elem pack (`tx`-th pack of input).
// Sum of squares is reduced across the whole block via per-warp partials.
{
__shared__ float partial_sums[kNumWarps];
Storage input_vec, weight_vec;
input_vec.load(input, tx);
weight_vec.load(params.weight, tx);
if (warp_id == kRopeWarp) freq.load(freqs_cis, lane_id);
float sum_of_squares = 0.0f;
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
const auto fp32_input = cast<float>(input_vec[i]);
sum_of_squares += fp32_input * fp32_input;
}
const auto warp_sum = warp::reduce_sum(sum_of_squares);
if (lane_id == 0) partial_sums[warp_id] = warp_sum;
__syncthreads();
// Replicate the per-warp partial sums to a full warp and reduce. Every
// lane-group of `kNumWarps` lanes ends up with the global sum.
sum_of_squares = warp::reduce_sum<kNumWarps>(partial_sums[lane_id % kNumWarps]);
const auto norm_factor = math::rsqrt(sum_of_squares / kHeadDim + params.eps);
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
const auto fp32_input = cast<float>(input_vec[i]);
const auto fp32_weight = cast<float>(weight_vec[i]);
data[i] = fp32_input * norm_factor * fp32_weight;
}
}
const int64_t page = out_loc >> kPageBits;
const int64_t offset = out_loc & ((1 << kPageBits) - 1);
const auto page_ptr = params.kvcache + page * kPageBytes;
const auto value_ptr = page_ptr + offset * (kBf16Store ? (kHeadDim * 2) : 576);
PDLTriggerSecondary<kUsePDL>();
// part 2: rope on the rope warp (BF16 store), or per-warp FP8 quant + store.
if constexpr (kBf16Store) {
Float2 d = data;
if (warp_id == kRopeWarp) {
const auto x_real = data[0];
const auto x_imag = data[1];
const auto freq_real = freq[0];
const auto freq_imag = freq[1];
d[0] = x_real * freq_real - x_imag * freq_imag;
d[1] = x_real * freq_imag + x_imag * freq_real;
}
reinterpret_cast<bf16x2_t*>(value_ptr)[tx] = cast<bf16x2_t>(fp32x2_t{d[0], d[1]});
} else if (warp_id == kRopeWarp) {
// Each rope-warp lane owns exactly one (real, imag) pair within the rope
// tail. Apply rotation, downcast to BF16, write to the slot's rope region.
const auto x_real = data[0];
const auto x_imag = data[1];
const auto freq_real = freq[0];
const auto freq_imag = freq[1];
data[0] = x_real * freq_real - x_imag * freq_imag;
data[1] = x_real * freq_imag + x_imag * freq_real;
const auto result = cast<bf16x2_t>(fp32x2_t{data[0], data[1]});
const auto rope_ptr = value_ptr + 448;
reinterpret_cast<bf16x2_t*>(rope_ptr)[lane_id] = result;
} else {
// Non-rope warp: per-warp UE8M0 group (64 elems -> 64 fp8 + 1 scale byte).
// BF16 round-trip to match the precision of the non-fused path
// (which goes through quant_to_nope_fp8_rope_bf16_pack_triton with bf16 input).
const auto x = cast<float>(cast<bf16_t>(data[0]));
const auto y = cast<float>(cast<bf16_t>(data[1]));
const auto abs_max = warp::reduce_max(fmaxf(fabs(x), fabs(y)));
const auto scale_raw = fmaxf(1e-4f, abs_max) / kFP8E4M3Max;
const auto scale_ue8m0 = cast_to_ue8m0(scale_raw);
const auto inv_scale = inv_scale_ue8m0(scale_ue8m0);
const auto result = pack_fp8(x * inv_scale, y * inv_scale);
const auto scale_ptr = page_ptr + (576 << kPageBits) + offset * 8;
reinterpret_cast<fp8x2_e4m3_t*>(value_ptr)[tx] = result;
// All lanes in this warp produce the same scale byte; let lane 0 publish.
if (lane_id == 0) static_cast<uint8_t*>(scale_ptr)[warp_id] = scale_ue8m0;
}
}
template <typename DType, int64_t kHeadDim, int64_t kRopeDim, uint32_t kPageSize, bool kUsePDL, bool kBf16Store = false>
struct FusedNormRopeKernel {
static constexpr int32_t kLogPageSize = std::countr_zero(kPageSize);
static constexpr bool kIsIndexer = (kHeadDim == 128);
static_assert(!(kIsIndexer && kBf16Store), "bf16 store only for flashmla head_dim=512");
static constexpr int64_t kIndexerBytes = 132 * kPageSize;
static constexpr int64_t kFlashMLABytes = host::div_ceil(584 * kPageSize, 576) * 576;
static constexpr int64_t kBf16Bytes = kHeadDim * 2 * kPageSize; // plain bf16 cache
static constexpr int64_t kPageBytes = kBf16Store ? kBf16Bytes : (kIsIndexer ? kIndexerBytes : kFlashMLABytes);
/// TODO: Let's fix the config for now.
static_assert(kRopeDim == 64 && (kHeadDim == 128 || kHeadDim == 512));
static_assert(std::has_single_bit(kPageSize), "kPageSize must be a power of 2");
template <ForwardMode kMode>
static constexpr auto select_kernel() {
if constexpr (kIsIndexer) {
return fused_norm_rope_indexer<DType, kMode, kLogPageSize, kUsePDL>;
} else {
return fused_norm_rope_flashmla<DType, kMode, kLogPageSize, kUsePDL, kBf16Store>;
}
}
template <ForwardMode kMode>
static constexpr auto select_fp4_kernel() {
static_assert(kIsIndexer, "FP4 fused store is only defined for the indexer");
return fused_norm_rope_indexer_fp4<DType, kMode, kLogPageSize, kUsePDL>;
}
static void forward(
const tvm::ffi::TensorView input,
const tvm::ffi::TensorView plan,
const tvm::ffi::TensorView weight,
const float eps,
const tvm::ffi::TensorView freqs_cis,
const tvm::ffi::TensorView out_loc,
const tvm::ffi::TensorView kvcache,
const bool is_decode,
const uint32_t compress_ratio) {
using namespace host;
using enum ForwardMode;
const auto mode = static_cast<ForwardMode>(is_decode);
auto N = SymbolicSize{"num_tokens"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLGPU>();
TensorMatcher({N, kHeadDim}) // input
.with_dtype<DType>()
.with_device(device_)
.verify(input);
TensorMatcher({kHeadDim}) // weight
.with_dtype<DType>()
.with_device(device_)
.verify(weight);
TensorMatcher({-1, kRopeDim}) // freqs_cis
.with_dtype<float>()
.with_device(device_)
.verify(freqs_cis);
TensorMatcher({-1}) // out_loc
.with_dtype<int64_t>()
.with_device(device_)
.verify(out_loc);
TensorMatcher({-1, -1}) // cache
.with_strides({kPageBytes, 1})
.with_dtype<uint8_t>()
.with_device(device_)
.verify(kvcache);
switch (mode) {
case CompressExtend:
compress::verify_plan_c(plan, N, device_);
RuntimeCheck(out_loc.size(0) >= N.unwrap());
break;
case CompressDecode:
compress::verify_plan_d(plan, N, device_);
RuntimeCheck(out_loc.size(0) == N.unwrap());
break;
}
const auto num_tokens = static_cast<uint32_t>(N.unwrap());
if (num_tokens == 0) return;
const auto params = FusedNormRopeStoreParams{
.input = input.data_ptr(),
.handle = plan.data_ptr(),
.weight = weight.data_ptr(),
.freqs_cis = static_cast<const float*>(freqs_cis.data_ptr()),
.out_loc = static_cast<const int64_t*>(out_loc.data_ptr()),
.kvcache = static_cast<uint8_t*>(kvcache.data_ptr()),
.eps = eps,
.compress_ratio = compress_ratio,
.num_tokens = num_tokens,
};
// Indexer packs `kNumWarps` tokens per block (warp-major); FlashMLA uses
// a whole block per token (cta-major sum-reduce over head_dim=512).
const uint32_t num_blocks = kIsIndexer ? div_ceil(num_tokens, kNumWarps) : num_tokens;
const auto device = device_.unwrap();
const auto kernel = mode == CompressExtend ? select_kernel<CompressExtend>() : select_kernel<CompressDecode>();
LaunchKernel(num_blocks, kBlockSize, device).enable_pdl(kUsePDL)(kernel, params);
}
static void forward_fp4(
const tvm::ffi::TensorView input,
const tvm::ffi::TensorView plan,
const tvm::ffi::TensorView weight,
const float eps,
const tvm::ffi::TensorView freqs_cis,
const tvm::ffi::TensorView out_loc,
const tvm::ffi::TensorView kvcache,
const bool is_decode,
const uint32_t compress_ratio) {
using namespace host;
using enum ForwardMode;
static_assert(kIsIndexer, "FP4 fused store is only defined for the indexer");
constexpr int64_t kFp4PageBytes = 68 * kPageSize;
const auto mode = static_cast<ForwardMode>(is_decode);
auto N = SymbolicSize{"num_tokens"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({N, kHeadDim}).with_dtype<DType>().with_device(device_).verify(input);
TensorMatcher({kHeadDim}).with_dtype<DType>().with_device(device_).verify(weight);
TensorMatcher({-1, kRopeDim}).with_dtype<float>().with_device(device_).verify(freqs_cis);
TensorMatcher({-1}).with_dtype<int64_t>().with_device(device_).verify(out_loc);
TensorMatcher({-1, -1}).with_strides({kFp4PageBytes, 1}).with_dtype<uint8_t>().with_device(device_).verify(kvcache);
switch (mode) {
case CompressExtend:
compress::verify_plan_c(plan, N, device_);
RuntimeCheck(out_loc.size(0) >= N.unwrap());
break;
case CompressDecode:
compress::verify_plan_d(plan, N, device_);
RuntimeCheck(out_loc.size(0) == N.unwrap());
break;
}
const auto num_tokens = static_cast<uint32_t>(N.unwrap());
if (num_tokens == 0) return;
const auto params = FusedNormRopeStoreParams{
.input = input.data_ptr(),
.handle = plan.data_ptr(),
.weight = weight.data_ptr(),
.freqs_cis = static_cast<const float*>(freqs_cis.data_ptr()),
.out_loc = static_cast<const int64_t*>(out_loc.data_ptr()),
.kvcache = static_cast<uint8_t*>(kvcache.data_ptr()),
.eps = eps,
.compress_ratio = compress_ratio,
.num_tokens = num_tokens,
};
const uint32_t num_blocks = div_ceil(num_tokens, kNumWarps);
const auto device = device_.unwrap();
const auto kernel =
mode == CompressExtend ? select_fp4_kernel<CompressExtend>() : select_fp4_kernel<CompressDecode>();
LaunchKernel(num_blocks, kBlockSize, device).enable_pdl(kUsePDL)(kernel, params);
}
};
} // namespace
@@ -0,0 +1,214 @@
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/runtime.cuh>
#include <sgl_kernel/utils.cuh>
#include <sgl_kernel/warp.cuh>
#include <tvm/ffi/container/tensor.h>
#include <cmath>
#include <cstdint>
namespace {
[[maybe_unused]]
SGL_DEVICE float act_sqrt_softplus(float x) {
const float softplus = fmaxf(x, 0.0f) + log1pf(expf(-fabsf(x)));
return sqrtf(softplus);
}
struct MoEHashTopKParams {
const float* __restrict__ router_logits;
const int64_t* __restrict__ input_id;
const int32_t* __restrict__ tid2eid;
int32_t* __restrict__ topk_ids;
float* __restrict__ topk_weights;
uint32_t num_tokens;
uint32_t topk;
uint32_t num_routed_experts;
uint32_t num_shared_experts;
float routed_scaling_factor;
};
template <auto Fn, bool kUsePDL>
__global__ void moe_hash_topk_fused(const MoEHashTopKParams __grid_constant__ params) {
using namespace device;
const auto& [
router_logits, input_id, tid2eid, topk_ids, topk_weights, // pointers
num_tokens, topk, num_routed_experts, num_shared_experts, routed_scaling_factor] =
params;
const uint32_t topk_fused = topk + num_shared_experts;
const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x;
const uint32_t warp_id = tid / kWarpThreads;
const uint32_t lane_id = tid % kWarpThreads;
if (warp_id >= num_tokens) return;
// we can safely prefetch the token id
const auto token_id = input_id[warp_id];
PDLWaitPrimary<kUsePDL>();
float routed_weight = 0.0f;
int32_t expert_id = 0;
if (lane_id < topk) {
expert_id = tid2eid[token_id * topk + lane_id];
routed_weight = Fn(router_logits[warp_id * num_routed_experts + expert_id]);
}
const auto routed_sum = device::warp::reduce_sum(routed_weight);
if (lane_id < topk_fused) {
const bool is_shared = lane_id >= topk;
const auto output_offset = warp_id * topk_fused + lane_id;
topk_ids[output_offset] = is_shared ? num_routed_experts + lane_id - topk : expert_id;
topk_weights[output_offset] = is_shared ? 1.0f / routed_scaling_factor : routed_weight / routed_sum;
}
PDLTriggerSecondary<kUsePDL>();
}
struct TopKParams {
int32_t* __restrict__ topk_ids;
// Exactly one is active: ntn_ptr == nullptr means use ntn_value.
const int32_t* __restrict__ ntn_ptr;
int32_t ntn_value;
int64_t stride;
uint32_t topk;
uint32_t num_tokens;
};
__global__ void mask_topk_ids_padded_region(const TopKParams __grid_constant__ params) {
const uint32_t tid = blockIdx.x * blockDim.x + threadIdx.x;
const uint32_t warp_id = tid / device::kWarpThreads;
const uint32_t lane_id = tid % device::kWarpThreads;
if (warp_id >= params.num_tokens || lane_id >= params.topk) return;
device::PDLWaitPrimary<true>();
const uint32_t num = (params.ntn_ptr != nullptr) //
? static_cast<uint32_t>(params.ntn_ptr[0])
: static_cast<uint32_t>(params.ntn_value);
if (warp_id >= num) params.topk_ids[warp_id * params.stride + lane_id] = -1;
device::PDLTriggerSecondary<true>();
}
template <auto Fn, bool kUsePDL>
struct HashTopKKernel {
static constexpr auto kernel = moe_hash_topk_fused<Fn, kUsePDL>;
static void
run(const tvm::ffi::TensorView router_logits,
const tvm::ffi::TensorView input_id,
const tvm::ffi::TensorView tid2eid,
const tvm::ffi::TensorView topk_weights,
const tvm::ffi::TensorView topk_ids,
float routed_scaling_factor) {
using namespace host;
auto N = SymbolicSize{"num_tokens"};
auto E = SymbolicSize{"num_routed_experts"};
auto K = SymbolicSize{"topk_fused"};
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
TensorMatcher({N, E}) //
.with_dtype<float>()
.with_device(device)
.verify(router_logits);
TensorMatcher({N}) //
.with_dtype<int64_t>()
.with_device(device)
.verify(input_id);
TensorMatcher({-1, -1}) //
.with_dtype<int32_t>()
.with_device(device)
.verify(tid2eid);
TensorMatcher({N, K}) //
.with_dtype<float>()
.with_device(device)
.verify(topk_weights);
TensorMatcher({N, K}) //
.with_dtype<int32_t>()
.with_device(device)
.verify(topk_ids);
const auto num_tokens = static_cast<uint32_t>(N.unwrap());
const auto topk_fused = static_cast<uint32_t>(K.unwrap());
const auto topk = static_cast<uint32_t>(tid2eid.size(1));
const auto shared_experts = topk_fused - topk;
RuntimeCheck(topk <= topk_fused, "HashTopKKernel requires topk <= topk_fused");
RuntimeCheck(topk_fused <= device::kWarpThreads, "HashTopKKernel requires topk_fused <= warp size");
const auto params = MoEHashTopKParams{
.router_logits = static_cast<const float*>(router_logits.data_ptr()),
.input_id = static_cast<const int64_t*>(input_id.data_ptr()),
.tid2eid = static_cast<const int32_t*>(tid2eid.data_ptr()),
.topk_ids = static_cast<int32_t*>(topk_ids.data_ptr()),
.topk_weights = static_cast<float*>(topk_weights.data_ptr()),
.num_tokens = num_tokens,
.topk = topk,
.num_routed_experts = static_cast<uint32_t>(E.unwrap()),
.num_shared_experts = shared_experts,
.routed_scaling_factor = routed_scaling_factor,
};
const auto kBlockSize = 128u;
const auto kNumWarps = kBlockSize / device::kWarpThreads;
const auto num_blocks = div_ceil(num_tokens, kNumWarps);
LaunchKernel(num_blocks, kBlockSize, device.unwrap()) //
.enable_pdl(kUsePDL)(kernel, params);
}
};
// TODO this may not be related to *hash* topk, thus may move
struct MaskKernel {
static constexpr auto kernel = mask_topk_ids_padded_region;
static void run(tvm::ffi::TensorView topk_ids, tvm::ffi::TensorView num_token_non_padded) {
using namespace host;
auto N = SymbolicSize{"num_tokens"};
auto K = SymbolicSize{"topk"};
auto D = SymbolicSize{"stride"};
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
TensorMatcher({N, K}) //
.with_strides({D, 1})
.with_dtype<int32_t>()
.with_device(device)
.verify(topk_ids);
RuntimeCheck(num_token_non_padded.numel() == 1, "num_token_non_padded should be a scalar");
RuntimeCheck(K.unwrap() <= device::kWarpThreads, "MaskKernel requires topk <= warp size");
const int32_t* ntn_ptr = nullptr;
int32_t ntn_value = 0;
const auto ntn_dev = num_token_non_padded.device().device_type;
if (ntn_dev == kDLCUDA) {
RuntimeCheck(is_type<int32_t>(num_token_non_padded.dtype()), "num_token_non_padded on CUDA must be int32");
ntn_ptr = static_cast<const int32_t*>(num_token_non_padded.data_ptr());
} else if (ntn_dev == kDLCPU) {
if (is_type<int32_t>(num_token_non_padded.dtype())) {
ntn_value = *static_cast<const int32_t*>(num_token_non_padded.data_ptr());
} else if (is_type<int64_t>(num_token_non_padded.dtype())) {
ntn_value = static_cast<int32_t>(*static_cast<const int64_t*>(num_token_non_padded.data_ptr()));
} else {
RuntimeCheck(false, "num_token_non_padded on CPU must be int32 or int64");
}
} else {
RuntimeCheck(false, "num_token_non_padded must be on CPU or CUDA");
}
const auto num_tokens = static_cast<uint32_t>(N.unwrap());
const auto params = TopKParams{
.topk_ids = static_cast<int32_t*>(topk_ids.data_ptr()),
.ntn_ptr = ntn_ptr,
.ntn_value = ntn_value,
.stride = static_cast<int64_t>(D.unwrap()),
.topk = static_cast<uint32_t>(K.unwrap()),
.num_tokens = num_tokens,
};
const auto kBlockSize = 128u;
const auto kNumWarps = kBlockSize / device::kWarpThreads;
const auto num_blocks = div_ceil(num_tokens, kNumWarps);
LaunchKernel(num_blocks, kBlockSize, device.unwrap()) //
.enable_pdl(true)(kernel, params);
}
};
} // namespace
@@ -0,0 +1,882 @@
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/math.cuh>
#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/deepseek_v4/fp8_utils.cuh>
#include <tvm/ffi/container/tensor.h>
#include <bit>
#include <cstdint>
namespace {
using deepseek_v4::fp8::cast_to_ue8m0;
using deepseek_v4::fp8::inv_scale_ue8m0;
using deepseek_v4::fp8::pack_fp8;
SGL_DEVICE uint8_t quant_fp4_e2m1(float x) {
const float ax = fminf(fabsf(x), 6.0f);
uint8_t idx = 0;
idx += ax > 0.25f;
idx += ax > 0.75f;
idx += ax > 1.25f;
idx += ax > 1.75f;
idx += ax > 2.5f;
idx += ax > 3.5f;
idx += ax > 5.0f;
if (x < 0.0f && idx != 0) idx |= 0x8;
return idx;
}
// 4 warps per block: warp-per-(token, head) work-item dispatch (Q kernel).
constexpr uint32_t kFusedQBlockSize = 128;
constexpr uint32_t kFusedQNumWarps = kFusedQBlockSize / device::kWarpThreads;
// 8 warps per block: block-per-token work-item dispatch (K kernel).
constexpr uint32_t kFusedKBlockSize = 256;
constexpr uint32_t kFusedKNumWarps = kFusedKBlockSize / device::kWarpThreads;
#define Q_KERNEL __global__ __launch_bounds__(kFusedQBlockSize, 16)
#define K_KERNEL __global__ __launch_bounds__(kFusedKBlockSize, 8)
template <int64_t kRopeDim>
SGL_DEVICE device::AlignedVector<float, 4>
load_rope_first_cos_sin(const float* __restrict__ cos_sin_cache, int32_t lane_id) {
constexpr int64_t kHalfRopeDim = kRopeDim / 2;
const int32_t pair0 = lane_id * 2;
const int32_t pair1 = pair0 + 1;
device::AlignedVector<float, 4> freq;
freq[0] = cos_sin_cache[pair0];
freq[1] = cos_sin_cache[kHalfRopeDim + pair0];
freq[2] = cos_sin_cache[pair1];
freq[3] = cos_sin_cache[kHalfRopeDim + pair1];
return freq;
}
// ============================================================================
// Q kernel: warp-per-(token, head) rmsnorm-self + RoPE + write to q_out.
// ============================================================================
struct FusedQNormRopeParams {
const void* __restrict__ q_input; // (B, num_q_heads, kHeadDim) DType
void* __restrict__ q_output; // (B, num_q_heads, kHeadDim) DType
const float* __restrict__ freqs_cis; // (max_pos, kRopeDim) fp32 (re/im interleaved)
const void* __restrict__ positions; // (B,) PosT
int64_t q_input_stride_batch;
int64_t q_output_stride_batch;
uint32_t batch_size;
uint32_t num_q_heads;
float eps;
};
template <typename DType, int64_t kHeadDim, int64_t kRopeDim, typename PosT, bool kUsePDL>
Q_KERNEL void fused_q_norm_rope(const __grid_constant__ FusedQNormRopeParams params) {
using namespace device;
constexpr int64_t kMaxVecSize = 16 / sizeof(DType);
constexpr int64_t kVecSize = std::min(kMaxVecSize, kHeadDim / kWarpThreads);
constexpr int64_t kLocalSize = kHeadDim / (kWarpThreads * kVecSize);
constexpr uint32_t kRopeSize = kRopeDim / kVecSize;
static_assert(kHeadDim % (kWarpThreads * kVecSize) == 0);
static_assert(kLocalSize * kVecSize * kWarpThreads == kHeadDim);
static_assert(kRopeDim % kVecSize == 0);
static_assert(kRopeSize <= kWarpThreads);
static_assert(kRopeDim == kWarpThreads * 2, "1 (real, imag) pair per lane");
using Storage = AlignedVector<DType, kVecSize>;
const auto warp_id = threadIdx.x / kWarpThreads;
const auto lane_id = threadIdx.x % kWarpThreads;
const auto work_id = blockIdx.x * kFusedQNumWarps + warp_id;
const uint32_t total_works = params.batch_size * params.num_q_heads;
if (work_id >= total_works) return;
const uint32_t batch_id = work_id / params.num_q_heads;
const uint32_t head_id = work_id % params.num_q_heads;
const auto input_ptr =
static_cast<const DType*>(params.q_input) + batch_id * params.q_input_stride_batch + head_id * kHeadDim;
const auto output_ptr =
static_cast<DType*>(params.q_output) + batch_id * params.q_output_stride_batch + head_id * kHeadDim;
const auto position = static_cast<int32_t>(static_cast<const PosT*>(params.positions)[batch_id]);
__shared__ Storage s_rope[kFusedQNumWarps][kRopeSize];
// Prefetch this lane's freq pair before the PDL gate so the wait happens
// outside the dependency chain on `position`.
const auto mem_freq = tile::Memory<fp32x2_t>{lane_id, kWarpThreads};
PDLWaitPrimary<kUsePDL>();
// part 1: rmsnorm-self (no weight).
const auto gmem = tile::Memory<Storage>{lane_id, kWarpThreads};
Storage input_vec[kLocalSize];
#pragma unroll
for (int i = 0; i < kLocalSize; ++i) {
input_vec[i] = gmem.load(input_ptr, i);
}
const auto freq = mem_freq.load(params.freqs_cis + position * kRopeDim);
float sum_of_squares = 0.0f;
#pragma unroll
for (int i = 0; i < kLocalSize; ++i) {
#pragma unroll
for (int j = 0; j < kVecSize; ++j) {
const auto x = cast<float>(input_vec[i][j]);
sum_of_squares += x * x;
}
}
sum_of_squares = warp::reduce_sum(sum_of_squares);
const auto norm_factor = math::rsqrt(sum_of_squares / kHeadDim + params.eps);
#pragma unroll
for (int i = 0; i < kLocalSize; ++i) {
#pragma unroll
for (int j = 0; j < kVecSize; ++j) {
const auto x = cast<float>(input_vec[i][j]);
input_vec[i][j] = cast<DType>(x * norm_factor);
}
}
// Stash the rope tail (last kRopeSize lanes' last tile) into shared memory;
// write nope tiles to gmem directly.
const bool is_rope_lane = lane_id >= kWarpThreads - kRopeSize;
#pragma unroll
for (int i = 0; i < kLocalSize; ++i) {
if (i == kLocalSize - 1 && is_rope_lane) {
const auto rope_id = lane_id - (kWarpThreads - kRopeSize);
s_rope[warp_id][rope_id] = input_vec[i];
} else {
gmem.store(output_ptr, input_vec[i], i);
}
}
__syncwarp();
PDLTriggerSecondary<kUsePDL>();
// part 2: RoPE on all 32 lanes -- one (real, imag) bf16x2 pair per lane.
using DType2 = packed_t<DType>;
const auto mem_elem = tile::Memory<DType2>{lane_id, kWarpThreads};
const auto elem = mem_elem.load(s_rope[warp_id]);
const auto [x_real, x_imag] = cast<fp32x2_t>(elem);
const auto [freq_real, freq_imag] = freq;
const fp32x2_t rotated = {
x_real * freq_real - x_imag * freq_imag,
x_real * freq_imag + x_imag * freq_real,
};
mem_elem.store(output_ptr + (kHeadDim - kRopeDim), cast<DType2>(rotated));
}
template <typename DType, int64_t kHeadDim, int64_t kRopeDim, bool kUsePDL>
struct FusedQNormRopeKernel {
template <typename PosT>
static constexpr auto kernel = fused_q_norm_rope<DType, kHeadDim, kRopeDim, PosT, kUsePDL>;
static void forward(
const tvm::ffi::TensorView q_input,
const tvm::ffi::TensorView q_output,
const tvm::ffi::TensorView freqs_cis,
const tvm::ffi::TensorView positions,
float eps) {
using namespace host;
auto B = SymbolicSize{"batch_size"};
auto H = SymbolicSize{"num_q_heads"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({B, H, kHeadDim}) //
.with_strides({-1, kHeadDim, 1})
.with_dtype<DType>()
.with_device(device_)
.verify(q_input);
TensorMatcher({B, H, kHeadDim}) //
.with_strides({-1, kHeadDim, 1})
.with_dtype<DType>()
.with_device(device_)
.verify(q_output);
TensorMatcher({-1, kRopeDim}) //
.with_dtype<float>()
.with_device(device_)
.verify(freqs_cis);
auto pos_dtype = SymbolicDType{};
TensorMatcher({B}) //
.with_dtype<int32_t, int64_t>(pos_dtype)
.with_device(device_)
.verify(positions);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto num_q_heads = static_cast<uint32_t>(H.unwrap());
if (batch_size == 0) return;
const auto params = FusedQNormRopeParams{
.q_input = q_input.data_ptr(),
.q_output = q_output.data_ptr(),
.freqs_cis = static_cast<const float*>(freqs_cis.data_ptr()),
.positions = positions.data_ptr(),
.q_input_stride_batch = q_input.stride(0),
.q_output_stride_batch = q_output.stride(0),
.batch_size = batch_size,
.num_q_heads = num_q_heads,
.eps = eps,
};
const auto total_works = batch_size * num_q_heads;
const auto num_blocks = div_ceil(total_works, kFusedQNumWarps);
const auto k_int32 = kernel<int32_t>;
const auto k_int64 = kernel<int64_t>;
const auto k = pos_dtype.is_type<int32_t>() ? k_int32 : k_int64;
LaunchKernel(num_blocks, kFusedQBlockSize, device_.unwrap()) //
.enable_pdl(kUsePDL)(k, params);
}
};
// ============================================================================
// K kernel: block-per-token rmsnorm (with kv_weight) + RoPE + FlashMLA store.
// ============================================================================
struct FusedKNormRopeFlashMLAParams {
const void* __restrict__ kv; // (B, kHeadDim) DType
const void* __restrict__ kv_weight; // (kHeadDim,) DType
const float* __restrict__ freqs_cis; // (max_pos, kRopeDim) fp32
const void* __restrict__ positions; // (B,) PosT
const int32_t* __restrict__ out_loc; // (B,) int32 -> cache slot id
uint8_t* __restrict__ kvcache; // (npages, kPageBytes) uint8
// Row stride for `kv` in elements. Required because the upstream caller often
// passes `qkv_a[..., q_lora_rank:]`, a non-contiguous slice whose stride[0]
// equals `q_lora_rank + kHeadDim` rather than `kHeadDim`.
int64_t kv_stride_batch;
uint32_t batch_size;
float eps;
};
template <typename DType, int64_t kHeadDim, int64_t kRopeDim, typename PosT, int32_t kPageBits, bool kUsePDL>
K_KERNEL void fused_k_norm_rope_flashmla(const __grid_constant__ FusedKNormRopeFlashMLAParams params) {
using namespace device;
constexpr int64_t kVecSize = 2;
constexpr uint32_t kRopeWarp = kFusedKNumWarps - 1;
constexpr int64_t kPageBytes = host::div_ceil(584ll << kPageBits, 576) * 576;
static_assert(kHeadDim == kFusedKBlockSize * kVecSize);
static_assert(kRopeDim == kWarpThreads * kVecSize);
static_assert(kHeadDim - kRopeDim == kRopeWarp * kWarpThreads * kVecSize);
using Storage = AlignedVector<DType, kVecSize>;
using Float2 = AlignedVector<float, kVecSize>;
const auto tx = threadIdx.x;
const auto warp_id = tx / kWarpThreads;
const auto lane_id = tx % kWarpThreads;
const auto work_id = blockIdx.x;
if (work_id >= params.batch_size) return;
const auto input_ptr = static_cast<const DType*>(params.kv) + work_id * params.kv_stride_batch;
const auto position = static_cast<int32_t>(static_cast<const PosT*>(params.positions)[work_id]);
const auto out_loc = params.out_loc[work_id];
const auto freqs_cis = params.freqs_cis + position * kRopeDim;
PDLWaitPrimary<kUsePDL>();
Float2 data, freq;
// part 1: norm. Each thread owns one 2-elem pack (the `tx`-th).
// Sum-of-squares is reduced block-wide via per-warp partials.
{
__shared__ float partial_sums[kFusedKNumWarps];
Storage input_vec, weight_vec;
input_vec.load(input_ptr, tx);
weight_vec.load(params.kv_weight, tx);
if (warp_id == kRopeWarp) freq.load(freqs_cis, lane_id);
float sum_of_squares = 0.0f;
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
const auto x = cast<float>(input_vec[i]);
sum_of_squares += x * x;
}
const auto warp_sum = warp::reduce_sum(sum_of_squares);
if (lane_id == 0) partial_sums[warp_id] = warp_sum;
__syncthreads();
// Replicate the per-warp partial sums onto all lanes of one warp and
// reduce. Every group of `kBlockItemNumWarps` lanes ends up with the
// global sum.
sum_of_squares = warp::reduce_sum<kFusedKNumWarps>(partial_sums[lane_id % kFusedKNumWarps]);
const auto norm_factor = math::rsqrt(sum_of_squares / kHeadDim + params.eps);
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
const auto x = cast<float>(input_vec[i]);
const auto w = cast<float>(weight_vec[i]);
data[i] = x * norm_factor * w;
}
}
// A negative out_loc marks a slot with no KV write target (e.g. the -1
// sentinel from the full->SWA translation for out-of-window tokens or
// padded rows); skip the row instead of writing out of bounds. Checked
// here, not at the load, so the out_loc prefetch overlaps the norm above.
if (out_loc < 0) return;
const int32_t page = out_loc >> kPageBits;
const int32_t offset = out_loc & ((1 << kPageBits) - 1);
const auto page_ptr = params.kvcache + page * kPageBytes;
const auto value_ptr = page_ptr + offset * 576;
PDLTriggerSecondary<kUsePDL>();
// part 2: rope on warp 7 (BF16 store), per-warp UE8M0 quant + store on warps 0..6.
if (warp_id == kRopeWarp) {
const auto x_real = data[0];
const auto x_imag = data[1];
const auto freq_real = freq[0];
const auto freq_imag = freq[1];
data[0] = x_real * freq_real - x_imag * freq_imag;
data[1] = x_real * freq_imag + x_imag * freq_real;
const auto result = cast<bf16x2_t>(fp32x2_t{data[0], data[1]});
const auto rope_ptr = value_ptr + 448;
reinterpret_cast<bf16x2_t*>(rope_ptr)[lane_id] = result;
} else {
const auto x = data[0];
const auto y = data[1];
const auto abs_max = warp::reduce_max(fmaxf(fabs(x), fabs(y)));
const auto scale_raw = fmaxf(1e-4f, abs_max) / math::FP8_E4M3_MAX;
const auto scale_ue8m0 = cast_to_ue8m0(scale_raw);
const auto inv_scale = inv_scale_ue8m0(scale_ue8m0);
const auto result = pack_fp8(x * inv_scale, y * inv_scale);
const auto scale_ptr = page_ptr + (576 << kPageBits) + offset * 8;
reinterpret_cast<fp8x2_e4m3_t*>(value_ptr)[tx] = result;
if (lane_id == 0) static_cast<uint8_t*>(scale_ptr)[warp_id] = scale_ue8m0;
}
}
template <typename DType, int64_t kHeadDim, int64_t kRopeDim, uint32_t kPageSize, bool kUsePDL>
struct FusedKNormRopeFlashMLAKernel {
static constexpr int32_t kLogPageSize = std::countr_zero(kPageSize);
static constexpr int64_t kPageBytes = host::div_ceil(584 * kPageSize, 576) * 576;
static_assert(std::has_single_bit(kPageSize), "kPageSize must be a power of 2");
static_assert(1 << kLogPageSize == kPageSize);
static_assert(kHeadDim == 512 && kRopeDim == 64, "FlashMLA layout requires (512, 64)");
template <typename PosT>
static constexpr auto kernel = fused_k_norm_rope_flashmla<DType, kHeadDim, kRopeDim, PosT, kLogPageSize, kUsePDL>;
static void forward(
const tvm::ffi::TensorView kv,
const tvm::ffi::TensorView kv_weight,
const tvm::ffi::TensorView freqs_cis,
const tvm::ffi::TensorView positions,
const tvm::ffi::TensorView out_loc,
const tvm::ffi::TensorView kvcache,
float eps) {
using namespace host;
auto B = SymbolicSize{"batch_size"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({B, kHeadDim}) //
.with_strides({-1, 1})
.with_dtype<DType>()
.with_device(device_)
.verify(kv);
TensorMatcher({kHeadDim}) //
.with_dtype<DType>()
.with_device(device_)
.verify(kv_weight);
TensorMatcher({-1, kRopeDim}) //
.with_dtype<float>()
.with_device(device_)
.verify(freqs_cis);
auto pos_dtype = SymbolicDType{};
TensorMatcher({B}) //
.with_dtype<int32_t, int64_t>(pos_dtype)
.with_device(device_)
.verify(positions);
TensorMatcher({B}) //
.with_dtype<int32_t>()
.with_device(device_)
.verify(out_loc);
TensorMatcher({-1, -1}) //
.with_strides({kPageBytes, 1})
.with_dtype<uint8_t>()
.with_device(device_)
.verify(kvcache);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
if (batch_size == 0) return;
const auto params = FusedKNormRopeFlashMLAParams{
.kv = kv.data_ptr(),
.kv_weight = kv_weight.data_ptr(),
.freqs_cis = static_cast<const float*>(freqs_cis.data_ptr()),
.positions = positions.data_ptr(),
.out_loc = static_cast<const int32_t*>(out_loc.data_ptr()),
.kvcache = static_cast<uint8_t*>(kvcache.data_ptr()),
.kv_stride_batch = kv.stride(0),
.batch_size = batch_size,
.eps = eps,
};
const auto k_int32 = kernel<int32_t>;
const auto k_int64 = kernel<int64_t>;
const auto k = pos_dtype.is_type<int32_t>() ? k_int32 : k_int64;
LaunchKernel(batch_size, kFusedKBlockSize, device_.unwrap()) //
.enable_pdl(kUsePDL)(k, params);
}
};
// ============================================================================
// Indexer Q kernel: warp-per-(token, head) RoPE + Hadamard + fp8 act-quant.
// ============================================================================
struct FusedQIndexerRopeHadamardQuantParams {
const void* __restrict__ q_input; // (B, num_heads, 128) DType
void* __restrict__ q_fp8; // (B, num_heads, 128) fp8_e4m3
// weights_out[b, h] = weight[b, h] * weight_scale * q_scale[b, h].
// q_scale is computed internally and not exposed -- the only consumer of
// it is `weights_out`.
const void* __restrict__ weight; // (B, num_heads) DType
float* __restrict__ weights_out; // (B, num_heads) fp32 (== (B, H, 1) flat)
float weight_scale; // scalar c4_indexer.weight_scale
// Template-dependent layout:
// kRopeFirst=false: (max_pos, 64) fp32 interleaved [cos0, sin0, ...]
// kRopeFirst=true : (max_pos, 64) fp32 halves [cos..., sin...]
const float* __restrict__ rope_cache;
const void* __restrict__ positions; // (B,) PosT
// Row stride for `weight` (caller passes the non-contiguous wk slice directly).
int64_t weight_stride_batch;
uint32_t batch_size;
uint32_t num_heads;
};
template <typename DType, typename PosT, bool kUsePDL, bool kRopeFirst = false, bool kHadamard = true>
Q_KERNEL void fused_q_indexer_rope_hadamard_quant(const __grid_constant__ FusedQIndexerRopeHadamardQuantParams params) {
using namespace device;
constexpr int64_t kHeadDim = 128;
constexpr int64_t kRopeDim = 64;
constexpr int64_t kVecSize = 4;
constexpr uint32_t kRopeSize = kRopeDim / kVecSize; // = 16
static_assert(kHeadDim == kWarpThreads * kVecSize);
static_assert(kRopeDim == kWarpThreads * 2);
static_assert(kRopeSize <= kWarpThreads);
using Storage = AlignedVector<DType, kVecSize>;
using Float4 = AlignedVector<float, kVecSize>;
using OutStorage = AlignedVector<fp8x2_e4m3_t, 2>; // 4 fp8 / lane
const auto warp_id = threadIdx.x / kWarpThreads;
const auto lane_id = threadIdx.x % kWarpThreads;
const auto work_id = blockIdx.x * kFusedQNumWarps + warp_id;
// V4 ropes the trailing kRopeDim dims (kRopeFirst=false); V3.2 ropes the
// leading kRopeDim dims (kRopeFirst=true). Select the owning lanes per layout.
const bool is_rope_lane = kRopeFirst ? (lane_id < kRopeSize) : (lane_id >= kWarpThreads - kRopeSize);
const uint32_t total_works = params.batch_size * params.num_heads;
if (work_id >= total_works) return;
const uint32_t batch_id = work_id / params.num_heads;
const auto input_ptr = static_cast<const DType*>(params.q_input) + work_id * kHeadDim;
const auto position = static_cast<int32_t>(static_cast<const PosT*>(params.positions)[batch_id]);
const auto rope_cache = params.rope_cache + position * kRopeDim;
// Lane 0 prefetches the weight scalar for this (token, head) work item.
// Weight is (B, num_heads) DType; we need one scalar per warp -- offload
// the load to lane 0 only. The multiply + store happens once the q_scale
// is known (part 4).
PDLWaitPrimary<kUsePDL>();
Float4 data, freq;
const uint32_t head_id = work_id - batch_id * params.num_heads;
const auto weight_val =
cast<float>(static_cast<const DType*>(params.weight)[batch_id * params.weight_stride_batch + head_id]);
// part 1: load (no norm). Each lane owns a 4-elem pack.
{
Storage input_vec;
input_vec.load(input_ptr, lane_id);
if (is_rope_lane) {
if constexpr (kRopeFirst) {
freq = load_rope_first_cos_sin<kRopeDim>(rope_cache, lane_id);
} else {
freq.load(rope_cache, lane_id - (kWarpThreads - kRopeSize));
}
}
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
data[i] = cast<float>(input_vec[i]);
}
}
// part 2: rope on rope lanes only (4 elems / lane = 2 (real, imag) pairs).
if (is_rope_lane) {
const auto x_real = data[0];
const auto x_imag = data[1];
const auto y_real = data[2];
const auto y_imag = data[3];
const auto fxr = freq[0];
const auto fxi = freq[1];
const auto fyr = freq[2];
const auto fyi = freq[3];
data[0] = x_real * fxr - x_imag * fxi;
data[1] = x_real * fxi + x_imag * fxr;
data[2] = y_real * fyr - y_imag * fyi;
data[3] = y_real * fyi + y_imag * fyr;
}
PDLTriggerSecondary<kUsePDL>();
// part 3: 128-point Hadamard (2 local stages + 5 cross-lane shfl_xor stages).
// Same recipe as `fused_norm_rope_indexer`; see comments there for the
// butterfly invariants and the early-return safety argument. V3.2 omits the
// rotation (kHadamard=false): it is logit-preserving (H orthonormal, applied
// to both q and k), so dropping it only trades fp8 quant accuracy.
if constexpr (kHadamard) {
{
const float a0 = data[0], a1 = data[1], a2 = data[2], a3 = data[3];
data[0] = a0 + a1;
data[1] = a0 - a1;
data[2] = a2 + a3;
data[3] = a2 - a3;
}
{
const float a0 = data[0], a1 = data[1], a2 = data[2], a3 = data[3];
data[0] = a0 + a2;
data[1] = a1 + a3;
data[2] = a0 - a2;
data[3] = a1 - a3;
}
#pragma unroll
for (uint32_t mask = 1; mask < kWarpThreads; mask <<= 1) {
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
const float other = __shfl_xor_sync(0xFFFFFFFFu, data[i], mask, kWarpThreads);
data[i] = (lane_id & mask) ? (other - data[i]) : (data[i] + other);
}
}
const float kHadamardScale = math::rsqrt(static_cast<float>(kHeadDim));
#pragma unroll
for (int i = 0; i < kVecSize; ++i)
data[i] *= kHadamardScale;
}
{
float local_max = math::abs(data[0]);
#pragma unroll
for (int i = 1; i < kVecSize; ++i) {
local_max = math::max(local_max, math::abs(data[i]));
}
const auto abs_max = warp::reduce_max(local_max);
const auto scale = fmaxf(1e-4f, abs_max) / math::FP8_E4M3_MAX;
const auto inv_scale = 1.0f / scale;
OutStorage result;
result[0] = pack_fp8(data[0] * inv_scale, data[1] * inv_scale);
result[1] = pack_fp8(data[2] * inv_scale, data[3] * inv_scale);
// q_fp8 row pointer: 128 fp8 / row = 32 OutStorage / row, one per lane.
auto out_row = static_cast<uint8_t*>(params.q_fp8) + work_id * kHeadDim;
result.store(out_row, lane_id);
params.weights_out[work_id] = weight_val * params.weight_scale * scale;
}
}
template <typename DType, bool kUsePDL, bool kRopeFirst = false, bool kHadamard = true>
struct FusedQIndexerRopeHadamardQuantKernel {
template <typename PosT>
static constexpr auto kernel = fused_q_indexer_rope_hadamard_quant<DType, PosT, kUsePDL, kRopeFirst, kHadamard>;
static void forward(
const tvm::ffi::TensorView q_input,
const tvm::ffi::TensorView q_fp8,
const tvm::ffi::TensorView weight,
const tvm::ffi::TensorView weights_out,
double weight_scale,
const tvm::ffi::TensorView rope_cache,
const tvm::ffi::TensorView positions) {
using namespace host;
constexpr int64_t kHeadDim = 128;
constexpr int64_t kRopeDim = 64;
auto B = SymbolicSize{"batch_size"};
auto H = SymbolicSize{"num_heads"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
// Caller path is `wq_b(q_lora).view(-1, H, D)` -> contiguous; the kernel
// assumes a flat `(B*H, kHeadDim)` layout for both q_input and q_fp8.
// Pin the head/innermost strides; assert the batch stride below.
TensorMatcher({B, H, kHeadDim}) //
.with_strides({-1, kHeadDim, 1})
.with_dtype<DType>()
.with_device(device_)
.verify(q_input);
TensorMatcher({B, H, kHeadDim}) //
.with_strides({-1, kHeadDim, 1})
.with_dtype<fp8_e4m3_t>()
.with_device(device_)
.verify(q_fp8);
TensorMatcher({B, H}) //
.with_strides({-1, 1})
.with_dtype<DType>()
.with_device(device_)
.verify(weight);
TensorMatcher({B, H, 1}) //
.with_dtype<float>()
.with_device(device_)
.verify(weights_out);
TensorMatcher({-1, kRopeDim}) //
.with_dtype<float>()
.with_device(device_)
.verify(rope_cache);
auto pos_dtype = SymbolicDType{};
TensorMatcher({B}) //
.with_dtype<int32_t, int64_t>(pos_dtype)
.with_device(device_)
.verify(positions);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto num_heads = static_cast<uint32_t>(H.unwrap());
if (batch_size == 0) return;
// The kernel computes row pointers as `base + work_id * kHeadDim`, so
// both inputs must be contiguous in (batch, head, elem) order.
const int64_t expected_batch_stride = static_cast<int64_t>(num_heads) * kHeadDim;
RuntimeCheck(
q_input.stride(0) == expected_batch_stride,
"q_input must be contiguous (B, H, kHeadDim); got stride[0]=",
q_input.stride(0));
RuntimeCheck(
q_fp8.stride(0) == expected_batch_stride,
"q_fp8 must be contiguous (B, H, kHeadDim); got stride[0]=",
q_fp8.stride(0));
const auto params = FusedQIndexerRopeHadamardQuantParams{
.q_input = q_input.data_ptr(),
.q_fp8 = q_fp8.data_ptr(),
.weight = weight.data_ptr(),
.weights_out = static_cast<float*>(weights_out.data_ptr()),
.weight_scale = static_cast<float>(weight_scale),
.rope_cache = static_cast<const float*>(rope_cache.data_ptr()),
.positions = positions.data_ptr(),
.weight_stride_batch = weight.stride(0),
.batch_size = batch_size,
.num_heads = num_heads,
};
const auto total_works = batch_size * num_heads;
const auto num_blocks = div_ceil(total_works, kFusedQNumWarps);
const auto k_int32 = kernel<int32_t>;
const auto k_int64 = kernel<int64_t>;
const auto k = pos_dtype.is_type<int32_t>() ? k_int32 : k_int64;
LaunchKernel(num_blocks, kFusedQBlockSize, device_.unwrap()) //
.enable_pdl(kUsePDL)(k, params);
}
};
struct FusedQIndexerRopeHadamardFp4QuantParams {
const void* __restrict__ q_input;
void* __restrict__ q_fp4;
int32_t* __restrict__ q_sf;
const void* __restrict__ weight;
float* __restrict__ weights_out;
float weight_scale;
const float* __restrict__ freqs_cis;
const void* __restrict__ positions;
uint32_t batch_size;
uint32_t num_heads;
};
template <typename DType, typename PosT, bool kUsePDL>
Q_KERNEL void
fused_q_indexer_rope_hadamard_fp4_quant(const __grid_constant__ FusedQIndexerRopeHadamardFp4QuantParams params) {
using namespace device;
constexpr int64_t kHeadDim = 128;
constexpr int64_t kRopeDim = 64;
constexpr int64_t kVecSize = 4;
constexpr uint32_t kRopeSize = kRopeDim / kVecSize;
static_assert(kHeadDim == kWarpThreads * kVecSize);
static_assert(kRopeDim == kWarpThreads * 2);
static_assert(kRopeSize <= kWarpThreads);
using Storage = AlignedVector<DType, kVecSize>;
using Float4 = AlignedVector<float, kVecSize>;
const auto warp_id = threadIdx.x / kWarpThreads;
const auto lane_id = threadIdx.x % kWarpThreads;
const auto work_id = blockIdx.x * kFusedQNumWarps + warp_id;
const bool is_rope_lane = lane_id >= kWarpThreads - kRopeSize;
const uint32_t total_works = params.batch_size * params.num_heads;
if (work_id >= total_works) return;
const uint32_t batch_id = work_id / params.num_heads;
const auto input_ptr = static_cast<const DType*>(params.q_input) + work_id * kHeadDim;
const auto position = static_cast<int32_t>(static_cast<const PosT*>(params.positions)[batch_id]);
const auto freqs_cis = params.freqs_cis + position * kRopeDim;
PDLWaitPrimary<kUsePDL>();
Float4 data, freq;
const auto weight_val = cast<float>(static_cast<const DType*>(params.weight)[work_id]);
{
Storage input_vec;
input_vec.load(input_ptr, lane_id);
if (is_rope_lane) freq.load(freqs_cis, lane_id - (kWarpThreads - kRopeSize));
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
data[i] = cast<float>(input_vec[i]);
}
}
if (is_rope_lane) {
const auto x_real = data[0];
const auto x_imag = data[1];
const auto y_real = data[2];
const auto y_imag = data[3];
const auto fxr = freq[0];
const auto fxi = freq[1];
const auto fyr = freq[2];
const auto fyi = freq[3];
data[0] = x_real * fxr - x_imag * fxi;
data[1] = x_real * fxi + x_imag * fxr;
data[2] = y_real * fyr - y_imag * fyi;
data[3] = y_real * fyi + y_imag * fyr;
#pragma unroll
for (int i = 0; i < kVecSize; ++i)
data[i] = cast<float>(cast<DType>(data[i]));
}
PDLTriggerSecondary<kUsePDL>();
{
{
const float a0 = data[0], a1 = data[1], a2 = data[2], a3 = data[3];
data[0] = a0 + a1;
data[1] = a0 - a1;
data[2] = a2 + a3;
data[3] = a2 - a3;
}
{
const float a0 = data[0], a1 = data[1], a2 = data[2], a3 = data[3];
data[0] = a0 + a2;
data[1] = a1 + a3;
data[2] = a0 - a2;
data[3] = a1 - a3;
}
#pragma unroll
for (uint32_t mask = 1; mask < kWarpThreads; mask <<= 1) {
#pragma unroll
for (int i = 0; i < kVecSize; ++i) {
const float other = __shfl_xor_sync(0xFFFFFFFFu, data[i], mask, kWarpThreads);
data[i] = (lane_id & mask) ? (other - data[i]) : (data[i] + other);
}
}
const float kHadamardScale = math::rsqrt(static_cast<float>(kHeadDim));
#pragma unroll
for (int i = 0; i < kVecSize; ++i)
data[i] *= kHadamardScale;
#pragma unroll
for (int i = 0; i < kVecSize; ++i)
data[i] = cast<float>(cast<DType>(data[i]));
}
{
float local_max = math::abs(data[0]);
#pragma unroll
for (int i = 1; i < kVecSize; ++i) {
local_max = math::max(local_max, math::abs(data[i]));
}
local_max = warp::reduce_max<8>(local_max);
const auto scale_raw = fmaxf(1e-4f, local_max) / 6.0f;
const auto scale_ue8m0 = static_cast<uint8_t>(cast_to_ue8m0(scale_raw));
const auto inv_scale = inv_scale_ue8m0(scale_ue8m0);
const uint8_t packed0 = quant_fp4_e2m1(data[0] * inv_scale) | (quant_fp4_e2m1(data[1] * inv_scale) << 4);
const uint8_t packed1 = quant_fp4_e2m1(data[2] * inv_scale) | (quant_fp4_e2m1(data[3] * inv_scale) << 4);
const uint16_t packed = static_cast<uint16_t>(packed0) | (static_cast<uint16_t>(packed1) << 8);
auto out_row = static_cast<uint8_t*>(params.q_fp4) + work_id * (kHeadDim / 2);
reinterpret_cast<uint16_t*>(out_row)[lane_id] = packed;
if ((lane_id & 7) == 0) {
reinterpret_cast<uint8_t*>(params.q_sf + work_id)[lane_id >> 3] = scale_ue8m0;
}
params.weights_out[work_id] = weight_val * params.weight_scale;
}
}
template <typename DType, bool kUsePDL>
struct FusedQIndexerRopeHadamardFp4QuantKernel {
template <typename PosT>
static constexpr auto kernel = fused_q_indexer_rope_hadamard_fp4_quant<DType, PosT, kUsePDL>;
static void forward(
const tvm::ffi::TensorView q_input,
const tvm::ffi::TensorView q_fp4,
const tvm::ffi::TensorView q_sf,
const tvm::ffi::TensorView weight,
const tvm::ffi::TensorView weights_out,
double weight_scale,
const tvm::ffi::TensorView freqs_cis,
const tvm::ffi::TensorView positions) {
using namespace host;
constexpr int64_t kHeadDim = 128;
constexpr int64_t kRopeDim = 64;
constexpr int64_t kFp4Dim = kHeadDim / 2;
auto B = SymbolicSize{"batch_size"};
auto H = SymbolicSize{"num_heads"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({B, H, kHeadDim})
.with_strides({-1, kHeadDim, 1})
.with_dtype<DType>()
.with_device(device_)
.verify(q_input);
TensorMatcher({B, H, kFp4Dim})
.with_strides({-1, kFp4Dim, 1})
.with_dtype<int8_t>()
.with_device(device_)
.verify(q_fp4);
TensorMatcher({B, H}).with_dtype<int32_t>().with_device(device_).verify(q_sf);
TensorMatcher({B, H}).with_dtype<DType>().with_device(device_).verify(weight);
TensorMatcher({B, H, 1}).with_dtype<float>().with_device(device_).verify(weights_out);
TensorMatcher({-1, kRopeDim}).with_dtype<float>().with_device(device_).verify(freqs_cis);
auto pos_dtype = SymbolicDType{};
TensorMatcher({B}).with_dtype<int32_t, int64_t>(pos_dtype).with_device(device_).verify(positions);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto num_heads = static_cast<uint32_t>(H.unwrap());
if (batch_size == 0) return;
const int64_t expected_q_stride = static_cast<int64_t>(num_heads) * kHeadDim;
const int64_t expected_fp4_stride = static_cast<int64_t>(num_heads) * kFp4Dim;
RuntimeCheck(q_input.stride(0) == expected_q_stride, "q_input must be contiguous");
RuntimeCheck(q_fp4.stride(0) == expected_fp4_stride, "q_fp4 must be contiguous");
RuntimeCheck(q_sf.stride(0) == static_cast<int64_t>(num_heads) && q_sf.stride(1) == 1, "q_sf must be contiguous");
const auto params = FusedQIndexerRopeHadamardFp4QuantParams{
.q_input = q_input.data_ptr(),
.q_fp4 = q_fp4.data_ptr(),
.q_sf = static_cast<int32_t*>(q_sf.data_ptr()),
.weight = weight.data_ptr(),
.weights_out = static_cast<float*>(weights_out.data_ptr()),
.weight_scale = static_cast<float>(weight_scale),
.freqs_cis = static_cast<const float*>(freqs_cis.data_ptr()),
.positions = positions.data_ptr(),
.batch_size = batch_size,
.num_heads = num_heads,
};
const auto total_works = batch_size * num_heads;
const auto num_blocks = div_ceil(total_works, kFusedQNumWarps);
const auto k_int32 = kernel<int32_t>;
const auto k_int64 = kernel<int64_t>;
const auto k = pos_dtype.is_type<int32_t>() ? k_int32 : k_int64;
LaunchKernel(num_blocks, kFusedQBlockSize, device_.unwrap()).enable_pdl(kUsePDL)(k, params);
}
};
} // namespace
@@ -0,0 +1,221 @@
#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 <cstdint>
#include <cuda_fp8.h>
namespace {
using deepseek_v4::fp8::cast_to_ue8m0;
using deepseek_v4::fp8::pack_fp8;
struct MegaMoEPreDispatchParams {
const bf16_t* __restrict__ x; // [num_tokens, hidden]
const int32_t* __restrict__ topk_idx; // [num_tokens, top_k]
const float* __restrict__ topk_weights; // [num_tokens, top_k]
fp8_e4m3_t* __restrict__ buf_x; // [padded_max, hidden]
int32_t* __restrict__ buf_x_sf; // contiguous int32 [P, G/4]; see layout comment
int64_t* __restrict__ buf_topk_idx; // [padded_max, top_k]
float* __restrict__ buf_topk_weights; // [padded_max, top_k]
uint32_t num_tokens;
uint32_t padded_max;
uint32_t hidden;
uint32_t num_groups; // hidden / group_size
uint32_t top_k;
};
// kGroupSize must match sglang_per_token_group_quant_fp8_ue8m0(group_size=).
template <uint32_t kGroupSize, bool kUsePDL>
__global__ __launch_bounds__(1024, 2) void //
mega_moe_pre_dispatch_kernel(const MegaMoEPreDispatchParams __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 bid = blockIdx.x;
const uint32_t tid = threadIdx.x;
PDLWaitPrimary<kUsePDL>();
if (bid < params.num_tokens) {
// ---- Quantize path: one CTA per valid token ----
const uint32_t token_id = bid;
const auto token_in = params.x + static_cast<uint64_t>(token_id) * params.hidden;
const auto token_out = params.buf_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)));
}
// 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);
// 2^-ue8m0_exp as fp32 (equivalent to 1 / __uint_as_float(ue8m0 << 23)).
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);
// One thread per group writes its UE8M0 byte into the contiguous
// row-major int32-packed layout: byte address = t*num_groups + g
// (see layout comment at the top of the file).
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.buf_x_sf)[byte_off] = static_cast<uint8_t>(ue8m0_exp);
}
// Copy this token's topk row (no alignment assumptions; top_k is small).
if (tid < params.top_k) {
const uint32_t off = token_id * params.top_k + tid;
params.buf_topk_idx[off] = params.topk_idx[off];
params.buf_topk_weights[off] = params.topk_weights[off];
}
} else {
// ---- Pad path: trailing blocks fill [num_tokens, padded_max) with (-1, 0) ----
const uint32_t copy_bid = bid - params.num_tokens;
const uint32_t pad_base = params.num_tokens * params.top_k;
const uint32_t slot = pad_base + copy_bid * blockDim.x + tid;
const uint32_t total_slots = params.padded_max * params.top_k;
if (slot < total_slots) {
params.buf_topk_idx[slot] = -1;
params.buf_topk_weights[slot] = 0.0f;
}
}
PDLTriggerSecondary<kUsePDL>();
}
// ---- Host wrapper
// ------------------------------------------------------------------------------------------------------------------------
template <int64_t kGroupSize, bool kUsePDL>
struct MegaMoEPreDispatchKernel {
static_assert(kGroupSize == 32 || kGroupSize == 64 || kGroupSize == 128, "unsupported group_size");
static constexpr auto kernel = mega_moe_pre_dispatch_kernel<static_cast<uint32_t>(kGroupSize), kUsePDL>;
static void
run(const tvm::ffi::TensorView x,
const tvm::ffi::TensorView topk_idx,
const tvm::ffi::TensorView topk_weights,
const tvm::ffi::TensorView buf_x,
const tvm::ffi::TensorView buf_x_sf,
const tvm::ffi::TensorView buf_topk_idx,
const tvm::ffi::TensorView buf_topk_weights) {
using namespace host;
auto device = SymbolicDevice{};
auto M = SymbolicSize{"num_tokens"};
auto P = SymbolicSize{"padded_max"};
auto H = SymbolicSize{"hidden"};
auto K = SymbolicSize{"top_k"};
auto G4 = SymbolicSize{"num_groups_div_4"};
device.set_options<kDLCUDA>();
TensorMatcher({M, H}) // input x
.with_dtype<bf16_t>()
.with_device(device)
.verify(x);
TensorMatcher({M, K}) // topk_idx
.with_dtype<int32_t>()
.with_device(device)
.verify(topk_idx);
TensorMatcher({M, K}) // topk_weights
.with_dtype<float>()
.with_device(device)
.verify(topk_weights);
// DeepGEMM versions expose this fp8 dispatch buffer either as raw int8
// storage or as torch.float8_e4m3fn; the kernel writes fp8 bytes in both.
TensorMatcher({P, H}) // buf.x
.with_dtype<int8_t, fp8_e4m3_t>()
.with_device(device)
.verify(buf_x);
// buf.x_sf is the contiguous row-major int32 view from DeepGEMM's mega
// symm buffer (DeepGEMM/csrc/apis/mega.hpp): shape (P, G/4), strides
// (G/4, 1). No explicit strides required -> TensorMatcher enforces
// is_contiguous().
TensorMatcher({P, G4}) // buf_x_sf
.with_dtype<int32_t>()
.with_device(device)
.verify(buf_x_sf);
TensorMatcher({P, K}) // buf.topk_idx
.with_dtype<int64_t>()
.with_device(device)
.verify(buf_topk_idx);
TensorMatcher({P, K}) // buf.topk_weights
.with_dtype<float>()
.with_device(device)
.verify(buf_topk_weights);
const auto num_tokens = static_cast<uint32_t>(M.unwrap());
const auto padded_max = static_cast<uint32_t>(P.unwrap());
const auto hidden = static_cast<uint32_t>(H.unwrap());
const auto top_k = static_cast<uint32_t>(K.unwrap());
const auto num_groups_div_4 = static_cast<uint32_t>(G4.unwrap());
RuntimeCheck(num_tokens <= padded_max, "num_tokens must not exceed padded_max");
RuntimeCheck(hidden % kGroupSize == 0, "hidden must be a multiple of group_size");
const auto num_groups = hidden / static_cast<uint32_t>(kGroupSize);
RuntimeCheck(num_groups == num_groups_div_4 * 4u, "num_groups must be a multiple of 4");
RuntimeCheck(hidden % 8u == 0, "hidden must be a multiple of 8 (16B bf16 loads)");
const auto num_threads = hidden / 8u;
RuntimeCheck(num_threads <= 1024, "hidden too large for single-block-per-row quant");
RuntimeCheck(num_threads >= top_k, "top_k must fit into one quant CTA");
const auto pad_slots = (padded_max - num_tokens) * top_k;
const uint32_t num_pad_blocks = pad_slots == 0 ? 0u : ((pad_slots + num_threads - 1u) / num_threads);
const auto num_total_blocks = num_tokens + num_pad_blocks;
const auto params = MegaMoEPreDispatchParams{
.x = static_cast<const bf16_t*>(x.data_ptr()),
.topk_idx = static_cast<const int32_t*>(topk_idx.data_ptr()),
.topk_weights = static_cast<const float*>(topk_weights.data_ptr()),
.buf_x = static_cast<fp8_e4m3_t*>(buf_x.data_ptr()),
.buf_x_sf = static_cast<int32_t*>(buf_x_sf.data_ptr()),
.buf_topk_idx = static_cast<int64_t*>(buf_topk_idx.data_ptr()),
.buf_topk_weights = static_cast<float*>(buf_topk_weights.data_ptr()),
.num_tokens = num_tokens,
.padded_max = padded_max,
.hidden = hidden,
.num_groups = num_groups,
.top_k = top_k,
};
if (num_total_blocks == 0) return;
LaunchKernel(num_total_blocks, num_threads, device.unwrap()) //
.enable_pdl(kUsePDL)(kernel, params);
}
};
} // namespace
@@ -0,0 +1,387 @@
#pragma once
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/runtime.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
namespace {
SGL_DEVICE int64_t clamp_accept_len(int64_t delta, int64_t max_accept) {
if (delta < 0) return 0;
return delta < max_accept ? delta : max_accept;
}
template <typename TSeq, typename TReq>
struct OnlineC128MTPWritePrefixParams {
const float* __restrict__ kv_score_input;
const TSeq* __restrict__ seq_lens;
const TReq* __restrict__ req_pool_indices;
const int32_t* __restrict__ req_to_token;
const float* __restrict__ ape;
float* __restrict__ state;
int64_t kv_score_stride_b;
int64_t req_to_token_stride_b;
int64_t ape_stride_r;
int64_t state_stride_b;
int64_t layer_bs;
int64_t num_verify_tokens;
int64_t state_slot_stride;
};
template <typename TSeq, typename TReq>
struct OnlineC128MTPMarkPendingParams {
const TSeq* __restrict__ seq_lens;
const TReq* __restrict__ req_pool_indices;
int64_t* __restrict__ pending_seq_lens;
int64_t bs;
int64_t max_num_reqs;
};
template <typename TSeq, typename TReq>
struct OnlineC128MTPCommitPendingParams {
const TSeq* __restrict__ cur_seq_lens;
const TReq* __restrict__ cur_req_pool_indices;
const int32_t* __restrict__ req_to_token;
const int64_t* __restrict__ pending_seq_lens;
float* __restrict__ state;
int64_t cur_bs;
int64_t req_to_token_stride_b;
int64_t state_stride_b;
int64_t num_verify_tokens;
int64_t state_slot_stride;
int64_t max_num_reqs;
};
__global__ void online_c128_mtp_clear_all_pending_kernel(int64_t* pending_seq_lens, int64_t max_num_reqs) {
const int64_t idx = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
if (idx < max_num_reqs) pending_seq_lens[idx] = -1;
}
template <typename TSeq, typename TReq>
__global__ void online_c128_mtp_mark_pending_kernel(const OnlineC128MTPMarkPendingParams<TSeq, TReq> params) {
const int64_t bid = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
if (bid >= params.bs) return;
const int64_t req = static_cast<int64_t>(params.req_pool_indices[bid]);
if (req >= 0 && req < params.max_num_reqs) {
params.pending_seq_lens[req] = static_cast<int64_t>(params.seq_lens[bid]);
}
}
template <int64_t kHeadDim, typename TSeq, typename TReq>
__global__ void online_c128_mtp_commit_pending_kernel(const OnlineC128MTPCommitPendingParams<TSeq, TReq> params) {
const int64_t bid = static_cast<int64_t>(blockIdx.x);
if (bid >= params.cur_bs) return;
const int64_t req = static_cast<int64_t>(params.cur_req_pool_indices[bid]);
if (req < 0 || req >= params.max_num_reqs) return;
const int64_t old_seq = params.pending_seq_lens[req];
if (old_seq < 0) return;
const int64_t cur_seq = static_cast<int64_t>(params.cur_seq_lens[bid]);
const int64_t accept = clamp_accept_len(cur_seq - old_seq, params.num_verify_tokens);
if (accept <= 0) return;
const int64_t final_seq = old_seq + accept;
if ((final_seq & 127) == 0) return;
const int64_t slot = req;
const float* const src = params.state + (slot + accept * params.state_slot_stride) * params.state_stride_b;
float* const dst = params.state + slot * params.state_stride_b;
for (int64_t d = static_cast<int64_t>(threadIdx.x); d < kHeadDim * 3; d += blockDim.x) {
dst[d] = src[d];
}
}
template <int64_t kHeadDim, typename TSeq, typename TReq>
__global__ void online_c128_mtp_write_prefix_kernel(const OnlineC128MTPWritePrefixParams<TSeq, TReq> params) {
const int64_t bid = static_cast<int64_t>(blockIdx.x);
if (bid >= params.layer_bs) return;
const int64_t seq_before = static_cast<int64_t>(params.seq_lens[bid]);
const int64_t req_idx = static_cast<int64_t>(params.req_pool_indices[bid]);
const int64_t start_pos = seq_before & 127;
const bool has_partial = seq_before > 0 && start_pos != 0;
int64_t init_slot = 0;
if (has_partial) {
init_slot = req_idx;
}
const int64_t d = static_cast<int64_t>(threadIdx.x);
float run_max = 0.0f;
float run_sum = 0.0f;
float run_kv = 0.0f;
if (has_partial) {
const float* const init = params.state + init_slot * params.state_stride_b;
run_max = init[d];
run_sum = init[kHeadDim + d];
run_kv = init[kHeadDim * 2 + d];
}
constexpr int kMaxVerifyTokens = 8;
float kv_steps[kMaxVerifyTokens];
float score_steps[kMaxVerifyTokens];
#pragma unroll
for (int step = 0; step < kMaxVerifyTokens; ++step) {
if (step >= params.num_verify_tokens) break;
const int64_t pos = (start_pos + step) & 127;
const float* const kv = params.kv_score_input + (bid * params.num_verify_tokens + step) * params.kv_score_stride_b;
kv_steps[step] = kv[d];
score_steps[step] = kv[kHeadDim + d] + params.ape[pos * params.ape_stride_r + d];
}
#pragma unroll
for (int step = 0; step < kMaxVerifyTokens; ++step) {
if (step >= params.num_verify_tokens) break;
const int64_t pos = (start_pos + step) & 127;
const float kv_step = kv_steps[step];
const float score_step = score_steps[step];
if (pos == 0) {
run_kv = kv_step;
run_max = score_step;
run_sum = 1.0f;
} else {
const float new_max = fmaxf(run_max, score_step);
const float old_sum_scaled = run_sum * __expf(run_max - new_max);
const float new_exp = __expf(score_step - new_max);
const float new_sum = old_sum_scaled + new_exp;
run_kv = (run_kv * old_sum_scaled + kv_step * new_exp) / new_sum;
run_max = new_max;
run_sum = new_sum;
}
const int64_t final_seq = seq_before + step + 1;
if ((final_seq & 127) != 0) {
const int64_t slot = req_idx + (step + 1) * params.state_slot_stride;
float* const out = params.state + slot * params.state_stride_b;
out[d] = run_max;
out[kHeadDim + d] = run_sum;
out[kHeadDim * 2 + d] = run_kv;
}
if (pos == 127) {
run_kv = 0.0f;
run_max = 0.0f;
run_sum = 0.0f;
}
}
}
template <int64_t kHeadDim, typename TSeq, typename TReq>
struct OnlineC128MTPWritePrefixKernel {
static void launch(
tvm::ffi::TensorView kv_score_input,
tvm::ffi::TensorView seq_lens,
tvm::ffi::TensorView req_pool_indices,
tvm::ffi::TensorView req_to_token,
tvm::ffi::TensorView ape,
tvm::ffi::TensorView state,
int64_t layer_bs,
int64_t num_verify_tokens,
int64_t state_slot_stride,
DLDevice device) {
using namespace host;
const auto params = OnlineC128MTPWritePrefixParams<TSeq, TReq>{
.kv_score_input = static_cast<const float*>(kv_score_input.data_ptr()),
.seq_lens = static_cast<const TSeq*>(seq_lens.data_ptr()),
.req_pool_indices = static_cast<const TReq*>(req_pool_indices.data_ptr()),
.req_to_token = static_cast<const int32_t*>(req_to_token.data_ptr()),
.ape = static_cast<const float*>(ape.data_ptr()),
.state = static_cast<float*>(state.data_ptr()),
.kv_score_stride_b = kv_score_input.stride(0),
.req_to_token_stride_b = req_to_token.stride(0),
.ape_stride_r = ape.stride(0),
.state_stride_b = state.stride(0),
.layer_bs = layer_bs,
.num_verify_tokens = num_verify_tokens,
.state_slot_stride = state_slot_stride,
};
static_assert(kHeadDim == 512, "online c128 MTP write-prefix only supports head_dim=512");
constexpr uint32_t kThreads = static_cast<uint32_t>(kHeadDim);
LaunchKernel(static_cast<uint32_t>(layer_bs), kThreads, device)(
online_c128_mtp_write_prefix_kernel<kHeadDim, TSeq, TReq>, params);
}
static void
run(tvm::ffi::TensorView kv_score_input,
tvm::ffi::TensorView seq_lens,
tvm::ffi::TensorView req_pool_indices,
tvm::ffi::TensorView req_to_token,
tvm::ffi::TensorView ape,
tvm::ffi::TensorView state,
int64_t layer_bs,
int64_t num_verify_tokens,
int64_t state_slot_stride) {
using namespace host;
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
TensorMatcher({-1, kHeadDim * 2}).with_dtype<float>().with_device(device).verify(kv_score_input);
TensorMatcher({-1}).with_dtype<TSeq>().with_device(device).verify(seq_lens);
TensorMatcher({-1}).with_dtype<TReq>().with_device(device).verify(req_pool_indices);
TensorMatcher({-1, -1}).with_dtype<int32_t>().with_device(device).verify(req_to_token);
TensorMatcher({128, kHeadDim}).with_dtype<float>().with_device(device).verify(ape);
TensorMatcher({-1, kHeadDim * 3}).with_dtype<float>().with_device(device).verify(state);
if (layer_bs <= 0) return;
RuntimeCheck(num_verify_tokens > 0 && num_verify_tokens <= 8, "unsupported num_verify_tokens=", num_verify_tokens);
RuntimeCheck(state_slot_stride > 0, "state_slot_stride must be positive");
RuntimeCheck(layer_bs <= seq_lens.shape()[0], "layer_bs exceeds seq_lens rows");
RuntimeCheck(layer_bs <= req_pool_indices.shape()[0], "layer_bs exceeds req_pool_indices rows");
RuntimeCheck(layer_bs * num_verify_tokens <= kv_score_input.shape()[0], "kv_score_input is too small");
launch(
kv_score_input,
seq_lens,
req_pool_indices,
req_to_token,
ape,
state,
layer_bs,
num_verify_tokens,
state_slot_stride,
device.unwrap());
}
};
template <int64_t kHeadDim, typename TSeq, typename TReq>
struct OnlineC128MTPMarkPendingKernel {
static void launch(
tvm::ffi::TensorView seq_lens,
tvm::ffi::TensorView req_pool_indices,
tvm::ffi::TensorView pending_seq_lens,
int64_t bs,
int64_t max_num_reqs,
DLDevice device) {
using namespace host;
const auto params = OnlineC128MTPMarkPendingParams<TSeq, TReq>{
.seq_lens = static_cast<const TSeq*>(seq_lens.data_ptr()),
.req_pool_indices = static_cast<const TReq*>(req_pool_indices.data_ptr()),
.pending_seq_lens = static_cast<int64_t*>(pending_seq_lens.data_ptr()),
.bs = bs,
.max_num_reqs = max_num_reqs,
};
constexpr uint32_t kThreads = 256;
const uint32_t clear_blocks = host::div_ceil(static_cast<uint32_t>(max_num_reqs), kThreads);
LaunchKernel(clear_blocks, kThreads, device)(
online_c128_mtp_clear_all_pending_kernel, params.pending_seq_lens, max_num_reqs);
const uint32_t mark_blocks = host::div_ceil(static_cast<uint32_t>(bs), kThreads);
LaunchKernel(mark_blocks, kThreads, device)(online_c128_mtp_mark_pending_kernel<TSeq, TReq>, params);
}
static void
run(tvm::ffi::TensorView seq_lens,
tvm::ffi::TensorView req_pool_indices,
tvm::ffi::TensorView pending_seq_lens,
int64_t bs,
int64_t max_num_reqs) {
using namespace host;
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
TensorMatcher({-1}).with_dtype<TSeq>().with_device(device).verify(seq_lens);
TensorMatcher({-1}).with_dtype<TReq>().with_device(device).verify(req_pool_indices);
TensorMatcher({-1}).with_dtype<int64_t>().with_device(device).verify(pending_seq_lens);
if (bs <= 0) return;
RuntimeCheck(bs <= seq_lens.shape()[0], "bs exceeds seq_lens rows");
RuntimeCheck(bs <= req_pool_indices.shape()[0], "bs exceeds req_pool_indices rows");
RuntimeCheck(max_num_reqs <= pending_seq_lens.shape()[0], "max_num_reqs exceeds pending rows");
launch(seq_lens, req_pool_indices, pending_seq_lens, bs, max_num_reqs, device.unwrap());
}
};
template <int64_t kHeadDim, typename TSeq, typename TReq>
struct OnlineC128MTPCommitPendingKernel {
static void launch(
tvm::ffi::TensorView cur_seq_lens,
tvm::ffi::TensorView cur_req_pool_indices,
tvm::ffi::TensorView req_to_token,
tvm::ffi::TensorView pending_seq_lens,
tvm::ffi::TensorView state,
int64_t cur_bs,
int64_t num_verify_tokens,
int64_t state_slot_stride,
int64_t max_num_reqs,
DLDevice device) {
using namespace host;
const auto params = OnlineC128MTPCommitPendingParams<TSeq, TReq>{
.cur_seq_lens = static_cast<const TSeq*>(cur_seq_lens.data_ptr()),
.cur_req_pool_indices = static_cast<const TReq*>(cur_req_pool_indices.data_ptr()),
.req_to_token = static_cast<const int32_t*>(req_to_token.data_ptr()),
.pending_seq_lens = static_cast<const int64_t*>(pending_seq_lens.data_ptr()),
.state = static_cast<float*>(state.data_ptr()),
.cur_bs = cur_bs,
.req_to_token_stride_b = req_to_token.stride(0),
.state_stride_b = state.stride(0),
.num_verify_tokens = num_verify_tokens,
.state_slot_stride = state_slot_stride,
.max_num_reqs = max_num_reqs,
};
constexpr uint32_t kThreads = 256;
LaunchKernel(static_cast<uint32_t>(cur_bs), kThreads, device)(
online_c128_mtp_commit_pending_kernel<kHeadDim, TSeq, TReq>, params);
}
static void
run(tvm::ffi::TensorView cur_seq_lens,
tvm::ffi::TensorView cur_req_pool_indices,
tvm::ffi::TensorView req_to_token,
tvm::ffi::TensorView pending_seq_lens,
tvm::ffi::TensorView state,
int64_t cur_bs,
int64_t num_verify_tokens,
int64_t state_slot_stride,
int64_t max_num_reqs) {
using namespace host;
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
TensorMatcher({-1}).with_dtype<TSeq>().with_device(device).verify(cur_seq_lens);
TensorMatcher({-1}).with_dtype<TReq>().with_device(device).verify(cur_req_pool_indices);
TensorMatcher({-1, -1}).with_dtype<int32_t>().with_device(device).verify(req_to_token);
TensorMatcher({-1}).with_dtype<int64_t>().with_device(device).verify(pending_seq_lens);
TensorMatcher({-1, kHeadDim * 3}).with_dtype<float>().with_device(device).verify(state);
if (cur_bs <= 0) return;
RuntimeCheck(num_verify_tokens > 0 && num_verify_tokens <= 8, "unsupported num_verify_tokens=", num_verify_tokens);
RuntimeCheck(state_slot_stride > 0, "state_slot_stride must be positive");
RuntimeCheck(cur_bs <= cur_seq_lens.shape()[0], "cur_bs exceeds seq_lens rows");
RuntimeCheck(cur_bs <= cur_req_pool_indices.shape()[0], "cur_bs exceeds req rows");
RuntimeCheck(max_num_reqs <= pending_seq_lens.shape()[0], "max_num_reqs exceeds pending rows");
launch(
cur_seq_lens,
cur_req_pool_indices,
req_to_token,
pending_seq_lens,
state,
cur_bs,
num_verify_tokens,
state_slot_stride,
max_num_reqs,
device.unwrap());
}
};
} // namespace
@@ -0,0 +1,119 @@
#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>
namespace {
constexpr uint32_t kBlockSize = 1024;
constexpr uint32_t kSplitKV = 256; // const for both SM90 and SM100
struct MetadataParams {
/// NOTE: batch_size > 0
uint32_t batch_size;
uint32_t num_sm;
const uint32_t* __restrict__ context_lens;
uint32_t* __restrict__ schedule_metadata;
bool use_smem = true;
};
__global__ __launch_bounds__(kBlockSize, 1) //
void smxx_paged_mqa_logits_metadata(const MetadataParams params) {
using namespace device;
extern __shared__ uint32_t s_length[];
static constexpr auto kNumWarps = kBlockSize / kWarpThreads;
static_assert(kNumWarps == kWarpThreads);
const auto tx = threadIdx.x;
const auto lane_id = tx % kWarpThreads;
const auto warp_id = tx / kWarpThreads;
__shared__ uint32_t s_warp_sum[kNumWarps];
uint32_t local_sum = 0;
for (uint32_t i = tx; i < params.batch_size; i += kBlockSize) {
const auto length = params.context_lens[i];
local_sum += (length + kSplitKV - 1) / kSplitKV;
if (params.use_smem) s_length[i] = length;
}
s_warp_sum[warp_id] = warp::reduce_sum(local_sum);
__syncthreads();
const auto global_sum = warp::reduce_sum(s_warp_sum[lane_id]);
if (lane_id != 0) return;
const auto length_ptr = params.use_smem ? s_length : params.context_lens;
const auto avg = global_sum / params.num_sm;
const auto ret = global_sum % params.num_sm;
uint32_t q = 0;
uint32_t num_work = (length_ptr[0] + kSplitKV - 1) / kSplitKV;
uint32_t sum_work = num_work;
for (auto i = warp_id; i <= params.num_sm; i += kNumWarps) {
const auto target = i * avg + min(i, ret);
while (sum_work <= target) {
if (++q >= params.batch_size) break;
num_work = (length_ptr[q] + kSplitKV - 1) / kSplitKV;
sum_work += num_work;
}
if (q >= params.batch_size) {
params.schedule_metadata[2 * i + 0] = params.batch_size;
params.schedule_metadata[2 * i + 1] = 0;
} else {
// sum > target && (sum - length) <= target
params.schedule_metadata[2 * i + 0] = q;
params.schedule_metadata[2 * i + 1] = target - (sum_work - num_work);
}
}
}
template <auto* f, size_t kMaxDynamicSMEM>
void setup_kernel_smem_once(host::DebugInfo where = {}) {
[[maybe_unused]]
static const auto result = [] {
const auto fptr = std::bit_cast<const void*>(f);
return ::cudaFuncSetAttribute(fptr, ::cudaFuncAttributeMaxDynamicSharedMemorySize, kMaxDynamicSMEM);
}();
host::RuntimeDeviceCheck(result, where);
}
struct IndexerMetadataKernel {
static constexpr auto kMaxBatchSizeInSmem = 16384 * 2; // 128 KB smeme
static void run(tvm::ffi::TensorView seq_lens, tvm::ffi::TensorView metadata) {
using namespace host;
auto N = SymbolicSize{"batch_size"};
auto M = SymbolicSize{"num_sm"};
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
TensorMatcher({N}) //
.with_dtype<int32_t>()
.with_device(device)
.verify(seq_lens);
TensorMatcher({M, 2}) //
.with_dtype<int32_t>()
.with_device(device)
.verify(metadata);
const auto batch_size = static_cast<uint32_t>(N.unwrap());
const auto num_sm = static_cast<uint32_t>(M.unwrap()) - 1;
RuntimeCheck(num_sm <= 1024);
const auto use_smem = batch_size <= kMaxBatchSizeInSmem;
const auto params = MetadataParams{
.batch_size = batch_size,
.num_sm = num_sm,
.context_lens = static_cast<uint32_t*>(seq_lens.data_ptr()),
.schedule_metadata = static_cast<uint32_t*>(metadata.data_ptr()),
.use_smem = use_smem,
};
constexpr auto kernel = smxx_paged_mqa_logits_metadata;
setup_kernel_smem_once<kernel, (kMaxBatchSizeInSmem + 1) * sizeof(uint32_t)>();
const auto smem = use_smem ? (batch_size + 1) * sizeof(uint32_t) : 0;
LaunchKernel(1, kBlockSize, device.unwrap(), smem)(kernel, params);
}
};
} // namespace
@@ -0,0 +1,169 @@
#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 <tvm/ffi/container/tensor.h>
#include <cstdint>
namespace {
using DType = bf16_t;
constexpr int64_t kRopeDim = 64;
constexpr uint32_t kBlockSize = 128;
constexpr uint32_t kNumWarps = kBlockSize / device::kWarpThreads;
struct FusedQKRopeParams {
void* __restrict__ q;
void* __restrict__ k;
const float* __restrict__ freqs_cis;
const void* __restrict__ positions;
int64_t q_stride_batch;
int64_t k_stride_batch;
int64_t q_stride_head;
int64_t k_stride_head;
uint32_t num_q_heads;
uint32_t num_k_heads;
uint32_t batch_size;
};
template <bool kUsePDL, bool kInverse, typename IndexType>
__global__ __launch_bounds__(kBlockSize, 16) //
void deepseek_rope_kernel(const __grid_constant__ FusedQKRopeParams param) {
using namespace device;
using DType2 = packed_t<DType>;
const auto warp_id = threadIdx.x / kWarpThreads;
const auto lane_id = threadIdx.x % kWarpThreads;
const auto global_warp_id = blockIdx.x * kNumWarps + warp_id;
const auto& [
q, k, freqs_cis, positions, //
q_stride_batch, k_stride_batch, q_stride_head, k_stride_head, //
num_q_heads, num_k_heads, batch_size
] = param;
const auto num_total_heads = num_q_heads + num_k_heads;
const auto head_id = global_warp_id % num_total_heads;
const auto batch_id = global_warp_id / num_total_heads;
if (batch_id >= batch_size) return;
const auto position = static_cast<const IndexType*>(positions)[batch_id];
const auto is_q = head_id < num_q_heads;
const auto local_head = is_q ? head_id : (head_id - num_q_heads);
const auto stride_batch = is_q ? q_stride_batch : k_stride_batch;
const auto stride_head = is_q ? q_stride_head : k_stride_head;
const auto base_ptr = is_q ? q : k;
const auto input = static_cast<DType2*>(pointer::offset(base_ptr, batch_id * stride_batch, local_head * stride_head));
const auto freq_ptr = reinterpret_cast<const fp32x2_t*>(freqs_cis + position * kRopeDim);
const auto [f_real, f_imag] = freq_ptr[lane_id];
PDLWaitPrimary<kUsePDL>();
const auto data = input[lane_id];
const auto [x_real, x_imag] = cast<fp32x2_t>(data);
fp32x2_t output;
if constexpr (kInverse) {
// (a + bi) * (c - di) = (ac + bd) + (bc - ad)i
output = {
x_real * f_real + x_imag * f_imag,
x_imag * f_real - x_real * f_imag,
};
} else {
// (a + bi) * (c + di) = (ac - bd) + (ad + bc)i
output = {
x_real * f_real - x_imag * f_imag,
x_real * f_imag + x_imag * f_real,
};
}
input[lane_id] = cast<DType2>(output);
PDLTriggerSecondary<kUsePDL>();
}
template <bool kUsePDL>
struct FusedQKRopeKernel {
// 4 kernel variants: {forward, inverse} x {int32, int64}
static constexpr auto kernel_fwd_i32 = deepseek_rope_kernel<kUsePDL, false, int32_t>;
static constexpr auto kernel_fwd_i64 = deepseek_rope_kernel<kUsePDL, false, int64_t>;
static constexpr auto kernel_inv_i32 = deepseek_rope_kernel<kUsePDL, true, int32_t>;
static constexpr auto kernel_inv_i64 = deepseek_rope_kernel<kUsePDL, true, int64_t>;
static void forward(
const tvm::ffi::TensorView q,
const tvm::ffi::Optional<tvm::ffi::TensorView> k,
const tvm::ffi::TensorView freqs_cis,
const tvm::ffi::TensorView positions,
bool inverse) {
using namespace host;
auto B = SymbolicSize{"batch_size"};
auto Q = SymbolicSize{"num_q_heads"};
auto K = SymbolicSize{"num_k_heads"};
constexpr auto D = kRopeDim;
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({B, Q, D}) //
.with_strides({-1, -1, 1})
.with_dtype<DType>()
.with_device(device_)
.verify(q);
if (k.has_value()) {
TensorMatcher({B, K, D}) //
.with_strides({-1, -1, 1})
.with_dtype<DType>()
.with_device(device_)
.verify(k.value());
} else {
K.set_value(0);
}
TensorMatcher({-1, D}) //
.with_dtype<float>()
.with_device(device_)
.verify(freqs_cis);
auto pos_dtype = SymbolicDType{};
TensorMatcher({B}) //
.with_dtype<int32_t, int64_t>(pos_dtype)
.with_device(device_)
.verify(positions);
const bool pos_i32 = pos_dtype.is_type<int32_t>();
const auto batch_size = static_cast<uint32_t>(B.unwrap());
if (batch_size == 0) return;
const auto num_q_heads = static_cast<uint32_t>(Q.unwrap());
const auto num_k_heads = static_cast<uint32_t>(K.unwrap());
const auto num_total_heads = num_q_heads + num_k_heads;
const auto total_warps = batch_size * num_total_heads;
const auto num_blocks = div_ceil(total_warps, kNumWarps);
const auto elem_size = static_cast<int64_t>(sizeof(DType));
const auto params = FusedQKRopeParams{
.q = q.data_ptr(),
.k = k ? k.value().data_ptr() : nullptr,
.freqs_cis = static_cast<const float*>(freqs_cis.data_ptr()),
.positions = positions.data_ptr(),
.q_stride_batch = q.stride(0) * elem_size,
.k_stride_batch = k ? k.value().stride(0) * elem_size : 0,
.q_stride_head = q.stride(1) * elem_size,
.k_stride_head = k ? k.value().stride(1) * elem_size : 0,
.num_q_heads = num_q_heads,
.num_k_heads = num_k_heads,
.batch_size = batch_size,
};
// dispatch: {inverse} x {pos_i32}
using KernelType = decltype(kernel_fwd_i32);
const KernelType kernel =
inverse ? (pos_i32 ? kernel_inv_i32 : kernel_inv_i64) : (pos_i32 ? kernel_fwd_i32 : kernel_fwd_i64);
LaunchKernel(num_blocks, kBlockSize, device_.unwrap()) //
.enable_pdl(kUsePDL)(kernel, params);
}
};
} // namespace
@@ -0,0 +1,540 @@
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/math.cuh>
#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/deepseek_v4/fp8_utils.cuh>
#include <cstdint>
#include <cuda_fp8.h>
#include <type_traits>
namespace {
using deepseek_v4::fp8::cast_to_ue8m0;
using deepseek_v4::fp8::pack_fp8;
struct SiluMulQuantVarlenParams {
const bf16_t* __restrict__ input;
fp8_e4m3_t* __restrict__ output;
float* __restrict__ output_scale;
const int32_t* __restrict__ masked_m;
float swiglu_limit; // only read when kApplySwigluLimit=true
int64_t hidden_dim;
uint32_t num_tokens;
uint32_t num_experts;
};
constexpr uint32_t kMaxExperts = 256;
struct alignas(16) CTAWork {
uint32_t expert_id;
uint32_t expert_token_id;
bool valid;
};
SGL_DEVICE uint32_t warp_inclusive_sum(uint32_t lane_id, uint32_t val) {
static_assert(device::kWarpThreads == 32);
#pragma unroll
for (uint32_t offset = 1; offset < 32; offset *= 2) {
uint32_t n = __shfl_up_sync(0xFFFFFFFF, val, offset);
if (lane_id >= offset) val += n;
}
return val;
}
template <bool kApplySwigluLimit, bool kPrecise = true, typename DType2>
SGL_DEVICE fp32x2_t silu_and_mul(DType2 gate, DType2 up, float limit) {
using namespace device;
// refer to as implementation. TL;DR: must clamp in bf16
// https://github.com/deepseek-ai/DeepGEMM/blob/7f2a703ed51ac1f7af07f5e1453b2d3267d37d50/deep_gemm/include/deep_gemm/impls/sm100_fp8_fp4_mega_moe.cuh#L984-L997
if constexpr (kApplySwigluLimit) {
static_assert(std::is_same_v<DType2, bf16x2_t>);
gate = __hmin2(gate, {limit, limit});
up = __hmax2(up, {-limit, -limit});
up = __hmin2(up, {limit, limit});
}
const auto [g0, g1] = cast<fp32x2_t>(gate);
const auto [u0, u1] = cast<fp32x2_t>(up);
const auto silu0 = g0 / (1.0f + __expf(-g0));
const auto silu1 = g1 / (1.0f + __expf(-g1));
const float val0 = silu0 * u0;
const float val1 = silu1 * u1;
if constexpr (kPrecise) { // I don't know if we should enable this?
return {val0, val1};
} else {
return cast<fp32x2_t>(cast<bf16x2_t>(fp32x2_t{val0, val1}));
}
}
[[maybe_unused]]
SGL_DEVICE CTAWork get_work(const SiluMulQuantVarlenParams& params) {
// Preconditions:
// 1. blockDim.x >= params.num_experts
// 2. params.num_experts <= kMaxExperts
using namespace device;
static_assert(kWarpThreads == 32);
static __shared__ uint32_t s_warp_sum[32];
static __shared__ CTAWork result;
result.valid = false;
const uint32_t tx = threadIdx.x;
const uint32_t lane_id = tx % kWarpThreads;
const uint32_t warp_id = tx / kWarpThreads;
const uint32_t val = tx < params.num_experts ? params.masked_m[tx] : 0u;
// Per-warp inclusive scan of masked_m.
const uint32_t warp_inclusive = warp_inclusive_sum(lane_id, val);
const uint32_t warp_exclusive = warp_inclusive - val;
// Write each warp total.
if (lane_id == kWarpThreads - 1) s_warp_sum[warp_id] = warp_inclusive;
__syncthreads();
const auto tmp_val = lane_id < warp_id ? s_warp_sum[lane_id] : 0u;
const auto prefix_exclusive = warp::reduce_sum(tmp_val) + warp_exclusive;
const auto bx = blockIdx.x;
if (prefix_exclusive <= bx && bx < prefix_exclusive + val) {
result = {tx, bx - prefix_exclusive, true};
}
__syncthreads();
return result;
}
template <bool kScaleUE8M0, bool kTransposed, bool kSwizzle, bool kUsePDL, bool kApplySwigluLimit>
__global__ __launch_bounds__(1024, 2) void // maximize occupancy
silu_mul_quant_varlen_kernel(const SiluMulQuantVarlenParams __grid_constant__ params) {
using namespace device;
constexpr uint32_t kGroupSize = 128u;
constexpr uint32_t kWorkThreads = 16u;
// each thread will handle 8 elements
using InputVec = AlignedVector<bf16x2_t, 4>;
using OutputVec = AlignedVector<fp8x2_e4m3_t, 4>;
static_assert(8 * kWorkThreads == 128, "Invalid tiling");
static_assert(!(kTransposed && !kScaleUE8M0), "transposed layout only supports ue8m0");
const auto [expert_id, token_id, valid] = get_work(params);
if (!valid) return;
const auto work_id = threadIdx.x / kWorkThreads;
const auto offset = expert_id * params.num_tokens + token_id;
const auto input = params.input + offset * params.hidden_dim * 2;
const auto output = params.output + offset * params.hidden_dim;
[[maybe_unused]]
const auto output_scale = [&] {
const auto num_groups = params.hidden_dim / kGroupSize;
if constexpr (kTransposed) {
const auto base = reinterpret_cast<uint8_t*>(params.output_scale);
// Physical layout is [E, G//4, N] int32. Each int32 packs 4 consecutive
// group scales for the same token, so the byte address is:
// expert_offset + (group/4)*N*4 + token*4 + group%4
return base + expert_id * num_groups * params.num_tokens + (work_id / 4u) * (params.num_tokens * 4u) +
token_id * 4u + (work_id % 4u);
} else {
return params.output_scale + offset * num_groups + work_id;
}
}();
PDLWaitPrimary<kUsePDL>();
InputVec gate_vec, up_vec;
if constexpr (kSwizzle) {
// gran=8 interleaved: every 16-element chunk on the N axis is
// [gate[0..7], up[0..7]]. Each thread handles 8 consecutive output
// elements, so its gate chunk lives at vec index 2*threadIdx.x and its
// up chunk at 2*threadIdx.x+1.
gate_vec.load(input, threadIdx.x * 2);
up_vec.load(input, threadIdx.x * 2 + 1);
} else {
gate_vec.load(input, threadIdx.x);
up_vec.load(input, threadIdx.x + blockDim.x);
}
float local_max = 0.0f;
float results[8];
#pragma unroll
for (uint32_t i = 0; i < 4; ++i) {
const auto [x, y] = silu_and_mul<kApplySwigluLimit>(gate_vec[i], up_vec[i], params.swiglu_limit);
results[2 * i + 0] = x;
results[2 * i + 1] = y;
local_max = fmaxf(local_max, fmaxf(fabsf(x), fabsf(y)));
}
local_max = warp::reduce_max<kWorkThreads>(local_max);
const float absmax = fmaxf(local_max, 1e-10f);
float scale;
uint32_t ue8m0_exp;
if constexpr (kScaleUE8M0) {
const float raw_scale = absmax / math::FP8_E4M3_MAX;
ue8m0_exp = cast_to_ue8m0(raw_scale);
scale = __uint_as_float(ue8m0_exp << 23);
} else {
scale = absmax / math::FP8_E4M3_MAX;
}
const auto inv_scale = 1.0f / scale;
OutputVec out_vec;
#pragma unroll
for (uint32_t i = 0; i < 4; ++i) {
const float scaled_val0 = results[2 * i + 0] * inv_scale;
const float scaled_val1 = results[2 * i + 1] * inv_scale;
out_vec[i] = pack_fp8(scaled_val0, scaled_val1);
}
PDLTriggerSecondary<kUsePDL>();
out_vec.store(output, threadIdx.x);
if constexpr (kTransposed) {
*output_scale = ue8m0_exp;
} else {
*output_scale = scale;
}
}
struct SiluAndMulClampParams {
const void* __restrict__ input;
void* __restrict__ output;
float swiglu_limit;
};
template <typename DType, bool kUsePDL>
__global__ __launch_bounds__(1024, 2) void // maximize occupancy
silu_mul_clamp_kernel(const SiluAndMulClampParams __grid_constant__ params) {
using namespace device;
static_assert(sizeof(DType) == 2, "only fp16/bf16 supported");
using DType2 = packed_t<DType>;
constexpr auto kVecSize = 16 / sizeof(DType);
static_assert(kVecSize % 2 == 0 && kVecSize > 0);
using Vec = AlignedVector<DType2, kVecSize / 2>;
const auto bid = blockIdx.x;
const auto tile = tile::Memory<Vec>::cta();
const float limit = params.swiglu_limit;
PDLWaitPrimary<kUsePDL>();
const auto gate = tile.load(params.input, bid * 2 + 0);
const auto up = tile.load(params.input, bid * 2 + 1);
Vec out;
#pragma unroll
for (uint32_t i = 0; i < kVecSize / 2; ++i) {
out[i] = cast<DType2>(silu_and_mul<true>(cast<bf16x2_t>(gate[i]), cast<bf16x2_t>(up[i]), limit));
}
tile.store(params.output, out, bid);
PDLTriggerSecondary<kUsePDL>();
}
// ---- Host wrapper
// ------------------------------------------------------------------------------------------------------------------------
template <int64_t kGroupSize, bool kScaleUE8M0, bool kSwizzle, bool kUsePDL, bool kApplySwigluLimit>
struct SiluAndMulMaskedPostQuantKernel {
static_assert(kGroupSize == 128);
static constexpr auto kernel_normal =
silu_mul_quant_varlen_kernel<kScaleUE8M0, false, kSwizzle, kUsePDL, kApplySwigluLimit>;
static constexpr auto kernel_transposed =
silu_mul_quant_varlen_kernel<true, true, kSwizzle, kUsePDL, kApplySwigluLimit>;
static void
run(const tvm::ffi::TensorView input,
const tvm::ffi::TensorView output,
const tvm::ffi::TensorView output_scale,
const tvm::ffi::TensorView masked_m,
const uint32_t topk,
const bool transposed,
const double swiglu_limit) {
using namespace host;
auto device = SymbolicDevice{};
auto E = SymbolicSize{"num_experts"};
auto T = SymbolicSize{"num_tokens_padded"};
auto D = SymbolicSize{"hidden_dim x 2"};
auto N = SymbolicSize{"hidden_dim"};
auto G = SymbolicSize{"num_groups"};
device.set_options<kDLCUDA>();
TensorMatcher({E, T, D}) // input
.with_dtype<bf16_t>()
.with_device(device)
.verify(input);
TensorMatcher({E, T, N}) // output
.with_dtype<fp8_e4m3_t>()
.with_device(device)
.verify(output);
if (!transposed) {
TensorMatcher({E, T, G}) //
.with_dtype<fp32_t>()
.with_device(device)
.verify(output_scale);
} else {
RuntimeCheck(kScaleUE8M0, "transposed layout only supports scale_ue8m0=true");
auto G_ = SymbolicSize{"G // 4"};
TensorMatcher({E, G_, T}) //
.with_dtype<int32_t>()
.with_device(device)
.verify(output_scale);
G.set_value(G_.unwrap() * 4);
}
TensorMatcher({E}) //
.with_dtype<int32_t>()
.with_device(device)
.verify(masked_m);
const auto num_experts = static_cast<uint32_t>(E.unwrap());
const auto num_tokens = static_cast<uint32_t>(T.unwrap());
const auto num_groups = static_cast<uint32_t>(G.unwrap());
const auto hidden_dim = N.unwrap();
RuntimeCheck(D.unwrap() == 2 * hidden_dim, "invalid dimension");
RuntimeCheck(hidden_dim % kGroupSize == 0);
RuntimeCheck(num_experts <= kMaxExperts, "num_experts exceeds maximum (256)");
RuntimeCheck(num_groups * kGroupSize == hidden_dim, "invalid num_groups");
const auto params = SiluMulQuantVarlenParams{
.input = static_cast<const bf16_t*>(input.data_ptr()),
.output = static_cast<fp8_e4m3_t*>(output.data_ptr()),
.output_scale = static_cast<float*>(output_scale.data_ptr()),
.masked_m = static_cast<const int32_t*>(masked_m.data_ptr()),
.swiglu_limit = static_cast<float>(swiglu_limit),
.hidden_dim = hidden_dim,
.num_tokens = num_tokens,
.num_experts = num_experts,
};
const auto num_threads = hidden_dim / 8;
RuntimeCheck(num_threads % device::kWarpThreads == 0);
RuntimeCheck(num_threads >= num_experts);
const auto kernel = transposed ? kernel_transposed : kernel_normal;
LaunchKernel(num_tokens * topk, num_threads, device.unwrap()) //
.enable_pdl(kUsePDL)(kernel, params);
}
};
template <typename DType, bool kUsePDL>
struct SiluAndMulClampKernel {
static constexpr auto kernel = silu_mul_clamp_kernel<DType, kUsePDL>;
static void run(const tvm::ffi::TensorView input, const tvm::ffi::TensorView output, const double swiglu_limit) {
using namespace host;
auto device = SymbolicDevice{};
auto M = SymbolicSize{"num_tokens"};
auto D = SymbolicSize{"gate_up_dim"}; // 2 * out_dim
auto H = SymbolicSize{"out_dim"};
device.set_options<kDLCUDA>();
TensorMatcher({M, D}) // input (gate || up)
.with_dtype<DType>()
.with_device(device)
.verify(input);
TensorMatcher({M, H}) // output
.with_dtype<DType>()
.with_device(device)
.verify(output);
RuntimeCheck(D.unwrap() == 2 * H.unwrap(), "input last dim must be 2 * output last dim");
constexpr uint32_t kVecSize = 16 / sizeof(DType);
const auto out_dim = static_cast<uint32_t>(H.unwrap());
const auto num_tokens = static_cast<uint32_t>(M.unwrap());
RuntimeCheck(out_dim % kVecSize == 0, "out_dim must be divisible by vector size");
const auto num_threads = out_dim / kVecSize;
RuntimeCheck(num_threads <= 1024, "out_dim too large for single-block-per-row launch");
const auto params = SiluAndMulClampParams{
.input = input.data_ptr(),
.output = output.data_ptr(),
.swiglu_limit = static_cast<float>(swiglu_limit),
};
LaunchKernel(num_tokens, num_threads, device.unwrap()) //
.enable_pdl(kUsePDL)(kernel, params);
}
};
struct SiluMulQuantContigParams {
const bf16_t* __restrict__ input;
fp8_e4m3_t* __restrict__ output;
float* __restrict__ output_scale;
float swiglu_limit; // only read when kApplySwigluLimit=true
int64_t hidden_dim;
uint32_t num_tokens;
uint32_t scale_row_stride_int32; // only used when kTransposed=true
};
template <bool kScaleUE8M0, bool kTransposed, bool kSwizzle, bool kUsePDL, bool kApplySwigluLimit>
__global__ __launch_bounds__(1024, 2) void // maximize occupancy
silu_mul_quant_contig_kernel(const SiluMulQuantContigParams __grid_constant__ params) {
using namespace device;
constexpr uint32_t kGroupSize = 128u;
constexpr uint32_t kWorkThreads = 16u;
using InputVec = AlignedVector<bf16x2_t, 4>;
using OutputVec = AlignedVector<fp8x2_e4m3_t, 4>;
static_assert(8 * kWorkThreads == 128, "Invalid tiling");
static_assert(!(kTransposed && !kScaleUE8M0), "transposed layout only supports ue8m0");
const auto token_id = blockIdx.x;
const auto work_id = threadIdx.x / kWorkThreads;
const auto input = params.input + token_id * params.hidden_dim * 2;
const auto output = params.output + token_id * params.hidden_dim;
[[maybe_unused]]
const auto output_scale = [&] {
const auto num_groups = params.hidden_dim / kGroupSize;
if constexpr (kTransposed) {
// Physical layout is (G//4_pad, M_pad) int32; each int32 packs 4
// consecutive UE8M0 exponents for the same token. Byte address:
// (work_id / 4) * M_pad * 4 + token * 4 + (work_id % 4).
const auto base = reinterpret_cast<uint8_t*>(params.output_scale);
return base + (work_id / 4u) * (params.scale_row_stride_int32 * 4u) + token_id * 4u + (work_id % 4u);
} else {
return params.output_scale + token_id * num_groups + work_id;
}
}();
PDLWaitPrimary<kUsePDL>();
InputVec gate_vec, up_vec;
if constexpr (kSwizzle) {
gate_vec.load(input, threadIdx.x * 2);
up_vec.load(input, threadIdx.x * 2 + 1);
} else {
gate_vec.load(input, threadIdx.x);
up_vec.load(input, threadIdx.x + blockDim.x);
}
float local_max = 0.0f;
float results[8];
#pragma unroll
for (uint32_t i = 0; i < 4; ++i) {
const auto [x, y] = silu_and_mul<kApplySwigluLimit>(gate_vec[i], up_vec[i], params.swiglu_limit);
results[2 * i + 0] = x;
results[2 * i + 1] = y;
local_max = fmaxf(local_max, fmaxf(fabsf(x), fabsf(y)));
}
local_max = warp::reduce_max<kWorkThreads>(local_max);
const float absmax = fmaxf(local_max, 1e-10f);
float scale;
uint32_t ue8m0_exp;
if constexpr (kScaleUE8M0) {
const float raw_scale = absmax / math::FP8_E4M3_MAX;
ue8m0_exp = cast_to_ue8m0(raw_scale);
scale = __uint_as_float(ue8m0_exp << 23);
} else {
scale = absmax / math::FP8_E4M3_MAX;
}
const auto inv_scale = 1.0f / scale;
OutputVec out_vec;
#pragma unroll
for (uint32_t i = 0; i < 4; ++i) {
const float scaled_val0 = results[2 * i + 0] * inv_scale;
const float scaled_val1 = results[2 * i + 1] * inv_scale;
out_vec[i] = pack_fp8(scaled_val0, scaled_val1);
}
PDLTriggerSecondary<kUsePDL>();
out_vec.store(output, threadIdx.x);
if constexpr (kTransposed) {
*output_scale = ue8m0_exp;
} else {
*output_scale = scale;
}
}
template <int64_t kGroupSize, bool kScaleUE8M0, bool kSwizzle, bool kUsePDL, bool kApplySwigluLimit>
struct SiluAndMulContigPostQuantKernel {
static_assert(kGroupSize == 128);
static constexpr auto kernel_normal =
silu_mul_quant_contig_kernel<kScaleUE8M0, false, kSwizzle, kUsePDL, kApplySwigluLimit>;
static constexpr auto kernel_transposed =
silu_mul_quant_contig_kernel<true, true, kSwizzle, kUsePDL, kApplySwigluLimit>;
static void
run(const tvm::ffi::TensorView input,
const tvm::ffi::TensorView output,
const tvm::ffi::TensorView output_scale,
const bool transposed,
const double swiglu_limit) {
using namespace host;
auto device = SymbolicDevice{};
auto M = SymbolicSize{"num_tokens"};
auto D = SymbolicSize{"hidden_dim x 2"};
auto N = SymbolicSize{"hidden_dim"};
auto G = SymbolicSize{"num_groups"};
device.set_options<kDLCUDA>();
TensorMatcher({M, D}) // input (gate/up, natural or gran=8 interleaved on last dim)
.with_dtype<bf16_t>()
.with_device(device)
.verify(input);
TensorMatcher({M, N}) // fp8 output
.with_dtype<fp8_e4m3_t>()
.with_device(device)
.verify(output);
const auto hidden_dim = N.unwrap();
RuntimeCheck(D.unwrap() == 2 * hidden_dim, "invalid dimension");
RuntimeCheck(hidden_dim % kGroupSize == 0);
const auto num_groups = static_cast<uint32_t>(hidden_dim / kGroupSize);
uint32_t scale_row_stride_int32 = 0;
if (!transposed) {
G.set_value(num_groups);
TensorMatcher({M, G}) // (M, G) fp32 natural row-major
.with_dtype<fp32_t>()
.with_device(device)
.verify(output_scale);
} else {
RuntimeCheck(kScaleUE8M0, "transposed layout only supports scale_ue8m0=true");
RuntimeCheck(num_groups % 4 == 0, "transposed layout requires num_groups % 4 == 0");
auto G_ = SymbolicSize{"G // 4"};
G_.set_value(num_groups / 4);
auto M_pad = SymbolicSize{"M padded"};
TensorMatcher({M, G_}) // `.transpose(-1,-2)[:M,:]` view of (G//4_pad, M_pad) int32
.with_strides({int64_t{1}, M_pad}) // col-major transposed
.with_dtype<int32_t>()
.with_device(device)
.verify(output_scale);
scale_row_stride_int32 = static_cast<uint32_t>(M_pad.unwrap());
}
const auto num_tokens = static_cast<uint32_t>(M.unwrap());
const auto params = SiluMulQuantContigParams{
.input = static_cast<const bf16_t*>(input.data_ptr()),
.output = static_cast<fp8_e4m3_t*>(output.data_ptr()),
.output_scale = static_cast<float*>(output_scale.data_ptr()),
.swiglu_limit = static_cast<float>(swiglu_limit),
.hidden_dim = hidden_dim,
.num_tokens = num_tokens,
.scale_row_stride_int32 = scale_row_stride_int32,
};
const auto num_threads = hidden_dim / 8;
RuntimeCheck(num_threads % device::kWarpThreads == 0);
const auto kernel = transposed ? kernel_transposed : kernel_normal;
LaunchKernel(num_tokens, num_threads, device.unwrap()) //
.enable_pdl(kUsePDL)(kernel, params);
}
};
} // namespace
@@ -0,0 +1,205 @@
#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 <bit>
#include <cstdint>
#include <cuda_fp8.h>
namespace {
using deepseek_v4::fp8::cast_to_ue8m0;
using deepseek_v4::fp8::inv_scale_ue8m0;
using deepseek_v4::fp8::pack_fp8;
struct FusedStoreCacheParam {
const void* __restrict__ input;
void* __restrict__ cache;
const void* __restrict__ indices;
uint32_t num_tokens;
};
template <typename Float, typename IndicesT, uint32_t kPageBits, bool kUsePDL>
__global__ void fused_store_flashmla_cache(const __grid_constant__ FusedStoreCacheParam param) {
using namespace device;
/// NOTE: 584 = 576 + 8
constexpr int64_t kPageBytes = host::div_ceil(584 << kPageBits, 576) * 576;
// each warp handles 64 elements, 8 warps, each block handles 1 row
const auto& [input, cache, indices, num_tokens] = param;
const uint32_t bid = blockIdx.x;
const uint32_t tid = threadIdx.x;
const uint32_t wid = tid / 32;
PDLWaitPrimary<kUsePDL>();
// prefetch the index
const auto index = static_cast<const IndicesT*>(indices)[bid];
// always load the value from input (don't store if invalid)
using Float2 = packed_t<Float>;
const auto elems = static_cast<const Float2*>(input)[tid + bid * 256];
if (wid != 7) {
const auto [x, y] = cast<fp32x2_t>(elems);
const auto abs_max = warp::reduce_max(fmaxf(fabs(x), fabs(y)));
const auto scale_raw = fmaxf(1e-4f, abs_max) / kFP8E4M3Max;
const auto scale_ue8m0 = cast_to_ue8m0(scale_raw);
const auto inv_scale = inv_scale_ue8m0(scale_ue8m0);
const auto result = pack_fp8(x * inv_scale, y * inv_scale);
const int32_t page = index >> kPageBits;
const int32_t offset = index & ((1 << kPageBits) - 1);
const auto page_ptr = pointer::offset(cache, page * kPageBytes);
const auto value_ptr = pointer::offset(page_ptr, offset * 576);
const auto scale_ptr = pointer::offset(page_ptr, 576 << kPageBits, offset * 8);
static_cast<fp8x2_e4m3_t*>(value_ptr)[tid] = result;
static_cast<uint8_t*>(scale_ptr)[wid] = scale_ue8m0;
} else {
const auto result = cast<bf16x2_t>(elems);
const int32_t page = index >> kPageBits;
const int32_t offset = index & ((1 << kPageBits) - 1);
const auto page_ptr = pointer::offset(cache, page * kPageBytes);
const auto value_ptr = pointer::offset(page_ptr, offset * 576, 448);
static_cast<bf16x2_t*>(value_ptr)[tid - 7 * 32] = result;
}
PDLTriggerSecondary<kUsePDL>();
}
template <typename Float, typename IndicesT, uint32_t kPageBits, bool kUsePDL>
__global__ void fused_store_indexer_cache(const __grid_constant__ FusedStoreCacheParam param) {
using namespace device;
/// NOTE: 132 = 128 + 4
constexpr int64_t kPageBytes = 132 << kPageBits;
// each warp handles 128 elements, 1 warp, each block handles multiple rows
const auto& [input, cache, indices, num_tokens] = param;
const auto global_tid = blockIdx.x * blockDim.x + threadIdx.x;
const auto global_wid = global_tid / 32;
const auto lane_id = threadIdx.x % 32;
if (global_wid >= num_tokens) return;
PDLWaitPrimary<kUsePDL>();
// prefetch the index
const auto index = static_cast<const IndicesT*>(indices)[global_wid];
// always load the value from input (don't store if invalid)
using Float2 = packed_t<Float>;
using InStorage = AlignedVector<Float2, 2>;
using OutStorage = AlignedVector<fp8x2_e4m3_t, 2>;
const auto elems = static_cast<const InStorage*>(input)[global_tid];
const auto [x0, x1] = cast<fp32x2_t>(elems[0]);
const auto [y0, y1] = cast<fp32x2_t>(elems[1]);
const auto local_max = fmaxf(fmaxf(fabs(x0), fabs(x1)), fmaxf(fabs(y0), fabs(y1)));
const auto abs_max = warp::reduce_max(local_max);
// use normal fp32 scale
const auto scale = fmaxf(1e-4f, abs_max) / kFP8E4M3Max;
const auto inv_scale = 1.0f / scale;
const int32_t page = index >> kPageBits;
const int32_t offset = index & ((1 << kPageBits) - 1);
const auto page_ptr = pointer::offset(cache, page * kPageBytes);
const auto value_ptr = pointer::offset(page_ptr, offset * 128);
const auto scale_ptr = pointer::offset(page_ptr, 128 << kPageBits, offset * 4);
OutStorage result;
result[0] = pack_fp8(x0 * inv_scale, x1 * inv_scale);
result[1] = pack_fp8(y0 * inv_scale, y1 * inv_scale);
static_cast<OutStorage*>(value_ptr)[lane_id] = result;
static_cast<float*>(scale_ptr)[0] = scale;
PDLTriggerSecondary<kUsePDL>();
}
template <typename Float, typename IndicesT, uint32_t kPageSize, bool kUsePDL>
struct FusedStoreCacheFlashMLAKernel {
static constexpr int32_t kLogSize = std::countr_zero(kPageSize);
static constexpr int64_t kPageBytes = host::div_ceil(584 * kPageSize, 576) * 576;
static constexpr auto kernel = fused_store_flashmla_cache<Float, IndicesT, kLogSize, kUsePDL>;
static_assert(std::has_single_bit(kPageSize), "kPageSize must be a power of 2");
static_assert(1 << kLogSize == kPageSize);
static void run(tvm::ffi::TensorView input, tvm::ffi::TensorView cache, tvm::ffi::TensorView indices) {
using namespace host;
auto N = SymbolicSize{"num_tokens"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({N, 512}) // input
.with_dtype<Float>()
.with_device(device_)
.verify(input);
TensorMatcher({-1, -1}) // cache
.with_strides({kPageBytes, 1})
.with_dtype<uint8_t>()
.with_device(device_)
.verify(cache);
TensorMatcher({N}) // indices
.with_dtype<IndicesT>()
.with_device(device_)
.verify(indices);
const auto num_tokens = static_cast<uint32_t>(N.unwrap());
const auto params = FusedStoreCacheParam{
.input = input.data_ptr(),
.cache = cache.data_ptr(),
.indices = indices.data_ptr(),
.num_tokens = num_tokens,
};
const auto kBlockSize = 256;
const auto num_blocks = num_tokens;
LaunchKernel(num_blocks, kBlockSize, device_.unwrap()).enable_pdl(kUsePDL)(kernel, params);
}
};
template <typename Float, typename IndicesT, uint32_t kPageSize, bool kUsePDL>
struct FusedStoreCacheIndexerKernel {
static constexpr int32_t kLogSize = std::countr_zero(kPageSize);
static constexpr int64_t kPageBytes = 132 * kPageSize;
static constexpr auto kernel = fused_store_indexer_cache<Float, IndicesT, kLogSize, kUsePDL>;
static_assert(std::has_single_bit(kPageSize), "kPageSize must be a power of 2");
static_assert(1 << kLogSize == kPageSize);
static void run(tvm::ffi::TensorView input, tvm::ffi::TensorView cache, tvm::ffi::TensorView indices) {
using namespace host;
auto N = SymbolicSize{"num_tokens"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({N, 128}) // input
.with_dtype<Float>()
.with_device(device_)
.verify(input);
TensorMatcher({-1, -1}) // cache
.with_strides({kPageBytes, 1})
.with_dtype<uint8_t>()
.with_device(device_)
.verify(cache);
TensorMatcher({N}) // indices
.with_dtype<IndicesT>()
.with_device(device_)
.verify(indices);
const auto num_tokens = static_cast<uint32_t>(N.unwrap());
const auto params = FusedStoreCacheParam{
.input = input.data_ptr(),
.cache = cache.data_ptr(),
.indices = indices.data_ptr(),
.num_tokens = num_tokens,
};
const auto kBlockSize = 128;
const auto num_blocks = div_ceil(num_tokens * 32, kBlockSize);
LaunchKernel(num_blocks, kBlockSize, device_.unwrap()).enable_pdl(kUsePDL)(kernel, params);
}
};
} // namespace
@@ -0,0 +1,340 @@
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/utils.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <bit>
#include <cstdint>
namespace {
#ifndef SGL_TOPK
#define SGL_TOPK 512
#endif
constexpr uint32_t kTopK = SGL_TOPK;
constexpr uint32_t kTopKBlockSize = SGL_TOPK;
constexpr uint32_t kSMEM = 16 * 1024 * sizeof(uint32_t); // 64KB (bytes)
struct TopKParams {
const float* __restrict__ scores;
const int32_t* __restrict__ seq_lens;
const int32_t* __restrict__ page_table;
int32_t* __restrict__ page_indices;
int32_t* __restrict__ raw_indices; // optional: output raw abs position indices before page transform
const int64_t score_stride;
const int64_t page_table_stride;
uint32_t page_bits;
};
SGL_DEVICE uint8_t convert_to_uint8(float x) {
__half h = __float2half_rn(x);
uint16_t bits = __half_as_ushort(h);
uint16_t key = (bits & 0x8000) ? static_cast<uint16_t>(~bits) : static_cast<uint16_t>(bits | 0x8000);
return static_cast<uint8_t>(key >> 8);
}
SGL_DEVICE uint32_t convert_to_uint32(float x) {
uint32_t bits = __float_as_uint(x);
return (bits & 0x80000000u) ? ~bits : (bits | 0x80000000u);
}
SGL_DEVICE int32_t page_to_indices(const int32_t* __restrict__ page_table, uint32_t i, uint32_t page_bits) {
const uint32_t mask = (1u << page_bits) - 1u;
return (page_table[i >> page_bits] << page_bits) | (i & mask);
}
[[maybe_unused]]
SGL_DEVICE void naive_transform(
const float* __restrict__, // unused
const int32_t* __restrict__ page_table,
int32_t* __restrict__ indices,
int32_t* __restrict__ raw_indices, // optional: output raw abs position indices
const uint32_t length,
const uint32_t page_bits) {
static_assert(kTopK <= kTopKBlockSize);
if (const auto tx = threadIdx.x; tx < length) {
indices[tx] = page_to_indices(page_table, tx, page_bits);
if (raw_indices != nullptr) {
raw_indices[tx] = tx;
}
} else if (kTopK == kTopKBlockSize || tx < kTopK) {
indices[tx] = -1; // fill invalid indices to -1
if (raw_indices != nullptr) {
raw_indices[tx] = -1;
}
}
}
[[maybe_unused]]
SGL_DEVICE void radix_topk(const float* __restrict__ input, int32_t* __restrict__ output, const uint32_t length) {
constexpr uint32_t RADIX = 256;
constexpr uint32_t BLOCK_SIZE = kTopKBlockSize;
constexpr uint32_t SMEM_INPUT_SIZE = kSMEM / (2 * sizeof(int32_t));
alignas(128) __shared__ uint32_t _s_histogram_buf[2][RADIX + 32];
alignas(128) __shared__ uint32_t s_counter;
alignas(128) __shared__ uint32_t s_threshold_bin_id;
alignas(128) __shared__ uint32_t s_num_input[2];
alignas(128) __shared__ int32_t s_last_remain;
extern __shared__ uint32_t s_input_idx[][kSMEM / (2 * sizeof(int32_t))];
const uint32_t tx = threadIdx.x;
uint32_t remain_topk = kTopK;
auto& s_histogram = _s_histogram_buf[0];
const auto run_cumsum = [&] {
#pragma unroll 8
for (int32_t i = 0; i < 8; ++i) {
static_assert(1 << 8 == RADIX);
if (tx < RADIX) {
const auto j = 1 << i;
const auto k = i & 1;
auto value = _s_histogram_buf[k][tx];
if (tx + j < RADIX) {
value += _s_histogram_buf[k][tx + j];
}
_s_histogram_buf[k ^ 1][tx] = value;
}
__syncthreads();
}
};
// stage 1: 8bit coarse histogram
if (tx < RADIX + 1) s_histogram[tx] = 0;
__syncthreads();
for (uint32_t idx = tx; idx < length; idx += BLOCK_SIZE) {
const auto bin = convert_to_uint8(input[idx]);
::atomicAdd(&s_histogram[bin], 1);
}
__syncthreads();
run_cumsum();
if (tx < RADIX && s_histogram[tx] > remain_topk && s_histogram[tx + 1] <= remain_topk) {
s_threshold_bin_id = tx;
s_num_input[0] = 0;
s_counter = 0;
}
__syncthreads();
const auto threshold_bin = s_threshold_bin_id;
remain_topk -= s_histogram[threshold_bin + 1];
if (remain_topk == 0) {
for (uint32_t idx = tx; idx < length; idx += BLOCK_SIZE) {
const uint32_t bin = convert_to_uint8(input[idx]);
if (bin > threshold_bin) {
const auto pos = ::atomicAdd(&s_counter, 1);
output[pos] = idx;
}
}
__syncthreads();
return;
} else {
__syncthreads();
if (tx < RADIX + 1) {
s_histogram[tx] = 0;
}
__syncthreads();
for (uint32_t idx = tx; idx < length; idx += BLOCK_SIZE) {
const float raw_input = input[idx];
const uint32_t bin = convert_to_uint8(raw_input);
if (bin > threshold_bin) {
const auto pos = ::atomicAdd(&s_counter, 1);
output[pos] = idx;
} else if (bin == threshold_bin) {
const auto pos = ::atomicAdd(&s_num_input[0], 1);
if (pos < SMEM_INPUT_SIZE) {
[[likely]] s_input_idx[0][pos] = idx;
const auto bin = convert_to_uint32(raw_input);
const auto sub_bin = (bin >> 24) & 0xFF;
::atomicAdd(&s_histogram[sub_bin], 1);
}
}
}
__syncthreads();
}
// stage 2: refine with 8bit radix passes
#pragma unroll 4
for (int round = 0; round < 4; ++round) {
const auto r_idx = round % 2;
// clip here to prevent overflow
const auto raw_num_input = s_num_input[r_idx];
const auto num_input = raw_num_input < SMEM_INPUT_SIZE ? raw_num_input : SMEM_INPUT_SIZE;
run_cumsum();
if (tx < RADIX && s_histogram[tx] > remain_topk && s_histogram[tx + 1] <= remain_topk) {
s_threshold_bin_id = tx;
s_num_input[r_idx ^ 1] = 0;
s_last_remain = remain_topk - s_histogram[tx + 1];
}
__syncthreads();
const auto threshold_bin = s_threshold_bin_id;
remain_topk -= s_histogram[threshold_bin + 1];
if (remain_topk == 0) {
for (uint32_t i = tx; i < num_input; i += BLOCK_SIZE) {
const auto idx = s_input_idx[r_idx][i];
const auto offset = 24 - round * 8;
const auto bin = (convert_to_uint32(input[idx]) >> offset) & 0xFF;
if (bin > threshold_bin) {
const auto pos = ::atomicAdd(&s_counter, 1);
output[pos] = idx;
}
}
__syncthreads();
break;
} else {
__syncthreads();
if (tx < RADIX + 1) {
s_histogram[tx] = 0;
}
__syncthreads();
for (uint32_t i = tx; i < num_input; i += BLOCK_SIZE) {
const auto idx = s_input_idx[r_idx][i];
const auto raw_input = input[idx];
const auto offset = 24 - round * 8;
const auto bin = (convert_to_uint32(raw_input) >> offset) & 0xFF;
if (bin > threshold_bin) {
const auto pos = ::atomicAdd(&s_counter, 1);
output[pos] = idx;
} else if (bin == threshold_bin) {
if (round == 3) {
const auto pos = ::atomicAdd(&s_last_remain, -1);
if (pos > 0) {
output[kTopK - pos] = idx;
}
} else {
const auto pos = ::atomicAdd(&s_num_input[r_idx ^ 1], 1);
if (pos < SMEM_INPUT_SIZE) {
/// NOTE: (dark) fuse the histogram computation here
[[likely]] s_input_idx[r_idx ^ 1][pos] = idx;
const auto bin = convert_to_uint32(raw_input);
const auto sub_bin = (bin >> (offset - 8)) & 0xFF;
::atomicAdd(&s_histogram[sub_bin], 1);
}
}
}
}
__syncthreads();
}
}
}
template <bool kUsePDL>
__global__ void topk_transform_kernel(const __grid_constant__ TopKParams params) {
const auto &[
scores, seq_lens, page_table, page_indices, raw_indices, // pointers
score_stride, page_table_stride, page_bits // sizes
] = params;
const uint32_t work_id = blockIdx.x;
/// NOTE: dangerous prefetch seq_len before PDL wait
const uint32_t seq_len = seq_lens[work_id];
const auto score_ptr = scores + work_id * score_stride;
const auto page_ptr = page_table + work_id * page_table_stride;
const auto indices_ptr = page_indices + work_id * kTopK;
const auto raw_indices_ptr = raw_indices != nullptr ? raw_indices + work_id * kTopK : nullptr;
device::PDLWaitPrimary<kUsePDL>();
if (seq_len <= kTopK) {
naive_transform(score_ptr, page_ptr, indices_ptr, raw_indices_ptr, seq_len, page_bits);
} else {
__shared__ int32_t s_topk_indices[kTopK];
radix_topk(score_ptr, s_topk_indices, seq_len);
static_assert(kTopK <= kTopKBlockSize);
const auto tx = threadIdx.x;
if (kTopK == kTopKBlockSize || tx < kTopK) {
indices_ptr[tx] = page_to_indices(page_ptr, s_topk_indices[tx], page_bits);
if (raw_indices_ptr != nullptr) {
raw_indices_ptr[tx] = s_topk_indices[tx];
}
}
}
device::PDLTriggerSecondary<kUsePDL>();
}
template <auto* f, size_t kMaxDynamicSMEM>
void setup_kernel_smem_once(host::DebugInfo where = {}) {
[[maybe_unused]]
static const auto result = [] {
const auto fptr = std::bit_cast<const void*>(f);
return ::cudaFuncSetAttribute(fptr, ::cudaFuncAttributeMaxDynamicSharedMemorySize, kMaxDynamicSMEM);
}();
host::RuntimeDeviceCheck(result, where);
}
template <bool kUsePDL>
struct TopKKernel {
static constexpr auto kernel = topk_transform_kernel<kUsePDL>;
static void transform(
const tvm::ffi::TensorView scores,
const tvm::ffi::TensorView seq_lens,
const tvm::ffi::TensorView page_table,
const tvm::ffi::TensorView page_indices,
const uint32_t page_size,
const tvm::ffi::Optional<tvm::ffi::TensorView> raw_indices) {
using namespace host;
auto B = SymbolicSize{"batch_size"};
auto S = SymbolicSize{"score_stride"};
auto P = SymbolicSize{"page_table_stride"};
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
TensorMatcher({B, -1}) // strided scores
.with_strides({S, 1})
.with_dtype<float>()
.with_device(device)
.verify(scores);
TensorMatcher({B}) // seq_lens, must be contiguous
.with_dtype<int32_t>()
.with_device(device)
.verify(seq_lens);
TensorMatcher({B, -1}) // strided page table
.with_strides({P, 1})
.with_dtype<int32_t>()
.with_device(device)
.verify(page_table);
TensorMatcher({B, kTopK}) // output, must be contiguous
.with_dtype<int32_t>()
.with_device(device)
.verify(page_indices);
int32_t* raw_indices_ptr = nullptr;
if (raw_indices.has_value()) {
TensorMatcher({B, kTopK}) // optional raw indices output, must be contiguous
.with_dtype<int32_t>()
.with_device(device)
.verify(raw_indices.value());
raw_indices_ptr = static_cast<int32_t*>(raw_indices.value().data_ptr());
}
RuntimeCheck(std::has_single_bit(page_size), "page_size must be power of 2");
const auto page_bits = static_cast<uint32_t>(std::countr_zero(page_size));
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto params = TopKParams{
.scores = static_cast<float*>(scores.data_ptr()),
.seq_lens = static_cast<int32_t*>(seq_lens.data_ptr()),
.page_table = static_cast<int32_t*>(page_table.data_ptr()),
.page_indices = static_cast<int32_t*>(page_indices.data_ptr()),
.raw_indices = raw_indices_ptr,
.score_stride = S.unwrap(),
.page_table_stride = P.unwrap(),
.page_bits = page_bits,
};
constexpr auto kSMEM_ = kSMEM + sizeof(int32_t); // align up a little
setup_kernel_smem_once<kernel, kSMEM_>();
LaunchKernel(batch_size, kTopKBlockSize, device.unwrap(), kSMEM_).enable_pdl(kUsePDL)(kernel, params);
}
};
} // namespace
@@ -0,0 +1,458 @@
/**
* \file topk_v2.cuh
* \brief TopK kernel for DeepSeek v4.
* Adapted from
* 1:
* https://github.com/vllm-project/vllm/blob/a8c6ee9b787d273916206a29b77feebadb80c368/csrc/persistent_topk.cuh
* 2:
* https://github.com/flashinfer-ai/flashinfer/blob/c2b4db2b1a84448d802f0e6ac445243312bd6a4c/include/flashinfer/topk.cuh
* DarkSharpness never took a detailed look at these 2 implementation, but his claude code did.
* So we add credit to the reference implementations.
*/
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/utils.cuh>
#include <sgl_kernel/deepseek_v4/topk_impl.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <bit>
#include <cstdint>
#include <iterator>
namespace {
namespace impl = device::topk;
using impl::TopKProblem;
using Register2 = impl::TopKRegister<2>; // <= 8192, register-resident, 1 read
using Register4 = impl::TopKRegister<4>; // <= 16384, register-resident, 1 read
using Streaming = impl::TopKStreaming;
using Cluster = impl::TopKCluster<8>;
constexpr uint32_t kBlockSize = impl::TopKConfig::kBlockSize;
constexpr uint32_t kOccupancy = impl::TopKConfig::kOccupancy;
constexpr uint32_t kMaxTopK = impl::TopKConfig::kMaxTopK;
constexpr uint32_t kClusterSize = Cluster::kClusterSize;
constexpr uint32_t kReg2MaxSeqLen = Register2::kMaxSeqLen; // 8192
constexpr uint32_t kReg4MaxSeqLen = Register4::kMaxSeqLen; // 16384
#define TOPK_KERNEL __global__ __launch_bounds__(kBlockSize, kOccupancy)
#define CLUSTER_TOPK_KERNEL TOPK_KERNEL __cluster_dims__(1, kClusterSize, 1)
constexpr uint32_t kClusterFloor = 65536;
constexpr uint32_t kClusterMaxBatch = 512;
constexpr uint32_t kNumPersistentClusters = 15 * kOccupancy;
/// Metadata tensor rows (each 8 B / 2 int32). Row 0 is the global plan result;
/// rows 1..N are the (batch_id, seq_len) of items routed to the cluster pool.
struct alignas(8) GlobalMetadata {
uint32_t cluster_threshold;
uint32_t num_cluster_items; // N = number of items routed to the cluster pool
};
struct alignas(8) PlanItem {
uint32_t batch_id;
uint32_t seq_len;
};
static_assert(sizeof(GlobalMetadata) == 2 * sizeof(int32_t) && sizeof(PlanItem) == sizeof(GlobalMetadata));
struct TopKLaunchParams {
const float* __restrict__ scores;
const int32_t* __restrict__ seq_lens;
const int32_t* __restrict__ page_table;
int32_t* __restrict__ page_indices;
int32_t* __restrict__ raw_indices; // optional raw (pre-transform) indices output; nullptr if unused
const PlanItem* __restrict__ metadata; // [0]=GlobalMetadata, [1+i]=PlanItem
int64_t score_stride;
int64_t page_table_stride;
uint32_t topk;
uint32_t page_bits;
uint32_t cluster_floor; // seq_len > this routes to the cluster path (batch-aware, host-set)
SGL_DEVICE const GlobalMetadata& global() const {
return *reinterpret_cast<const GlobalMetadata*>(metadata);
}
SGL_DEVICE uint32_t cluster_threshold() const {
return global().cluster_threshold;
}
SGL_DEVICE const PlanItem& item(uint32_t i) const {
return metadata[1 + i];
}
SGL_DEVICE int32_t* get_output_ptr(uint32_t batch_id) const {
return page_indices + batch_id * static_cast<int64_t>(topk);
}
SGL_DEVICE TopKProblem problem(uint32_t batch_id, uint32_t seq_len) const {
const auto k = static_cast<int64_t>(topk);
return TopKProblem{
.in = scores + batch_id * score_stride,
.out = page_indices + batch_id * k,
.raw_out = raw_indices != nullptr ? raw_indices + batch_id * k : nullptr,
.page_table = page_table + batch_id * page_table_stride,
.topk = topk,
.seq_len = seq_len,
.page_bits = page_bits,
};
}
SGL_DEVICE TopKProblem problem(uint32_t batch_id) const {
return this->problem(batch_id, static_cast<uint32_t>(seq_lens[batch_id]));
}
};
/**
* \brief Persistent cluster kernel for the long items. It will handle long inputs.
* The short items are handled by the separate topk_kernel.
*/
template <bool kPDL>
CLUSTER_TOPK_KERNEL void topk_persistent_cluster_kernel(const __grid_constant__ TopKLaunchParams params) {
device::enable_smem_spilling();
__shared__ impl::MaxSmem<Cluster::Smem> smem;
const uint32_t num_cluster_items = params.global().num_cluster_items;
device::PDLWaitPrimary<kPDL>();
device::PDLTriggerSecondary<kPDL>();
#pragma unroll 1
for (uint32_t w = blockIdx.x; w < num_cluster_items; w += kNumPersistentClusters) {
const auto it = params.item(w);
const auto problem = params.problem(it.batch_id, it.seq_len);
Cluster::forward<false>(problem, &smem);
__syncthreads();
}
}
template <typename F>
SGL_DEVICE void for_each_item(uint32_t topk, const F& f) {
constexpr uint32_t kNumElems = kMaxTopK / kBlockSize;
#pragma unroll
for (uint32_t i = 0; i < kNumElems; ++i) {
if (const auto tx = i * kBlockSize + threadIdx.x; tx < topk) {
__builtin_assume(tx < kMaxTopK);
f(tx, i);
}
}
}
template <bool kPDL>
SGL_DEVICE void trivial_transform(const TopKProblem& problem) {
device::PDLWaitPrimary<kPDL>();
device::PDLTriggerSecondary<kPDL>();
for_each_item(problem.topk, [&](uint32_t tx, uint32_t) {
problem.transform_output(tx, tx < problem.seq_len ? static_cast<int32_t>(tx) : -1);
});
}
SGL_DEVICE void problem_transform(TopKProblem& problem, int32_t* output_ptr) {
static_assert(kMaxTopK % kBlockSize == 0);
constexpr uint32_t kNumElems = kMaxTopK / kBlockSize;
int32_t source_index[kNumElems];
for_each_item(problem.topk, [&](uint32_t tx, uint32_t i) { source_index[i] = problem.out[tx]; });
problem.out = output_ptr;
for_each_item(problem.topk, [&](uint32_t tx, uint32_t i) { problem.transform_output(tx, source_index[i]); });
}
/**
* \brief Main kernel for the short items and epilogue of long items.
* \tparam kPDL whether to use PDL to synchronize with the cluster kernel (if any)
* \tparam kLevel:
* - Level 0: max_seq_len <= 8192 -> trivial + register<2>
* - Level 1: max_seq_len <= 16384 -> trivial + register<4>
* - Level 2: max_seq_len <= cluster_floor -> trivial + register<4> + streaming
* - Level 3: max_seq_len > cluster_floor -> + epilogue process of cluster path
*/
template <bool kPDL, int kLevel>
TOPK_KERNEL void topk_main_kernel(const __grid_constant__ TopKLaunchParams params) {
device::enable_smem_spilling();
auto problem = params.problem(blockIdx.x);
constexpr uint32_t kU32Max = std::numeric_limits<uint32_t>::max();
__shared__ impl::MaxSmem<Register2::Smem, Register4::Smem, Streaming::Smem> smem;
if (problem.seq_len <= problem.topk) return trivial_transform<kPDL>(problem);
__shared__ int32_t topk_indices[kMaxTopK];
problem.out = topk_indices;
constexpr bool kHandleCluster = (kLevel == 3);
// non-trivial path: dispatch based on level and seq_len
const auto cluster_threshold = kHandleCluster ? params.cluster_threshold() : kU32Max;
if constexpr (kLevel == 0) {
__builtin_assume(problem.seq_len <= kReg2MaxSeqLen);
Register2::forward<kPDL>(problem, &smem);
} else if constexpr (kLevel == 1) {
__builtin_assume(problem.seq_len <= kReg4MaxSeqLen);
Register4::forward<kPDL>(problem, &smem); // max_seq_len <= 16384 guarantees seq <= 16384
} else {
static_assert(kLevel == 2 || kLevel == 3, "we only support level = 0,1,2,3 now");
// if using cluster, we can delay the PDL wait
constexpr bool kPDLEarly = kPDL && !kHandleCluster;
constexpr bool kPDLFinal = kPDL && kHandleCluster;
if (problem.seq_len <= kReg4MaxSeqLen) {
Register4::forward<kPDLEarly>(problem, &smem);
} else if (problem.seq_len <= cluster_threshold) {
Streaming::forward<kPDLEarly>(problem, &smem);
} else { // cluster path do nothing here
problem.out = params.get_output_ptr(blockIdx.x);
}
device::PDLWaitPrimary<kPDLFinal>();
}
// page-table transform pass (gathers kept out of the hot scatter loop),
// then trigger the dependent kernel only after the full output is written.
device::PDLTriggerSecondary<kPDL>();
__syncthreads();
problem_transform(problem, params.get_output_ptr(blockIdx.x));
}
template <bool kPDL>
CLUSTER_TOPK_KERNEL void topk_small_batch_kernel(const __grid_constant__ TopKLaunchParams params) {
device::enable_smem_spilling();
auto problem = params.problem(blockIdx.x);
__shared__ impl::MaxSmem<Streaming::Smem, Cluster::Smem> smem;
if (problem.seq_len <= problem.topk) return trivial_transform<kPDL>(problem);
__shared__ int32_t topk_indices[kMaxTopK];
problem.out = topk_indices;
// randomly elect one worker rank to avoid workload imbalance
const auto worker_rank = blockIdx.x % kClusterSize;
// for small batch, we will fuse in the cluster case
if (problem.seq_len <= kReg4MaxSeqLen) {
if (blockIdx.y == worker_rank) Register4::forward<kPDL>(problem, &smem);
} else if (problem.seq_len <= params.cluster_floor) {
if (blockIdx.y == worker_rank) Streaming::forward<kPDL>(problem, &smem);
} else {
auto cluster = cooperative_groups::this_cluster();
problem.out = cluster.map_shared_rank(topk_indices, worker_rank);
Cluster::forward<kPDL>(problem, &smem); // write to peer's output shared memory
cluster.sync();
}
device::PDLWaitPrimary<kPDL>();
__syncthreads();
if (blockIdx.y == worker_rank) problem_transform(problem, params.get_output_ptr(blockIdx.x));
}
// --- Plan: choose cluster_threshold from the seq_len distribution -----------
__global__ __launch_bounds__(kBlockSize, 1) void topk_plan(
const uint32_t* __restrict__ seq_lens,
PlanItem* __restrict__ metadata, // [0]=GlobalMetadata, [1+i]=PlanItem
const uint32_t batch_size,
const uint32_t static_cluster_threshold) {
// Candidate (threshold T_j, cap_j) pairs, T strictly increasing. The plan lowers
// cluster_threshold to T_j while #(items with seq_len > T_j) <= cap_j, so cap_j
// bounds how many long items go to the persistent pool. The pool runs N items in
// ceil(N / kNumPersistentClusters) waves; the longer the seq the more waves pay
// off (streaming a single block over a long item is very slow), so cap_j is the
// measured cluster-vs-streaming crossover (B200, occ2) and GROWS with T -- a flat
// cap = pool size only fits the shortest (~98K, one-wave) bucket. (Plan is tunable.)
struct Pair {
uint32_t threshold;
uint32_t max_batch_size;
};
constexpr Pair kCandidates[] = {
{65536, 30}, // (65536,98304]: ~1 pool wave, streams beyond 30
{98304, 48}, // (98304,131072]
{131072, 60}, // (131072,196608]
{196608, 80}, // (196608,262144]
{262144, 112}, // (262144,393216]
{393216, 128}, // (393216,inf): longest -- worth many pool waves; a top
// threshold here lets overloaded ~280-393K batches still stream
};
constexpr uint32_t kNumCandidates = std::size(kCandidates);
static_assert(kCandidates[0].threshold == kClusterFloor);
__shared__ uint32_t s_counts[kNumCandidates];
__shared__ uint32_t s_threshold;
__shared__ uint32_t s_count;
const auto tx = threadIdx.x;
if (tx < kNumCandidates) s_counts[tx] = 0;
if (tx == 0) s_count = 0;
__syncthreads();
if (static_cluster_threshold > 0) {
if (tx == 0) s_threshold = static_cluster_threshold;
} else {
for (uint32_t i = tx; i < batch_size; i += kBlockSize) {
const uint32_t sl = seq_lens[i];
uint32_t count = 0;
#pragma unroll
for (uint32_t j = 0; j < kNumCandidates; ++j) {
count += (sl > kCandidates[j].threshold ? 1 : 0);
}
if (count > 0) atomicAdd(&s_counts[count - 1], 1);
}
__syncthreads();
if (tx == 0) {
uint32_t accum = 0;
uint32_t chosen = kCandidates[kNumCandidates - 1].threshold;
#pragma unroll
for (uint32_t i = 0; i < kNumCandidates; ++i) {
const auto j = kNumCandidates - 1 - i;
accum += s_counts[j]; // # items with seq_len > kCandidates[j].threshold
if (accum > kCandidates[j].max_batch_size) break;
chosen = kCandidates[j].threshold;
}
s_threshold = chosen;
}
}
__syncthreads();
const auto cluster_threshold = max(s_threshold, kClusterFloor);
// Compact items with seq_len > threshold into metadata[1..N]: their batch ids
// are the work list the persistent cluster pool fetches.
for (uint32_t i = tx; i < batch_size; i += kBlockSize) {
const uint32_t sl = seq_lens[i];
if (sl > cluster_threshold) {
const auto pos = atomicAdd(&s_count, 1);
metadata[1 + pos] = {i, sl};
}
}
__syncthreads();
if (tx == 0) {
auto* g = reinterpret_cast<GlobalMetadata*>(metadata);
*g = {.cluster_threshold = cluster_threshold, .num_cluster_items = s_count};
}
}
struct TopKKernel {
static void plan( //
const tvm::ffi::TensorView seq_lens,
const tvm::ffi::TensorView metadata,
const uint32_t static_cluster_threshold) {
using namespace host;
auto B = SymbolicSize{"batch_size"};
auto Bp1 = SymbolicSize{"batch_size_plus_1"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({B}) // seq_lens
.with_dtype<int32_t>()
.with_device(device_)
.verify(seq_lens);
TensorMatcher({Bp1, 2}) // metadata: [0]=GlobalMetadata, [1..N]=PlanItem(batch_id, seq_len)
.with_dtype<int32_t>()
.with_device(device_)
.verify(metadata);
const auto batch_size = static_cast<uint32_t>(B.unwrap());
RuntimeCheck(Bp1.unwrap() == B.unwrap() + 1, "invalid metadata shape");
const auto device = device_.unwrap();
LaunchKernel(1, kBlockSize, device)( //
topk_plan,
static_cast<const uint32_t*>(seq_lens.data_ptr()),
static_cast<PlanItem*>(metadata.data_ptr()),
batch_size,
static_cluster_threshold);
}
static void transform(
const tvm::ffi::TensorView scores,
const tvm::ffi::TensorView seq_lens,
const tvm::ffi::TensorView page_table,
const tvm::ffi::TensorView page_indices,
const uint32_t page_size,
const tvm::ffi::TensorView metadata,
const tvm::ffi::Optional<tvm::ffi::TensorView> raw_indices) {
using namespace host;
auto B = SymbolicSize{"batch_size"};
auto Bp1 = SymbolicSize{"batch_size_plus_1"};
auto L = SymbolicSize{"max_seq_len"};
auto S = SymbolicSize{"score_stride"};
auto P = SymbolicSize{"page_table_stride"};
auto K = SymbolicSize{"topk"};
auto device_ = SymbolicDevice{};
device_.set_options<kDLCUDA>();
TensorMatcher({B, L}) // score
.with_strides({S, 1})
.with_dtype<float>()
.with_device(device_)
.verify(scores);
TensorMatcher({B}) // seq_lens
.with_dtype<int32_t>()
.with_device(device_)
.verify(seq_lens);
TensorMatcher({B, -1}) // page_table
.with_strides({P, 1})
.with_dtype<int32_t>()
.with_device(device_)
.verify(page_table);
TensorMatcher({B, K}) // page_indices
.with_dtype<int32_t>()
.with_device(device_)
.verify(page_indices);
TensorMatcher({Bp1, 2}) // metadata: [0]=GlobalMetadata, [1..N]=PlanItem(batch_id, seq_len)
.with_dtype<int32_t>()
.with_device(device_)
.verify(metadata);
int32_t* raw_indices_ptr = nullptr;
if (raw_indices.has_value()) {
TensorMatcher({B, K}).with_dtype<int32_t>().with_device(device_).verify(raw_indices.value());
raw_indices_ptr = static_cast<int32_t*>(raw_indices.value().data_ptr());
}
RuntimeCheck(std::has_single_bit(page_size), "page_size must be power of 2");
RuntimeCheck(S.unwrap() % 4 == 0, "score_stride must be a multiple of 4 (16-byte vectorized load)");
RuntimeCheck(Bp1.unwrap() == B.unwrap() + 1, "invalid metadata shape");
const auto topk = static_cast<uint32_t>(K.unwrap());
RuntimeCheck(topk > 0 && topk <= kMaxTopK, "topk must be in (0, 2048]");
const auto page_bits = static_cast<uint32_t>(std::countr_zero(page_size));
const auto batch_size = static_cast<uint32_t>(B.unwrap());
const auto max_seq_len = static_cast<uint32_t>(L.unwrap());
const auto device = device_.unwrap();
// The fused kernel runs one 8-block cluster per batch element, and B200 fits one
// wave of exactly 15 such clusters (occ2). For batch <= 15 it stays latency-bound,
// so the 8-way split beats streaming from a much lower seq (measured crossover
// ~36-40K); batch 16 spills into a 2nd wave (+25%) and keeps the 64K floor.
// The floor is chosen on the host per launch.
constexpr uint32_t kClusterFloorSmall = 32768;
constexpr uint32_t kSmallBatchLowFloor = 15;
const auto params = TopKLaunchParams{
.scores = static_cast<const float*>(scores.data_ptr()),
.seq_lens = static_cast<const int32_t*>(seq_lens.data_ptr()),
.page_table = static_cast<const int32_t*>(page_table.data_ptr()),
.page_indices = static_cast<int32_t*>(page_indices.data_ptr()),
.raw_indices = raw_indices_ptr,
.metadata = static_cast<const PlanItem*>(metadata.data_ptr()),
.score_stride = S.unwrap(),
.page_table_stride = P.unwrap(),
.topk = topk,
.page_bits = page_bits,
.cluster_floor = (batch_size <= kSmallBatchLowFloor) ? kClusterFloorSmall : kClusterFloor,
};
const bool use_cluster = (max_seq_len > params.cluster_floor) && (batch_size <= kClusterMaxBatch);
constexpr bool kUsePDL = true;
if (use_cluster) {
if (batch_size <= kNumPersistentClusters) {
LaunchKernel({batch_size, kClusterSize}, kBlockSize, device)
.config({.use_pdl = kUsePDL, .cluster_dim = dim3{1, kClusterSize}})
.launch(topk_small_batch_kernel<kUsePDL>, params);
} else {
const uint32_t num_clusters = std::min(batch_size, kNumPersistentClusters);
LaunchKernel({num_clusters, kClusterSize}, kBlockSize, device)
.config({.use_pdl = kUsePDL, .cluster_dim = dim3{1, kClusterSize}})
.launch(topk_persistent_cluster_kernel<kUsePDL>, params);
LaunchKernel(batch_size, kBlockSize, device)
.config({.use_pdl = kUsePDL})
.launch(topk_main_kernel<kUsePDL, /*kLevel=*/3>, params);
}
} else if (max_seq_len <= kReg2MaxSeqLen) {
LaunchKernel(batch_size, kBlockSize, device)
.config({.use_pdl = kUsePDL})
.launch(topk_main_kernel<kUsePDL, /*kLevel=*/0>, params);
} else if (max_seq_len <= kReg4MaxSeqLen) {
LaunchKernel(batch_size, kBlockSize, device)
.config({.use_pdl = kUsePDL})
.launch(topk_main_kernel<kUsePDL, /*kLevel=*/1>, params);
} else {
LaunchKernel(batch_size, kBlockSize, device)
.config({.use_pdl = kUsePDL})
.launch(topk_main_kernel<kUsePDL, /*kLevel=*/2>, params);
}
}
};
} // namespace