Deepseek V4 (#23882)
Co-authored-by: Baizhou Zhang <sobereddiezhang@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: fzyzcjy <ch271828n@outlook.com> Co-authored-by: ispobock <ispobaoke@gmail.com> Co-authored-by: Zhiqiang Xie <xiezhq@stanford.edu> Co-authored-by: yueming-yuan <yym022502@gmail.com> Co-authored-by: DarkSharpness <2040703891@qq.com> Co-authored-by: Yuhao Yang <47235274+yhyang201@users.noreply.github.com> Co-authored-by: yhyang201 <yhyang201@users.noreply.github.com> Co-authored-by: yhyang201 <yhyang201@gmail.com> Co-authored-by: Qiaolin Yu <90088090+qiaolin-yu@users.noreply.github.com> Co-authored-by: Ethan (Yusheng) Su <11704492+yushengsu-thu@users.noreply.github.com> Co-authored-by: Mingyi <27337995+wisclmy0611@users.noreply.github.com> Co-authored-by: Cheng Wan <54331508+ch-wan@users.noreply.github.com> Co-authored-by: Yihao Wang <42559837+againstentropy@users.noreply.github.com>
This commit is contained in:
co-authored by
Baizhou Zhang
Claude Opus 4.7
fzyzcjy
ispobock
Zhiqiang Xie
yueming-yuan
DarkSharpness
Yuhao Yang
yhyang201
yhyang201
Qiaolin Yu
Ethan Su
Mingyi
Cheng Wan
Yihao Wang
parent
55224fff08
commit
35870d55ac
@@ -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,543 @@
|
||||
#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;
|
||||
|
||||
uint32_t num_q_tokens;
|
||||
uint32_t batch_size;
|
||||
uint32_t num_indices;
|
||||
};
|
||||
|
||||
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
|
||||
_num_q_tokens, _batch_size, _num_indices
|
||||
] = 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;
|
||||
|
||||
if (ragged_id >= _num_q_tokens) [[unlikely]]
|
||||
return;
|
||||
if (global_bid >= _batch_size) [[unlikely]]
|
||||
return;
|
||||
|
||||
const int32_t index = indices_ptr[global_bid];
|
||||
|
||||
if (index < 0 || static_cast<uint32_t>(index) >= _num_indices) [[unlikely]]
|
||||
return;
|
||||
|
||||
// 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 K = SymbolicSize{"num_indices"};
|
||||
auto device_ = SymbolicDevice{};
|
||||
device_.set_options<kDLCUDA>();
|
||||
|
||||
TensorMatcher({K, 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 num_indices = static_cast<uint32_t>(K.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,
|
||||
.num_q_tokens = num_q_tokens,
|
||||
.batch_size = batch_size,
|
||||
.num_indices = num_indices,
|
||||
};
|
||||
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,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,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,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,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,82 @@
|
||||
#include <sgl_kernel/tensor.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
|
||||
#include <sgl_kernel/deepseek_v4/kvcacheio.cuh>
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace {
|
||||
|
||||
/// NOTE: for offload to cpu kernel, we use persistent kernel
|
||||
inline constexpr uint32_t kBlockSize = 1024;
|
||||
inline constexpr uint32_t kBlockQuota = 4;
|
||||
|
||||
#define OFFLOAD_KERNEL __global__ __launch_bounds__(kBlockSize, 1)
|
||||
|
||||
struct OffloadParams {
|
||||
void** gpu_caches;
|
||||
void** cpu_caches;
|
||||
const int64_t* gpu_indices;
|
||||
const int64_t* cpu_indices;
|
||||
uint32_t num_items;
|
||||
uint32_t num_layers;
|
||||
};
|
||||
|
||||
OFFLOAD_KERNEL void offload_to_cpu(const __grid_constant__ OffloadParams params) {
|
||||
using namespace device::hisparse;
|
||||
const auto [gpu_caches, cpu_caches, gpu_indices, cpu_indices, num_items, num_layers] = params;
|
||||
const auto global_tid = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
constexpr auto kNumWarps = (kBlockSize / 32) * kBlockQuota;
|
||||
for (auto i = global_tid / 32; i < num_items; i += kNumWarps) {
|
||||
const int32_t gpu_index = gpu_indices[i];
|
||||
const int32_t cpu_index = cpu_indices[i];
|
||||
for (auto j = 0u; j < num_layers; ++j) {
|
||||
const auto gpu_cache = gpu_caches[j];
|
||||
const auto cpu_cache = cpu_caches[j];
|
||||
transfer_item<TransferDirection::DeviceToHost>(
|
||||
/*dst_cache=*/cpu_cache,
|
||||
/*src_cache=*/gpu_cache,
|
||||
/*dst_index=*/cpu_index,
|
||||
/*src_index=*/gpu_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[maybe_unused]]
|
||||
void hisparse_transfer(
|
||||
tvm::ffi::TensorView gpu_ptrs,
|
||||
tvm::ffi::TensorView cpu_ptrs,
|
||||
tvm::ffi::TensorView gpu_indices,
|
||||
tvm::ffi::TensorView cpu_indices) {
|
||||
using namespace host;
|
||||
auto N = SymbolicSize{"num_items"};
|
||||
auto L = SymbolicSize{"num_layers"};
|
||||
auto device_ = SymbolicDevice{};
|
||||
device_.set_options<kDLCUDA>();
|
||||
TensorMatcher({L}) // 1D cache pointers
|
||||
.with_dtype<uint64_t>()
|
||||
.with_device(device_)
|
||||
.verify(gpu_ptrs)
|
||||
.verify(cpu_ptrs);
|
||||
TensorMatcher({N}) // 1D indices
|
||||
.with_dtype<int64_t>()
|
||||
.with_device(device_)
|
||||
.verify(gpu_indices)
|
||||
.verify(cpu_indices);
|
||||
const auto params = OffloadParams{
|
||||
.gpu_caches = static_cast<void**>(gpu_ptrs.data_ptr()),
|
||||
.cpu_caches = static_cast<void**>(cpu_ptrs.data_ptr()),
|
||||
.gpu_indices = static_cast<const int64_t*>(gpu_indices.data_ptr()),
|
||||
.cpu_indices = static_cast<const int64_t*>(cpu_indices.data_ptr()),
|
||||
.num_items = static_cast<uint32_t>(N.unwrap()),
|
||||
.num_layers = static_cast<uint32_t>(L.unwrap()),
|
||||
};
|
||||
LaunchKernel(kBlockQuota, kBlockSize, device_.unwrap())(offload_to_cpu, params);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,219 @@
|
||||
#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);
|
||||
TensorMatcher({P, H}) // buf.x
|
||||
.with_dtype<int8_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,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,133 @@
|
||||
#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 <tvm/ffi/container/tensor.h>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kBlockSize = 128;
|
||||
constexpr uint32_t kNumWarps = kBlockSize / device::kWarpThreads;
|
||||
|
||||
struct RMSNormSelfParams {
|
||||
const void* __restrict__ input;
|
||||
void* __restrict__ output;
|
||||
int64_t stride_batch_bytes;
|
||||
int64_t stride_head_bytes;
|
||||
uint32_t batch_size;
|
||||
uint32_t num_head;
|
||||
float eps;
|
||||
};
|
||||
|
||||
template <typename DType, int64_t kHeadDim, bool kUsePDL>
|
||||
__global__ __launch_bounds__(kBlockSize, 20) //
|
||||
void rmsnorm_self(const __grid_constant__ RMSNormSelfParams params) {
|
||||
using namespace device;
|
||||
constexpr int64_t kVecSize = 16 / sizeof(DType);
|
||||
constexpr uint32_t kNumLoop = kHeadDim / (kVecSize * kWarpThreads);
|
||||
static_assert(kHeadDim % (kWarpThreads * kVecSize) == 0);
|
||||
using DType2 = packed_t<DType>;
|
||||
using Vec = AlignedVector<DType2, kVecSize / 2>;
|
||||
|
||||
const auto warp_id = blockIdx.x * kNumWarps + threadIdx.x / kWarpThreads;
|
||||
const auto batch_id = warp_id / params.num_head;
|
||||
const auto head_id = warp_id % params.num_head;
|
||||
const auto gmem = tile::Memory<Vec>::warp();
|
||||
if (batch_id >= params.batch_size) return;
|
||||
const auto input_ptr = pointer::offset( //
|
||||
params.input,
|
||||
batch_id * params.stride_batch_bytes,
|
||||
head_id * params.stride_head_bytes);
|
||||
// use contiguous layout
|
||||
const auto output_ptr = pointer::offset( //
|
||||
params.output,
|
||||
warp_id * kHeadDim * sizeof(DType));
|
||||
PDLWaitPrimary<kUsePDL>(); // wait for primary kernel
|
||||
|
||||
Vec inputs[kNumLoop];
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kNumLoop; ++i) {
|
||||
inputs[i] = gmem.load(input_ptr, i);
|
||||
}
|
||||
|
||||
// compute sum of squares
|
||||
float local_sum = 0;
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kNumLoop; ++i) {
|
||||
#pragma unroll
|
||||
for (uint32_t j = 0; j < kVecSize / 2; ++j) {
|
||||
const auto [x, y] = cast<fp32x2_t>(inputs[i][j]);
|
||||
local_sum += x * x + y * y;
|
||||
}
|
||||
}
|
||||
|
||||
const auto sum_of_squares = warp::reduce_sum(local_sum);
|
||||
const auto factor = math::rsqrt(sum_of_squares / kHeadDim + params.eps);
|
||||
|
||||
// weight must be identity (null, not used)
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kNumLoop; ++i) {
|
||||
#pragma unroll
|
||||
for (uint32_t j = 0; j < kVecSize / 2; ++j) {
|
||||
const auto [x, y] = cast<fp32x2_t>(inputs[i][j]);
|
||||
inputs[i][j] = cast<DType2>(fp32x2_t{x * factor, y * factor});
|
||||
}
|
||||
gmem.store(output_ptr, inputs[i], i);
|
||||
}
|
||||
|
||||
PDLTriggerSecondary<kUsePDL>(); // launch secondary kernel
|
||||
}
|
||||
|
||||
template <int64_t kHeadDim, typename DType, bool kUsePDL>
|
||||
struct RMSNormKernel {
|
||||
static constexpr auto kernel_self = rmsnorm_self<DType, kHeadDim, kUsePDL>;
|
||||
|
||||
static void run_self(tvm::ffi::TensorView input, tvm::ffi::TensorView output, float eps) {
|
||||
using namespace host;
|
||||
|
||||
auto N = SymbolicSize{"batch_size"};
|
||||
auto H = SymbolicSize{"num_heads"};
|
||||
auto Dn = SymbolicSize{"stride_head"};
|
||||
auto Dh = SymbolicSize{"stride_batch"};
|
||||
constexpr auto D = kHeadDim;
|
||||
auto device = SymbolicDevice{};
|
||||
device.set_options<kDLCUDA>();
|
||||
|
||||
TensorMatcher({N, H, D}) // input
|
||||
.with_strides({Dh, Dn, 1})
|
||||
.with_dtype<DType>()
|
||||
.with_device(device)
|
||||
.verify(input);
|
||||
TensorMatcher({N, H, D}) // output, must be contiguous
|
||||
.with_dtype<DType>()
|
||||
.with_device(device)
|
||||
.verify(output);
|
||||
|
||||
const auto batch_size = static_cast<uint32_t>(N.unwrap());
|
||||
const auto num_head = static_cast<uint32_t>(H.unwrap());
|
||||
const auto stride_head_bytes = static_cast<int64_t>(Dn.unwrap() * sizeof(DType));
|
||||
const auto stride_batch_bytes = static_cast<int64_t>(Dh.unwrap() * sizeof(DType));
|
||||
const auto params = RMSNormSelfParams{
|
||||
.input = input.data_ptr(),
|
||||
.output = output.data_ptr(),
|
||||
.stride_batch_bytes = stride_batch_bytes,
|
||||
.stride_head_bytes = stride_head_bytes,
|
||||
.batch_size = batch_size,
|
||||
.num_head = num_head,
|
||||
.eps = eps,
|
||||
};
|
||||
if (batch_size == 0 || num_head == 0) return;
|
||||
const auto needed_warps = batch_size * num_head;
|
||||
const auto num_blocks = div_ceil(needed_warps, kNumWarps);
|
||||
LaunchKernel(num_blocks, kBlockSize, device.unwrap()) //
|
||||
.enable_pdl(kUsePDL)(kernel_self, 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,371 @@
|
||||
#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>
|
||||
|
||||
namespace {
|
||||
|
||||
using deepseek_v4::fp8::cast_to_ue8m0;
|
||||
using deepseek_v4::fp8::pack_fp8;
|
||||
|
||||
struct SiluMulQuantParams {
|
||||
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;
|
||||
}
|
||||
|
||||
[[maybe_unused]]
|
||||
SGL_DEVICE CTAWork get_work(const SiluMulQuantParams& 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 kUsePDL, bool kApplySwigluLimit>
|
||||
__global__ __launch_bounds__(1024, 2) void // maximize occupancy
|
||||
silu_mul_quant_kernel(const SiluMulQuantParams __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;
|
||||
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) {
|
||||
if constexpr (kApplySwigluLimit) {
|
||||
// Fused fp32 path: bf16 load ??? fp32 clamp ??? fp32 silu ??? fp32 mul ??? fp32 result.
|
||||
// Avoids the silu???bf16???mul???fp32 round-trip of the non-fused path since we already
|
||||
// have gate/up in fp32 registers after clamp.
|
||||
const float limit = params.swiglu_limit;
|
||||
|
||||
const auto [g0_raw, g1_raw] = cast<fp32x2_t>(gate_vec[i]);
|
||||
const float g0 = fminf(g0_raw, limit);
|
||||
const float g1 = fminf(g1_raw, limit);
|
||||
|
||||
const float silu0 = g0 / (1.0f + expf(-g0));
|
||||
const float silu1 = g1 / (1.0f + expf(-g1));
|
||||
|
||||
const auto [u0_raw, u1_raw] = cast<fp32x2_t>(up_vec[i]);
|
||||
const float u0 = fmaxf(fminf(u0_raw, limit), -limit);
|
||||
const float u1 = fmaxf(fminf(u1_raw, limit), -limit);
|
||||
|
||||
const float val0 = u0 * silu0;
|
||||
const float val1 = u1 * silu1;
|
||||
results[2 * i + 0] = val0;
|
||||
results[2 * i + 1] = val1;
|
||||
local_max = fmaxf(local_max, fmaxf(fabsf(val0), fabsf(val1)));
|
||||
} else {
|
||||
// original code path ??? must stay byte-equal to pre-fusion kernel.
|
||||
const auto [g0, g1] = cast<fp32x2_t>(gate_vec[i]);
|
||||
|
||||
float silu0 = g0 / (1.0f + expf(-g0));
|
||||
float silu1 = g1 / (1.0f + expf(-g1));
|
||||
|
||||
bf16x2_t silu_d = cast<bf16x2_t>(fp32x2_t{silu0, silu1});
|
||||
auto [val0, val1] = cast<fp32x2_t>(up_vec[i] * silu_d);
|
||||
results[2 * i + 0] = val0;
|
||||
results[2 * i + 1] = val1;
|
||||
local_max = fmaxf(local_max, fmaxf(fabsf(val0), fabsf(val1)));
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const auto [g0_raw, g1_raw] = cast<fp32x2_t>(gate[i]);
|
||||
const float g0 = fminf(g0_raw, limit);
|
||||
const float g1 = fminf(g1_raw, limit);
|
||||
const float silu0 = g0 / (1.0f + expf(-g0));
|
||||
const float silu1 = g1 / (1.0f + expf(-g1));
|
||||
const auto [u0_raw, u1_raw] = cast<fp32x2_t>(up[i]);
|
||||
const float u0 = fmaxf(fminf(u0_raw, limit), -limit);
|
||||
const float u1 = fmaxf(fminf(u1_raw, limit), -limit);
|
||||
const float val0 = u0 * silu0;
|
||||
const float val1 = u1 * silu1;
|
||||
out[i] = cast<DType2>(fp32x2_t{val0, val1});
|
||||
}
|
||||
|
||||
tile.store(params.output, out, bid);
|
||||
PDLTriggerSecondary<kUsePDL>();
|
||||
}
|
||||
|
||||
// ---- Host wrapper
|
||||
// ------------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
template <int64_t kGroupSize, bool kScaleUE8M0, bool kUsePDL, bool kApplySwigluLimit>
|
||||
struct SiluAndMulMaskedPostQuantKernel {
|
||||
static_assert(kGroupSize == 128);
|
||||
static constexpr auto kernel_normal = silu_mul_quant_kernel<kScaleUE8M0, false, kUsePDL, kApplySwigluLimit>;
|
||||
static constexpr auto kernel_transposed = silu_mul_quant_kernel<true, true, 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 = SiluMulQuantParams{
|
||||
.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);
|
||||
}
|
||||
};
|
||||
|
||||
} // 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) / 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 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) / math::FP8_E4M3_MAX;
|
||||
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,336 @@
|
||||
#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 {
|
||||
|
||||
constexpr uint32_t kTopK = 512;
|
||||
constexpr uint32_t kTopKBlockSize = 512;
|
||||
constexpr uint32_t kSMEM = 16 * 1024 * sizeof(uint32_t); // 64KB (bytes)
|
||||
|
||||
struct TopK512Params {
|
||||
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_512_transform(const __grid_constant__ TopK512Params 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 TopK512Kernel {
|
||||
static constexpr auto kernel = topk_512_transform<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, 512}) // 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, 512}) // 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 = TopK512Params{
|
||||
.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,336 @@
|
||||
#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 {
|
||||
|
||||
constexpr uint32_t kTopK = 1024;
|
||||
constexpr uint32_t kTopKBlockSize = 1024;
|
||||
constexpr uint32_t kSMEM = 16 * 1024 * sizeof(uint32_t); // 64KB (bytes)
|
||||
|
||||
struct TopK1024Params {
|
||||
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_1024_transform(const __grid_constant__ TopK1024Params 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 TopK1024Kernel {
|
||||
static constexpr auto kernel = topk_1024_transform<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, 1024}) // 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, 1024}) // 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 = TopK1024Params{
|
||||
.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,493 @@
|
||||
#include <sgl_kernel/tensor.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/type.cuh>
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/vec.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include <sgl_kernel/deepseek_v4/topk/cluster.cuh>
|
||||
#include <sgl_kernel/deepseek_v4/topk/register.cuh>
|
||||
#include <sgl_kernel/deepseek_v4/topk/streaming.cuh>
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
#include <tvm/ffi/object.h>
|
||||
|
||||
#include <cfloat>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
|
||||
namespace {
|
||||
|
||||
#ifndef SGL_TOPK
|
||||
#define SGL_TOPK 512
|
||||
#endif
|
||||
|
||||
inline constexpr uint32_t K = SGL_TOPK;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
namespace impl = device::top512;
|
||||
using Large = impl::ClusterTopK<K>;
|
||||
using Medium = impl::StreamingTopK<K>;
|
||||
using Small = impl::RegisterTopK<K>;
|
||||
|
||||
using Metadata = Large::Metadata;
|
||||
constexpr uint32_t kBlockSize = impl::kBlockSize;
|
||||
constexpr uint32_t kNumClusters = 15; // based on hardware limits
|
||||
constexpr uint32_t kClusterSize = Large::kClusterSize;
|
||||
constexpr uint32_t kMax2PassLength = Small::kMax2PassLength;
|
||||
constexpr uint32_t kMaxSupportedLength = Large::kMaxLength;
|
||||
|
||||
/// Common metadata lives at metadata[0] (first row of the [batch_size+1, 4] tensor).
|
||||
/// Per-item metadata starts at metadata[1..batch_size]. The plan kernel writes both.
|
||||
struct alignas(16) GlobalMetadata {
|
||||
uint32_t cluster_threshold; // decided per-batch in plan kernel
|
||||
uint32_t num_cluster_items; // N = number of items routed to the cluster path
|
||||
uint32_t reserved[2];
|
||||
};
|
||||
static_assert(sizeof(GlobalMetadata) == sizeof(Metadata), "layout: row 0 must occupy one Metadata-sized slot");
|
||||
|
||||
// optimize occupancy for prefill
|
||||
#define SMALL_TOPK_KERNEL __global__ __launch_bounds__(kBlockSize, 2)
|
||||
// cluster at y dim
|
||||
#define LARGE_CLUSTER __cluster_dims__(1, kClusterSize, 1)
|
||||
// stage-1 is persistent cluster, and shared memory usage is huge (can not 2)
|
||||
#define LARGE_TOPK_STAGE_1 __global__ __launch_bounds__(kBlockSize, 1) LARGE_CLUSTER
|
||||
// stage-2 is non-persistent non-cluster, with less shared memory and higher occupancy
|
||||
#define LARGE_TOPK_STAGE_2 __global__ __launch_bounds__(kBlockSize, 2)
|
||||
// fused into 1 stage when batch-size <= kNumPersistentClusters
|
||||
#define FUSED_COMBINE_KERNEL __global__ __launch_bounds__(kBlockSize, 1) LARGE_CLUSTER
|
||||
// plan runs once as a single block before the combine kernels
|
||||
#define PLAN_KERNEL __global__ __launch_bounds__(kBlockSize, 1)
|
||||
|
||||
struct TopKParams {
|
||||
const uint32_t* __restrict__ seq_lens;
|
||||
const float* __restrict__ scores;
|
||||
const int32_t* __restrict__ page_table;
|
||||
int32_t* __restrict__ page_indices;
|
||||
int64_t score_stride;
|
||||
int64_t page_table_stride;
|
||||
uint8_t* __restrict__ workspace; // [batch, kWorkspaceBytes] -- internally allocated
|
||||
/// Pointer to the full metadata tensor: metadata[0] is GlobalMetadata, metadata[1..]
|
||||
/// are per-item entries (at most kNumClusters * rounds of them).
|
||||
const Metadata* __restrict__ metadata = nullptr;
|
||||
int64_t workspace_stride; // bytes per batch
|
||||
uint32_t batch_size;
|
||||
uint32_t page_bits;
|
||||
|
||||
SGL_DEVICE const float* get_scores(const uint32_t batch_id) const {
|
||||
return scores + batch_id * score_stride;
|
||||
}
|
||||
SGL_DEVICE impl::TransformParams get_transform(const uint32_t batch_id, int32_t* indices) const {
|
||||
return {
|
||||
.page_table = page_table + batch_id * page_table_stride,
|
||||
.indices_in = indices,
|
||||
.indices_out = page_indices + batch_id * K,
|
||||
.page_bits = page_bits,
|
||||
};
|
||||
}
|
||||
SGL_DEVICE const GlobalMetadata& get_global_metadata() const {
|
||||
return *reinterpret_cast<const GlobalMetadata*>(metadata);
|
||||
}
|
||||
SGL_DEVICE const Metadata& get_item_metadata(uint32_t work_id) const {
|
||||
return metadata[1 + work_id]; // +1 to skip the GlobalMetadata row
|
||||
}
|
||||
};
|
||||
|
||||
SGL_DEVICE uint2 partition_work(uint32_t length, uint32_t rank) {
|
||||
constexpr uint32_t kTMAAlign = 4;
|
||||
const auto total_units = (length + kTMAAlign - 1) / kTMAAlign;
|
||||
const auto base = total_units / kClusterSize;
|
||||
const auto extra = total_units % kClusterSize;
|
||||
const auto local_units = base + (rank < extra ? 1u : 0u);
|
||||
const auto offset_units = rank * base + min(rank, extra);
|
||||
const auto offset = offset_units * kTMAAlign;
|
||||
const auto finish = min(offset + local_units * kTMAAlign, length);
|
||||
return {offset, finish - offset};
|
||||
}
|
||||
|
||||
/// Persistent scheduler. A single block:
|
||||
/// 1. Decides a cluster_threshold from the real seq_lens distribution (or
|
||||
/// uses the caller-supplied `static_cluster_threshold` when non-zero).
|
||||
/// 2. Writes that threshold + N into metadata[0] (the GlobalMetadata row).
|
||||
/// 3. Compacts items with seq_len > threshold into metadata[1..N+1), laid out
|
||||
/// to match the persistent consumer's round-robin stride (kNumClusters).
|
||||
/// Entries for clusters that get no work are zero-filled.
|
||||
PLAN_KERNEL void topk_plan(
|
||||
const uint32_t* __restrict__ seq_lens,
|
||||
Metadata* __restrict__ metadata,
|
||||
const uint32_t batch_size,
|
||||
const uint32_t static_cluster_threshold) {
|
||||
// Candidate thresholds, strictly increasing. Picked to give the auto-heuristic
|
||||
// reasonable granularity without needing a full sort. Must all be >= kMax2PassLength.
|
||||
|
||||
struct Pair {
|
||||
uint32_t threshold;
|
||||
uint32_t max_batch_size;
|
||||
};
|
||||
/// NOTE: only tuned on B200
|
||||
constexpr Pair kCandidates[] = {
|
||||
{32768, 30},
|
||||
{40960, 45},
|
||||
{49152, 45},
|
||||
{65536, 60},
|
||||
{98304, 60},
|
||||
{131072, 75},
|
||||
{196608, 90},
|
||||
{262144, 105},
|
||||
};
|
||||
constexpr uint32_t kNumCandidates = std::size(kCandidates);
|
||||
constexpr uint32_t kMinBatchSize = kCandidates[0].max_batch_size;
|
||||
static_assert(kCandidates[0].threshold == kMax2PassLength);
|
||||
static_assert(kCandidates[kNumCandidates - 1].threshold == kMaxSupportedLength);
|
||||
|
||||
__shared__ uint32_t s_count; // final N after compaction
|
||||
__shared__ uint32_t s_counts[kNumCandidates];
|
||||
__shared__ uint32_t s_threshold;
|
||||
|
||||
const auto tx = threadIdx.x;
|
||||
if (tx == 0) s_count = 0;
|
||||
if (tx < kNumCandidates) s_counts[tx] = 0;
|
||||
__syncthreads();
|
||||
|
||||
// --- Phase 1: decide threshold ------------------------------------------
|
||||
if (static_cluster_threshold > 0) {
|
||||
if (tx == 0) s_threshold = static_cluster_threshold;
|
||||
} else if (batch_size <= kMinBatchSize) {
|
||||
if (tx == 0) s_threshold = kMax2PassLength; // always prefer cluster
|
||||
} else {
|
||||
// Count items above each candidate threshold. Monotonically non-increasing in T.
|
||||
for (uint32_t i = tx; i < batch_size; i += kBlockSize) {
|
||||
const uint32_t sl = seq_lens[i];
|
||||
assert(sl <= kMaxSupportedLength);
|
||||
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 = kMaxSupportedLength;
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kNumCandidates; ++i) {
|
||||
const auto j = kNumCandidates - 1 - i;
|
||||
accum += s_counts[j];
|
||||
/// NOTE: `accum` increasing, while `max_batch_size` decreasing
|
||||
if (accum > kCandidates[j].max_batch_size) break;
|
||||
chosen = kCandidates[j].threshold;
|
||||
}
|
||||
s_threshold = chosen;
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
// sanity check: below 2 pass threshold, must fits in small path
|
||||
const auto cluster_threshold = max(s_threshold, kMax2PassLength);
|
||||
|
||||
// --- Phase 2: compact items with seq_len > threshold into metadata[1..] -
|
||||
// Per-item rows live at metadata[1 + pos]; metadata[0] is the GlobalMetadata row.
|
||||
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, false};
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
const auto N = s_count;
|
||||
|
||||
// --- Phase 3: has_next + sentinels + GlobalMetadata ---------------------
|
||||
for (uint32_t i = tx; i < N; i += kBlockSize) {
|
||||
if (i + kNumClusters < N) metadata[1 + i].has_next = true;
|
||||
}
|
||||
// Zero-fill the first kNumClusters sentinel slots that got no valid entry.
|
||||
if (tx < kNumClusters && tx >= N) metadata[1 + tx] = {0, 0, false};
|
||||
// Write global metadata (row 0).
|
||||
if (tx == 0) {
|
||||
auto* g = reinterpret_cast<GlobalMetadata*>(metadata);
|
||||
*g = {
|
||||
.cluster_threshold = cluster_threshold,
|
||||
.num_cluster_items = N,
|
||||
.reserved = {0, 0},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
SMALL_TOPK_KERNEL void // short context
|
||||
topk_short_transform(const __grid_constant__ TopKParams params) {
|
||||
alignas(128) extern __shared__ uint8_t smem[];
|
||||
__shared__ int32_t s_topk_indices[K];
|
||||
const auto batch_id = blockIdx.x;
|
||||
const auto seq_len = params.seq_lens[batch_id];
|
||||
const auto transform = params.get_transform(batch_id, s_topk_indices);
|
||||
// trivial case
|
||||
if (seq_len <= K) {
|
||||
impl::trivial_transform(transform, seq_len, K);
|
||||
} else {
|
||||
Small::run(params.get_scores(batch_id), s_topk_indices, seq_len, smem, /*use_pdl=*/true);
|
||||
device::PDLTriggerSecondary<true>();
|
||||
Small::transform(transform);
|
||||
}
|
||||
}
|
||||
|
||||
LARGE_TOPK_STAGE_1 void // long context, middle to large batch size
|
||||
topk_combine_preprocess(const __grid_constant__ TopKParams params) {
|
||||
alignas(128) extern __shared__ uint8_t smem[];
|
||||
__shared__ int32_t s_topk_indices[K];
|
||||
uint32_t work_id = blockIdx.x;
|
||||
uint32_t batch_id;
|
||||
uint32_t seq_len;
|
||||
bool has_next;
|
||||
uint32_t length;
|
||||
uint32_t offset;
|
||||
const auto cluster_rank = blockIdx.y;
|
||||
|
||||
const auto prefetch_metadata = [&] {
|
||||
const auto metadata = params.get_item_metadata(work_id);
|
||||
batch_id = metadata.batch_id;
|
||||
seq_len = metadata.seq_len;
|
||||
has_next = metadata.has_next;
|
||||
work_id += kNumClusters; // advance to the next item for this cluster
|
||||
};
|
||||
const auto launch_prologue = [&] {
|
||||
const auto partition = partition_work(seq_len, cluster_rank);
|
||||
offset = partition.x;
|
||||
length = partition.y;
|
||||
Large::stage1_prologue(params.get_scores(batch_id) + offset, length, smem);
|
||||
};
|
||||
|
||||
device::PDLWaitPrimary<true>();
|
||||
device::PDLTriggerSecondary<true>();
|
||||
|
||||
prefetch_metadata();
|
||||
if (seq_len == 0) return;
|
||||
Large::stage1_init(smem);
|
||||
launch_prologue();
|
||||
while (true) {
|
||||
const auto this_length = length;
|
||||
const auto this_offset = offset;
|
||||
const auto need_prefetch = has_next;
|
||||
const auto transform = params.get_transform(batch_id, s_topk_indices);
|
||||
const auto ws = params.workspace + batch_id * params.workspace_stride;
|
||||
if (need_prefetch) prefetch_metadata();
|
||||
Large::stage1(s_topk_indices, this_length, smem, /*reuse=*/true);
|
||||
if (need_prefetch) launch_prologue();
|
||||
Large::stage1_epilogue(transform, this_offset, ws, smem);
|
||||
if (!need_prefetch) break;
|
||||
}
|
||||
}
|
||||
|
||||
LARGE_TOPK_STAGE_2 void // long context, middle to large batch size
|
||||
topk_combine_transform(const __grid_constant__ TopKParams params) {
|
||||
alignas(128) extern __shared__ uint8_t smem[];
|
||||
__shared__ int32_t s_topk_indices[K];
|
||||
const auto batch_id = blockIdx.x;
|
||||
const auto seq_len = params.seq_lens[batch_id];
|
||||
const auto cluster_threshold = params.get_global_metadata().cluster_threshold;
|
||||
const auto transform = params.get_transform(batch_id, s_topk_indices);
|
||||
if (seq_len <= K) {
|
||||
impl::trivial_transform(transform, seq_len, K);
|
||||
} else if (seq_len <= kMax2PassLength) {
|
||||
if (seq_len <= Small::kMax1PassLength) {
|
||||
Small::run(params.get_scores(batch_id), s_topk_indices, seq_len, smem);
|
||||
} else {
|
||||
__syncwarp();
|
||||
Small::run<true>(params.get_scores(batch_id), s_topk_indices, seq_len, smem);
|
||||
}
|
||||
Small::transform(transform);
|
||||
} else if (seq_len <= cluster_threshold) {
|
||||
Medium::run(params.get_scores(batch_id), seq_len, s_topk_indices, smem);
|
||||
Medium::transform(transform, smem);
|
||||
} else {
|
||||
const auto ws = params.workspace + batch_id * params.workspace_stride;
|
||||
device::PDLWaitPrimary<true>();
|
||||
Large::transform(transform, ws, smem);
|
||||
}
|
||||
}
|
||||
|
||||
FUSED_COMBINE_KERNEL void // long context, small batch size
|
||||
topk_fused_transform(const __grid_constant__ TopKParams params) {
|
||||
alignas(128) extern __shared__ uint8_t smem[];
|
||||
__shared__ int32_t s_topk_indices[K];
|
||||
const auto batch_id = blockIdx.x;
|
||||
const auto cluster_rank = blockIdx.y;
|
||||
const auto seq_len = params.seq_lens[batch_id];
|
||||
const auto transform = params.get_transform(batch_id, s_topk_indices);
|
||||
if (seq_len <= K) {
|
||||
if (cluster_rank != 0) return; // only first rank work
|
||||
impl::trivial_transform(transform, seq_len, K);
|
||||
} else if (seq_len <= Small::kMax1PassLength) {
|
||||
if (cluster_rank != 0) return; // only first rank work
|
||||
Small::run(params.get_scores(batch_id), s_topk_indices, seq_len, smem, /*use_pdl=*/true);
|
||||
Small::transform(transform);
|
||||
} else {
|
||||
const auto [offset, length] = partition_work(seq_len, cluster_rank);
|
||||
const auto ws = params.workspace + batch_id * params.workspace_stride;
|
||||
Large::stage1_init(smem);
|
||||
device::PDLWaitPrimary<true>();
|
||||
Large::stage1_prologue(params.get_scores(batch_id) + offset, length, smem);
|
||||
Large::stage1(s_topk_indices, length, smem);
|
||||
Large::stage1_epilogue(transform, offset, ws, smem);
|
||||
cooperative_groups::this_cluster().sync();
|
||||
if (cluster_rank != 0) return; // only first rank do the stage-2
|
||||
Large::transform(transform, ws, smem);
|
||||
}
|
||||
}
|
||||
|
||||
struct CombinedTopKKernel {
|
||||
static constexpr auto kStage1SMEM = sizeof(Large::Smem) + 128;
|
||||
static constexpr auto kStage2SMEM = std::max(sizeof(Small::Smem), sizeof(Medium::Smem)) + 128;
|
||||
|
||||
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}) //
|
||||
.with_dtype<int32_t>()
|
||||
.with_device(device_)
|
||||
.verify(seq_lens);
|
||||
TensorMatcher({Bp1, 4}) //
|
||||
.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);
|
||||
if (batch_size <= kNumClusters) return; // metadata unused in fused path
|
||||
|
||||
const auto device = device_.unwrap();
|
||||
constexpr auto kernel = topk_plan;
|
||||
LaunchKernel(1, kBlockSize, device)( //
|
||||
kernel,
|
||||
static_cast<uint32_t*>(seq_lens.data_ptr()),
|
||||
static_cast<Metadata*>(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 workspace,
|
||||
const tvm::ffi::TensorView metadata) {
|
||||
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 W = SymbolicSize{"workspace_stride"};
|
||||
constexpr auto D = Large::kWorkspaceInts;
|
||||
auto device_ = SymbolicDevice{};
|
||||
device_.set_options<kDLCUDA>();
|
||||
|
||||
TensorMatcher({B, L}) //
|
||||
.with_strides({S, 1})
|
||||
.with_dtype<float>()
|
||||
.with_device(device_)
|
||||
.verify(scores);
|
||||
TensorMatcher({B}) //
|
||||
.with_dtype<int32_t>()
|
||||
.with_device(device_)
|
||||
.verify(seq_lens);
|
||||
TensorMatcher({B, -1}) //
|
||||
.with_strides({P, 1})
|
||||
.with_dtype<int32_t>()
|
||||
.with_device(device_)
|
||||
.verify(page_table);
|
||||
TensorMatcher({B, K}) //
|
||||
.with_dtype<int32_t>()
|
||||
.with_device(device_)
|
||||
.verify(page_indices);
|
||||
TensorMatcher({B, D}) //
|
||||
.with_strides({W, 1})
|
||||
.with_dtype<int32_t>()
|
||||
.with_device(device_)
|
||||
.verify(workspace);
|
||||
TensorMatcher({Bp1, 4}) //
|
||||
.with_dtype<int32_t>()
|
||||
.with_device(device_)
|
||||
.verify(metadata);
|
||||
|
||||
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();
|
||||
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 (TMA 16-byte alignment)");
|
||||
RuntimeCheck(Bp1.unwrap() == B.unwrap() + 1, "invalid metadata shape");
|
||||
|
||||
// NOTE: this should be fixed later
|
||||
// RuntimeCheck(max_seq_len <= kMaxSupportedLength, max_seq_len, " exceeds the maximum supported length");
|
||||
|
||||
const auto params = TopKParams{
|
||||
.seq_lens = static_cast<uint32_t*>(seq_lens.data_ptr()),
|
||||
.scores = static_cast<float*>(scores.data_ptr()),
|
||||
.page_table = static_cast<int32_t*>(page_table.data_ptr()),
|
||||
.page_indices = static_cast<int32_t*>(page_indices.data_ptr()),
|
||||
.score_stride = S.unwrap(),
|
||||
.page_table_stride = P.unwrap(),
|
||||
.workspace = static_cast<uint8_t*>(workspace.data_ptr()),
|
||||
.metadata = static_cast<const Metadata*>(metadata.data_ptr()),
|
||||
.workspace_stride = W.unwrap() * static_cast<int64_t>(sizeof(int32_t)),
|
||||
.batch_size = batch_size,
|
||||
.page_bits = page_bits,
|
||||
};
|
||||
|
||||
if (max_seq_len <= Small::kMax1PassLength) {
|
||||
// All items fit in the short path -- no stage-1 needed
|
||||
constexpr auto kernel = topk_short_transform;
|
||||
setup_kernel_smem_once<kernel, kStage2SMEM>();
|
||||
LaunchKernel(batch_size, kBlockSize, device, kStage2SMEM) //
|
||||
.enable_pdl(true)(kernel, params);
|
||||
} else {
|
||||
// Some items may be large -- launch stage-1 + main
|
||||
if (batch_size <= kNumClusters) {
|
||||
// can fuse into 1 stage
|
||||
constexpr auto kernel = topk_fused_transform;
|
||||
constexpr auto kSMEM = std::max(kStage1SMEM, kStage2SMEM);
|
||||
setup_kernel_smem_once<kernel, kSMEM>();
|
||||
LaunchKernel({batch_size, kClusterSize}, kBlockSize, device, kSMEM)
|
||||
.enable_cluster({1, kClusterSize})
|
||||
.enable_pdl(true)(kernel, params);
|
||||
} else {
|
||||
// stage 1 + stage 2
|
||||
constexpr auto kernel_stage_1 = topk_combine_preprocess;
|
||||
setup_kernel_smem_once<kernel_stage_1, kStage1SMEM>();
|
||||
const auto num_clusters = std::min(batch_size, kNumClusters);
|
||||
LaunchKernel({num_clusters, kClusterSize}, kBlockSize, device, kStage1SMEM)
|
||||
.enable_cluster({1, kClusterSize})
|
||||
.enable_pdl(true)(kernel_stage_1, params);
|
||||
constexpr auto kernel_stage_2 = topk_combine_transform;
|
||||
setup_kernel_smem_once<kernel_stage_2, kStage2SMEM>();
|
||||
LaunchKernel(batch_size, kBlockSize, device, kStage2SMEM) //
|
||||
.enable_pdl(true)(kernel_stage_2, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "../marlin/dequant.h"
|
||||
#include "../marlin/marlin.cuh"
|
||||
#include "../marlin/marlin_dtypes.cuh"
|
||||
#include <type_traits>
|
||||
|
||||
#define STATIC_ASSERT_SCALAR_TYPE_VALID(scalar_t) \
|
||||
static_assert( \
|
||||
@@ -355,6 +356,7 @@ __global__ void Marlin(
|
||||
constexpr bool has_zp = w_type == host::kU4 || w_type == host::kU8;
|
||||
constexpr bool is_int_type =
|
||||
w_type == host::kU4 || w_type == host::kU8 || w_type == host::kU4B8 || w_type == host::kU8B128;
|
||||
constexpr bool is_8bit_scale = s_type.size_bits() == 8;
|
||||
// see comments of dequant.h for more details
|
||||
constexpr bool dequant_skip_flop = w_type == host::kFE4M3fn || w_type == host::kFE2M1f && s_type == host::kFE4M3fn ||
|
||||
has_zp && !is_zp_float && !std::is_same<scalar_t, nv_bfloat16>::value ||
|
||||
@@ -368,7 +370,7 @@ __global__ void Marlin(
|
||||
static_assert(thread_m_blocks == 1 || !m_block_size_8);
|
||||
constexpr int moe_block_size = m_block_size_8 ? 8 : (16 * thread_m_blocks);
|
||||
const int group_size = (!has_act_order && group_blocks == -1) ? prob_k : prob_k / num_groups;
|
||||
const int scales_expert_stride = prob_n * prob_k / group_size / (w_type == host::kFE2M1f ? 16 : 8);
|
||||
const int scales_expert_stride = prob_n * prob_k / group_size / (is_8bit_scale ? 16 : 8);
|
||||
const int zp_expert_stride =
|
||||
is_zp_float ? prob_n * prob_k / group_size / 8 : prob_n * prob_k / group_size / (pack_factor * 4);
|
||||
const int b_bias_expert_stride = prob_n / 8;
|
||||
@@ -439,52 +441,69 @@ __global__ void Marlin(
|
||||
locks_off = (iters * blockIdx.x) / k_tiles - 1;
|
||||
}
|
||||
|
||||
int prob_m_top_k = prob_m * top_k;
|
||||
// read moe block data given block_id
|
||||
// block_sorted_ids / block_num_valid_tokens / block_topk_weights
|
||||
auto read_moe_block_data = [&](int block_id) {
|
||||
block_num_valid_tokens = moe_block_size;
|
||||
|
||||
cp_async4_pred(
|
||||
sh_block_sorted_ids_int4 + threadIdx.x,
|
||||
reinterpret_cast<const int4*>(sorted_token_ids_ptr) + (block_id * moe_block_size / 4 + threadIdx.x),
|
||||
threadIdx.x < moe_block_size / 4);
|
||||
|
||||
cp_async_fence();
|
||||
cp_async_wait<0>();
|
||||
|
||||
__syncthreads();
|
||||
|
||||
if (threadIdx.x >= threads - 32) {
|
||||
constexpr int size_per_thread = div_ceil(moe_block_size, 32);
|
||||
int lane_id = threadIdx.x - (threads - 32);
|
||||
|
||||
int local_count = 0;
|
||||
#pragma unroll
|
||||
for (int i = 0; i < moe_block_size / 4; i++) {
|
||||
int4 sorted_token_ids_int4 =
|
||||
reinterpret_cast<const int4*>(sorted_token_ids_ptr)[block_id * moe_block_size / 4 + i];
|
||||
int* sorted_token_ids = reinterpret_cast<int*>(&sorted_token_ids_int4);
|
||||
#pragma unroll
|
||||
for (int j = 0; j < 4; j++) {
|
||||
if (sorted_token_ids[j] >= prob_m * top_k) {
|
||||
block_num_valid_tokens = i * 4 + j;
|
||||
break;
|
||||
for (int i = 0; i < size_per_thread; i++) {
|
||||
int j = lane_id * size_per_thread + i;
|
||||
if (j < moe_block_size) {
|
||||
int idx = sh_block_sorted_ids[j];
|
||||
if (idx < prob_m_top_k) local_count++;
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ == 750
|
||||
if constexpr (moe_block_size >= 16) local_count += __shfl_down_sync(0xFFFFFFFF, local_count, 16);
|
||||
if constexpr (moe_block_size >= 8) local_count += __shfl_down_sync(0xFFFFFFFF, local_count, 8);
|
||||
if constexpr (moe_block_size >= 4) local_count += __shfl_down_sync(0xFFFFFFFF, local_count, 4);
|
||||
if constexpr (moe_block_size >= 2) local_count += __shfl_down_sync(0xFFFFFFFF, local_count, 2);
|
||||
|
||||
local_count += __shfl_down_sync(0xFFFFFFFF, local_count, 1);
|
||||
block_num_valid_tokens = local_count;
|
||||
#else
|
||||
block_num_valid_tokens = __reduce_add_sync(0xffffffff, local_count);
|
||||
#endif
|
||||
|
||||
if (lane_id == 0) reinterpret_cast<int*>(sh_new)[0] = block_num_valid_tokens;
|
||||
}
|
||||
|
||||
if (threadIdx.x < moe_block_size) {
|
||||
int idx = sh_block_sorted_ids[threadIdx.x];
|
||||
sh_rd_block_sorted_ids[threadIdx.x] = idx / top_k;
|
||||
|
||||
if (mul_topk_weights) {
|
||||
idx = idx < prob_m_top_k ? idx : 0;
|
||||
scalar_t topk_weight_tmp = Dtype::float2num(topk_weights_ptr[idx]);
|
||||
if constexpr (w_type == host::kFE2M1f && s_type == host::kFE4M3fn) {
|
||||
sh_block_topk_weights[threadIdx.x] = __hmul2(global_scale, Dtype::num2num2(topk_weight_tmp));
|
||||
} else {
|
||||
sh_block_topk_weights[threadIdx.x] = Dtype::num2num2(topk_weight_tmp);
|
||||
}
|
||||
}
|
||||
if (block_num_valid_tokens != moe_block_size) break;
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
int tid4 = threadIdx.x / 4;
|
||||
if (threadIdx.x % 4 == 0 && threadIdx.x < block_num_valid_tokens) {
|
||||
sh_block_sorted_ids_int4[tid4] =
|
||||
reinterpret_cast<const int4*>(sorted_token_ids_ptr)[block_id * moe_block_size / 4 + tid4];
|
||||
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 4; i++)
|
||||
sh_rd_block_sorted_ids[tid4 * 4 + i] = sh_block_sorted_ids[tid4 * 4 + i] / top_k;
|
||||
|
||||
if (mul_topk_weights) {
|
||||
#pragma unroll
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int idx = tid4 * 4 + i;
|
||||
// idx = idx < block_num_valid_tokens ? idx : 0;
|
||||
if (idx < block_num_valid_tokens) {
|
||||
if constexpr (w_type == host::kFE2M1f && s_type == host::kFE4M3fn) {
|
||||
sh_block_topk_weights[idx] =
|
||||
__hmul2(global_scale, Dtype::num2num2(Dtype::float2num(topk_weights_ptr[sh_block_sorted_ids[idx]])));
|
||||
} else {
|
||||
sh_block_topk_weights[idx] =
|
||||
Dtype::num2num2(Dtype::float2num(topk_weights_ptr[sh_block_sorted_ids[idx]]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
block_num_valid_tokens = reinterpret_cast<int*>(sh_new)[0];
|
||||
__syncthreads();
|
||||
};
|
||||
|
||||
@@ -626,11 +645,10 @@ __global__ void Marlin(
|
||||
constexpr int b_sh_wr_iters = b_sh_stage / b_sh_wr_delta;
|
||||
|
||||
// Scale sizes/strides without act_order
|
||||
int s_gl_stride = prob_n / 8;
|
||||
constexpr int s_sh_stride = 16 * thread_n_blocks / 8;
|
||||
constexpr int s_tb_groups = !has_act_order && group_blocks != -1 && group_blocks < thread_k_blocks
|
||||
? thread_k_blocks / group_blocks / (w_type == host::kFE2M1f ? 2 : 1)
|
||||
: 1;
|
||||
int s_gl_stride = prob_n / (is_8bit_scale ? 16 : 8);
|
||||
constexpr int s_sh_stride = 16 * thread_n_blocks / (is_8bit_scale ? 16 : 8);
|
||||
constexpr int s_tb_groups =
|
||||
!has_act_order && group_blocks != -1 && group_blocks < thread_k_blocks ? thread_k_blocks / group_blocks : 1;
|
||||
constexpr int s_sh_stage = s_tb_groups * s_sh_stride;
|
||||
int s_gl_rd_delta = s_gl_stride;
|
||||
|
||||
@@ -681,13 +699,15 @@ __global__ void Marlin(
|
||||
if constexpr (!has_act_order) {
|
||||
if constexpr (group_blocks == -1) {
|
||||
s_gl_rd = s_sh_stride * slice_col + threadIdx.x;
|
||||
} else if constexpr (group_blocks >= thread_k_blocks) {
|
||||
s_gl_rd = s_gl_stride * ((thread_k_blocks * slice_row) / group_blocks) + s_sh_stride * slice_col + threadIdx.x;
|
||||
} else {
|
||||
s_gl_rd = s_gl_stride * ((thread_k_blocks * slice_row) / group_blocks) / (w_type == host::kFE2M1f ? 2 : 1) +
|
||||
s_sh_stride * slice_col + threadIdx.x;
|
||||
s_gl_rd = s_gl_stride * ((thread_k_blocks * slice_row) / group_blocks + threadIdx.x / s_sh_stride) +
|
||||
s_sh_stride * slice_col + threadIdx.x % s_sh_stride;
|
||||
}
|
||||
}
|
||||
auto s_sh_wr = threadIdx.x;
|
||||
bool s_sh_wr_pred = threadIdx.x < s_sh_stride;
|
||||
bool s_sh_wr_pred = threadIdx.x < s_sh_stage;
|
||||
|
||||
// Zero-points
|
||||
int zp_gl_rd;
|
||||
@@ -705,15 +725,7 @@ __global__ void Marlin(
|
||||
// we scale a `half2` tile in column-major layout in the former and in
|
||||
// row-major in the latter case.
|
||||
int s_sh_rd;
|
||||
if constexpr (group_blocks != -1 && w_type == host::kFE2M1f) {
|
||||
auto warp_id = threadIdx.x / 32;
|
||||
int n_warps = thread_n_blocks / 4;
|
||||
int warp_row = warp_id / n_warps;
|
||||
|
||||
s_sh_rd = 8 * ((threadIdx.x / 32) % (thread_n_blocks / 4)) + (threadIdx.x % 32) / 4;
|
||||
s_sh_rd = s_sh_rd * 2 + (warp_row / group_blocks) % 2;
|
||||
|
||||
} else if constexpr (group_blocks != -1)
|
||||
if constexpr (group_blocks != -1)
|
||||
s_sh_rd = 8 * ((threadIdx.x / 32) % (thread_n_blocks / 4)) + (threadIdx.x % 32) / 4;
|
||||
else if constexpr (group_blocks == -1 && (m_block_size_8 || (has_zp && !dequant_skip_flop)))
|
||||
s_sh_rd = 8 * ((threadIdx.x / 32) % (thread_n_blocks / 4)) + (threadIdx.x % 32) / 8;
|
||||
@@ -907,43 +919,21 @@ __global__ void Marlin(
|
||||
} else {
|
||||
if constexpr (group_blocks != -1) {
|
||||
int4* sh_s_stage = sh_s + s_sh_stage * pipe;
|
||||
|
||||
if constexpr (group_blocks >= thread_k_blocks) {
|
||||
// Only fetch scales if this tile starts a new group
|
||||
if (pipe % (group_blocks / thread_k_blocks) == 0) {
|
||||
if (s_sh_wr_pred) {
|
||||
cp_async4(&sh_s_stage[s_sh_wr], &scales_ptr[s_gl_rd]);
|
||||
}
|
||||
s_gl_rd += s_gl_rd_delta;
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < s_tb_groups; i++) {
|
||||
if (s_sh_wr_pred) {
|
||||
cp_async4(&sh_s_stage[i * s_sh_stride + s_sh_wr], &scales_ptr[s_gl_rd]);
|
||||
}
|
||||
s_gl_rd += s_gl_rd_delta;
|
||||
if (pipe % div_ceil(group_blocks, thread_k_blocks) == 0) {
|
||||
if (s_sh_wr_pred) {
|
||||
cp_async4(&sh_s_stage[s_sh_wr], &scales_ptr[s_gl_rd]);
|
||||
}
|
||||
s_gl_rd += s_gl_rd_delta * s_tb_groups;
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr (has_zp && group_blocks != -1) {
|
||||
int4* sh_zp_stage = sh_zp + zp_sh_stage * pipe;
|
||||
|
||||
if constexpr (group_blocks >= thread_k_blocks) {
|
||||
// Only fetch zero-points if this tile starts a new group
|
||||
if (pipe % (group_blocks / thread_k_blocks) == 0) {
|
||||
if (zp_sh_wr_pred) {
|
||||
cp_async4(&sh_zp_stage[zp_sh_wr], &zp_ptr[zp_gl_rd]);
|
||||
}
|
||||
zp_gl_rd += zp_gl_rd_delta;
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < zp_tb_groups; i++) {
|
||||
if (zp_sh_wr_pred) {
|
||||
cp_async4(&sh_zp_stage[i * zp_sh_stride + zp_sh_wr], &zp_ptr[zp_gl_rd]);
|
||||
}
|
||||
zp_gl_rd += zp_gl_rd_delta;
|
||||
if (pipe % div_ceil(group_blocks, thread_k_blocks) == 0) {
|
||||
if (zp_sh_wr_pred) {
|
||||
cp_async4(&sh_zp_stage[zp_sh_wr], &zp_ptr[zp_gl_rd]);
|
||||
}
|
||||
zp_gl_rd += zp_gl_rd_delta * zp_tb_groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1021,35 +1011,32 @@ __global__ void Marlin(
|
||||
}
|
||||
} else if constexpr (group_blocks != -1) {
|
||||
if constexpr (group_blocks >= thread_k_blocks) {
|
||||
if (k % b_sh_wr_iters == 0) {
|
||||
int4* sh_s_stage =
|
||||
sh_s + s_sh_stage * ((group_blocks / thread_k_blocks) * (pipe / (group_blocks / thread_k_blocks)));
|
||||
reinterpret_cast<int4*>(&frag_s[k % 2])[0] = sh_s_stage[s_sh_rd];
|
||||
} else {
|
||||
reinterpret_cast<int4*>(&frag_s[1])[0] = reinterpret_cast<int4*>(&frag_s[0])[0];
|
||||
constexpr int g = group_blocks / thread_k_blocks;
|
||||
if (pipe % g == 0) {
|
||||
if (k % b_sh_wr_iters == 0) {
|
||||
int4* sh_s_stage = sh_s + s_sh_stage * (g * (pipe / g));
|
||||
reinterpret_cast<int4*>(&frag_s[k % 2])[0] = sh_s_stage[s_sh_rd];
|
||||
} else {
|
||||
reinterpret_cast<int4*>(&frag_s[1])[0] = reinterpret_cast<int4*>(&frag_s[0])[0];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
auto warp_id = threadIdx.x / 32;
|
||||
int n_warps = thread_n_blocks / 4;
|
||||
|
||||
int warp_row = warp_id / n_warps;
|
||||
|
||||
int cur_k = warp_row * 16;
|
||||
cur_k += k_iter_size * (k % b_sh_wr_iters);
|
||||
|
||||
int k_blocks = cur_k / 16;
|
||||
int cur_group_id = k_blocks / (group_blocks * (w_type == host::kFE2M1f ? 2 : 1));
|
||||
int cur_group_id = k_blocks / group_blocks;
|
||||
|
||||
int4* sh_s_stage = sh_s + s_sh_stage * pipe;
|
||||
|
||||
if constexpr (w_type_id != host::kFE2M1f.id()) {
|
||||
if constexpr (!is_8bit_scale) {
|
||||
reinterpret_cast<int4*>(&frag_s[k % 2])[0] = sh_s_stage[s_sh_rd + cur_group_id * s_sh_stride];
|
||||
} else if constexpr (group_blocks == 1 || thread_k_blocks > 4) {
|
||||
reinterpret_cast<int2*>(&frag_s[k % 2])[0] =
|
||||
reinterpret_cast<int2*>(sh_s_stage)[s_sh_rd + cur_group_id * (2 * s_sh_stride)];
|
||||
} else {
|
||||
reinterpret_cast<int2*>(&frag_s[k % 2])[0] =
|
||||
reinterpret_cast<int2*>(sh_s_stage)[s_sh_rd + cur_group_id * (2 * s_sh_stride) + k % 2];
|
||||
reinterpret_cast<int2*>(sh_s_stage)[s_sh_rd + cur_group_id * (2 * s_sh_stride)];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1243,17 +1230,16 @@ __global__ void Marlin(
|
||||
}
|
||||
}
|
||||
|
||||
// Commented out FP4/FP8 scale dequantization since we don't generate
|
||||
// kFE2M1f kernels to reduce compilation time
|
||||
// if constexpr (w_type == host::kFE2M1f) {
|
||||
// int s_quant_0 = reinterpret_cast<int*>(frag_s[k2])[0];
|
||||
// int s_quant_1 = reinterpret_cast<int*>(frag_s[k2])[1];
|
||||
//
|
||||
// dequant_fp8_scales<scalar_t2, s_type_id>(
|
||||
// s_quant_0, reinterpret_cast<scalar_t2*>(&frag_s[k2]));
|
||||
// dequant_fp8_scales<scalar_t2, s_type_id>(
|
||||
// s_quant_1, reinterpret_cast<scalar_t2*>(&frag_s[k2]) + 2);
|
||||
// }
|
||||
// FP4/FP8 scale dequantization (E4M3 for NVFP4 and E8M0 for MXFP4).
|
||||
if constexpr (
|
||||
(s_type == host::kFE4M3fn || s_type == host::kFE8M0fnu) &&
|
||||
!(std::is_same<scalar_t2, half2>::value && s_type == host::kFE8M0fnu)) {
|
||||
int s_quant_0 = reinterpret_cast<int*>(frag_s[k2])[0];
|
||||
int s_quant_1 = reinterpret_cast<int*>(frag_s[k2])[1];
|
||||
|
||||
dequant_fp8_scales<scalar_t2, s_type_id>(s_quant_0, reinterpret_cast<scalar_t2*>(&frag_s[k2]));
|
||||
dequant_fp8_scales<scalar_t2, s_type_id>(s_quant_1, reinterpret_cast<scalar_t2*>(&frag_s[k2]) + 2);
|
||||
}
|
||||
|
||||
// We have the m dimension as the inner loop in order to encourage overlapping
|
||||
// dequantization and matmul operations.
|
||||
@@ -1882,8 +1868,20 @@ __global__ void Marlin(
|
||||
slice_k_start_shared_fetch = slice_k_start;
|
||||
slice_n_offset = act_s_col_tb_stride * slice_col;
|
||||
} else {
|
||||
s_gl_rd = s_sh_stride * slice_col + threadIdx.x;
|
||||
zp_gl_rd = zp_sh_stride * slice_col + threadIdx.x;
|
||||
if constexpr (group_blocks == -1) {
|
||||
s_gl_rd = s_sh_stride * slice_col + threadIdx.x;
|
||||
zp_gl_rd = zp_sh_stride * slice_col + threadIdx.x;
|
||||
} else if constexpr (group_blocks >= thread_k_blocks) {
|
||||
s_gl_rd =
|
||||
s_gl_stride * ((thread_k_blocks * slice_row) / group_blocks) + s_sh_stride * slice_col + threadIdx.x;
|
||||
zp_gl_rd =
|
||||
zp_gl_stride * ((thread_k_blocks * slice_row) / group_blocks) + zp_sh_stride * slice_col + threadIdx.x;
|
||||
} else {
|
||||
s_gl_rd = s_gl_stride * ((thread_k_blocks * slice_row) / group_blocks + threadIdx.x / s_sh_stride) +
|
||||
s_sh_stride * slice_col + threadIdx.x % s_sh_stride;
|
||||
zp_gl_rd = zp_gl_stride * ((thread_k_blocks * slice_row) / group_blocks + threadIdx.x / zp_sh_stride) +
|
||||
zp_sh_stride * slice_col + threadIdx.x % zp_sh_stride;
|
||||
}
|
||||
}
|
||||
start_pipes();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
|
||||
#include <sgl_kernel/deepseek_v4/kvcacheio.cuh>
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
|
||||
@@ -81,10 +83,21 @@ struct SmemLayout {
|
||||
};
|
||||
|
||||
// Each block processes one request
|
||||
// req_pool_indices are int64_t (pool indices can be large), seq_lens can be int32_t or int64_t
|
||||
// req_pool_indices and seq_lens can each be int32_t or int64_t
|
||||
// Layout: [HOT_BUFFER_SIZE slots for LRU] + [page_size slots for newest token]
|
||||
// newest_slot is at HOT_BUFFER_SIZE (first position of extra page)
|
||||
template <int BLOCK_SIZE, int NUM_TOP_K, int HOT_BUFFER_SIZE, bool IsMLA, typename SeqLensT>
|
||||
//
|
||||
// IsDsv4Layout selects the miss-copy addressing:
|
||||
// false -> generic byte-stride: device + host both linear, stride = item_size_bytes
|
||||
// true -> DSv4 page-padded device + linear host (kvcacheio.cuh hardcoded constants)
|
||||
template <
|
||||
int BLOCK_SIZE,
|
||||
int NUM_TOP_K,
|
||||
int HOT_BUFFER_SIZE,
|
||||
bool IsMLA,
|
||||
bool IsDsv4Layout,
|
||||
typename SeqLensT,
|
||||
typename ReqPoolIndicesT>
|
||||
__global__ void load_cache_to_device_buffer_kernel(
|
||||
const int32_t* __restrict__ top_k_tokens,
|
||||
int32_t* __restrict__ device_buffer_tokens,
|
||||
@@ -95,7 +108,7 @@ __global__ void load_cache_to_device_buffer_kernel(
|
||||
void* __restrict__ device_buffer_k,
|
||||
void* __restrict__ device_buffer_v,
|
||||
int32_t* __restrict__ top_k_device_locs,
|
||||
const int64_t* __restrict__ req_pool_indices,
|
||||
const ReqPoolIndicesT* __restrict__ req_pool_indices,
|
||||
const SeqLensT* __restrict__ seq_lens,
|
||||
int16_t* __restrict__ lru_slots,
|
||||
const int32_t* __restrict__ num_real_reqs,
|
||||
@@ -106,6 +119,7 @@ __global__ void load_cache_to_device_buffer_kernel(
|
||||
int64_t top_k_device_locs_stride,
|
||||
int64_t page_size,
|
||||
int64_t item_size_bytes) {
|
||||
static_assert(!IsDsv4Layout || IsMLA, "DSv4 page-padded layout is K-only (MLA).");
|
||||
// todo hisparse: support page wise sparsity
|
||||
constexpr int NUM_WARPS = BLOCK_SIZE / WARP_SIZE;
|
||||
constexpr int NUM_TOKEN_CHUNKS = (NUM_TOP_K + WARP_SIZE - 1) / WARP_SIZE;
|
||||
@@ -157,16 +171,16 @@ __global__ void load_cache_to_device_buffer_kernel(
|
||||
int32_t* s_chunk_offset = s_top_k_tokens + NUM_TOP_K;
|
||||
// Prefix-sum offsets for evictable counting
|
||||
int32_t* s_evict_chunk_offset = s_chunk_offset + (NUM_BUFFER_CHUNKS + 1);
|
||||
// Open-addressing hash table: top-k token_id → top-k index (keys)
|
||||
// Open-addressing hash table: top-k token_id -> top-k index (keys)
|
||||
int32_t* s_hash_keys = s_evict_chunk_offset + (NUM_BUFFER_CHUNKS + 1);
|
||||
// Scalar counters
|
||||
int32_t& s_total_hits = s_hash_keys[HASH_SIZE];
|
||||
int32_t& s_newest_hit = s_hash_keys[HASH_SIZE + 1];
|
||||
|
||||
int16_t* smem_i16 = reinterpret_cast<int16_t*>(smem_i32 + Layout::TOTAL_INT32);
|
||||
// Compacted slot ordering: [hits fwd→ ... ←evictables bwd]
|
||||
// Compacted slot ordering: [hits fwd-> ... <-evictables bwd]
|
||||
int16_t* s_lru_slots_out = smem_i16;
|
||||
// Open-addressing hash table: top-k token_id → top-k index (values)
|
||||
// Open-addressing hash table: top-k token_id -> top-k index (values)
|
||||
int16_t* s_hash_vals = s_lru_slots_out + HOT_BUFFER_SIZE;
|
||||
|
||||
// Initialize shared memory: counters, hash table, prefix-sum offsets.
|
||||
@@ -362,19 +376,30 @@ __global__ void load_cache_to_device_buffer_kernel(
|
||||
const int64_t src_loc = req_host_cache_locs[miss_token];
|
||||
const int64_t dst_loc = static_cast<int64_t>(req_device_buffer_locs[evict_slot]);
|
||||
|
||||
const auto src_k = static_cast<const char*>(host_cache_k) + src_loc * item_size_bytes;
|
||||
auto dst_k = static_cast<char*>(device_buffer_k) + dst_loc * item_size_bytes;
|
||||
transfer_item_warp(lane_id, src_k, dst_k, item_size_bytes);
|
||||
if constexpr (IsDsv4Layout) {
|
||||
// DSv4 path: page-padded device layout + linear host layout, K-only.
|
||||
// Uses kvcacheio.cuh's hardcoded constants (kGPUPageSize=64, kCPUItemBytes=584).
|
||||
device::hisparse::transfer_item<device::hisparse::TransferDirection::HostToDevice>(
|
||||
/*dst_cache=*/device_buffer_k,
|
||||
/*src_cache=*/const_cast<void*>(host_cache_k),
|
||||
/*dst_index=*/static_cast<int32_t>(dst_loc),
|
||||
/*src_index=*/static_cast<int32_t>(src_loc));
|
||||
} else {
|
||||
// Generic path: device + host both linear, stride = item_size_bytes.
|
||||
const auto src_k = static_cast<const char*>(host_cache_k) + src_loc * item_size_bytes;
|
||||
auto dst_k = static_cast<char*>(device_buffer_k) + dst_loc * item_size_bytes;
|
||||
transfer_item_warp(lane_id, src_k, dst_k, item_size_bytes);
|
||||
|
||||
if constexpr (!IsMLA) {
|
||||
const auto src_v = static_cast<const char*>(host_cache_v) + src_loc * item_size_bytes;
|
||||
auto dst_v = static_cast<char*>(device_buffer_v) + dst_loc * item_size_bytes;
|
||||
transfer_item_warp(lane_id, src_v, dst_v, item_size_bytes);
|
||||
if constexpr (!IsMLA) {
|
||||
const auto src_v = static_cast<const char*>(host_cache_v) + src_loc * item_size_bytes;
|
||||
auto dst_v = static_cast<char*>(device_buffer_v) + dst_loc * item_size_bytes;
|
||||
transfer_item_warp(lane_id, src_v, dst_v, item_size_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <int BLOCK_SIZE, int NUM_TOP_K, int HOT_BUFFER_SIZE, bool IsMLA>
|
||||
template <int BLOCK_SIZE, int NUM_TOP_K, int HOT_BUFFER_SIZE, bool IsMLA, bool IsDsv4Layout>
|
||||
void load_cache_to_device_buffer(
|
||||
tvm::ffi::TensorView top_k_tokens,
|
||||
tvm::ffi::TensorView device_buffer_tokens,
|
||||
@@ -401,9 +426,9 @@ void load_cache_to_device_buffer(
|
||||
const int64_t top_k_device_locs_stride = top_k_device_locs.strides()[0];
|
||||
const auto device = LaunchKernel::resolve_device(top_k_tokens.device());
|
||||
|
||||
// Generic lambda: both int32 and int64 kernel variants are compiled;
|
||||
// the correct one is selected at runtime based on seq_lens dtype.
|
||||
auto launch = [&](auto kernel_fn, const auto* seq_lens_ptr) {
|
||||
// Generic lambda: int32/int64 kernel variants are compiled for both
|
||||
// seq_lens and req_pool_indices; the correct combo is selected at runtime.
|
||||
auto launch = [&](auto kernel_fn, const auto* seq_lens_ptr, const auto* req_pool_indices_ptr) {
|
||||
constexpr size_t smem_bytes = SmemLayout<NUM_TOP_K, HOT_BUFFER_SIZE>::BYTES;
|
||||
if constexpr (smem_bytes > 48u * 1024u) {
|
||||
cudaFuncSetAttribute(kernel_fn, cudaFuncAttributeMaxDynamicSharedMemorySize, smem_bytes);
|
||||
@@ -419,7 +444,7 @@ void load_cache_to_device_buffer(
|
||||
device_buffer_k.data_ptr(),
|
||||
(IsMLA || device_buffer_v.ndim() == 0) ? (void*)nullptr : device_buffer_v.data_ptr(),
|
||||
static_cast<int32_t*>(top_k_device_locs.data_ptr()),
|
||||
static_cast<const int64_t*>(req_pool_indices.data_ptr()),
|
||||
req_pool_indices_ptr,
|
||||
seq_lens_ptr,
|
||||
static_cast<int16_t*>(lru_slots.data_ptr()),
|
||||
static_cast<const int32_t*>(num_real_reqs.data_ptr()),
|
||||
@@ -432,15 +457,59 @@ void load_cache_to_device_buffer(
|
||||
item_size_bytes);
|
||||
};
|
||||
|
||||
const auto dtype = seq_lens.dtype();
|
||||
if (dtype.code == kDLInt && dtype.bits == 64) {
|
||||
const auto seq_dtype = seq_lens.dtype();
|
||||
const auto rpi_dtype = req_pool_indices.dtype();
|
||||
const bool seq_is_i64 = (seq_dtype.code == kDLInt && seq_dtype.bits == 64);
|
||||
const bool rpi_is_i64 = (rpi_dtype.code == kDLInt && rpi_dtype.bits == 64);
|
||||
|
||||
if (seq_is_i64 && rpi_is_i64) {
|
||||
launch(
|
||||
load_cache_to_device_buffer_kernel<BLOCK_SIZE, NUM_TOP_K, HOT_BUFFER_SIZE, IsMLA, int64_t>,
|
||||
static_cast<const int64_t*>(seq_lens.data_ptr()));
|
||||
load_cache_to_device_buffer_kernel<
|
||||
BLOCK_SIZE,
|
||||
NUM_TOP_K,
|
||||
HOT_BUFFER_SIZE,
|
||||
IsMLA,
|
||||
IsDsv4Layout,
|
||||
int64_t,
|
||||
int64_t>,
|
||||
static_cast<const int64_t*>(seq_lens.data_ptr()),
|
||||
static_cast<const int64_t*>(req_pool_indices.data_ptr()));
|
||||
} else if (seq_is_i64 && !rpi_is_i64) {
|
||||
launch(
|
||||
load_cache_to_device_buffer_kernel<
|
||||
BLOCK_SIZE,
|
||||
NUM_TOP_K,
|
||||
HOT_BUFFER_SIZE,
|
||||
IsMLA,
|
||||
IsDsv4Layout,
|
||||
int64_t,
|
||||
int32_t>,
|
||||
static_cast<const int64_t*>(seq_lens.data_ptr()),
|
||||
static_cast<const int32_t*>(req_pool_indices.data_ptr()));
|
||||
} else if (!seq_is_i64 && rpi_is_i64) {
|
||||
launch(
|
||||
load_cache_to_device_buffer_kernel<
|
||||
BLOCK_SIZE,
|
||||
NUM_TOP_K,
|
||||
HOT_BUFFER_SIZE,
|
||||
IsMLA,
|
||||
IsDsv4Layout,
|
||||
int32_t,
|
||||
int64_t>,
|
||||
static_cast<const int32_t*>(seq_lens.data_ptr()),
|
||||
static_cast<const int64_t*>(req_pool_indices.data_ptr()));
|
||||
} else {
|
||||
launch(
|
||||
load_cache_to_device_buffer_kernel<BLOCK_SIZE, NUM_TOP_K, HOT_BUFFER_SIZE, IsMLA, int32_t>,
|
||||
static_cast<const int32_t*>(seq_lens.data_ptr()));
|
||||
load_cache_to_device_buffer_kernel<
|
||||
BLOCK_SIZE,
|
||||
NUM_TOP_K,
|
||||
HOT_BUFFER_SIZE,
|
||||
IsMLA,
|
||||
IsDsv4Layout,
|
||||
int32_t,
|
||||
int32_t>,
|
||||
static_cast<const int32_t*>(seq_lens.data_ptr()),
|
||||
static_cast<const int32_t*>(req_pool_indices.data_ptr()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
#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 <cfloat>
|
||||
#include <cstdint>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kWarpSize = 32;
|
||||
constexpr uint32_t kWarpsPerCTA = 6;
|
||||
constexpr uint32_t kSmallTokenThreshold = 512;
|
||||
constexpr uint32_t kMaxExperts = 512;
|
||||
constexpr uint32_t kMaxTopK = 16;
|
||||
|
||||
enum class ScoringFunc : uint32_t {
|
||||
kSigmoid = 0,
|
||||
kSqrtSoftplus = 1,
|
||||
};
|
||||
|
||||
struct MoEFusedGateParams {
|
||||
const float* __restrict__ input;
|
||||
const float* __restrict__ bias;
|
||||
float* __restrict__ output;
|
||||
int32_t* __restrict__ indices;
|
||||
uint32_t num_rows;
|
||||
uint32_t num_experts;
|
||||
uint32_t topk;
|
||||
uint32_t num_fused_shared_experts;
|
||||
bool renormalize;
|
||||
float routed_scaling_factor;
|
||||
bool apply_routed_scaling_factor_on_output;
|
||||
};
|
||||
|
||||
template <ScoringFunc kScoringFunc>
|
||||
__device__ __forceinline__ float compute_score(float x) {
|
||||
if constexpr (kScoringFunc == ScoringFunc::kSigmoid) {
|
||||
// sigmoid(x) = 1 / (1 + exp(-x))
|
||||
return 1.0f / (1.0f + expf(-x));
|
||||
} else {
|
||||
// sqrt(softplus(x)) = sqrt(log(1 + exp(x)))
|
||||
float softplus = log1pf(expf(x));
|
||||
return sqrtf(softplus);
|
||||
}
|
||||
}
|
||||
|
||||
template <uint32_t kWarpsPerToken, ScoringFunc kScoringFunc>
|
||||
__global__ void moe_fused_gate_kernel_small_token(const MoEFusedGateParams __grid_constant__ params) {
|
||||
const auto& [input, bias, output, indices, num_rows, num_experts, topk, num_fused_shared_experts, renormalize, routed_scaling_factor, apply_routed_scaling_factor_on_output] =
|
||||
params;
|
||||
|
||||
uint32_t row_idx = blockIdx.x;
|
||||
if (row_idx >= num_rows) return;
|
||||
|
||||
// number of routed experts to select (excluding fused shared experts)
|
||||
const uint32_t topk_routed = topk - num_fused_shared_experts;
|
||||
|
||||
uint32_t tid = threadIdx.x;
|
||||
uint32_t warp_id = tid / kWarpSize;
|
||||
uint32_t lane_id = tid % kWarpSize;
|
||||
|
||||
extern __shared__ float shared_mem[];
|
||||
float* shared_scores = shared_mem;
|
||||
float* shared_original_scores = shared_mem + num_experts;
|
||||
|
||||
// For warp-level reduction
|
||||
__shared__ float warp_maxs[kWarpsPerToken];
|
||||
__shared__ int warp_experts[kWarpsPerToken];
|
||||
__shared__ int selected_experts[kMaxTopK];
|
||||
|
||||
for (uint32_t e = tid; e < num_experts; e += blockDim.x) {
|
||||
float input_val = input[row_idx * num_experts + e];
|
||||
float bias_val = bias[e];
|
||||
float score_val = compute_score<kScoringFunc>(input_val);
|
||||
float biased_val = score_val + bias_val;
|
||||
shared_scores[e] = biased_val;
|
||||
shared_original_scores[e] = score_val;
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
|
||||
// only select topk_routed experts (excluding shared experts)
|
||||
for (uint32_t k = 0; k < topk_routed; k++) {
|
||||
float my_val = -FLT_MAX;
|
||||
int my_expert = -1;
|
||||
for (uint32_t e = tid; e < num_experts; e += blockDim.x) {
|
||||
if (shared_scores[e] > my_val) {
|
||||
my_val = shared_scores[e];
|
||||
my_expert = e;
|
||||
}
|
||||
}
|
||||
|
||||
float warp_max_val = my_val;
|
||||
int warp_max_expert = my_expert;
|
||||
|
||||
#pragma unroll
|
||||
for (int offset = 16; offset > 0; offset /= 2) {
|
||||
float other_val = __shfl_down_sync(0xFFFFFFFF, warp_max_val, offset);
|
||||
int other_expert = __shfl_down_sync(0xFFFFFFFF, warp_max_expert, offset);
|
||||
if (other_val > warp_max_val) {
|
||||
warp_max_val = other_val;
|
||||
warp_max_expert = other_expert;
|
||||
}
|
||||
}
|
||||
|
||||
if (lane_id == 0 && warp_id < kWarpsPerToken) {
|
||||
warp_maxs[warp_id] = warp_max_val;
|
||||
warp_experts[warp_id] = warp_max_expert;
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
|
||||
if (warp_id == 0) {
|
||||
float final_max = (lane_id < kWarpsPerToken) ? warp_maxs[lane_id] : -FLT_MAX;
|
||||
int final_expert = (lane_id < kWarpsPerToken) ? warp_experts[lane_id] : -1;
|
||||
|
||||
#pragma unroll
|
||||
for (int offset = 16; offset > 0; offset /= 2) {
|
||||
float other_val = __shfl_down_sync(0xFFFFFFFF, final_max, offset);
|
||||
int other_expert = __shfl_down_sync(0xFFFFFFFF, final_expert, offset);
|
||||
if (other_val > final_max) {
|
||||
final_max = other_val;
|
||||
final_expert = other_expert;
|
||||
}
|
||||
}
|
||||
|
||||
if (lane_id == 0) {
|
||||
selected_experts[k] = final_expert;
|
||||
}
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
|
||||
int selected = selected_experts[k];
|
||||
if (selected >= 0 && tid == 0) {
|
||||
shared_scores[selected] = -FLT_MAX;
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
static_assert(kMaxTopK <= device::kWarpThreads);
|
||||
if (tid >= device::kWarpThreads) return;
|
||||
|
||||
// only use the first warp to perform write to global operation
|
||||
float routed_weight = 0.0f;
|
||||
int32_t selected_expert = 0;
|
||||
if (tid < topk_routed) {
|
||||
int expert_id = selected_experts[tid];
|
||||
float score = shared_original_scores[expert_id];
|
||||
if (expert_id >= 0 && expert_id < static_cast<int>(num_experts)) {
|
||||
routed_weight = score;
|
||||
selected_expert = expert_id;
|
||||
}
|
||||
}
|
||||
const auto routed_sum = device::warp::reduce_sum<kMaxTopK>(routed_weight);
|
||||
if (tid < topk) {
|
||||
const bool is_shared = tid >= topk_routed;
|
||||
const auto output_offset = row_idx * topk + tid;
|
||||
const auto weight = is_shared ? (routed_sum / routed_scaling_factor) : routed_weight;
|
||||
const auto expert_id = is_shared ? (num_experts + tid - topk_routed) : selected_expert;
|
||||
const auto scale = apply_routed_scaling_factor_on_output ? routed_scaling_factor : 1.0f;
|
||||
const auto norm = renormalize && routed_sum > 0.0f ? routed_sum : 1.0f;
|
||||
output[output_offset] = weight / norm * scale;
|
||||
indices[output_offset] = expert_id;
|
||||
}
|
||||
}
|
||||
|
||||
template <ScoringFunc kScoringFunc>
|
||||
__global__ void moe_fused_gate_kernel(const MoEFusedGateParams __grid_constant__ params) {
|
||||
const auto& [input, bias, output, indices, num_rows, num_experts, topk, num_fused_shared_experts, renormalize, routed_scaling_factor, apply_routed_scaling_factor_on_output] =
|
||||
params;
|
||||
|
||||
uint32_t row_idx = blockIdx.x * kWarpsPerCTA + threadIdx.y;
|
||||
if (row_idx >= num_rows) return;
|
||||
|
||||
// number of routed experts to select (excluding fused shared experts)
|
||||
const uint32_t topk_routed = topk - num_fused_shared_experts;
|
||||
|
||||
uint32_t lane_id = threadIdx.x;
|
||||
uint32_t warp_id = threadIdx.y;
|
||||
|
||||
extern __shared__ float shared_mem[];
|
||||
float* shared_scores = shared_mem + warp_id * num_experts * 2;
|
||||
float* shared_original_scores = shared_scores + num_experts;
|
||||
__shared__ int selected_experts[kWarpsPerCTA][kMaxTopK];
|
||||
int* warp_selected_experts = selected_experts[warp_id];
|
||||
|
||||
for (uint32_t e = lane_id; e < num_experts; e += kWarpSize) {
|
||||
float input_val = input[row_idx * num_experts + e];
|
||||
float bias_val = bias[e];
|
||||
float score_val = compute_score<kScoringFunc>(input_val);
|
||||
float biased_val = score_val + bias_val;
|
||||
shared_scores[e] = biased_val;
|
||||
shared_original_scores[e] = score_val;
|
||||
}
|
||||
|
||||
__syncwarp();
|
||||
|
||||
// only select topk_routed experts
|
||||
for (uint32_t k = 0; k < topk_routed; k++) {
|
||||
float max_val = -FLT_MAX;
|
||||
int max_expert = -1;
|
||||
|
||||
for (uint32_t expert = lane_id; expert < num_experts; expert += kWarpSize) {
|
||||
if (shared_scores[expert] > max_val) {
|
||||
max_val = shared_scores[expert];
|
||||
max_expert = expert;
|
||||
}
|
||||
}
|
||||
|
||||
for (int offset = kWarpSize / 2; offset > 0; offset /= 2) {
|
||||
float other_val = __shfl_down_sync(0xFFFFFFFF, max_val, offset);
|
||||
int other_expert = __shfl_down_sync(0xFFFFFFFF, max_expert, offset);
|
||||
|
||||
if (other_val > max_val || (other_val == max_val && other_expert < max_expert)) {
|
||||
max_val = other_val;
|
||||
max_expert = other_expert;
|
||||
}
|
||||
}
|
||||
|
||||
if (lane_id == 0) {
|
||||
warp_selected_experts[k] = max_expert;
|
||||
if (max_expert != -1) {
|
||||
shared_scores[max_expert] = -FLT_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
__syncwarp();
|
||||
}
|
||||
|
||||
static_assert(kMaxTopK <= device::kWarpThreads);
|
||||
|
||||
float routed_weight = 0.0f;
|
||||
int32_t selected_expert = 0;
|
||||
if (lane_id < topk_routed) {
|
||||
int expert_id = warp_selected_experts[lane_id];
|
||||
if (expert_id >= 0 && expert_id < static_cast<int>(num_experts)) {
|
||||
routed_weight = shared_original_scores[expert_id];
|
||||
selected_expert = expert_id;
|
||||
}
|
||||
}
|
||||
const auto routed_sum = device::warp::reduce_sum<kMaxTopK>(routed_weight);
|
||||
if (lane_id < topk) {
|
||||
const bool is_shared = lane_id >= topk_routed;
|
||||
const auto output_idx = row_idx * topk + lane_id;
|
||||
const auto weight = is_shared ? (routed_sum / routed_scaling_factor) : routed_weight;
|
||||
const auto expert_id = is_shared ? (num_experts + lane_id - topk_routed) : selected_expert;
|
||||
const auto scale = apply_routed_scaling_factor_on_output ? routed_scaling_factor : 1.0f;
|
||||
const auto norm = renormalize && routed_sum > 0.0f ? routed_sum : 1.0f;
|
||||
output[output_idx] = weight / norm * scale;
|
||||
indices[output_idx] = expert_id;
|
||||
}
|
||||
}
|
||||
|
||||
template <ScoringFunc kScoringFunc>
|
||||
void dispatch_small_token_kernel(
|
||||
uint32_t num_rows,
|
||||
uint32_t threads_per_block,
|
||||
uint32_t warps_per_token,
|
||||
DLDevice device,
|
||||
size_t smem_per_row,
|
||||
const MoEFusedGateParams& params) {
|
||||
using namespace host;
|
||||
if (warps_per_token <= 8) {
|
||||
LaunchKernel(num_rows, threads_per_block, device, smem_per_row)(
|
||||
moe_fused_gate_kernel_small_token<8, kScoringFunc>, params);
|
||||
} else if (warps_per_token <= 12) {
|
||||
LaunchKernel(num_rows, threads_per_block, device, smem_per_row)(
|
||||
moe_fused_gate_kernel_small_token<12, kScoringFunc>, params);
|
||||
} else {
|
||||
LaunchKernel(num_rows, threads_per_block, device, smem_per_row)(
|
||||
moe_fused_gate_kernel_small_token<16, kScoringFunc>, params);
|
||||
}
|
||||
}
|
||||
|
||||
struct MoEFusedGateKernel {
|
||||
static void
|
||||
run(const tvm::ffi::TensorView input,
|
||||
const tvm::ffi::TensorView bias,
|
||||
const tvm::ffi::TensorView output,
|
||||
const tvm::ffi::TensorView indices,
|
||||
uint32_t topk,
|
||||
uint32_t scoring_func, // 0 = sigmoid, 1 = sqrtsoftplus
|
||||
uint32_t num_fused_shared_experts,
|
||||
bool renormalize,
|
||||
float routed_scaling_factor,
|
||||
bool apply_routed_scaling_factor_on_output) {
|
||||
using namespace host;
|
||||
|
||||
auto N = SymbolicSize{"num_rows"};
|
||||
auto E = SymbolicSize{"num_experts"};
|
||||
auto K = SymbolicSize{"topk"};
|
||||
auto device = SymbolicDevice{};
|
||||
K.set_value(topk);
|
||||
device.set_options<kDLCUDA>();
|
||||
|
||||
TensorMatcher({N, E}).with_dtype<float>().with_device(device).verify(input);
|
||||
TensorMatcher({E}).with_dtype<float>().with_device(device).verify(bias);
|
||||
TensorMatcher({N, K}).with_dtype<float>().with_device(device).verify(output);
|
||||
TensorMatcher({N, K}).with_dtype<int32_t>().with_device(device).verify(indices);
|
||||
|
||||
const auto num_rows = static_cast<uint32_t>(N.unwrap());
|
||||
const auto num_experts = static_cast<uint32_t>(E.unwrap());
|
||||
|
||||
RuntimeCheck(num_experts <= kMaxExperts, "num_experts exceeds maximum supported value");
|
||||
RuntimeCheck(scoring_func <= 1, "scoring_func must be 0 (sigmoid) or 1 (sqrtsoftplus)");
|
||||
RuntimeCheck(topk > num_fused_shared_experts, "topk must be greater than num_fused_shared_experts");
|
||||
|
||||
const auto params = MoEFusedGateParams{
|
||||
.input = static_cast<const float*>(input.data_ptr()),
|
||||
.bias = static_cast<const float*>(bias.data_ptr()),
|
||||
.output = static_cast<float*>(output.data_ptr()),
|
||||
.indices = static_cast<int32_t*>(indices.data_ptr()),
|
||||
.num_rows = num_rows,
|
||||
.num_experts = num_experts,
|
||||
.topk = topk,
|
||||
.num_fused_shared_experts = num_fused_shared_experts,
|
||||
.renormalize = renormalize,
|
||||
.routed_scaling_factor = routed_scaling_factor,
|
||||
.apply_routed_scaling_factor_on_output = apply_routed_scaling_factor_on_output,
|
||||
};
|
||||
|
||||
const size_t smem_per_row = 2 * num_experts * sizeof(float);
|
||||
|
||||
bool use_small_token_kernel = num_rows <= kSmallTokenThreshold;
|
||||
|
||||
if (use_small_token_kernel) {
|
||||
// 1 token per block
|
||||
uint32_t warps_per_token = div_ceil(num_experts, kWarpSize);
|
||||
warps_per_token = std::min(warps_per_token, 16u);
|
||||
uint32_t threads_per_block = warps_per_token * kWarpSize;
|
||||
|
||||
if (scoring_func == 0) {
|
||||
dispatch_small_token_kernel<ScoringFunc::kSigmoid>(
|
||||
num_rows, threads_per_block, warps_per_token, device.unwrap(), smem_per_row, params);
|
||||
} else {
|
||||
dispatch_small_token_kernel<ScoringFunc::kSqrtSoftplus>(
|
||||
num_rows, threads_per_block, warps_per_token, device.unwrap(), smem_per_row, params);
|
||||
}
|
||||
} else {
|
||||
// multiple tokens per block
|
||||
uint32_t num_blocks = div_ceil(num_rows, kWarpsPerCTA);
|
||||
dim3 block_dim(kWarpSize, kWarpsPerCTA);
|
||||
size_t large_smem = smem_per_row * kWarpsPerCTA;
|
||||
|
||||
if (scoring_func == 0) {
|
||||
LaunchKernel(num_blocks, block_dim, device.unwrap(), large_smem)(
|
||||
moe_fused_gate_kernel<ScoringFunc::kSigmoid>, params);
|
||||
} else {
|
||||
LaunchKernel(num_blocks, block_dim, device.unwrap(), large_smem)(
|
||||
moe_fused_gate_kernel<ScoringFunc::kSqrtSoftplus>, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,908 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Optional, Tuple, Union
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.jit_kernel.utils import (
|
||||
cache_once,
|
||||
is_arch_support_pdl,
|
||||
load_jit,
|
||||
make_cpp_args,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tvm_ffi.module import Module
|
||||
|
||||
|
||||
def make_name(name: str) -> str:
|
||||
return f"dpsk_v4_{name}"
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_common_module() -> Module:
|
||||
return load_jit(
|
||||
make_name("common"),
|
||||
cuda_files=["deepseek_v4/common.cuh"],
|
||||
cuda_wrappers=[("plan_compress_prefill", "plan_compress_prefill")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_compress_128_online_plan_module() -> Module:
|
||||
"""Host-side plan generator for online compress 128 (no template args)."""
|
||||
return load_jit(
|
||||
make_name("compress_128_online_plan"),
|
||||
cuda_files=["deepseek_v4/c128_online.cuh"],
|
||||
cuda_wrappers=[
|
||||
("plan_compress_online_prefill", "plan_compress_online_prefill"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_compress_128_online_module(head_dim: int) -> Module:
|
||||
"""Online compress 128 kernel: ring_size=1, per-index (max, sum, kv) state."""
|
||||
args = make_cpp_args(head_dim, is_arch_support_pdl())
|
||||
kernel_class = f"FlashCompress128OnlineKernel<{args}>"
|
||||
return load_jit(
|
||||
make_name("compress_128_online"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/c128_online.cuh"],
|
||||
cuda_wrappers=[
|
||||
("decode", f"{kernel_class}::run_decode"),
|
||||
("prefill", f"{kernel_class}::run_prefill"),
|
||||
],
|
||||
extra_cuda_cflags=["-use_fast_math"],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_topk_module() -> Module:
|
||||
args = make_cpp_args(is_arch_support_pdl())
|
||||
return load_jit(
|
||||
make_name("topk"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/topk.cuh"],
|
||||
cuda_wrappers=[("topk_transform", f"TopK512Kernel<{args}>::transform")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_topk1024_module() -> Module:
|
||||
args = make_cpp_args(is_arch_support_pdl())
|
||||
return load_jit(
|
||||
make_name("topk1024"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/topk_1024.cuh"],
|
||||
cuda_wrappers=[("topk_transform", f"TopK1024Kernel<{args}>::transform")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_topk_v2_module(topk: int) -> Module:
|
||||
return load_jit(
|
||||
make_name("topk_v2"),
|
||||
str(topk),
|
||||
cuda_files=["deepseek_v4/topk_v2.cuh"],
|
||||
cuda_wrappers=[
|
||||
("topk_transform", "CombinedTopKKernel::transform"),
|
||||
("topk_plan", "CombinedTopKKernel::plan"),
|
||||
],
|
||||
extra_cuda_cflags=[f"-DSGL_TOPK={topk}"],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_mask_topk_module() -> Module:
|
||||
return load_jit(
|
||||
make_name("mask_topk"),
|
||||
cuda_files=["deepseek_v4/hash_topk.cuh"],
|
||||
cuda_wrappers=[("run", "MaskKernel::run")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_hash_topk_module() -> Module:
|
||||
args = make_cpp_args("act_sqrt_softplus", is_arch_support_pdl())
|
||||
return load_jit(
|
||||
make_name("hash_topk"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/hash_topk.cuh"],
|
||||
cuda_wrappers=[("hash_topk", f"HashTopKKernel<{args}>::run")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_compress_module(
|
||||
head_dim: int,
|
||||
dtype_in: torch.dtype,
|
||||
dtype_out: torch.dtype,
|
||||
ratio: Literal[4, 128],
|
||||
) -> Module:
|
||||
args = make_cpp_args(head_dim, dtype_in, dtype_out, is_arch_support_pdl())
|
||||
kernel_class = f"FlashCompress{ratio}Kernel<{args}>"
|
||||
return load_jit(
|
||||
make_name(f"compress_{ratio}"),
|
||||
*args,
|
||||
cuda_files=[f"deepseek_v4/c{ratio}.cuh"],
|
||||
cuda_wrappers=[
|
||||
("decode", f"{kernel_class}::run_decode"),
|
||||
("prefill", f"{kernel_class}::run_prefill"),
|
||||
],
|
||||
extra_cuda_cflags=["-use_fast_math"],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_rmsnorm_head_module(head_dim: int, dtype: torch.dtype):
|
||||
args = make_cpp_args(head_dim, dtype, is_arch_support_pdl())
|
||||
kernel_class = f"RMSNormKernel<{args}>"
|
||||
return load_jit(
|
||||
make_name("rmsnorm_head"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/rmsnorm.cuh"],
|
||||
cuda_wrappers=[("run_self", f"{kernel_class}::run_self")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_fused_rope_module() -> Module:
|
||||
args = make_cpp_args(is_arch_support_pdl())
|
||||
return load_jit(
|
||||
make_name("fused_rope"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/rope.cuh"],
|
||||
cuda_wrappers=[("forward", f"FusedQKRopeKernel<{args}>::forward")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_norm_rope_module(
|
||||
dtype: torch.dtype,
|
||||
head_dim: int,
|
||||
rope_dim: int,
|
||||
) -> Module:
|
||||
args = make_cpp_args(dtype, head_dim, rope_dim, is_arch_support_pdl())
|
||||
return load_jit(
|
||||
make_name("fused_norm_rope"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/fused_norm_rope.cuh"],
|
||||
cuda_wrappers=[
|
||||
("forward", f"FusedNormRopeKernel<{args}>::forward"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_fused_store_module(
|
||||
name: Literal["flashmla", "indexer"],
|
||||
input_dtype: torch.dtype,
|
||||
index_dtype: torch.dtype,
|
||||
page_size: int,
|
||||
) -> Module:
|
||||
args = make_cpp_args(input_dtype, index_dtype, page_size, is_arch_support_pdl())
|
||||
cname = "FlashMLA" if name == "flashmla" else "Indexer"
|
||||
kernel_class = f"FusedStoreCache{cname}Kernel<{args}>"
|
||||
return load_jit(
|
||||
make_name("store_" + name),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/store.cuh"],
|
||||
cuda_wrappers=[("run", f"{kernel_class}::run")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_metadata_module():
|
||||
return load_jit(
|
||||
make_name("metadata"),
|
||||
cuda_files=["deepseek_v4/paged_mqa_metadata.cuh"],
|
||||
cuda_wrappers=[("run", "IndexerMetadataKernel::run")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_silu_mul_quant_varlen_module(
|
||||
quant_group_size: int,
|
||||
scale_ue8m0: bool,
|
||||
swizzle: bool,
|
||||
apply_swiglu_limit: bool,
|
||||
) -> Module:
|
||||
args = make_cpp_args(
|
||||
quant_group_size,
|
||||
scale_ue8m0,
|
||||
swizzle,
|
||||
is_arch_support_pdl(),
|
||||
apply_swiglu_limit,
|
||||
)
|
||||
return load_jit(
|
||||
make_name("silu_mul_quant_varlen"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/silu_and_mul_masked_post_quant.cuh"],
|
||||
cuda_wrappers=[("run", f"SiluAndMulMaskedPostQuantKernel<{args}>::run")],
|
||||
extra_cuda_cflags=["-use_fast_math"],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_silu_mul_quant_contig_module(
|
||||
quant_group_size: int,
|
||||
scale_ue8m0: bool,
|
||||
swizzle: bool,
|
||||
apply_swiglu_limit: bool,
|
||||
) -> Module:
|
||||
args = make_cpp_args(
|
||||
quant_group_size,
|
||||
scale_ue8m0,
|
||||
swizzle,
|
||||
is_arch_support_pdl(),
|
||||
apply_swiglu_limit,
|
||||
)
|
||||
return load_jit(
|
||||
make_name("silu_mul_quant_contig"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/silu_and_mul_masked_post_quant.cuh"],
|
||||
cuda_wrappers=[("run", f"SiluAndMulContigPostQuantKernel<{args}>::run")],
|
||||
extra_cuda_cflags=["-use_fast_math"],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_silu_and_mul_clamp_module(dtype: torch.dtype) -> Module:
|
||||
args = make_cpp_args(dtype, is_arch_support_pdl())
|
||||
return load_jit(
|
||||
make_name("silu_and_mul_clamp"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/silu_and_mul_masked_post_quant.cuh"],
|
||||
cuda_wrappers=[("run", f"SiluAndMulClampKernel<{args}>::run")],
|
||||
extra_cuda_cflags=["-use_fast_math"],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_mega_moe_pre_dispatch_module(quant_group_size: int) -> Module:
|
||||
args = make_cpp_args(quant_group_size, is_arch_support_pdl())
|
||||
return load_jit(
|
||||
make_name("mega_moe_pre_dispatch"),
|
||||
*args,
|
||||
cuda_files=["deepseek_v4/mega_moe_pre_dispatch.cuh"],
|
||||
cuda_wrappers=[("run", f"MegaMoEPreDispatchKernel<{args}>::run")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_hisparse_transfer_module() -> Module:
|
||||
return load_jit(
|
||||
make_name("hisparse_transfer"),
|
||||
cuda_files=["deepseek_v4/hisparse_transfer.cuh"],
|
||||
cuda_wrappers=[("hisparse_transfer", "hisparse_transfer")],
|
||||
)
|
||||
|
||||
|
||||
def hisparse_offload_to_host(
|
||||
gpu_ptrs: torch.Tensor,
|
||||
cpu_ptrs: torch.Tensor,
|
||||
gpu_indices: torch.Tensor,
|
||||
cpu_indices: torch.Tensor,
|
||||
) -> None:
|
||||
module = _jit_hisparse_transfer_module()
|
||||
module.hisparse_transfer(gpu_ptrs, cpu_ptrs, gpu_indices, cpu_indices)
|
||||
|
||||
|
||||
def topk_transform_512(
|
||||
scores: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
page_tables: torch.Tensor,
|
||||
out_page_indices: torch.Tensor,
|
||||
page_size: int,
|
||||
out_raw_indices: Optional[torch.Tensor] = None,
|
||||
) -> None:
|
||||
if out_page_indices.shape[1] == 512:
|
||||
module = _jit_topk_module()
|
||||
else:
|
||||
module = _jit_topk1024_module()
|
||||
module.topk_transform(
|
||||
scores, seq_lens, page_tables, out_page_indices, page_size, out_raw_indices
|
||||
)
|
||||
|
||||
|
||||
_WORKSPACE_INTS_PER_BATCH = 2 + 1024 * 2
|
||||
_PLAN_METADATA_INTS_PER_BATCH = 4
|
||||
|
||||
|
||||
def plan_topk_v2(seq_lens: torch.Tensor, static_threshold: int = 0) -> torch.Tensor:
|
||||
module = _jit_topk_v2_module(512) # does not matter
|
||||
bs = seq_lens.shape[0]
|
||||
metadata = seq_lens.new_empty(bs + 1, _PLAN_METADATA_INTS_PER_BATCH)
|
||||
module.topk_plan(seq_lens, metadata, static_threshold)
|
||||
return metadata
|
||||
|
||||
|
||||
def topk_transform_512_v2(
|
||||
scores: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
page_tables: torch.Tensor,
|
||||
out_page_indices: torch.Tensor,
|
||||
page_size: int,
|
||||
metadata: torch.Tensor,
|
||||
) -> None:
|
||||
module = _jit_topk_v2_module(out_page_indices.shape[1])
|
||||
bs = scores.shape[0]
|
||||
workspace = seq_lens.new_empty(bs, _WORKSPACE_INTS_PER_BATCH)
|
||||
module.topk_transform(
|
||||
scores,
|
||||
seq_lens,
|
||||
page_tables,
|
||||
out_page_indices,
|
||||
page_size,
|
||||
workspace,
|
||||
metadata,
|
||||
)
|
||||
|
||||
|
||||
def hash_topk(
|
||||
router_logits: torch.Tensor,
|
||||
input_ids: torch.Tensor,
|
||||
tid2eid: torch.Tensor,
|
||||
num_fused_shared_experts: int = 0,
|
||||
routed_scaling_factor: float = 1.0,
|
||||
scoring_func: str = "sqrtsoftplus",
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
assert scoring_func == "sqrtsoftplus"
|
||||
num_tokens = router_logits.size(0)
|
||||
topk_routed = tid2eid.size(1)
|
||||
topk_fused = topk_routed + num_fused_shared_experts
|
||||
topk_ids = torch.empty(
|
||||
(num_tokens, topk_fused), dtype=torch.int32, device=router_logits.device
|
||||
)
|
||||
topk_weights = torch.empty(
|
||||
(num_tokens, topk_fused), dtype=torch.float32, device=router_logits.device
|
||||
)
|
||||
module = _jit_hash_topk_module()
|
||||
module.hash_topk(
|
||||
router_logits,
|
||||
input_ids,
|
||||
tid2eid,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
routed_scaling_factor,
|
||||
)
|
||||
return topk_weights, topk_ids
|
||||
|
||||
|
||||
def mask_topk_ids(topk_ids: torch.Tensor, num_token_non_padded: torch.Tensor):
|
||||
return _jit_mask_topk_module().run(topk_ids, num_token_non_padded)
|
||||
|
||||
|
||||
class CompressorPrefillPlan(NamedTuple):
|
||||
compress_ratio: int
|
||||
compress_plan: torch.Tensor
|
||||
write_plan: torch.Tensor
|
||||
|
||||
def copy_(self, other: CompressorPrefillPlan) -> None:
|
||||
assert self.compress_ratio == other.compress_ratio
|
||||
self.compress_plan.copy_(other.compress_plan)
|
||||
self.write_plan.copy_(other.write_plan)
|
||||
|
||||
@staticmethod
|
||||
def generate(
|
||||
compress_ratio: Literal[4, 128],
|
||||
num_q_tokens: int,
|
||||
seq_lens: torch.Tensor,
|
||||
extend_lens: torch.Tensor,
|
||||
device: torch.device,
|
||||
use_cuda_graph: bool = False,
|
||||
) -> CompressorPrefillPlan:
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
# Online c128 keeps the same NamedTuple shape (compress_plan, write_plan)
|
||||
# so call sites that splat `*plan[1:]` continue to work, but the C++
|
||||
# plan struct semantics differ (last-token coords + window_len).
|
||||
if compress_ratio == 128 and envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
||||
return CompressorPrefillPlan._generate_online(
|
||||
num_q_tokens=num_q_tokens,
|
||||
seq_lens=seq_lens,
|
||||
extend_lens=extend_lens,
|
||||
device=device,
|
||||
use_cuda_graph=use_cuda_graph,
|
||||
)
|
||||
assert seq_lens.device == extend_lens.device
|
||||
seq_lens = seq_lens.to(torch.int64)
|
||||
extend_lens = extend_lens.to(torch.int64)
|
||||
plan_tensor = torch.empty(
|
||||
(2, num_q_tokens, 16),
|
||||
dtype=torch.uint8,
|
||||
device=seq_lens.device,
|
||||
pin_memory=seq_lens.is_cpu,
|
||||
)
|
||||
module = _jit_common_module()
|
||||
is_overlap = compress_ratio == 4
|
||||
plan_lens = module.plan_compress_prefill(
|
||||
extend_lens,
|
||||
seq_lens,
|
||||
plan_tensor[0],
|
||||
plan_tensor[1],
|
||||
compress_ratio,
|
||||
is_overlap,
|
||||
use_cuda_graph,
|
||||
)
|
||||
return CompressorPrefillPlan(
|
||||
compress_ratio,
|
||||
plan_tensor[0, : plan_lens[0]].to(device, non_blocking=True),
|
||||
plan_tensor[1, : plan_lens[1]].to(device, non_blocking=True),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _generate_online(
|
||||
num_q_tokens: int,
|
||||
seq_lens: torch.Tensor,
|
||||
extend_lens: torch.Tensor,
|
||||
device: torch.device,
|
||||
use_cuda_graph: bool,
|
||||
) -> CompressorPrefillPlan:
|
||||
# Online plan host-side path: only CPU/cuda-host implemented today.
|
||||
# Move inputs to CPU pinned memory then bounce the result to device.
|
||||
seq_lens_cpu = seq_lens.detach().to(torch.int64).cpu()
|
||||
extend_lens_cpu = extend_lens.detach().to(torch.int64).cpu()
|
||||
plan_tensor = torch.empty(
|
||||
(2, num_q_tokens, 16),
|
||||
dtype=torch.uint8,
|
||||
device="cpu",
|
||||
pin_memory=True,
|
||||
)
|
||||
module = _jit_compress_128_online_plan_module()
|
||||
plan_lens = module.plan_compress_online_prefill(
|
||||
extend_lens_cpu,
|
||||
seq_lens_cpu,
|
||||
plan_tensor[0],
|
||||
plan_tensor[1],
|
||||
use_cuda_graph,
|
||||
)
|
||||
return CompressorPrefillPlan(
|
||||
128,
|
||||
plan_tensor[0, : plan_lens[0]].to(device, non_blocking=True),
|
||||
plan_tensor[1, : plan_lens[1]].to(device, non_blocking=True),
|
||||
)
|
||||
|
||||
@property
|
||||
def is_decode(self) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
class CompressorDecodePlan(NamedTuple):
|
||||
compress_ratio: int
|
||||
seq_lens: torch.Tensor
|
||||
|
||||
def copy_(self, other: CompressorDecodePlan) -> None:
|
||||
assert self.compress_ratio == other.compress_ratio
|
||||
self.seq_lens.copy_(other.seq_lens)
|
||||
|
||||
@property
|
||||
def is_decode(self) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def compress_plan(
|
||||
compress_ratio: Literal[4, 128],
|
||||
num_q_tokens: int,
|
||||
seq_lens: torch.Tensor,
|
||||
extend_lens: Optional[torch.Tensor],
|
||||
device: torch.device,
|
||||
) -> Union[CompressorDecodePlan, CompressorPrefillPlan]:
|
||||
if extend_lens is not None:
|
||||
return CompressorPrefillPlan.generate(
|
||||
compress_ratio,
|
||||
num_q_tokens,
|
||||
seq_lens,
|
||||
extend_lens,
|
||||
device,
|
||||
)
|
||||
else:
|
||||
assert num_q_tokens == len(seq_lens)
|
||||
seq_lens = seq_lens.to(device, non_blocking=True)
|
||||
return CompressorDecodePlan(compress_ratio, seq_lens)
|
||||
|
||||
|
||||
def compress_forward(
|
||||
kv_score_buffer: torch.Tensor,
|
||||
kv_score_input: torch.Tensor,
|
||||
ape: torch.Tensor,
|
||||
indices: torch.Tensor,
|
||||
plan: Union[CompressorDecodePlan, CompressorPrefillPlan, None] = None,
|
||||
extra_data: Optional[torch.Tensor] = None,
|
||||
*,
|
||||
head_dim: int,
|
||||
compress_ratio: Literal[4, 128],
|
||||
out: Optional[torch.Tensor] = None,
|
||||
seq_lens: Optional[torch.Tensor] = None,
|
||||
extend_lens: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
assert head_dim % 128 == 0
|
||||
num_q_tokens = kv_score_input.shape[0]
|
||||
if out is None:
|
||||
out = kv_score_input.new_empty((num_q_tokens, head_dim))
|
||||
if plan is None:
|
||||
assert seq_lens is not None
|
||||
plan = compress_plan(
|
||||
compress_ratio,
|
||||
num_q_tokens,
|
||||
seq_lens,
|
||||
extend_lens,
|
||||
kv_score_input.device,
|
||||
)
|
||||
assert plan.compress_ratio == compress_ratio, "Mismatched compress ratio in plan!"
|
||||
# Online c128: separate JIT module, fp32 state, no compile-time dtypes.
|
||||
if compress_ratio == 128 and envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
||||
online_module = _jit_compress_128_online_module(head_dim=head_dim)
|
||||
F = online_module.decode if plan.is_decode else online_module.prefill
|
||||
F(kv_score_buffer, kv_score_input, out, ape, indices, *plan[1:], extra_data)
|
||||
return out
|
||||
module = _jit_compress_module(
|
||||
head_dim,
|
||||
kv_score_input.dtype,
|
||||
out.dtype,
|
||||
compress_ratio,
|
||||
)
|
||||
F = module.decode if plan.is_decode else module.prefill
|
||||
F(kv_score_buffer, kv_score_input, out, ape, indices, *plan[1:], extra_data)
|
||||
return out
|
||||
|
||||
|
||||
def compress_fused_norm_rope_inplace(
|
||||
kv: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
eps: float,
|
||||
freq_cis: torch.Tensor,
|
||||
plan: Union[CompressorDecodePlan, CompressorPrefillPlan],
|
||||
) -> None:
|
||||
freq_cis = torch.view_as_real(freq_cis).flatten(-2)
|
||||
module = _jit_norm_rope_module(kv.dtype, kv.shape[-1], freq_cis.shape[-1])
|
||||
module.forward(
|
||||
kv,
|
||||
weight,
|
||||
plan[1],
|
||||
freq_cis,
|
||||
int(plan.is_decode),
|
||||
eps,
|
||||
plan.compress_ratio,
|
||||
)
|
||||
|
||||
|
||||
def fused_rope(
|
||||
q: torch.Tensor,
|
||||
k: Optional[torch.Tensor],
|
||||
freqs_cis: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
inverse: bool = False,
|
||||
) -> None:
|
||||
freqs_real = torch.view_as_real(freqs_cis).flatten(-2).contiguous()
|
||||
module = _jit_fused_rope_module()
|
||||
module.forward(q, k, freqs_real, positions, inverse)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def create_paged_compress_data_kernel(
|
||||
req_pool_indices_ptr,
|
||||
seq_lens_ptr,
|
||||
extend_seq_lens_ptr,
|
||||
req_to_token_ptr,
|
||||
full_to_swa_index_mapping_ptr,
|
||||
out_0_ptr,
|
||||
out_1_ptr,
|
||||
batch_size,
|
||||
stride_req_to_token_0,
|
||||
stride_req_to_token_1: tl.constexpr,
|
||||
stride_out_1_0,
|
||||
stride_out_1_1: tl.constexpr,
|
||||
compress_ratio: tl.constexpr,
|
||||
is_overlap: tl.constexpr,
|
||||
swa_page_size: tl.constexpr,
|
||||
ring_size: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
) -> None:
|
||||
pid = tl.program_id(0)
|
||||
offs = pid * BLOCK + tl.arange(0, BLOCK)
|
||||
mask = offs < batch_size
|
||||
|
||||
rid = tl.load(req_pool_indices_ptr + offs, mask=mask, other=0).to(tl.int32)
|
||||
seq_len = tl.load(seq_lens_ptr + offs, mask=mask, other=0).to(tl.int32)
|
||||
extend_len = tl.load(extend_seq_lens_ptr + offs, mask=mask, other=0).to(tl.int32)
|
||||
prefix_len = seq_len - extend_len
|
||||
|
||||
cr = compress_ratio
|
||||
write_pos = ((seq_len - 1) // cr) * cr
|
||||
load_pos = ((prefix_len - 1) // cr) * cr
|
||||
write_overlap_pos = write_pos - cr
|
||||
load_overlap_pos = load_pos - cr
|
||||
v0 = tl.zeros([BLOCK], tl.int32)
|
||||
v1 = tl.zeros([BLOCK], tl.int32)
|
||||
v2 = tl.zeros([BLOCK], tl.int32)
|
||||
v3 = tl.zeros([BLOCK], tl.int32)
|
||||
|
||||
for i in tl.static_range(4):
|
||||
if i == 0:
|
||||
pos = load_pos
|
||||
elif i == 1:
|
||||
pos = write_pos
|
||||
elif i == 2:
|
||||
pos = load_overlap_pos
|
||||
else:
|
||||
pos = write_overlap_pos
|
||||
pos = tl.maximum(pos, 0)
|
||||
loc = tl.load(
|
||||
req_to_token_ptr
|
||||
+ rid.to(tl.int64) * stride_req_to_token_0
|
||||
+ pos.to(tl.int64) * stride_req_to_token_1,
|
||||
mask=mask,
|
||||
other=0,
|
||||
).to(tl.int32)
|
||||
swa_loc = tl.load(full_to_swa_index_mapping_ptr + loc, mask=mask, other=0).to(
|
||||
tl.int32
|
||||
)
|
||||
swa_page = swa_loc // swa_page_size
|
||||
state_loc = swa_page * ring_size + (swa_loc % ring_size)
|
||||
state_loc = state_loc // cr
|
||||
if i == 0:
|
||||
v0 = state_loc
|
||||
elif i == 1:
|
||||
v1 = state_loc
|
||||
elif i == 2:
|
||||
v2 = state_loc
|
||||
else:
|
||||
v3 = state_loc
|
||||
|
||||
tl.store(out_0_ptr + offs, v1, mask=mask)
|
||||
|
||||
if is_overlap:
|
||||
base = out_1_ptr + offs * stride_out_1_0
|
||||
tl.store(base + 0 * stride_out_1_1, v2, mask=mask)
|
||||
tl.store(base + 1 * stride_out_1_1, v0, mask=mask)
|
||||
tl.store(base + 2 * stride_out_1_1, v3, mask=mask)
|
||||
tl.store(base + 3 * stride_out_1_1, write_pos.to(tl.int32), mask=mask)
|
||||
else:
|
||||
base = out_1_ptr + offs * stride_out_1_0
|
||||
tl.store(base + 0 * stride_out_1_1, v0, mask=mask)
|
||||
|
||||
|
||||
def triton_create_paged_compress_data(
|
||||
*,
|
||||
compress_ratio: int,
|
||||
is_overlap: bool,
|
||||
swa_page_size: int,
|
||||
ring_size: int,
|
||||
req_pool_indices: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
extend_seq_lens: torch.Tensor,
|
||||
req_to_token: torch.Tensor,
|
||||
full_to_swa_index_mapping: torch.Tensor,
|
||||
block: int = 128,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
batch_size = req_pool_indices.shape[0]
|
||||
out_dim = 4 if is_overlap else 1
|
||||
device_args: dict = dict(device=req_pool_indices.device, dtype=torch.int32)
|
||||
out_0 = torch.empty((batch_size,), **device_args)
|
||||
out_1 = torch.empty((batch_size, out_dim), **device_args)
|
||||
grid = (triton.cdiv(batch_size, block),)
|
||||
create_paged_compress_data_kernel[grid](
|
||||
req_pool_indices,
|
||||
seq_lens,
|
||||
extend_seq_lens,
|
||||
req_to_token,
|
||||
full_to_swa_index_mapping,
|
||||
out_0,
|
||||
out_1,
|
||||
batch_size=batch_size,
|
||||
stride_req_to_token_0=req_to_token.stride(0),
|
||||
stride_req_to_token_1=req_to_token.stride(1),
|
||||
stride_out_1_0=out_1.stride(0),
|
||||
stride_out_1_1=out_1.stride(1),
|
||||
compress_ratio=compress_ratio,
|
||||
is_overlap=1 if is_overlap else 0,
|
||||
swa_page_size=swa_page_size,
|
||||
ring_size=ring_size,
|
||||
BLOCK=block,
|
||||
)
|
||||
|
||||
if not is_overlap:
|
||||
out_1.squeeze_(1)
|
||||
return out_0, out_1
|
||||
|
||||
|
||||
def fused_store_cache(
|
||||
input: torch.Tensor,
|
||||
cache: torch.Tensor,
|
||||
indices: torch.Tensor,
|
||||
*,
|
||||
page_size: int,
|
||||
type: Literal["flashmla", "indexer"],
|
||||
) -> None:
|
||||
module = _jit_fused_store_module(
|
||||
name=type,
|
||||
input_dtype=input.dtype,
|
||||
index_dtype=indices.dtype,
|
||||
page_size=page_size,
|
||||
)
|
||||
module.run(input, cache, indices)
|
||||
|
||||
|
||||
def silu_and_mul_clamp(
|
||||
input: torch.Tensor,
|
||||
output: torch.Tensor,
|
||||
swiglu_limit: float,
|
||||
) -> None:
|
||||
module = _jit_silu_and_mul_clamp_module(input.dtype)
|
||||
module.run(input, output, float(swiglu_limit))
|
||||
|
||||
|
||||
def silu_and_mul_masked_post_quant(
|
||||
input: torch.Tensor,
|
||||
output: torch.Tensor,
|
||||
output_scale: torch.Tensor,
|
||||
quant_group_size: int,
|
||||
masked_m: torch.Tensor,
|
||||
scale_ue8m0: bool = False,
|
||||
topk: int = 8,
|
||||
transposed: bool = False,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
swizzle: bool = False,
|
||||
) -> None:
|
||||
apply_swiglu_limit = swiglu_limit is not None
|
||||
module = _jit_silu_mul_quant_varlen_module(
|
||||
quant_group_size, scale_ue8m0, swizzle, apply_swiglu_limit
|
||||
)
|
||||
module.run(
|
||||
input,
|
||||
output,
|
||||
output_scale,
|
||||
masked_m,
|
||||
topk,
|
||||
transposed,
|
||||
float(swiglu_limit) if apply_swiglu_limit else 0.0,
|
||||
)
|
||||
|
||||
|
||||
def silu_and_mul_contig_post_quant(
|
||||
input: torch.Tensor,
|
||||
output: torch.Tensor,
|
||||
output_scale: torch.Tensor,
|
||||
quant_group_size: int,
|
||||
scale_ue8m0: bool = False,
|
||||
transposed: bool = False,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
swizzle: bool = False,
|
||||
) -> None:
|
||||
apply_swiglu_limit = swiglu_limit is not None
|
||||
module = _jit_silu_mul_quant_contig_module(
|
||||
quant_group_size, scale_ue8m0, swizzle, apply_swiglu_limit
|
||||
)
|
||||
module.run(
|
||||
input,
|
||||
output,
|
||||
output_scale,
|
||||
transposed,
|
||||
float(swiglu_limit) if apply_swiglu_limit else 0.0,
|
||||
)
|
||||
|
||||
|
||||
def mega_moe_pre_dispatch(
|
||||
x: torch.Tensor,
|
||||
topk_idx: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
buf_x: torch.Tensor,
|
||||
buf_x_sf: torch.Tensor,
|
||||
buf_topk_idx: torch.Tensor,
|
||||
buf_topk_weights: torch.Tensor,
|
||||
quant_group_size: int = 32,
|
||||
) -> None:
|
||||
module = _jit_mega_moe_pre_dispatch_module(quant_group_size)
|
||||
module.run(
|
||||
x,
|
||||
topk_idx,
|
||||
topk_weights,
|
||||
buf_x,
|
||||
buf_x_sf,
|
||||
buf_topk_idx,
|
||||
buf_topk_weights,
|
||||
)
|
||||
|
||||
|
||||
def get_paged_mqa_logits_metadata(seq_lens: torch.Tensor, page_size: int, num_sm: int):
|
||||
assert page_size == 64
|
||||
seq_lens = seq_lens.view(-1).to(torch.int32)
|
||||
metadata = seq_lens.new_empty(num_sm + 1, 2)
|
||||
module = _jit_metadata_module()
|
||||
module.run(seq_lens, metadata)
|
||||
return metadata
|
||||
|
||||
|
||||
def rmsnorm_self(q: torch.Tensor, eps: float) -> torch.Tensor:
|
||||
module = _jit_rmsnorm_head_module(q.shape[-1], q.dtype)
|
||||
out = q.new_empty(q.shape)
|
||||
module.run_self(q, out, eps)
|
||||
return out
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_torch_cublas_bf16_fp32() -> Any:
|
||||
import torch.utils.cpp_extension
|
||||
|
||||
source = """
|
||||
#include <torch/extension.h>
|
||||
#include <ATen/cuda/CUDAContext.h>
|
||||
#include <cublas_v2.h>
|
||||
|
||||
torch::Tensor linear_bf16_fp32(
|
||||
torch::Tensor X,
|
||||
torch::Tensor W)
|
||||
{
|
||||
int batch = X.size(0);
|
||||
int in_features = X.size(1);
|
||||
int out_features = W.size(0);
|
||||
|
||||
auto Y = torch::empty(
|
||||
{batch, out_features},
|
||||
torch::dtype(torch::kFloat32).device(X.device()));
|
||||
|
||||
cublasHandle_t handle = at::cuda::getCurrentCUDABlasHandle();
|
||||
|
||||
float alpha = 1.0f;
|
||||
float beta = 0.0f;
|
||||
|
||||
cublasGemmEx(
|
||||
handle,
|
||||
CUBLAS_OP_T,
|
||||
CUBLAS_OP_N,
|
||||
out_features,
|
||||
batch,
|
||||
in_features,
|
||||
&alpha,
|
||||
W.data_ptr(), CUDA_R_16BF, in_features,
|
||||
X.data_ptr(), CUDA_R_16BF, in_features,
|
||||
&beta,
|
||||
Y.data_ptr(), CUDA_R_32F, out_features,
|
||||
CUBLAS_COMPUTE_32F,
|
||||
CUBLAS_GEMM_DEFAULT_TENSOR_OP
|
||||
);
|
||||
|
||||
return Y;
|
||||
}
|
||||
|
||||
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
||||
m.def("linear_bf16_fp32", &linear_bf16_fp32, "BF16xBF16 -> FP32 linear (no bias)");
|
||||
}
|
||||
"""
|
||||
module = torch.utils.cpp_extension.load_inline(
|
||||
name="linear_bf16_fp32",
|
||||
cpp_sources="",
|
||||
cuda_sources=source,
|
||||
extra_cflags=["-O3"],
|
||||
extra_cuda_cflags=["-O3"],
|
||||
verbose=False,
|
||||
)
|
||||
return module
|
||||
|
||||
|
||||
def linear_bf16_fp32(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
algo = envs.SGLANG_OPT_BF16_FP32_GEMM_ALGO.get()
|
||||
return _dispatch_bf16_fp32_backend(x, y, algo=algo)
|
||||
|
||||
|
||||
def _dispatch_bf16_fp32_backend(
|
||||
x: torch.Tensor, y: torch.Tensor, *, algo: str
|
||||
) -> torch.Tensor:
|
||||
if algo == "cublas":
|
||||
module = _jit_torch_cublas_bf16_fp32()
|
||||
return module.linear_bf16_fp32(x, y)
|
||||
elif algo == "deep_gemm":
|
||||
import deep_gemm
|
||||
|
||||
z = x.new_empty(x.size(0), y.size(0), dtype=torch.float32)
|
||||
deep_gemm.bf16_gemm_nt(x, y, z)
|
||||
return z
|
||||
else:
|
||||
return torch.nn.functional.linear(x.float(), y.float())
|
||||
@@ -18,10 +18,13 @@ def _jit_sparse_module(
|
||||
num_top_k: int,
|
||||
hot_buffer_size: int,
|
||||
is_mla: bool = False,
|
||||
is_dsv4_layout: bool = False,
|
||||
) -> Module:
|
||||
template_args = make_cpp_args(block_size, num_top_k, hot_buffer_size, is_mla)
|
||||
template_args = make_cpp_args(
|
||||
block_size, num_top_k, hot_buffer_size, is_mla, is_dsv4_layout
|
||||
)
|
||||
cache_args = make_cpp_args(
|
||||
item_size_bytes, block_size, num_top_k, hot_buffer_size, is_mla
|
||||
item_size_bytes, block_size, num_top_k, hot_buffer_size, is_mla, is_dsv4_layout
|
||||
)
|
||||
return load_jit(
|
||||
"sparse_cache",
|
||||
@@ -36,7 +39,9 @@ def _jit_sparse_module(
|
||||
)
|
||||
|
||||
|
||||
def load_cache_to_device_buffer_mla(
|
||||
def _load_cache_to_device_buffer_mla(
|
||||
*,
|
||||
is_dsv4_layout: bool,
|
||||
top_k_tokens: torch.Tensor,
|
||||
device_buffer_tokens: torch.Tensor,
|
||||
host_cache_locs: torch.Tensor,
|
||||
@@ -50,16 +55,21 @@ def load_cache_to_device_buffer_mla(
|
||||
item_size_bytes: int,
|
||||
num_top_k: int,
|
||||
hot_buffer_size: int,
|
||||
page_size: int = 1,
|
||||
block_size: int = 256,
|
||||
num_real_reqs: torch.Tensor | None = None,
|
||||
page_size: int,
|
||||
block_size: int,
|
||||
num_real_reqs: torch.Tensor | None,
|
||||
) -> None:
|
||||
assert (
|
||||
hot_buffer_size >= num_top_k
|
||||
), f"hot_buffer_size ({hot_buffer_size}) must be >= num_top_k ({num_top_k})"
|
||||
|
||||
module = _jit_sparse_module(
|
||||
item_size_bytes, block_size, num_top_k, hot_buffer_size, is_mla=True
|
||||
item_size_bytes,
|
||||
block_size,
|
||||
num_top_k,
|
||||
hot_buffer_size,
|
||||
is_mla=True,
|
||||
is_dsv4_layout=is_dsv4_layout,
|
||||
)
|
||||
|
||||
empty = torch.empty(0)
|
||||
@@ -86,3 +96,83 @@ def load_cache_to_device_buffer_mla(
|
||||
page_size,
|
||||
item_size_bytes,
|
||||
)
|
||||
|
||||
|
||||
def load_cache_to_device_buffer_mla(
|
||||
top_k_tokens: torch.Tensor,
|
||||
device_buffer_tokens: torch.Tensor,
|
||||
host_cache_locs: torch.Tensor,
|
||||
device_buffer_locs: torch.Tensor,
|
||||
host_cache: torch.Tensor,
|
||||
device_buffer: torch.Tensor,
|
||||
top_k_device_locs: torch.Tensor,
|
||||
req_pool_indices: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
lru_slots: torch.Tensor,
|
||||
item_size_bytes: int,
|
||||
num_top_k: int,
|
||||
hot_buffer_size: int,
|
||||
page_size: int = 1,
|
||||
block_size: int = 256,
|
||||
num_real_reqs: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
"""Generic MLA hisparse swap-in: device + host both linear (stride=item_size_bytes)."""
|
||||
_load_cache_to_device_buffer_mla(
|
||||
is_dsv4_layout=False,
|
||||
top_k_tokens=top_k_tokens,
|
||||
device_buffer_tokens=device_buffer_tokens,
|
||||
host_cache_locs=host_cache_locs,
|
||||
device_buffer_locs=device_buffer_locs,
|
||||
host_cache=host_cache,
|
||||
device_buffer=device_buffer,
|
||||
top_k_device_locs=top_k_device_locs,
|
||||
req_pool_indices=req_pool_indices,
|
||||
seq_lens=seq_lens,
|
||||
lru_slots=lru_slots,
|
||||
item_size_bytes=item_size_bytes,
|
||||
num_top_k=num_top_k,
|
||||
hot_buffer_size=hot_buffer_size,
|
||||
page_size=page_size,
|
||||
block_size=block_size,
|
||||
num_real_reqs=num_real_reqs,
|
||||
)
|
||||
|
||||
|
||||
def load_cache_to_device_buffer_dsv4_mla(
|
||||
top_k_tokens: torch.Tensor,
|
||||
device_buffer_tokens: torch.Tensor,
|
||||
host_cache_locs: torch.Tensor,
|
||||
device_buffer_locs: torch.Tensor,
|
||||
host_cache: torch.Tensor,
|
||||
device_buffer: torch.Tensor,
|
||||
top_k_device_locs: torch.Tensor,
|
||||
req_pool_indices: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
lru_slots: torch.Tensor,
|
||||
item_size_bytes: int,
|
||||
num_top_k: int,
|
||||
hot_buffer_size: int,
|
||||
page_size: int = 1,
|
||||
block_size: int = 256,
|
||||
num_real_reqs: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
"""DSv4 hisparse swap-in: page-padded device + linear host (kvcacheio.cuh layout)."""
|
||||
_load_cache_to_device_buffer_mla(
|
||||
is_dsv4_layout=True,
|
||||
top_k_tokens=top_k_tokens,
|
||||
device_buffer_tokens=device_buffer_tokens,
|
||||
host_cache_locs=host_cache_locs,
|
||||
device_buffer_locs=device_buffer_locs,
|
||||
host_cache=host_cache,
|
||||
device_buffer=device_buffer,
|
||||
top_k_device_locs=top_k_device_locs,
|
||||
req_pool_indices=req_pool_indices,
|
||||
seq_lens=seq_lens,
|
||||
lru_slots=lru_slots,
|
||||
item_size_bytes=item_size_bytes,
|
||||
num_top_k=num_top_k,
|
||||
hot_buffer_size=hot_buffer_size,
|
||||
page_size=page_size,
|
||||
block_size=block_size,
|
||||
num_real_reqs=num_real_reqs,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
|
||||
#include <dlpack/dlpack.h>
|
||||
#include <tvm/ffi/container/tuple.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace device::compress {
|
||||
|
||||
struct alignas(16) PrefillPlan {
|
||||
uint32_t ragged_id;
|
||||
uint32_t batch_id;
|
||||
uint32_t position;
|
||||
uint32_t window_len; // must be in `[0, compress_ratio * (1 + is_overlap))`
|
||||
|
||||
bool is_valid(const uint32_t ratio, const bool is_overlap) const {
|
||||
const uint32_t max_window_len = ratio * (1 + is_overlap);
|
||||
return window_len < max_window_len;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace device::compress
|
||||
|
||||
namespace host::compress {
|
||||
|
||||
using device::compress::PrefillPlan;
|
||||
using PrefillPlanTensorDtype = uint8_t;
|
||||
inline constexpr int64_t kPrefillPlanDim = 16;
|
||||
|
||||
static_assert(alignof(PrefillPlan) == sizeof(PrefillPlan));
|
||||
static_assert(sizeof(PrefillPlan) == kPrefillPlanDim * sizeof(PrefillPlanTensorDtype));
|
||||
|
||||
} // namespace host::compress
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <sgl_kernel/math.cuh>
|
||||
#include <sgl_kernel/type.cuh>
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cuda_fp8.h>
|
||||
|
||||
// Small helpers shared by the DeepSeek-V4 FP8/UE8M0 quantization kernels
|
||||
// (silu_and_mul_masked_post_quant, store, mega_moe_pre_dispatch, ...).
|
||||
// All functions are `SGL_DEVICE` (= `__forceinline__ __device__`) so
|
||||
// including this header in multiple translation units is ODR-safe.
|
||||
|
||||
namespace deepseek_v4::fp8 {
|
||||
|
||||
// Round `x` to the nearest representable UE8M0 value. Returns the raw
|
||||
// 8-bit biased exponent; the actual fp32 scale is `2^(exp - 127)`
|
||||
// (i.e. `__uint_as_float(exp << 23)`).
|
||||
SGL_DEVICE int32_t cast_to_ue8m0(float x) {
|
||||
uint32_t u = __float_as_uint(x);
|
||||
int32_t exp = int32_t((u >> 23) & 0xFF);
|
||||
uint32_t mant = u & 0x7FFFFF;
|
||||
return exp + (mant != 0);
|
||||
}
|
||||
|
||||
// 1 / 2^(exp - 127) as fp32. Equivalent to `1.0f / __uint_as_float(exp << 23)`.
|
||||
SGL_DEVICE float inv_scale_ue8m0(int32_t exp) {
|
||||
return __uint_as_float((127 + 127 - exp) << 23);
|
||||
}
|
||||
|
||||
// Clamp to [-FP8_E4M3_MAX, FP8_E4M3_MAX].
|
||||
SGL_DEVICE float fp8_e4m3_clip(float val) {
|
||||
namespace math = device::math;
|
||||
return math::max(math::min(val, math::FP8_E4M3_MAX), -math::FP8_E4M3_MAX);
|
||||
}
|
||||
|
||||
// Pack two fp32 values into a single fp8x2_e4m3 with clamping.
|
||||
SGL_DEVICE fp8x2_e4m3_t pack_fp8(float x, float y) {
|
||||
return fp8x2_e4m3_t{fp32x2_t{fp8_e4m3_clip(x), fp8_e4m3_clip(y)}};
|
||||
}
|
||||
|
||||
} // namespace deepseek_v4::fp8
|
||||
@@ -0,0 +1,96 @@
|
||||
#include <sgl_kernel/tensor.h>
|
||||
#include <sgl_kernel/utils.h>
|
||||
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
|
||||
#include <tvm/ffi/container/tensor.h>
|
||||
|
||||
namespace device::hisparse {
|
||||
|
||||
/// NOTE: We call nope+rope as a "value" here.
|
||||
/// GPU Cache layout:
|
||||
/// VALUE 0, VALUE 1, ..., VALUE 63,
|
||||
/// SCALE 0, SCALE 1, ..., SCALE 63,
|
||||
/// [Padding to align to 576 bytes]
|
||||
/// CPU Cache follow a trivial linear layout without any padding.
|
||||
inline constexpr int64_t kGPUPageSize = 64;
|
||||
inline constexpr int64_t kGPUPageBits = 6; // log2(kGPUPageSize)
|
||||
inline constexpr int64_t kValueBytes = 576;
|
||||
inline constexpr int64_t kScaleBytes = 8;
|
||||
/// NOTE: FlashMLA requires each page to be aligned to 576 bytes
|
||||
inline constexpr int64_t kCPUItemBytes = kValueBytes + kScaleBytes;
|
||||
inline constexpr int64_t kGPUPageBytes = host::div_ceil(kCPUItemBytes * kGPUPageSize, 576) * 576;
|
||||
inline constexpr int64_t kGPUScaleOffset = kValueBytes * kGPUPageSize;
|
||||
|
||||
struct PointerInfo {
|
||||
int64_t* value_ptr;
|
||||
int64_t* scale_ptr;
|
||||
};
|
||||
|
||||
SGL_DEVICE PointerInfo get_pointer_gpu(void* cache, int32_t index) {
|
||||
using namespace device;
|
||||
static_assert(1 << kGPUPageBits == kGPUPageSize);
|
||||
const int32_t page_num = index >> kGPUPageBits;
|
||||
const int32_t page_offset = index & (kGPUPageSize - 1);
|
||||
const auto page_ptr = pointer::offset(cache, page_num * kGPUPageBytes);
|
||||
const auto value_ptr = pointer::offset(page_ptr, page_offset * kValueBytes);
|
||||
const auto scale_ptr = pointer::offset(page_ptr, kGPUScaleOffset + page_offset * kScaleBytes);
|
||||
return {static_cast<int64_t*>(value_ptr), static_cast<int64_t*>(scale_ptr)};
|
||||
}
|
||||
|
||||
SGL_DEVICE PointerInfo get_pointer_cpu(void* cache, int32_t index) {
|
||||
using namespace device;
|
||||
const auto value_ptr = pointer::offset(cache, index * kCPUItemBytes);
|
||||
const auto scale_ptr = pointer::offset(value_ptr, kValueBytes);
|
||||
return {static_cast<int64_t*>(value_ptr), static_cast<int64_t*>(scale_ptr)};
|
||||
}
|
||||
|
||||
enum class TransferDirection {
|
||||
DeviceToDevice = 0,
|
||||
DeviceToHost = 1,
|
||||
HostToDevice = 2,
|
||||
};
|
||||
|
||||
template <TransferDirection direction>
|
||||
SGL_DEVICE void transfer_item(void* dst_cache, void* src_cache, const int32_t dst_index, const int32_t src_index) {
|
||||
constexpr bool is_dst_device = (direction != TransferDirection::DeviceToHost);
|
||||
constexpr bool is_src_device = (direction != TransferDirection::HostToDevice);
|
||||
constexpr auto dst_fn = is_dst_device ? get_pointer_gpu : get_pointer_cpu;
|
||||
constexpr auto src_fn = is_src_device ? get_pointer_gpu : get_pointer_cpu;
|
||||
|
||||
const auto [dst_value_ptr, dst_scale_ptr] = dst_fn(dst_cache, dst_index);
|
||||
const auto [src_value_ptr, src_scale_ptr] = src_fn(src_cache, src_index);
|
||||
|
||||
int64_t local_items[2];
|
||||
const int64_t* tail_src_ptr;
|
||||
int64_t* tail_dst_ptr;
|
||||
|
||||
const int32_t lane_id = threadIdx.x % 32;
|
||||
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const auto j = lane_id + i * 32;
|
||||
local_items[i] = src_value_ptr[j];
|
||||
}
|
||||
|
||||
if (lane_id < 8) { // handle the tail element safely
|
||||
const auto last_id = 64 + lane_id;
|
||||
tail_src_ptr = src_value_ptr + last_id;
|
||||
tail_dst_ptr = dst_value_ptr + last_id;
|
||||
} else { // broadcast load/store is safe
|
||||
tail_src_ptr = src_scale_ptr;
|
||||
tail_dst_ptr = dst_scale_ptr;
|
||||
}
|
||||
|
||||
const auto tail_item = *tail_src_ptr;
|
||||
|
||||
// store first 512 bytes of value
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
const auto j = lane_id + i * 32;
|
||||
dst_value_ptr[j] = local_items[i];
|
||||
}
|
||||
|
||||
// store the tail element
|
||||
*tail_dst_ptr = tail_item;
|
||||
}
|
||||
|
||||
} // namespace device::hisparse
|
||||
@@ -0,0 +1,257 @@
|
||||
#pragma once
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/vec.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include "common.cuh"
|
||||
#include "ptx.cuh"
|
||||
#include <cooperative_groups.h>
|
||||
#include <cstdint>
|
||||
|
||||
namespace device::top512 {
|
||||
|
||||
template <uint32_t K>
|
||||
struct ClusterTopK {
|
||||
static constexpr uint32_t kClusterSize = 8;
|
||||
static constexpr uint32_t kHistBits = 10;
|
||||
static constexpr uint32_t kHistBins = 1 << kHistBits;
|
||||
static constexpr uint32_t kRadixBins = 256;
|
||||
static constexpr uint32_t kElemPerStage = 8;
|
||||
static constexpr uint32_t kSizePerStage = kElemPerStage * kBlockSize;
|
||||
static constexpr uint32_t kNumStages = 4;
|
||||
static constexpr uint32_t kMaxLength = kClusterSize * kNumStages * kSizePerStage;
|
||||
static constexpr uint32_t kStoreLane = kBlockSize - 1;
|
||||
static constexpr uint32_t kAboveBits = 11;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared memory layouts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct Smem {
|
||||
uint64_t barrier[kNumStages];
|
||||
uint32_t local_above_equal[kClusterSize];
|
||||
uint32_t prefix_above_equal;
|
||||
alignas(128) uint32_t counter_gt;
|
||||
alignas(128) uint32_t counter_eq;
|
||||
alignas(128) MatchBin match;
|
||||
alignas(128) uint32_t warp_sum[kNumWarps];
|
||||
uint32_t histogram[kHistBins];
|
||||
alignas(128) float score_buffer[kNumStages][kSizePerStage];
|
||||
Tie tie_buffer[kMaxTies];
|
||||
};
|
||||
|
||||
struct alignas(16) Metadata {
|
||||
uint32_t batch_id;
|
||||
uint32_t seq_len;
|
||||
bool has_next;
|
||||
};
|
||||
|
||||
struct WorkSpace {
|
||||
uint2 metadata; // {num_above, num_ties}
|
||||
Tie ties[kMaxTies];
|
||||
};
|
||||
|
||||
static constexpr uint32_t kWorkspaceInts = sizeof(WorkSpace) / sizeof(uint32_t);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stage 1: histogram + cluster reduce + find threshold + scatter
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
SGL_DEVICE static void stage1_init(void* _smem) {
|
||||
const auto tx = threadIdx.x;
|
||||
__builtin_assume(tx < kBlockSize);
|
||||
const auto smem = static_cast<Smem*>(_smem);
|
||||
if (tx < kHistBins) smem->histogram[tx] = 0;
|
||||
if (tx < kNumStages) ptx::mbarrier_init(&smem->barrier[tx], 1);
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
SGL_DEVICE static void stage1_prologue(const float* scores, uint32_t length, void* _smem) {
|
||||
if (threadIdx.x == 0) {
|
||||
const auto smem = static_cast<Smem*>(_smem);
|
||||
const auto num_stages = (length + kSizePerStage - 1) / kSizePerStage;
|
||||
const auto length_aligned = (length + 3u) & ~3u; // align to 4 for TMA
|
||||
#pragma unroll
|
||||
for (uint32_t stage = 0; stage < kNumStages; stage++) {
|
||||
if (stage >= num_stages) break;
|
||||
const auto offset = stage * kSizePerStage;
|
||||
const auto size = min(kSizePerStage, length_aligned - offset);
|
||||
const auto size_bytes = size * sizeof(float);
|
||||
const auto bar = &smem->barrier[stage];
|
||||
ptx::tma_load(smem->score_buffer[stage], scores + offset, size_bytes, bar);
|
||||
ptx::mbarrier_arrive_expect_tx(bar, size_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SGL_DEVICE static void stage1(int32_t* indices, uint32_t length, void* _smem, bool reuse = false) {
|
||||
const auto smem = static_cast<Smem*>(_smem);
|
||||
const auto tx = threadIdx.x;
|
||||
__builtin_assume(tx < kBlockSize);
|
||||
const auto lane_id = tx % kWarpThreads;
|
||||
const auto warp_id = tx / kWarpThreads;
|
||||
|
||||
// Initialize shared memory histogram, counters, and barriers
|
||||
#pragma unroll
|
||||
for (uint32_t stage = 0; stage < kNumStages; stage++) {
|
||||
const auto offset = stage * kSizePerStage;
|
||||
if (offset >= length) break;
|
||||
const auto size = min(kSizePerStage, length - offset);
|
||||
if (lane_id == 0) ptx::mbarrier_wait(&smem->barrier[stage], 0);
|
||||
__syncwarp();
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kElemPerStage; ++i) {
|
||||
const auto idx = tx + i * kBlockSize;
|
||||
if (idx >= size) break;
|
||||
const auto score = smem->score_buffer[stage][idx];
|
||||
const auto bin = extract_coarse_bin<kHistBits>(score);
|
||||
atomicAdd(&smem->histogram[bin], 1);
|
||||
}
|
||||
}
|
||||
|
||||
static_assert(kHistBins <= kBlockSize);
|
||||
|
||||
// 2-shot all-reduce
|
||||
{
|
||||
auto cluster = cooperative_groups::this_cluster();
|
||||
cluster.sync();
|
||||
const auto cluster_rank = blockIdx.y;
|
||||
const auto kLocalSize = kHistBins / kClusterSize;
|
||||
const auto offset = kLocalSize * cluster_rank;
|
||||
|
||||
const auto src_tx = tx / kClusterSize;
|
||||
const auto src_rank = tx % kClusterSize;
|
||||
|
||||
if (tx < kHistBins) {
|
||||
const auto addr = &smem->histogram[offset + src_tx];
|
||||
const auto src_addr = cluster.map_shared_rank(addr, src_rank);
|
||||
*src_addr = warp::reduce_sum<kClusterSize>(*src_addr);
|
||||
}
|
||||
cluster.sync();
|
||||
}
|
||||
|
||||
// now each block holds the whole histogram, find the threshold bin
|
||||
{
|
||||
const auto value = tx < kHistBins ? smem->histogram[tx] : 0;
|
||||
const auto warp_inc = warp_inclusive_sum(lane_id, value);
|
||||
if (lane_id == kWarpThreads - 1) {
|
||||
smem->warp_sum[warp_id] = warp_inc;
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
const auto tmp = smem->warp_sum[lane_id];
|
||||
// total_length = sum of all bins in the globally-reduced histogram
|
||||
// (problem.length is block-local; after cluster reduction we need the global total)
|
||||
const auto total_length = warp::reduce_sum(tmp);
|
||||
uint32_t prefix_sum = warp::reduce_sum(lane_id < warp_id ? tmp : 0);
|
||||
prefix_sum += warp_inc;
|
||||
const auto above = total_length - prefix_sum;
|
||||
if (tx < kHistBins && above < K && above + value >= K) {
|
||||
smem->counter_gt = smem->counter_eq = 0;
|
||||
smem->match = {
|
||||
.bin = tx,
|
||||
.above_count = above,
|
||||
.equal_count = value,
|
||||
};
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
const auto [thr_bin, num_above, num_equal] = smem->match;
|
||||
|
||||
// write above and equal results to global memory
|
||||
#pragma unroll
|
||||
for (uint32_t stage = 0; stage < kNumStages; stage++) {
|
||||
const auto offset = stage * kSizePerStage;
|
||||
if (offset >= length) break;
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kElemPerStage; ++i) {
|
||||
const auto buf_idx = tx + i * kBlockSize;
|
||||
const auto global_idx = offset + buf_idx;
|
||||
if (global_idx >= length) break;
|
||||
const auto score = smem->score_buffer[stage][buf_idx];
|
||||
const auto bin = extract_coarse_bin<kHistBits>(score);
|
||||
if (bin > thr_bin) {
|
||||
indices[atomicAdd(&smem->counter_gt, 1)] = global_idx;
|
||||
} else if (bin == thr_bin) {
|
||||
const auto pos = atomicAdd(&smem->counter_eq, 1);
|
||||
if (pos < kMaxTies) smem->tie_buffer[pos] = {global_idx, score};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reuse) {
|
||||
const auto num_stages = (length + kSizePerStage - 1) / kSizePerStage;
|
||||
if (tx < kHistBins) smem->histogram[tx] = 0;
|
||||
if (tx < num_stages) ptx::mbarrier_arrive(&smem->barrier[tx]);
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stage 1 epilogue: cross-block prefix sum + page translate + tie store
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
SGL_DEVICE static void stage1_epilogue(const TransformParams params, const uint32_t offset, void* _ws, void* _smem) {
|
||||
auto cluster = cooperative_groups::this_cluster();
|
||||
const auto smem = static_cast<Smem*>(_smem);
|
||||
const auto tx = threadIdx.x;
|
||||
const auto local_above = smem->counter_gt;
|
||||
const auto local_equal = smem->counter_eq;
|
||||
const auto cluster_rank = blockIdx.y;
|
||||
|
||||
constexpr uint32_t kAboveMask = (1 << kAboveBits) - 1;
|
||||
static_assert(kAboveMask >= K);
|
||||
|
||||
// Pack local counts -- NO alignment rounding (contiguous layout)
|
||||
static_assert(kMaxTies <= kBlockSize);
|
||||
const auto idx_above = tx < local_above ? params.indices_in[tx] : 0;
|
||||
const auto tie_value = tx < local_equal ? smem->tie_buffer[tx] : Tie{0, 0.0f};
|
||||
|
||||
// push to remote shared memory, can reduce latency of reading remote
|
||||
if (tx < kClusterSize) {
|
||||
const auto value = (local_equal << kAboveBits) | local_above;
|
||||
const auto dst_addr = cluster.map_shared_rank(smem->local_above_equal, tx);
|
||||
dst_addr[cluster_rank] = value;
|
||||
}
|
||||
// after this last sync, only read local shared memory
|
||||
// so that it is safe when peer rank has already exited the kernel
|
||||
cluster.sync();
|
||||
if (tx < kClusterSize) {
|
||||
const auto value = tx < cluster_rank ? smem->local_above_equal[tx] : 0;
|
||||
const auto kActiveMask = (1u << kClusterSize) - 1;
|
||||
smem->prefix_above_equal = warp::reduce_sum<kClusterSize>(value, kActiveMask);
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
const auto prefix_packed = smem->prefix_above_equal;
|
||||
const auto prefix_above = prefix_packed & kAboveMask;
|
||||
const auto prefix_equal = prefix_packed >> kAboveBits;
|
||||
|
||||
// Page-translate above elements
|
||||
if (tx < local_above) {
|
||||
params.write(tx + prefix_above, idx_above + offset);
|
||||
}
|
||||
// Contiguous tie store via regular global writes (no TMA, no gaps)
|
||||
const auto ws = static_cast<WorkSpace*>(_ws);
|
||||
if (tx < local_equal && tx + prefix_equal < kMaxTies) {
|
||||
ws->ties[tx + prefix_equal] = {tie_value.idx + offset, tie_value.score};
|
||||
}
|
||||
// Block 0 writes global metadata {num_above, num_ties}
|
||||
if (cluster_rank == kClusterSize - 1 && tx == 0) {
|
||||
const auto sum_above = prefix_above + local_above;
|
||||
const auto sum_equal = prefix_equal + local_equal;
|
||||
ws->metadata = make_uint2(sum_above, sum_equal);
|
||||
}
|
||||
}
|
||||
|
||||
SGL_DEVICE static void transform(const TransformParams params, const void* _ws, void* _smem) {
|
||||
const auto ws = static_cast<const WorkSpace*>(_ws);
|
||||
const auto meta = &ws->metadata;
|
||||
const auto [num_above, num_equal] = *meta;
|
||||
if (num_above >= K || num_equal == 0) return;
|
||||
const auto clamped_ties = min(num_equal, kMaxTies);
|
||||
tie_handle_transform(ws->ties, clamped_ties, num_above, K, params, _smem);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace device::top512
|
||||
@@ -0,0 +1,176 @@
|
||||
#pragma once
|
||||
#include <sgl_kernel/type.cuh>
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/vec.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace device::top512 {
|
||||
|
||||
inline constexpr uint32_t kMaxTopK = 1024;
|
||||
inline constexpr uint32_t kBlockSize = 1024;
|
||||
inline constexpr uint32_t kNumWarps = kBlockSize / kWarpThreads;
|
||||
inline constexpr uint32_t kMaxTies = 1024; // == kBlockSize: 1 element per thread in stage2
|
||||
static constexpr uint32_t kRadixBins = 256;
|
||||
static_assert(kMaxTopK <= kBlockSize && kMaxTies <= kBlockSize);
|
||||
|
||||
// always use float4 to load from global memory
|
||||
using Vec4 = AlignedVector<float, 4>;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
struct TransformParams {
|
||||
const int32_t* __restrict__ page_table;
|
||||
const int32_t* __restrict__ indices_in;
|
||||
int32_t* __restrict__ indices_out;
|
||||
uint32_t page_bits;
|
||||
|
||||
SGL_DEVICE void transform(const uint32_t idx) const {
|
||||
indices_out[idx] = page_to_indices(page_table, indices_in[idx], page_bits);
|
||||
}
|
||||
SGL_DEVICE void write(const uint32_t dst, const uint32_t src) const {
|
||||
indices_out[dst] = page_to_indices(page_table, src, page_bits);
|
||||
}
|
||||
};
|
||||
|
||||
struct alignas(16) MatchBin {
|
||||
uint32_t bin;
|
||||
uint32_t above_count;
|
||||
uint32_t equal_count;
|
||||
};
|
||||
|
||||
struct alignas(8) Tie {
|
||||
uint32_t idx;
|
||||
float score;
|
||||
};
|
||||
|
||||
struct TieHandleSmem {
|
||||
alignas(128) uint32_t counter; // output position counter
|
||||
alignas(128) MatchBin match;
|
||||
uint32_t histogram[kRadixBins]; // 256-bin radix histogram
|
||||
uint32_t warp_sum[kNumWarps]; // for 2-pass prefix sum
|
||||
};
|
||||
|
||||
template <uint32_t kBits>
|
||||
SGL_DEVICE uint32_t extract_coarse_bin(float x) {
|
||||
static_assert(0 < kBits && kBits < 15);
|
||||
const auto hx = cast<fp16_t>(x);
|
||||
const uint16_t bits = *reinterpret_cast<const uint16_t*>(&hx);
|
||||
const uint16_t key = (bits & 0x8000) ? ~bits : bits | 0x8000;
|
||||
return key >> (16 - kBits);
|
||||
}
|
||||
|
||||
SGL_DEVICE uint32_t warp_inclusive_sum(uint32_t lane_id, uint32_t val) {
|
||||
static_assert(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;
|
||||
}
|
||||
|
||||
/// Order-preserving float32 -> uint32 for radix select
|
||||
SGL_DEVICE uint32_t extract_exact_bin(float x) {
|
||||
uint32_t bits = __float_as_uint(x);
|
||||
return (bits & 0x80000000u) ? ~bits : (bits | 0x80000000u);
|
||||
}
|
||||
|
||||
SGL_DEVICE void trivial_transform(const TransformParams& params, uint32_t length, uint32_t K) {
|
||||
if (const auto tx = threadIdx.x; tx < length) {
|
||||
params.write(tx, tx);
|
||||
} else if (tx < K) {
|
||||
params.indices_out[tx] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
SGL_DEVICE void tie_handle_transform(
|
||||
const Tie* __restrict__ ties, //
|
||||
const uint32_t num_ties,
|
||||
const uint32_t num_above,
|
||||
const uint32_t K,
|
||||
const TransformParams params,
|
||||
void* _smem) {
|
||||
auto* smem = static_cast<TieHandleSmem*>(_smem);
|
||||
const auto tx = threadIdx.x;
|
||||
const auto lane_id = tx % kWarpThreads;
|
||||
const auto warp_id = tx / kWarpThreads;
|
||||
|
||||
// Each thread loads one element (or becomes inactive)
|
||||
const bool has_elem = tx < num_ties;
|
||||
const auto tie = has_elem ? ties[tx] : Tie{0, 0.0f};
|
||||
const uint32_t key = extract_exact_bin(tie.score);
|
||||
const uint32_t idx = tie.idx;
|
||||
bool active = has_elem;
|
||||
uint32_t topk_remain = K - num_above;
|
||||
uint32_t write_pos = K;
|
||||
|
||||
smem->counter = 0;
|
||||
__syncthreads();
|
||||
|
||||
// Number of warps covering the 256-bin histogram (256/32 = 8)
|
||||
constexpr uint32_t kRadixWarps = kRadixBins / kWarpThreads;
|
||||
|
||||
#pragma unroll
|
||||
for (int round = 0; round < 4; round++) {
|
||||
const uint32_t shift = 24 - round * 8;
|
||||
const uint32_t bin = (key >> shift) & 0xFFu;
|
||||
|
||||
// 1. Build histogram
|
||||
if (tx < kRadixBins) smem->histogram[tx] = 0;
|
||||
__syncthreads();
|
||||
if (active) atomicAdd(&smem->histogram[bin], 1);
|
||||
__syncthreads();
|
||||
|
||||
// 2. v2-style 2-pass prefix sum on 256 bins
|
||||
// Only first 256 threads (8 warps) carry histogram bins.
|
||||
// Other threads get hist_val=0 and harmless prefix results.
|
||||
uint32_t hist_val = 0;
|
||||
uint32_t warp_inc = 0;
|
||||
if (tx < kRadixBins) {
|
||||
hist_val = smem->histogram[tx];
|
||||
warp_inc = warp_inclusive_sum(lane_id, hist_val);
|
||||
if (lane_id == kWarpThreads - 1) smem->warp_sum[warp_id] = warp_inc;
|
||||
}
|
||||
__syncthreads();
|
||||
if (tx < kRadixBins) {
|
||||
// Inter-warp prefix (only first kHistWarps warp totals matter)
|
||||
const auto tmp = (lane_id < kRadixWarps) ? smem->warp_sum[lane_id] : 0;
|
||||
const auto total = warp::reduce_sum(tmp);
|
||||
const auto inter = warp::reduce_sum(lane_id < warp_id ? tmp : 0);
|
||||
const auto prefix = inter + warp_inc; // inclusive prefix through this bin
|
||||
const auto above = total - prefix; // elements in bins ABOVE this one
|
||||
// 3. Find threshold bin
|
||||
if (above < topk_remain && above + hist_val >= topk_remain) {
|
||||
smem->match = {tx, above, topk_remain - above};
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
const auto [thr, n_above, _] = smem->match;
|
||||
|
||||
// 4. Scatter
|
||||
if (active) {
|
||||
if (bin > thr) {
|
||||
write_pos = num_above + atomicAdd(&smem->counter, 1);
|
||||
active = false;
|
||||
} else if (bin < thr) {
|
||||
active = false;
|
||||
} else if (round == 3) {
|
||||
write_pos = K - atomicAdd(&smem->match.equal_count, -1u);
|
||||
}
|
||||
// my_bin == thr && round < 3: stay active for next round
|
||||
}
|
||||
|
||||
topk_remain -= n_above;
|
||||
if (topk_remain == 0) break;
|
||||
}
|
||||
|
||||
if (write_pos < K) params.write(write_pos, idx);
|
||||
}
|
||||
|
||||
} // namespace device::top512
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
|
||||
#include <cuda/ptx>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace device::top512 {
|
||||
|
||||
namespace ptx {
|
||||
|
||||
SGL_DEVICE void mbarrier_wait(uint64_t* addr, uint32_t phase) {
|
||||
while (!cuda::ptx::mbarrier_try_wait_parity(cuda::ptx::sem_relaxed, cuda::ptx::scope_cta, addr, phase))
|
||||
;
|
||||
}
|
||||
|
||||
SGL_DEVICE void mbarrier_init(uint64_t* addr, uint32_t arrives) {
|
||||
cuda::ptx::mbarrier_init(addr, arrives);
|
||||
}
|
||||
|
||||
SGL_DEVICE void mbarrier_arrive_expect_tx(uint64_t* addr, uint32_t tx) {
|
||||
cuda::ptx::mbarrier_arrive_expect_tx(cuda::ptx::sem_relaxed, cuda::ptx::scope_cta, cuda::ptx::space_shared, addr, tx);
|
||||
}
|
||||
|
||||
SGL_DEVICE void mbarrier_arrive(uint64_t* addr) {
|
||||
cuda::ptx::mbarrier_arrive(cuda::ptx::sem_relaxed, cuda::ptx::scope_cta, cuda::ptx::space_shared, addr);
|
||||
}
|
||||
|
||||
SGL_DEVICE void tma_load(void* dst, const void* src, uint32_t num_bytes, uint64_t* mbar) {
|
||||
cuda::ptx::cp_async_bulk(cuda::ptx::space_shared, cuda::ptx::space_global, dst, src, num_bytes, mbar);
|
||||
}
|
||||
|
||||
SGL_DEVICE uint32_t elect_sync() {
|
||||
uint32_t pred = 0;
|
||||
asm volatile(
|
||||
"{\n\t"
|
||||
".reg .pred %%px;\n\t"
|
||||
"elect.sync _|%%px, %1;\n\t"
|
||||
"@%%px mov.s32 %0, 1;\n\t"
|
||||
"}"
|
||||
: "+r"(pred)
|
||||
: "r"(0xFFFFFFFF));
|
||||
return pred;
|
||||
}
|
||||
|
||||
SGL_DEVICE bool elect_sync_cta(uint32_t tx) {
|
||||
const auto warp_id = tx / 32;
|
||||
const auto uniform_warp_id = __shfl_sync(0xFFFFFFFF, warp_id, 0);
|
||||
return (uniform_warp_id == 0 && elect_sync());
|
||||
}
|
||||
|
||||
} // namespace ptx
|
||||
|
||||
} // namespace device::top512
|
||||
@@ -0,0 +1,302 @@
|
||||
#pragma once
|
||||
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/vec.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include "common.cuh"
|
||||
#include "ptx.cuh"
|
||||
#include <cfloat>
|
||||
#include <cstdint>
|
||||
|
||||
namespace device::top512 {
|
||||
|
||||
template <uint32_t K>
|
||||
struct RegisterTopK {
|
||||
static constexpr uint32_t kHistBits = 12;
|
||||
static constexpr uint32_t kHistBins = 1 << kHistBits;
|
||||
static constexpr uint32_t kVecsPerThread = 4;
|
||||
static constexpr uint32_t kMaxTolerance = 0;
|
||||
static constexpr uint32_t kMax1PassLength = kVecsPerThread * 4 * kBlockSize;
|
||||
static constexpr uint32_t kMaxExtraLength = kMax1PassLength;
|
||||
static constexpr uint32_t kMax2PassLength = kMax1PassLength + kMaxExtraLength;
|
||||
|
||||
struct Smem {
|
||||
using HistVec = AlignedVector<uint32_t, kHistBins / kBlockSize>;
|
||||
alignas(128) uint32_t counter_gt;
|
||||
alignas(128) uint32_t counter_eq;
|
||||
uint64_t mbarrier; // for cp.async
|
||||
MatchBin match;
|
||||
uint32_t warp_sum[kNumWarps];
|
||||
union {
|
||||
uint32_t histogram[kHistBins];
|
||||
HistVec histogram_vec[kBlockSize];
|
||||
Tie tie_buffer[kMaxTies];
|
||||
};
|
||||
alignas(16) float score_buffer[kMaxExtraLength];
|
||||
};
|
||||
|
||||
template <bool kIs2Pass = false>
|
||||
SGL_DEVICE static void
|
||||
run(const float* scores, //
|
||||
int32_t* indices,
|
||||
const uint32_t length,
|
||||
void* _smem,
|
||||
const bool use_pdl = false) {
|
||||
const auto smem = static_cast<Smem*>(_smem);
|
||||
const auto tx = threadIdx.x;
|
||||
const auto lane_id = tx % kWarpThreads;
|
||||
const auto warp_id = tx / kWarpThreads;
|
||||
|
||||
// Initialize shared memory histogram
|
||||
{
|
||||
typename Smem::HistVec hist_vec;
|
||||
hist_vec.fill(0);
|
||||
smem->histogram_vec[tx] = hist_vec;
|
||||
if (tx == 0) {
|
||||
smem->counter_gt = smem->counter_eq = 0;
|
||||
if constexpr (kIs2Pass) {
|
||||
ptx::mbarrier_init(&smem->mbarrier, 1);
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
if (use_pdl) device::PDLWaitPrimary<true>();
|
||||
|
||||
// Load scores into registers
|
||||
Vec4 local[kVecsPerThread];
|
||||
#pragma unroll
|
||||
for (uint32_t v = 0; v < kVecsPerThread; ++v) {
|
||||
const uint32_t base = (tx + v * kBlockSize) * 4;
|
||||
if (base >= length) break;
|
||||
local[v].load(scores, tx + v * kBlockSize);
|
||||
}
|
||||
|
||||
// Fetch the next chunk of scores
|
||||
if constexpr (kIs2Pass) {
|
||||
if (ptx::elect_sync_cta(tx)) {
|
||||
const auto length_aligned = (length + 3u - kMax1PassLength) & ~3u;
|
||||
const auto size_bytes = length_aligned * sizeof(float);
|
||||
ptx::tma_load(smem->score_buffer, scores + kMax1PassLength, size_bytes, &smem->mbarrier);
|
||||
ptx::mbarrier_arrive_expect_tx(&smem->mbarrier, size_bytes);
|
||||
}
|
||||
__syncwarp(); // avoid warp divergence on
|
||||
}
|
||||
|
||||
// Accumulate histogram via shared-memory atomics
|
||||
#pragma unroll
|
||||
for (uint32_t v = 0; v < kVecsPerThread; ++v) {
|
||||
#pragma unroll
|
||||
for (uint32_t e = 0; e < 4; ++e) {
|
||||
if constexpr (!kIs2Pass) {
|
||||
const uint32_t idx = (tx + v * kBlockSize) * 4 + e;
|
||||
if (idx >= length) goto LABEL_ACC_FINISH;
|
||||
}
|
||||
atomicAdd(&smem->histogram[extract_coarse_bin<kHistBits>(local[v][e])], 1);
|
||||
}
|
||||
}
|
||||
if constexpr (kIs2Pass) {
|
||||
// 16K ~ 32K. `i` is a float4 index
|
||||
if (lane_id == 0) ptx::mbarrier_wait(&smem->mbarrier, 0);
|
||||
__syncwarp();
|
||||
for (uint32_t i = tx; i + kMax1PassLength < length; i += kBlockSize) {
|
||||
const auto val = smem->score_buffer[i];
|
||||
atomicAdd(&smem->histogram[extract_coarse_bin<kHistBits>(val)], 1);
|
||||
}
|
||||
}
|
||||
[[maybe_unused]] LABEL_ACC_FINISH:
|
||||
__syncthreads();
|
||||
|
||||
// Phase 2: Exclusive prefix scan -> find threshold bin
|
||||
{
|
||||
constexpr uint32_t kItems = kHistBins / kBlockSize;
|
||||
uint32_t orig[kItems];
|
||||
const auto hist_vec = smem->histogram_vec[tx];
|
||||
uint32_t tmp_local_sum = 0;
|
||||
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kItems; ++i) {
|
||||
orig[i] = hist_vec[i];
|
||||
tmp_local_sum += orig[i];
|
||||
}
|
||||
|
||||
const auto warp_inc = warp_inclusive_sum(lane_id, tmp_local_sum);
|
||||
const auto warp_exc = warp_inc - tmp_local_sum;
|
||||
if (lane_id == kWarpThreads - 1) {
|
||||
smem->warp_sum[warp_id] = warp_inc;
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
|
||||
const auto tmp = smem->warp_sum[lane_id];
|
||||
// Exactly one bin satisfies: above < K && above + count >= K
|
||||
uint32_t prefix_sum = warp::reduce_sum(lane_id < warp_id ? tmp : 0);
|
||||
prefix_sum += warp_exc;
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kItems; ++i) {
|
||||
prefix_sum += orig[i];
|
||||
const auto above = length - prefix_sum;
|
||||
if (above < K && above + orig[i] >= K) {
|
||||
smem->match = {
|
||||
.bin = tx * kItems + i,
|
||||
.above_count = above,
|
||||
.equal_count = orig[i],
|
||||
};
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
const auto [thr_bin, num_above, num_equal] = smem->match;
|
||||
|
||||
// Phase 3: Scatter
|
||||
// Elements strictly above threshold go directly to output.
|
||||
// Tied elements: simple path admits first-come; tiebreak path collects into tie_buffer.
|
||||
const bool need_tiebreak = (num_equal + num_above > K + kMaxTolerance);
|
||||
const auto topk_indices = indices;
|
||||
const auto tie_buffer = smem->tie_buffer;
|
||||
|
||||
#pragma unroll
|
||||
for (uint32_t v = 0; v < kVecsPerThread; ++v) {
|
||||
#pragma unroll
|
||||
for (uint32_t e = 0; e < 4; ++e) {
|
||||
const uint32_t idx = (tx + v * kBlockSize) * 4 + e;
|
||||
if constexpr (!kIs2Pass) {
|
||||
if (idx >= length) goto LABEL_SCATTER_DONE;
|
||||
}
|
||||
const uint32_t bin = extract_coarse_bin<kHistBits>(local[v][e]);
|
||||
if (bin > thr_bin) {
|
||||
topk_indices[atomicAdd(&smem->counter_gt, 1)] = idx;
|
||||
} else if (bin == thr_bin) {
|
||||
const auto pos = atomicAdd(&smem->counter_eq, 1);
|
||||
if (need_tiebreak) {
|
||||
if (pos < kMaxTies) {
|
||||
tie_buffer[pos] = {.idx = idx, .score = local[v][e]};
|
||||
}
|
||||
} else {
|
||||
if (const auto which = pos + num_above; which < K) {
|
||||
topk_indices[which] = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// prefetch the next scores
|
||||
if constexpr (kIs2Pass) {
|
||||
local[v].load(smem->score_buffer, tx + v * kBlockSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 16K ~ 32K, already in registers: similar loop as above but read from smem->score_buffer
|
||||
if constexpr (kIs2Pass) {
|
||||
#pragma unroll
|
||||
for (uint32_t v = 0; v < kVecsPerThread; ++v) {
|
||||
#pragma unroll
|
||||
for (uint32_t e = 0; e < 4; ++e) {
|
||||
const uint32_t idx = (tx + v * kBlockSize) * 4 + e + kMax1PassLength;
|
||||
if (idx >= length) goto LABEL_SCATTER_DONE;
|
||||
const uint32_t bin = extract_coarse_bin<kHistBits>(local[v][e]);
|
||||
if (bin > thr_bin) {
|
||||
topk_indices[atomicAdd(&smem->counter_gt, 1)] = idx;
|
||||
} else if (bin == thr_bin) {
|
||||
const auto pos = atomicAdd(&smem->counter_eq, 1);
|
||||
if (need_tiebreak) {
|
||||
if (pos < kMaxTies) {
|
||||
tie_buffer[pos] = {.idx = idx, .score = local[v][e]};
|
||||
}
|
||||
} else {
|
||||
if (const auto which = pos + num_above; which < K) {
|
||||
topk_indices[which] = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[[maybe_unused]] LABEL_SCATTER_DONE:
|
||||
if (!need_tiebreak) return;
|
||||
|
||||
// Phase 4: Tie-breaking within the threshold bin.
|
||||
// Assume num_ties <= kBlockSize (at most 1 block of ties).
|
||||
// Each thread takes one tied element, computes its rank (number of
|
||||
// elements with strictly higher score, breaking exact float ties by
|
||||
// original index), and writes to output if rank < topk_remain.
|
||||
__syncthreads();
|
||||
static_assert(kMaxTies <= kBlockSize);
|
||||
|
||||
const uint32_t num_ties = min(num_equal, kMaxTies);
|
||||
const uint32_t topk_remain = K - num_above;
|
||||
|
||||
const auto is_greater = [](const Tie& a, const Tie& b) {
|
||||
return (a.score > b.score) || (a.score == b.score && a.idx < b.idx);
|
||||
};
|
||||
|
||||
if (num_ties <= kWarpThreads) {
|
||||
static_assert(kWarpThreads <= kNumWarps);
|
||||
if (lane_id >= num_ties || warp_id >= num_ties) return; // some threads are idle
|
||||
/// NOTE: use long long to avoid mask overflow when num_ties == 32
|
||||
const uint32_t mask = (1ull << num_ties) - 1u;
|
||||
const auto tie = tie_buffer[lane_id];
|
||||
const auto target_tie = tie_buffer[warp_id];
|
||||
const bool pred = is_greater(tie, target_tie);
|
||||
const auto rank = static_cast<uint32_t>(__popc(__ballot_sync(mask, pred)));
|
||||
if (lane_id == 0 && rank < topk_remain) {
|
||||
topk_indices[num_above + rank] = target_tie.idx;
|
||||
}
|
||||
} else if (num_ties <= kWarpThreads * 2) {
|
||||
// 64 x 64 topk implementation: each thread takes 2 elements
|
||||
const auto lane_id_1 = lane_id + kWarpThreads;
|
||||
const auto warp_id_1 = warp_id + kWarpThreads;
|
||||
const auto invalid = Tie{.idx = 0xFFFFFFFF, .score = -FLT_MAX};
|
||||
const auto tie_0 = tie_buffer[lane_id];
|
||||
const auto tie_1 = lane_id_1 < num_ties ? tie_buffer[lane_id_1] : invalid;
|
||||
if (true) {
|
||||
const auto target = tie_buffer[warp_id];
|
||||
const bool pred_0 = is_greater(tie_0, target);
|
||||
const bool pred_1 = is_greater(tie_1, target);
|
||||
const auto rank_0 = static_cast<uint32_t>(__popc(__ballot_sync(0xFFFFFFFF, pred_0)));
|
||||
const auto rank_1 = static_cast<uint32_t>(__popc(__ballot_sync(0xFFFFFFFF, pred_1)));
|
||||
const auto rank = rank_0 + rank_1;
|
||||
if (lane_id == 0 && rank < topk_remain) {
|
||||
topk_indices[num_above + rank] = target.idx;
|
||||
}
|
||||
}
|
||||
if (warp_id_1 < num_ties) {
|
||||
const auto target = tie_buffer[warp_id_1];
|
||||
const bool pred_0 = is_greater(tie_0, target);
|
||||
const bool pred_1 = is_greater(tie_1, target);
|
||||
const auto rank_0 = static_cast<uint32_t>(__popc(__ballot_sync(0xFFFFFFFF, pred_0)));
|
||||
const auto rank_1 = static_cast<uint32_t>(__popc(__ballot_sync(0xFFFFFFFF, pred_1)));
|
||||
const auto rank = rank_0 + rank_1;
|
||||
if (lane_id == 0 && rank < topk_remain) {
|
||||
topk_indices[num_above + rank] = target.idx;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/// NOTE: Based on my observation, this path is very rarely reached
|
||||
[[unlikely]];
|
||||
// Block-level: each thread reads from tie_buffer in shared memory
|
||||
for (auto i = warp_id; i < num_ties; i += kNumWarps) {
|
||||
const auto target_tie = tie_buffer[i];
|
||||
uint32_t local_rank = 0;
|
||||
for (auto j = lane_id; j < num_ties; j += kWarpThreads) {
|
||||
const auto tie = tie_buffer[j];
|
||||
if (is_greater(tie, target_tie)) local_rank++;
|
||||
}
|
||||
// sum the rank across the warp
|
||||
const auto rank = warp::reduce_sum(local_rank);
|
||||
if (lane_id == 0 && rank < topk_remain) {
|
||||
topk_indices[num_above + rank] = target_tie.idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SGL_DEVICE static void transform(const TransformParams params) {
|
||||
__syncthreads();
|
||||
if (const auto tx = threadIdx.x; tx < K) params.transform(tx);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace device::top512
|
||||
@@ -0,0 +1,213 @@
|
||||
#pragma once
|
||||
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
#include <sgl_kernel/vec.cuh>
|
||||
#include <sgl_kernel/warp.cuh>
|
||||
|
||||
#include "common.cuh"
|
||||
#include "ptx.cuh"
|
||||
#include <cfloat>
|
||||
#include <cstdint>
|
||||
|
||||
namespace device::top512 {
|
||||
|
||||
template <uint32_t K>
|
||||
struct StreamingTopK {
|
||||
static constexpr uint32_t kHistBits = 12;
|
||||
static constexpr uint32_t kHistBins = 1 << kHistBits;
|
||||
static constexpr uint32_t kRadixBins = 256;
|
||||
static constexpr uint32_t kElemPerStage = 8;
|
||||
static constexpr uint32_t kSizePerStage = kElemPerStage * kBlockSize;
|
||||
static constexpr uint32_t kNumStages = 2; // double buffer
|
||||
|
||||
static constexpr uint32_t kHistItems = kHistBins / kBlockSize; // 4
|
||||
static_assert(kHistItems * kBlockSize == kHistBins);
|
||||
using HistVec = AlignedVector<uint32_t, kHistItems>;
|
||||
|
||||
struct Smem {
|
||||
uint64_t barrier[2][kNumStages];
|
||||
alignas(128) uint32_t counter_gt;
|
||||
alignas(128) uint32_t counter_eq;
|
||||
alignas(128) MatchBin match;
|
||||
alignas(128) uint32_t warp_sum[kNumWarps];
|
||||
union {
|
||||
uint32_t histogram[kHistBins];
|
||||
HistVec histogram_vec[kBlockSize];
|
||||
Tie tie_buffer[kMaxTies];
|
||||
};
|
||||
union {
|
||||
float score_buffer[kNumStages][kSizePerStage];
|
||||
TieHandleSmem stage2; // reuse smem for tie handling in phase D
|
||||
};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// NOTE: length must be 4-aligned since we load 4 floats/thread. Caller should round up.
|
||||
template <bool kIsScatter>
|
||||
SGL_DEVICE static void issue_tma(const float* scores, uint32_t stage, uint32_t length, Smem* smem) {
|
||||
const auto buf_idx = stage % kNumStages;
|
||||
const auto offset = stage * kSizePerStage;
|
||||
const auto size = min(kSizePerStage, length - offset);
|
||||
const auto size_bytes = size * sizeof(float);
|
||||
const auto bar = &smem->barrier[kIsScatter][buf_idx];
|
||||
ptx::tma_load(smem->score_buffer[buf_idx], scores + offset, size_bytes, bar);
|
||||
ptx::mbarrier_arrive_expect_tx(bar, size_bytes);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unified streaming pass. Used for both phase A (kIsScatter=false) and
|
||||
// phase C (kIsScatter=true). Each buffer is reused across iterations via the
|
||||
// reuse-arrive trick (same pattern as ClusterTopKImpl::stage1).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
template <bool kIsScatter>
|
||||
SGL_DEVICE static void stream_pass(
|
||||
const float* scores,
|
||||
const uint32_t length,
|
||||
const uint32_t thr_bin, // ignored when !kIsScatter
|
||||
int32_t* s_topk_indices, // ignored when !kIsScatter
|
||||
Smem* smem) {
|
||||
const auto tx = threadIdx.x;
|
||||
const auto num_iters = (length + kSizePerStage - 1) / kSizePerStage;
|
||||
const auto lane_id = tx % kWarpThreads;
|
||||
|
||||
// Initial double-buffer TMA prologue.
|
||||
const auto length_aligned = (length + 3u) & ~3u;
|
||||
if (tx == 0) {
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kNumStages; i++) {
|
||||
if (i >= num_iters) break;
|
||||
issue_tma<kIsScatter>(scores, i, length_aligned, smem);
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t iter = 0; iter < num_iters; iter++) {
|
||||
const auto buf_idx = iter % kNumStages;
|
||||
const auto offset = iter * kSizePerStage;
|
||||
const auto this_size = min(kSizePerStage, length - offset);
|
||||
|
||||
if (lane_id == 1) {
|
||||
const auto phase_bit = (iter / kNumStages) & 1;
|
||||
ptx::mbarrier_wait(&smem->barrier[kIsScatter][buf_idx], phase_bit);
|
||||
}
|
||||
__syncwarp();
|
||||
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kElemPerStage; i++) {
|
||||
const auto local_idx = tx + i * kBlockSize;
|
||||
if (local_idx >= this_size) break;
|
||||
const auto score = smem->score_buffer[buf_idx][local_idx];
|
||||
const auto bin = extract_coarse_bin<kHistBits>(score);
|
||||
if constexpr (kIsScatter) {
|
||||
const auto global_idx = offset + local_idx;
|
||||
if (bin > thr_bin) {
|
||||
const auto pos = atomicAdd(&smem->counter_gt, 1);
|
||||
if (pos < K) s_topk_indices[pos] = global_idx;
|
||||
} else if (bin == thr_bin) {
|
||||
const auto pos = atomicAdd(&smem->counter_eq, 1);
|
||||
if (pos < kMaxTies) smem->tie_buffer[pos] = {global_idx, score};
|
||||
}
|
||||
} else {
|
||||
atomicAdd(&smem->histogram[bin], 1);
|
||||
}
|
||||
}
|
||||
|
||||
__syncthreads();
|
||||
if (tx == 0) {
|
||||
if (const auto next_iter = iter + kNumStages; next_iter < num_iters) {
|
||||
issue_tma<kIsScatter>(scores, next_iter, length_aligned, smem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Phase B: find the threshold bin via a warp-level prefix scan.
|
||||
// Same structure as SmallTopKImpl's phase 2 (4 bins/thread, warp_sum relay).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
SGL_DEVICE static void find_threshold(uint32_t length, Smem* smem) {
|
||||
const auto tx = threadIdx.x;
|
||||
const auto lane_id = tx % kWarpThreads;
|
||||
const auto warp_id = tx / kWarpThreads;
|
||||
|
||||
uint32_t orig[kHistItems];
|
||||
const auto hist_vec = smem->histogram_vec[tx];
|
||||
uint32_t local_sum = 0;
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kHistItems; ++i) {
|
||||
orig[i] = hist_vec[i];
|
||||
local_sum += orig[i];
|
||||
}
|
||||
|
||||
const auto warp_inc = warp_inclusive_sum(lane_id, local_sum);
|
||||
const auto warp_exc = warp_inc - local_sum;
|
||||
if (lane_id == kWarpThreads - 1) smem->warp_sum[warp_id] = warp_inc;
|
||||
__syncthreads();
|
||||
|
||||
const auto tmp = smem->warp_sum[lane_id];
|
||||
uint32_t prefix_sum = warp::reduce_sum(lane_id < warp_id ? tmp : 0);
|
||||
prefix_sum += warp_exc;
|
||||
#pragma unroll
|
||||
for (uint32_t i = 0; i < kHistItems; ++i) {
|
||||
prefix_sum += orig[i];
|
||||
const auto above = length - prefix_sum;
|
||||
if (above < K && above + orig[i] >= K) {
|
||||
smem->match = {
|
||||
.bin = tx * kHistItems + i,
|
||||
.above_count = above,
|
||||
.equal_count = orig[i],
|
||||
};
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
SGL_DEVICE static void run(const float* scores, const uint32_t length, int32_t* topk_indices, void* _smem) {
|
||||
const auto smem = static_cast<Smem*>(_smem);
|
||||
const auto tx = threadIdx.x;
|
||||
__builtin_assume(tx < kBlockSize);
|
||||
|
||||
// Init histogram, barriers, counters.
|
||||
{
|
||||
HistVec zero;
|
||||
zero.fill(0);
|
||||
smem->histogram_vec[tx] = zero;
|
||||
if (tx < 2 * kNumStages) {
|
||||
const auto base_barrier = &smem->barrier[0][0];
|
||||
ptx::mbarrier_init(&base_barrier[tx], 1);
|
||||
}
|
||||
if (tx == 0) {
|
||||
smem->counter_gt = 0;
|
||||
smem->counter_eq = 0;
|
||||
}
|
||||
__syncthreads();
|
||||
}
|
||||
|
||||
// Phase A: histogram pass (pipelined TMA stream).
|
||||
stream_pass<false>(scores, length, 0, nullptr, smem);
|
||||
|
||||
// Phase B: locate threshold bin & re-init barriers
|
||||
find_threshold(length, smem);
|
||||
|
||||
// Phase C: scatter pass.
|
||||
stream_pass<true>(scores, length, smem->match.bin, topk_indices, smem);
|
||||
}
|
||||
|
||||
SGL_DEVICE static void transform(const TransformParams params, void* _smem) {
|
||||
// Phase D: page-translate above entries, then refine ties.
|
||||
const auto smem = static_cast<Smem*>(_smem);
|
||||
const auto tx = threadIdx.x;
|
||||
const auto num_above = smem->match.above_count;
|
||||
if (tx < num_above) params.transform(tx);
|
||||
const auto num_equal = smem->counter_eq;
|
||||
if (num_above >= K || num_equal == 0) return;
|
||||
const auto clamped_ties = min(num_equal, kMaxTies);
|
||||
tie_handle_transform(smem->tie_buffer, clamped_ties, num_above, K, params, &smem->stage2);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace device::top512
|
||||
@@ -259,17 +259,27 @@ struct LaunchKernel {
|
||||
m_config.numAttrs = 0;
|
||||
#else
|
||||
if (enabled) {
|
||||
m_attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization;
|
||||
m_attrs[0].val.programmaticStreamSerializationAllowed = true;
|
||||
m_config.numAttrs = 1;
|
||||
auto& attr = m_attrs[m_config.numAttrs++];
|
||||
attr.id = cudaLaunchAttributeProgrammaticStreamSerialization;
|
||||
attr.val.programmaticStreamSerializationAllowed = true;
|
||||
m_config.attrs = m_attrs;
|
||||
} else {
|
||||
m_config.numAttrs = 0;
|
||||
}
|
||||
#endif
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto enable_cluster(dim3 cluster_dim) -> LaunchKernel& {
|
||||
#ifdef USE_ROCM
|
||||
(void)cluster_dim;
|
||||
#else
|
||||
auto& attr = m_attrs[m_config.numAttrs++];
|
||||
attr.id = cudaLaunchAttributeClusterDimension;
|
||||
attr.val.clusterDim = {cluster_dim.x, cluster_dim.y, cluster_dim.z};
|
||||
m_config.attrs = m_attrs;
|
||||
#endif
|
||||
return *this;
|
||||
}
|
||||
|
||||
template <typename T, typename... Args>
|
||||
auto operator()(T&& kernel, Args&&... args) const -> void {
|
||||
#ifdef USE_ROCM
|
||||
@@ -303,7 +313,7 @@ struct LaunchKernel {
|
||||
|
||||
cudaLaunchConfig_t m_config;
|
||||
const DebugInfo m_location;
|
||||
cudaLaunchAttribute m_attrs[1];
|
||||
cudaLaunchAttribute m_attrs[2];
|
||||
};
|
||||
|
||||
} // namespace host
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#pragma once
|
||||
#include <sgl_kernel/math.cuh>
|
||||
#include <sgl_kernel/utils.cuh>
|
||||
|
||||
namespace device::warp {
|
||||
|
||||
@@ -16,6 +17,7 @@ static constexpr uint32_t kFullMask = 0xffffffffu;
|
||||
* `active_mask` using butterfly (XOR) shuffles. The result is
|
||||
* broadcast to all participating lanes.
|
||||
*
|
||||
* \tparam kNumThreads Group size for the reduction (defaults to a full warp).
|
||||
* \tparam T Numeric type (e.g. float).
|
||||
* \param value Per-lane input value.
|
||||
* \param active_mask Bitmask of participating lanes (default: all 32).
|
||||
@@ -38,15 +40,18 @@ SGL_DEVICE T reduce_sum(T value, uint32_t active_mask = kFullMask) {
|
||||
* butterfly shuffles. The result is broadcast to all participating
|
||||
* lanes.
|
||||
*
|
||||
* \tparam kNumThreads Group size for the reduction (defaults to a full warp).
|
||||
* \tparam T Numeric type (must be supported by `math::max`).
|
||||
* \param value Per-lane input value.
|
||||
* \param active_mask Bitmask of participating lanes (default: all 32).
|
||||
* \return The maximum across all active lanes.
|
||||
*/
|
||||
template <typename T>
|
||||
template <uint32_t kNumThreads = kWarpThreads, typename T>
|
||||
SGL_DEVICE T reduce_max(T value, uint32_t active_mask = kFullMask) {
|
||||
static_assert(kNumThreads >= 1 && kNumThreads <= kWarpThreads);
|
||||
static_assert(std::has_single_bit(kNumThreads), "must be pow of 2");
|
||||
#pragma unroll
|
||||
for (int mask = 16; mask > 0; mask >>= 1)
|
||||
for (int mask = kNumThreads / 2; mask > 0; mask >>= 1)
|
||||
value = math::max(value, __shfl_xor_sync(active_mask, value, mask, 32));
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.utils import cache_once, load_jit
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tvm_ffi.module import Module
|
||||
|
||||
|
||||
_SCORING_FUNC_MAP = {
|
||||
"sigmoid": 0,
|
||||
"sqrtsoftplus": 1,
|
||||
}
|
||||
|
||||
|
||||
@cache_once
|
||||
def _jit_moe_fused_gate_module() -> Module:
|
||||
return load_jit(
|
||||
"moe_fused_gate",
|
||||
cuda_files=["moe/moe_fused_gate.cuh"],
|
||||
cuda_wrappers=[("moe_fused_gate", "MoEFusedGateKernel::run")],
|
||||
)
|
||||
|
||||
|
||||
@cache_once
|
||||
def can_use_moe_fused_gate() -> bool:
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
_jit_moe_fused_gate_module()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load JIT MoE fused gate kernel: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def moe_fused_gate(
|
||||
input: torch.Tensor,
|
||||
bias: torch.Tensor,
|
||||
topk: int,
|
||||
scoring_func: str = "sigmoid",
|
||||
num_fused_shared_experts: int = 0,
|
||||
renormalize: bool = True,
|
||||
routed_scaling_factor: float = 1.0,
|
||||
apply_routed_scaling_factor_on_output: bool = False,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
scoring_func_int = _SCORING_FUNC_MAP.get(scoring_func.lower())
|
||||
assert (
|
||||
scoring_func_int is not None
|
||||
), f"Unknown scoring_func '{scoring_func}', must be one of {list(_SCORING_FUNC_MAP.keys())}"
|
||||
|
||||
assert input.dtype == torch.float32, "input must be float32"
|
||||
assert bias.dtype == torch.float32, "bias must be float32"
|
||||
assert input.ndim == 2, "input must be 2D"
|
||||
assert bias.ndim == 1, "bias must be 1D"
|
||||
assert input.size(1) == bias.size(0), "input and bias must have same num_experts"
|
||||
assert topk > num_fused_shared_experts, "topk must be > num_fused_shared_experts"
|
||||
|
||||
num_rows, _ = input.shape
|
||||
device = input.device
|
||||
|
||||
output = torch.empty(num_rows, topk, dtype=torch.float32, device=device)
|
||||
indices = torch.empty(num_rows, topk, dtype=torch.int32, device=device)
|
||||
|
||||
module = _jit_moe_fused_gate_module()
|
||||
module.moe_fused_gate(
|
||||
input,
|
||||
bias,
|
||||
output,
|
||||
indices,
|
||||
topk,
|
||||
scoring_func_int,
|
||||
num_fused_shared_experts,
|
||||
renormalize,
|
||||
routed_scaling_factor,
|
||||
apply_routed_scaling_factor_on_output,
|
||||
)
|
||||
|
||||
return output, indices
|
||||
Reference in New Issue
Block a user