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
@@ -17,7 +17,7 @@ classifiers = [
|
||||
dependencies = [
|
||||
"IPython",
|
||||
"aiohttp",
|
||||
"apache-tvm-ffi>=0.1.5,<0.2",
|
||||
"apache-tvm-ffi==0.1.9",
|
||||
"anthropic>=0.20.0",
|
||||
"blobfile==3.0.0",
|
||||
"build",
|
||||
@@ -63,6 +63,7 @@ dependencies = [
|
||||
"sglang-kernel==0.4.2.post1",
|
||||
"soundfile==0.13.1",
|
||||
"tiktoken",
|
||||
"tilelang==0.1.8",
|
||||
"timm==1.0.16",
|
||||
"torch_memory_saver>=0.0.9.post1",
|
||||
"torch==2.11.0",
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,86 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def apply_deepseek_v4_defaults(server_args: "ServerArgs", model_arch: str) -> None:
|
||||
"""Apply DeepSeek V4 model-specific server arg defaults and constraints."""
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
server_args.attention_backend = "dsv4"
|
||||
server_args.page_size = 256
|
||||
logger.info(
|
||||
f"Use dsv4 attention backend for {model_arch}, setting page_size to 256."
|
||||
)
|
||||
|
||||
if server_args.max_running_requests is None:
|
||||
server_args.max_running_requests = 256
|
||||
logger.warning(
|
||||
f"Setting max_running_requests to {server_args.max_running_requests} for {model_arch}."
|
||||
)
|
||||
|
||||
if server_args.kv_cache_dtype == "auto":
|
||||
server_args.kv_cache_dtype = "fp8_e4m3"
|
||||
logger.warning(
|
||||
f"Setting KV cache dtype to {server_args.kv_cache_dtype} for {model_arch}."
|
||||
)
|
||||
assert server_args.kv_cache_dtype in [
|
||||
"fp8_e4m3"
|
||||
], f"{server_args.kv_cache_dtype} is not supported for {model_arch}"
|
||||
|
||||
if server_args.speculative_algorithm is not None:
|
||||
assert (
|
||||
server_args.speculative_algorithm == "EAGLE"
|
||||
), f"Only EAGLE speculative algorithm is supported for {model_arch}"
|
||||
assert (
|
||||
server_args.speculative_eagle_topk == 1
|
||||
), f"Only EAGLE speculative algorithm with topk == 1 is supported for {model_arch}"
|
||||
|
||||
if not envs.SGLANG_ENABLE_SPEC_V2.get():
|
||||
envs.SGLANG_ENABLE_SPEC_V2.set(True)
|
||||
logger.warning("Spec v2 is enabled for EAGLE speculative decoding.")
|
||||
|
||||
if server_args.swa_full_tokens_ratio == ServerArgs.swa_full_tokens_ratio:
|
||||
server_args.swa_full_tokens_ratio = 0.1
|
||||
logger.info(
|
||||
f"Setting swa_full_tokens_ratio to {server_args.swa_full_tokens_ratio} for {model_arch}."
|
||||
)
|
||||
|
||||
if server_args.disaggregation_mode != "null" and server_args.pp_size > 1:
|
||||
# get_mla_kv_ptrs_with_pp cannot slice V4's buffer-type-organized
|
||||
# flat KV ptrs by PP layer range.
|
||||
raise ValueError(
|
||||
f"V4 PD disaggregation requires pp_size=1, got pp_size={server_args.pp_size}."
|
||||
)
|
||||
|
||||
|
||||
def validate_deepseek_v4_cp(server_args: "ServerArgs") -> None:
|
||||
"""Validate DeepSeek V4 context-parallel configuration."""
|
||||
if not server_args.enable_nsa_prefill_context_parallel:
|
||||
return
|
||||
|
||||
if server_args.nsa_prefill_cp_mode != "round-robin-split":
|
||||
raise ValueError(
|
||||
f"DeepSeekV4 only supports round-robin-split CP mode, "
|
||||
f"got {server_args.nsa_prefill_cp_mode}"
|
||||
)
|
||||
|
||||
server_args.enable_dp_attention = True
|
||||
server_args.moe_dense_tp_size = 1
|
||||
server_args.attn_cp_size = server_args.tp_size // server_args.dp_size
|
||||
assert (
|
||||
server_args.dp_size == 1
|
||||
), "For round-robin split mode, dp attention is not supported."
|
||||
assert (
|
||||
server_args.tp_size <= 8
|
||||
), "Context parallel only supports single machine (tp_size <= 8). Cross-machine CP has precision issues."
|
||||
logger.warning(
|
||||
f"Enable Context Parallel for DeepSeekV4, "
|
||||
f"dp_size={server_args.dp_size}, moe_dense_tp_size={server_args.moe_dense_tp_size}, "
|
||||
f"attn_cp_size={server_args.attn_cp_size}, ep_size={server_args.ep_size}, tp_size={server_args.tp_size}"
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Backend/dtype pairing: flashmla_sparse only takes BF16 KV;
|
||||
# flashmla_kv only supports FP8 (it always reads KV as FP8 via
|
||||
# is_fp8_kvcache=True, inline-quantizing BF16 would defeat HiSparse).
|
||||
_HISPARSE_ALLOWED_BACKENDS_BY_DTYPE = {
|
||||
"bfloat16": {"flashmla_sparse"},
|
||||
"fp8_e4m3": {"flashmla_kv"},
|
||||
}
|
||||
|
||||
|
||||
def _hisparse_default_backend(kv_cache_dtype: str) -> str:
|
||||
return "flashmla_kv" if kv_cache_dtype == "fp8_e4m3" else "flashmla_sparse"
|
||||
|
||||
|
||||
def apply_hisparse_nsa_backend_defaults(
|
||||
server_args: "ServerArgs",
|
||||
user_set_prefill: bool,
|
||||
user_set_decode: bool,
|
||||
kv_cache_dtype: str,
|
||||
) -> bool:
|
||||
"""Pick NSA backends for --enable-hisparse based on KV dtype.
|
||||
|
||||
BF16 KV -> flashmla_sparse, FP8 KV -> flashmla_kv. Returns True if hisparse
|
||||
handled backend selection (caller should skip its own default logic).
|
||||
"""
|
||||
if not server_args.enable_hisparse:
|
||||
return False
|
||||
|
||||
backend = _hisparse_default_backend(kv_cache_dtype)
|
||||
if not user_set_prefill:
|
||||
server_args.nsa_prefill_backend = backend
|
||||
if not user_set_decode:
|
||||
server_args.nsa_decode_backend = backend
|
||||
logger.warning(
|
||||
f"HiSparse enabled ({kv_cache_dtype}): using NSA backends "
|
||||
f"prefill={server_args.nsa_prefill_backend}, decode={server_args.nsa_decode_backend}."
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def validate_hisparse(server_args: "ServerArgs") -> None:
|
||||
"""Validate --enable-hisparse constraints (model class, radix cache, NSA backend)."""
|
||||
if not server_args.enable_hisparse:
|
||||
return
|
||||
|
||||
from sglang.srt.configs.model_config import (
|
||||
is_deepseek_nsa,
|
||||
is_deepseek_v4,
|
||||
)
|
||||
|
||||
hf_config = server_args.get_model_config().hf_config
|
||||
is_v4_hisparse = is_deepseek_v4(hf_config)
|
||||
assert is_deepseek_nsa(hf_config) or is_v4_hisparse, (
|
||||
"--enable-hisparse is only supported for DSA (DeepSeek Sparse Attention) "
|
||||
"models (e.g., DeepSeek V3.2, GLM-5) and DeepSeek V4 now. "
|
||||
)
|
||||
|
||||
assert (
|
||||
server_args.disable_radix_cache
|
||||
), "Hierarchical sparse attention currently requires --disable-radix-cache."
|
||||
|
||||
# DSv4 hisparse handles its own dtype/backend pairing elsewhere; the dtype-
|
||||
# aware checks below only apply to the DSA hisparse path.
|
||||
if is_v4_hisparse:
|
||||
return
|
||||
|
||||
if server_args.kv_cache_dtype not in ("bfloat16", "auto", "fp8_e4m3"):
|
||||
raise ValueError(
|
||||
f"HiSparse requires bfloat16 or fp8_e4m3 KV cache, "
|
||||
f"but got --kv-cache-dtype={server_args.kv_cache_dtype}. "
|
||||
f"Please use --kv-cache-dtype=bfloat16 or fp8_e4m3."
|
||||
)
|
||||
|
||||
allowed_backends = _HISPARSE_ALLOWED_BACKENDS_BY_DTYPE.get(
|
||||
server_args.kv_cache_dtype, {"flashmla_sparse", "flashmla_kv"}
|
||||
)
|
||||
for attr, label in [
|
||||
("nsa_prefill_backend", "prefill"),
|
||||
("nsa_decode_backend", "decode"),
|
||||
]:
|
||||
backend = getattr(server_args, attr)
|
||||
if backend is not None and backend not in allowed_backends:
|
||||
raise ValueError(
|
||||
f"HiSparse with --kv-cache-dtype={server_args.kv_cache_dtype} requires "
|
||||
f"--nsa-{label}-backend in {sorted(allowed_backends)}, "
|
||||
f"but got {backend}."
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from transformers import PretrainedConfig
|
||||
|
||||
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def try_detect_fp4_experts(model_path: str) -> Optional[bool]:
|
||||
"""True = mxfp4-packed (U8/I8/F4), False = converted FP8 (F8_E4M3),
|
||||
None when the header isn't readable (HF slug not cached yet, etc.).
|
||||
Caller falls back to user default. Pure read; never mutates env.
|
||||
"""
|
||||
from sglang.srt.model_loader.weight_utils import (
|
||||
probe_routed_expert_weight_dtype,
|
||||
)
|
||||
from sglang.srt.utils import find_local_repo_dir
|
||||
|
||||
if os.path.isdir(model_path):
|
||||
local_path = model_path
|
||||
else:
|
||||
local_path = find_local_repo_dir(model_path)
|
||||
if not local_path or not os.path.isdir(local_path):
|
||||
return None
|
||||
|
||||
try:
|
||||
dtype = probe_routed_expert_weight_dtype(local_path)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to probe routed-expert dtype for %s: %s", model_path, e)
|
||||
return None
|
||||
if dtype is None:
|
||||
return None
|
||||
if dtype in ("U8", "I8", "F4"):
|
||||
return True
|
||||
if dtype == "F8_E4M3":
|
||||
return False
|
||||
logger.warning(
|
||||
"Unexpected routed-expert safetensors dtype=%s for DeepSeek V4", dtype
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class DeepSeekV4Config(PretrainedConfig):
|
||||
architectures: List[str]
|
||||
attention_bias: bool = False
|
||||
attention_dropout: float = 0.0
|
||||
bos_token_id: int = 0
|
||||
eos_token_id: int = 1
|
||||
ep_size: int = 1
|
||||
first_k_dense_replace: int = 0
|
||||
hidden_act: str = "silu"
|
||||
hidden_size: int = 4096
|
||||
index_head_dim: int = 128
|
||||
index_n_heads: int = 64
|
||||
index_topk: int = 512
|
||||
initializer_range: float = 0.02
|
||||
intermediate_size: int = 2048
|
||||
kv_lora_rank: int = 512
|
||||
max_position_embeddings: int = 65536
|
||||
model_type: str = "deepseek_v4"
|
||||
moe_intermediate_size: int = 2048
|
||||
moe_layer_freq: int = 1
|
||||
n_group: int = 8
|
||||
n_routed_experts: int = 256
|
||||
n_shared_experts: int = 1
|
||||
norm_topk_prob: bool = True
|
||||
|
||||
num_attention_heads: int = 64
|
||||
num_experts_per_tok: int = 6
|
||||
num_hidden_layers: int = 43
|
||||
num_key_value_heads: int = 1
|
||||
|
||||
q_lora_rank: int = 1024
|
||||
qk_nope_head_dim: int = 448
|
||||
qk_rope_head_dim: int = 64
|
||||
|
||||
quantization_config: QuantizationConfig = field(default_factory=QuantizationConfig)
|
||||
|
||||
rms_norm_eps: float = 1e-6
|
||||
|
||||
rope_scaling: Dict[str, float] = field(default_factory=dict)
|
||||
rope_theta: int = 10000
|
||||
|
||||
routed_scaling_factor: float = 1.5
|
||||
scoring_func: str = "sqrtsoftplus"
|
||||
|
||||
tie_word_embeddings: bool = False
|
||||
|
||||
topk_group: int = 8
|
||||
topk_method: str = "noaux_tc"
|
||||
|
||||
use_cache: bool = True
|
||||
v_head_dim: int = 512
|
||||
vocab_size: int = 129280
|
||||
o_lora_rank: int = 1024
|
||||
o_groups: int = 8
|
||||
window_size: int = 128
|
||||
|
||||
compress_rope_theta: int = 40000
|
||||
compress_ratios: List[int] = field(default_factory=list)
|
||||
|
||||
n_hash_layers: int = 3
|
||||
hc_mult: int = 4
|
||||
hc_sinkhorn_iters: int = 20
|
||||
hc_eps: float = 1e-6
|
||||
@@ -114,8 +114,15 @@ def is_deepseek_nsa(config) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def is_deepseek_v4(config) -> bool:
|
||||
return _hf_arch(config) in (
|
||||
"DeepseekV4ForCausalLM",
|
||||
"DeepseekV4ForCausalLMNextN",
|
||||
)
|
||||
|
||||
|
||||
def get_nsa_index_head_dim(config: PretrainedConfig) -> int:
|
||||
assert is_deepseek_nsa(config)
|
||||
assert is_deepseek_nsa(config) or is_deepseek_v4(config)
|
||||
return config.index_head_dim
|
||||
|
||||
|
||||
@@ -134,11 +141,15 @@ def get_num_indexer_layers(config) -> int:
|
||||
|
||||
NSA models (V3.2) instantiate an Indexer on every transformer layer.
|
||||
With index_topk_freq > 1 some layers reuse prev layer's topk; those still
|
||||
get a slot (mirrored at the MLA call site). Other architectures: set
|
||||
get a slot (mirrored at the MLA call site). DSv4 has C4 indexers only on
|
||||
layers whose compress_ratio == 4. Other architectures: set
|
||||
num_indexer_layers on hf_text_config; 0 disables the capturer.
|
||||
"""
|
||||
if is_deepseek_nsa(config):
|
||||
return config.num_hidden_layers
|
||||
if is_deepseek_v4(config):
|
||||
compress_ratios = getattr(config, "compress_ratios", None) or []
|
||||
return sum(1 for r in compress_ratios if r == 4)
|
||||
return getattr(config, "num_indexer_layers", 0)
|
||||
|
||||
|
||||
@@ -221,6 +232,34 @@ class ModelConfig:
|
||||
# Config draft model
|
||||
self._config_draft_model()
|
||||
|
||||
# DSV4 expert layout: env (default True = mxfp4) applies only to V4.
|
||||
# Other FP8 MoE models (for example DeepSeek V3.2) must keep the normal
|
||||
# FP8 expert tensor layout.
|
||||
self.is_fp4_experts: bool = False
|
||||
if is_deepseek_v4(self.hf_config):
|
||||
self.is_fp4_experts = envs.SGLANG_DSV4_FP4_EXPERTS.get()
|
||||
if not envs.SGLANG_DSV4_FP4_EXPERTS.is_set():
|
||||
from sglang.srt.configs.deepseek_v4 import try_detect_fp4_experts
|
||||
|
||||
detected = try_detect_fp4_experts(self.model_path)
|
||||
if detected is not None:
|
||||
self.is_fp4_experts = detected
|
||||
logger.info(
|
||||
"Auto-detected DSV4 routed-expert layout: is_fp4_experts=%s",
|
||||
self.is_fp4_experts,
|
||||
)
|
||||
|
||||
# HF config.json inherits topk_group=4 from the V3 template, but
|
||||
# DSV4 trains with no group limiting (sqrtsoftplus + full-expert
|
||||
# top-k). Force topk_group == n_group so deepseek_v2.py:531's
|
||||
# `n_group > topk_group` evaluates False and routes to the
|
||||
# ungrouped sqrtsoftplus path. The grouped impl only supports
|
||||
# sigmoid scoring (topk.py:722) and would silently corrupt expert
|
||||
# weights if hit.
|
||||
n_group = getattr(self.hf_config, "n_group", None)
|
||||
if n_group is not None:
|
||||
self.hf_config.topk_group = n_group
|
||||
|
||||
# Check model type
|
||||
self.attention_chunk_size = getattr(
|
||||
self.hf_text_config, "attention_chunk_size", None
|
||||
@@ -367,6 +406,13 @@ class ModelConfig:
|
||||
]:
|
||||
self.hf_config.architectures[0] = "DeepseekV3ForCausalLMNextN"
|
||||
|
||||
if (
|
||||
is_draft_model
|
||||
and self.hf_config.architectures[0] == "DeepseekV4ForCausalLM"
|
||||
):
|
||||
self.hf_config.architectures[0] = "DeepseekV4ForCausalLMNextN"
|
||||
self.hf_config.num_nextn_predict_layers = 1
|
||||
|
||||
if is_draft_model and self.hf_config.architectures[0] in [
|
||||
"Glm4MoeForCausalLM",
|
||||
"Glm4MoeLiteForCausalLM",
|
||||
@@ -434,13 +480,21 @@ class ModelConfig:
|
||||
)
|
||||
|
||||
if self.is_hybrid_swa:
|
||||
self.swa_attention_layer_ids, self.full_attention_layer_ids = (
|
||||
get_hybrid_layer_ids(
|
||||
self.hf_config.architectures,
|
||||
self.hf_text_config,
|
||||
)
|
||||
logger.info(f"Hybrid swa model: {self.hf_config.architectures=}")
|
||||
|
||||
self.is_deepseek_v4_arch = any(
|
||||
arch in ["DeepseekV4ForCausalLM", "DeepseekV4ForCausalLMNextN"]
|
||||
for arch in self.hf_config.architectures
|
||||
)
|
||||
|
||||
if not self.is_deepseek_v4_arch:
|
||||
self.swa_attention_layer_ids, self.full_attention_layer_ids = (
|
||||
get_hybrid_layer_ids(
|
||||
self.hf_config.architectures,
|
||||
self.hf_text_config,
|
||||
)
|
||||
)
|
||||
|
||||
self.has_attention_sinks = self._detect_attention_sinks()
|
||||
|
||||
self.is_hybrid_swa_compress = self.hf_config.architectures[0] in [
|
||||
@@ -571,6 +625,23 @@ class ModelConfig:
|
||||
self.scaling = compute_mla_mscale_scaling(
|
||||
rope_scaling, self.scaling
|
||||
)
|
||||
elif (
|
||||
"DeepseekV4ForCausalLM" in self.hf_config.architectures
|
||||
or "DeepseekV4ForCausalLMNextN" in self.hf_config.architectures
|
||||
):
|
||||
self.qk_rope_head_dim = self.hf_config.qk_rope_head_dim
|
||||
self.qk_nope_head_dim = self.hf_config.head_dim - self.qk_rope_head_dim
|
||||
self.window_size = self.hf_config.sliding_window
|
||||
self.head_dim = self.qk_nope_head_dim + self.qk_rope_head_dim
|
||||
self.v_head_dim = self.head_dim
|
||||
self.index_head_dim = self.hf_config.index_head_dim
|
||||
self.compress_ratios = self.hf_config.compress_ratios
|
||||
self.attention_arch = AttentionArch.MHA
|
||||
self.scaling = 1 / math.sqrt(self.qk_nope_head_dim + self.qk_rope_head_dim)
|
||||
if self.hf_config.rope_scaling:
|
||||
self.scaling = compute_mla_mscale_scaling(
|
||||
self.hf_config.rope_scaling, self.scaling
|
||||
)
|
||||
elif "MiniCPM3ForCausalLM" in self.hf_config.architectures:
|
||||
self.head_dim = 128
|
||||
self.attention_arch = AttentionArch.MLA
|
||||
@@ -1466,6 +1537,8 @@ multimodal_model_archs = [
|
||||
|
||||
piecewise_cuda_graph_disabled_model_archs = [
|
||||
"DeepseekV32ForCausalLM",
|
||||
"DeepseekV4ForCausalLM",
|
||||
"DeepseekV4ForCausalLMNextN",
|
||||
"Qwen3NextForCausalLM",
|
||||
"GlmMoeDsaForCausalLM",
|
||||
"BailingMoeV2_5ForCausalLM",
|
||||
@@ -1568,6 +1641,8 @@ def is_hybrid_swa_model(model_architectures: List[str]):
|
||||
|
||||
hybrid_swa_archs = {
|
||||
"Llama4ForConditionalGeneration",
|
||||
"DeepseekV4ForCausalLM",
|
||||
"DeepseekV4ForCausalLMNextN",
|
||||
"GptOssForCausalLM",
|
||||
*MIMO_V2_MODEL_ARCHS,
|
||||
"MiMoV2MTP",
|
||||
|
||||
@@ -31,7 +31,7 @@ class KVArgs:
|
||||
state_data_ptrs: List[int]
|
||||
state_data_lens: List[int]
|
||||
state_item_lens: List[int]
|
||||
state_type: str # "none", "mamba", "swa"
|
||||
state_type: str # "none", "mamba", "swa", "nsa", "dsv4"
|
||||
# for mamba state different tp slice transfer
|
||||
state_dim_per_tensor: List[int] # dimension to slice for each state tensor
|
||||
ib_device: str
|
||||
|
||||
@@ -55,10 +55,8 @@ from sglang.srt.managers.schedule_batch import FINISH_ABORT, ScheduleBatch
|
||||
from sglang.srt.managers.schedule_policy import match_prefix_for_req
|
||||
from sglang.srt.managers.utils import GenerationBatchResult
|
||||
from sglang.srt.mem_cache.allocator import BaseTokenToKVPoolAllocator
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
BasePrefixCache,
|
||||
EvictParams,
|
||||
)
|
||||
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache, EvictParams
|
||||
from sglang.srt.mem_cache.base_swa_memory_pool import BaseSWAKVPool
|
||||
from sglang.srt.mem_cache.common import (
|
||||
kv_to_page_indices,
|
||||
page_align_floor,
|
||||
@@ -71,7 +69,6 @@ from sglang.srt.mem_cache.memory_pool import (
|
||||
NSATokenToKVPool,
|
||||
ReqToTokenPool,
|
||||
)
|
||||
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
|
||||
from sglang.srt.observability.req_time_stats import (
|
||||
set_schedule_time_batch,
|
||||
set_time_batch,
|
||||
@@ -822,8 +819,7 @@ class DecodePreallocQueue:
|
||||
.cpu()
|
||||
.numpy()
|
||||
]
|
||||
elif isinstance(self.token_to_kv_pool, SWAKVPool):
|
||||
# SWA hybrid model: send decode-side SWA window indices
|
||||
elif isinstance(self.token_to_kv_pool, BaseSWAKVPool):
|
||||
seq_len = len(decode_req.req.origin_input_ids)
|
||||
window_size = self.scheduler.sliding_window_size
|
||||
|
||||
|
||||
@@ -1010,6 +1010,62 @@ class NixlKVManager(CommonKVManager):
|
||||
raise Exception("Failed to post Mamba state slice transfer")
|
||||
return xfer_handle
|
||||
|
||||
def _send_state_pages_flat(
|
||||
self,
|
||||
peer_name: str,
|
||||
prefill_state_indices: List[int],
|
||||
dst_state_data_ptrs: list[int],
|
||||
dst_state_indices: List[int],
|
||||
dst_state_item_lens: list[int],
|
||||
dst_gpu_id: int,
|
||||
notif: str,
|
||||
):
|
||||
"""Per-page WRITE transfer of a flat (heterogeneous) state pool.
|
||||
|
||||
Used by V4 whose state pool is a flat list of buffers (SWA + compress
|
||||
+ indexer pools) that does not match the per-layer K/V layout assumed
|
||||
by ``_send_kvcache_generic``. Both sides must have identical
|
||||
``state_item_lens`` (no TP-slicing path).
|
||||
"""
|
||||
src_state_ptrs = self.kv_args.state_data_ptrs
|
||||
src_state_item_lens = self.kv_args.state_item_lens
|
||||
assert len(src_state_ptrs) == len(dst_state_data_ptrs)
|
||||
assert len(src_state_item_lens) == len(dst_state_item_lens)
|
||||
assert len(prefill_state_indices) == len(dst_state_indices), (
|
||||
f"State index length mismatch: prefill={len(prefill_state_indices)}, "
|
||||
f"dst={len(dst_state_indices)}"
|
||||
)
|
||||
for i in range(len(src_state_item_lens)):
|
||||
assert src_state_item_lens[i] == dst_state_item_lens[i], (
|
||||
f"V4 state item length mismatch at index {i}: "
|
||||
f"{src_state_item_lens[i]} != {dst_state_item_lens[i]}"
|
||||
)
|
||||
|
||||
src_addrs = []
|
||||
dst_addrs = []
|
||||
for i in range(len(src_state_ptrs)):
|
||||
item_len = src_state_item_lens[i]
|
||||
for src_idx, dst_idx in zip(prefill_state_indices, dst_state_indices):
|
||||
src_addr = src_state_ptrs[i] + int(src_idx) * item_len
|
||||
dst_addr = dst_state_data_ptrs[i] + int(dst_idx) * item_len
|
||||
src_addrs.append((src_addr, item_len, self.kv_args.gpu_id))
|
||||
dst_addrs.append((dst_addr, item_len, dst_gpu_id))
|
||||
|
||||
if not src_addrs:
|
||||
return None
|
||||
|
||||
src_descs = self.agent.get_xfer_descs(src_addrs, "VRAM")
|
||||
dst_descs = self.agent.get_xfer_descs(dst_addrs, "VRAM")
|
||||
xfer_handle = self.agent.initialize_xfer(
|
||||
"WRITE", src_descs, dst_descs, peer_name, notif.encode("ascii")
|
||||
)
|
||||
if not xfer_handle:
|
||||
raise Exception("KVSender failed to create state transfer")
|
||||
state = self.agent.transfer(xfer_handle)
|
||||
if state == "ERR":
|
||||
raise Exception("KVSender failed to post state transfer")
|
||||
return xfer_handle
|
||||
|
||||
def maybe_send_extra(
|
||||
self,
|
||||
peer_name: str,
|
||||
@@ -1068,6 +1124,16 @@ class NixlKVManager(CommonKVManager):
|
||||
dst_gpu_id=dst_gpu_id,
|
||||
notif=notif,
|
||||
)
|
||||
elif state_type == "dsv4":
|
||||
return self._send_state_pages_flat(
|
||||
peer_name,
|
||||
prefill_state_indices,
|
||||
dst_state_data_ptrs,
|
||||
dst_state_indices,
|
||||
dst_state_item_lens or [],
|
||||
dst_gpu_id,
|
||||
notif,
|
||||
)
|
||||
else:
|
||||
if state_type != "none":
|
||||
raise RuntimeError(
|
||||
|
||||
@@ -48,6 +48,7 @@ from sglang.srt.managers.schedule_batch import (
|
||||
Req,
|
||||
ScheduleBatch,
|
||||
)
|
||||
from sglang.srt.mem_cache.base_swa_memory_pool import BaseSWAKVPool
|
||||
from sglang.srt.mem_cache.common import (
|
||||
kv_to_page_indices,
|
||||
kv_to_page_num,
|
||||
@@ -55,7 +56,6 @@ from sglang.srt.mem_cache.common import (
|
||||
release_kv_cache,
|
||||
)
|
||||
from sglang.srt.mem_cache.memory_pool import HybridLinearKVPool, NSATokenToKVPool
|
||||
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
|
||||
from sglang.srt.observability.req_time_stats import set_schedule_time_batch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -787,7 +787,9 @@ class SchedulerDisaggregationPrefillMixin:
|
||||
.cpu()
|
||||
.numpy()
|
||||
]
|
||||
elif isinstance(self.token_to_kv_pool_allocator.get_kvcache(), SWAKVPool):
|
||||
elif isinstance(
|
||||
self.token_to_kv_pool_allocator.get_kvcache(), BaseSWAKVPool
|
||||
):
|
||||
# SWA hybrid model: send last window KV indices
|
||||
seq_len = len(req.fill_ids)
|
||||
window_size = self.sliding_window_size
|
||||
|
||||
@@ -526,9 +526,10 @@ def filter_kv_indices_for_cp_rank(
|
||||
|
||||
|
||||
def is_mla_backend(target_kv_pool) -> bool:
|
||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
||||
from sglang.srt.mem_cache.memory_pool import MLATokenToKVPool
|
||||
|
||||
return isinstance(target_kv_pool, MLATokenToKVPool)
|
||||
return isinstance(target_kv_pool, (MLATokenToKVPool, DeepSeekV4TokenToKVPool))
|
||||
|
||||
|
||||
def setup_state_kv_args(
|
||||
@@ -541,8 +542,9 @@ def setup_state_kv_args(
|
||||
Shared by prefill and decode bootstrap paths so the state_type dispatch
|
||||
lives in one place.
|
||||
"""
|
||||
from sglang.srt.mem_cache.base_swa_memory_pool import BaseSWAKVPool
|
||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
||||
from sglang.srt.mem_cache.memory_pool import HybridLinearKVPool, NSATokenToKVPool
|
||||
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
|
||||
|
||||
if not hasattr(token_to_kv_pool, "get_state_buf_infos"):
|
||||
kv_args.state_data_ptrs = []
|
||||
@@ -558,7 +560,12 @@ def setup_state_kv_args(
|
||||
kv_args.state_data_lens = state_data_lens
|
||||
kv_args.state_item_lens = state_item_lens
|
||||
|
||||
if isinstance(token_to_kv_pool, SWAKVPool):
|
||||
# V4 must be checked before BaseSWAKVPool: V4's state pool is a flat
|
||||
# heterogeneous list (SWA + compress + indexer), so the per-layer K/V
|
||||
# transfer path used for "swa"/"nsa" does not apply.
|
||||
if isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool):
|
||||
kv_args.state_type = "dsv4"
|
||||
elif isinstance(token_to_kv_pool, BaseSWAKVPool):
|
||||
kv_args.state_type = "swa"
|
||||
elif isinstance(token_to_kv_pool, HybridLinearKVPool):
|
||||
kv_args.state_type = "mamba"
|
||||
|
||||
@@ -0,0 +1,850 @@
|
||||
# Adapted from the DeepSeek-V4 release reference implementation.
|
||||
"""
|
||||
DeepSeek-V4 Encoding
|
||||
|
||||
A self-contained implementation for encoding/decoding DeepSeek-V4 chat messages
|
||||
with tool calling, thinking mode, and quick instruction task support.
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
# ============================================================
|
||||
# Special Tokens
|
||||
# ============================================================
|
||||
|
||||
bos_token: str = "<|begin▁of▁sentence|>"
|
||||
eos_token: str = "<|end▁of▁sentence|>"
|
||||
thinking_start_token: str = "<think>"
|
||||
thinking_end_token: str = "</think>"
|
||||
dsml_token: str = "|DSML|"
|
||||
|
||||
USER_SP_TOKEN = "<|User|>"
|
||||
ASSISTANT_SP_TOKEN = "<|Assistant|>"
|
||||
LATEST_REMINDER_SP_TOKEN = "<|latest_reminder|>"
|
||||
|
||||
# Task special tokens for internal classification tasks
|
||||
DS_TASK_SP_TOKENS = {
|
||||
"action": "<|action|>",
|
||||
"query": "<|query|>",
|
||||
"authority": "<|authority|>",
|
||||
"domain": "<|domain|>",
|
||||
"title": "<|title|>",
|
||||
"read_url": "<|read_url|>",
|
||||
}
|
||||
VALID_TASKS = set(DS_TASK_SP_TOKENS.keys())
|
||||
|
||||
# ============================================================
|
||||
# Templates
|
||||
# ============================================================
|
||||
|
||||
system_msg_template: str = "{content}"
|
||||
user_msg_template: str = "{content}"
|
||||
latest_reminder_msg_template: str = "{content}"
|
||||
assistant_msg_template: str = "{reasoning}{content}{tool_calls}" + eos_token
|
||||
assistant_msg_wo_eos_template: str = "{reasoning}{content}{tool_calls}"
|
||||
thinking_template: str = "{reasoning_content}"
|
||||
|
||||
response_format_template: str = (
|
||||
"## Response Format:\n\nYou MUST strictly adhere to the following schema to reply:\n{schema}"
|
||||
)
|
||||
tool_call_template: str = (
|
||||
'<{dsml_token}invoke name="{name}">\n{arguments}\n</{dsml_token}invoke>'
|
||||
)
|
||||
tool_calls_template = (
|
||||
"<{dsml_token}{tc_block_name}>\n{tool_calls}\n</{dsml_token}{tc_block_name}>"
|
||||
)
|
||||
tool_calls_block_name: str = "tool_calls"
|
||||
|
||||
tool_output_template: str = "<tool_result>{content}</tool_result>"
|
||||
|
||||
REASONING_EFFORT_MAX = (
|
||||
"Reasoning Effort: Absolute maximum with no shortcuts permitted.\n"
|
||||
"You MUST be very thorough in your thinking and comprehensively decompose the problem to resolve the root cause, rigorously stress-testing your logic against all potential paths, edge cases, and adversarial scenarios.\n"
|
||||
"Explicitly write out your entire deliberation process, documenting every intermediate step, considered alternative, and rejected hypothesis to ensure absolutely no assumption is left unchecked.\n\n"
|
||||
)
|
||||
|
||||
TOOLS_TEMPLATE = """## Tools
|
||||
|
||||
You have access to a set of tools to help answer the user's question. You can invoke tools by writing a "<{dsml_token}tool_calls>" block like the following:
|
||||
|
||||
<{dsml_token}tool_calls>
|
||||
<{dsml_token}invoke name="$TOOL_NAME">
|
||||
<{dsml_token}parameter name="$PARAMETER_NAME" string="true|false">$PARAMETER_VALUE</{dsml_token}parameter>
|
||||
...
|
||||
</{dsml_token}invoke>
|
||||
<{dsml_token}invoke name="$TOOL_NAME2">
|
||||
...
|
||||
</{dsml_token}invoke>
|
||||
</{dsml_token}tool_calls>
|
||||
|
||||
String parameters should be specified as is and set `string="true"`. For all other types (numbers, booleans, arrays, objects), pass the value in JSON format and set `string="false"`.
|
||||
|
||||
If thinking_mode is enabled (triggered by {thinking_start_token}), you MUST output your complete reasoning inside {thinking_start_token}...{thinking_end_token} BEFORE any tool calls or final response.
|
||||
|
||||
Otherwise, output directly after {thinking_end_token} with tool calls or final response.
|
||||
|
||||
### Available Tool Schemas
|
||||
|
||||
{tool_schemas}
|
||||
|
||||
You MUST strictly follow the above defined tool name and parameter schemas to invoke tool calls.
|
||||
"""
|
||||
|
||||
# ============================================================
|
||||
# Utility Functions
|
||||
# ============================================================
|
||||
|
||||
|
||||
def to_json(value: Any) -> str:
|
||||
"""Serialize a value to JSON string."""
|
||||
try:
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
except:
|
||||
return json.dumps(value, ensure_ascii=True)
|
||||
|
||||
|
||||
def tools_from_openai_format(tools):
|
||||
"""Extract function definitions from OpenAI-format tool list."""
|
||||
return [tool["function"] for tool in tools]
|
||||
|
||||
|
||||
def tool_calls_from_openai_format(tool_calls):
|
||||
"""Convert OpenAI-format tool calls to internal format."""
|
||||
return [
|
||||
{
|
||||
"name": tool_call["function"]["name"],
|
||||
"arguments": tool_call["function"]["arguments"],
|
||||
}
|
||||
for tool_call in tool_calls
|
||||
]
|
||||
|
||||
|
||||
def tool_calls_to_openai_format(tool_calls):
|
||||
"""Convert internal tool calls to OpenAI format."""
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_call["name"],
|
||||
"arguments": tool_call["arguments"],
|
||||
},
|
||||
}
|
||||
for tool_call in tool_calls
|
||||
]
|
||||
|
||||
|
||||
def encode_arguments_to_dsml(tool_call: Dict[str, str]) -> str:
|
||||
"""
|
||||
Encode tool call arguments into DSML parameter format.
|
||||
|
||||
Args:
|
||||
tool_call: Dict with "name" and "arguments" (JSON string) keys.
|
||||
|
||||
Returns:
|
||||
DSML-formatted parameter string.
|
||||
"""
|
||||
p_dsml_template = '<{dsml_token}parameter name="{key}" string="{is_str}">{value}</{dsml_token}parameter>'
|
||||
P_dsml_strs = []
|
||||
|
||||
try:
|
||||
arguments = json.loads(tool_call["arguments"])
|
||||
except Exception as err:
|
||||
arguments = {"arguments": tool_call["arguments"]}
|
||||
|
||||
for k, v in arguments.items():
|
||||
p_dsml_str = p_dsml_template.format(
|
||||
dsml_token=dsml_token,
|
||||
key=k,
|
||||
is_str="true" if isinstance(v, str) else "false",
|
||||
value=v if isinstance(v, str) else to_json(v),
|
||||
)
|
||||
P_dsml_strs.append(p_dsml_str)
|
||||
|
||||
return "\n".join(P_dsml_strs)
|
||||
|
||||
|
||||
def decode_dsml_to_arguments(
|
||||
tool_name: str, tool_args: Dict[str, Tuple[str, str]]
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
Decode DSML parameters back to a tool call dict.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool.
|
||||
tool_args: Dict mapping param_name -> (value, is_string_flag).
|
||||
|
||||
Returns:
|
||||
Dict with "name" and "arguments" (JSON string) keys.
|
||||
"""
|
||||
|
||||
def _decode_value(key: str, value: str, string: str):
|
||||
if string == "true":
|
||||
value = to_json(value)
|
||||
return f"{to_json(key)}: {value}"
|
||||
|
||||
tool_args_json = (
|
||||
"{"
|
||||
+ ", ".join(
|
||||
[_decode_value(k, v, string=is_str) for k, (v, is_str) in tool_args.items()]
|
||||
)
|
||||
+ "}"
|
||||
)
|
||||
return dict(name=tool_name, arguments=tool_args_json)
|
||||
|
||||
|
||||
def render_tools(tools: List[Dict[str, Union[str, Dict[str, Any]]]]) -> str:
|
||||
"""
|
||||
Render tool schemas into the system prompt format.
|
||||
|
||||
Args:
|
||||
tools: List of tool schema dicts (each with name, description, parameters).
|
||||
|
||||
Returns:
|
||||
Formatted tools section string.
|
||||
"""
|
||||
tools_json = [to_json(t) for t in tools]
|
||||
|
||||
return TOOLS_TEMPLATE.format(
|
||||
tool_schemas="\n".join(tools_json),
|
||||
dsml_token=dsml_token,
|
||||
thinking_start_token=thinking_start_token,
|
||||
thinking_end_token=thinking_end_token,
|
||||
)
|
||||
|
||||
|
||||
def find_last_user_index(messages: List[Dict[str, Any]]) -> int:
|
||||
"""Find the index of the last user/developer message."""
|
||||
last_user_index = -1
|
||||
for idx in range(len(messages) - 1, -1, -1):
|
||||
if messages[idx].get("role") in ["user", "developer"]:
|
||||
last_user_index = idx
|
||||
break
|
||||
return last_user_index
|
||||
|
||||
|
||||
def attach_task_to_last_user_message(messages: List[Dict[str, Any]], task: str) -> None:
|
||||
"""Set `task` on the most recent user/developer message; raise if none exists."""
|
||||
idx = find_last_user_index(messages)
|
||||
if idx == -1:
|
||||
raise ValueError(
|
||||
"`task` requires at least one message with role='user' or 'developer'."
|
||||
)
|
||||
messages[idx]["task"] = task
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Message Rendering
|
||||
# ============================================================
|
||||
|
||||
|
||||
def render_message(
|
||||
index: int,
|
||||
messages: List[Dict[str, Any]],
|
||||
thinking_mode: str,
|
||||
drop_thinking: bool = True,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Render a single message at the given index into its encoded string form.
|
||||
|
||||
This is the core function that converts each message in the conversation
|
||||
into the DeepSeek-V4 format.
|
||||
|
||||
Args:
|
||||
index: Index of the message to render.
|
||||
messages: Full list of messages in the conversation.
|
||||
thinking_mode: Either "chat" or "thinking".
|
||||
drop_thinking: Whether to drop reasoning content from earlier turns.
|
||||
reasoning_effort: Optional reasoning effort level ("max", "high", or None).
|
||||
|
||||
Returns:
|
||||
Encoded string for this message.
|
||||
"""
|
||||
assert 0 <= index < len(messages)
|
||||
assert thinking_mode in [
|
||||
"chat",
|
||||
"thinking",
|
||||
], f"Invalid thinking_mode `{thinking_mode}`"
|
||||
|
||||
prompt = ""
|
||||
msg = messages[index]
|
||||
last_user_idx = find_last_user_index(messages)
|
||||
|
||||
role = msg.get("role")
|
||||
content = msg.get("content")
|
||||
tools = msg.get("tools")
|
||||
response_format = msg.get("response_format")
|
||||
tool_calls = msg.get("tool_calls")
|
||||
reasoning_content = msg.get("reasoning_content")
|
||||
wo_eos = msg.get("wo_eos", False)
|
||||
|
||||
if tools:
|
||||
tools = tools_from_openai_format(tools)
|
||||
if tool_calls:
|
||||
tool_calls = tool_calls_from_openai_format(tool_calls)
|
||||
|
||||
# Reasoning effort prefix (only at index 0 in thinking mode with max effort)
|
||||
assert reasoning_effort in [
|
||||
"max",
|
||||
None,
|
||||
"high",
|
||||
], f"Invalid reasoning effort: {reasoning_effort}"
|
||||
if index == 0 and thinking_mode == "thinking" and reasoning_effort == "max":
|
||||
prompt += REASONING_EFFORT_MAX
|
||||
|
||||
if role == "system":
|
||||
prompt += system_msg_template.format(content=content or "")
|
||||
if tools:
|
||||
prompt += "\n\n" + render_tools(tools)
|
||||
if response_format:
|
||||
prompt += "\n\n" + response_format_template.format(
|
||||
schema=to_json(response_format)
|
||||
)
|
||||
|
||||
elif role == "developer":
|
||||
assert content, f"Invalid message for role `{role}`: {msg}"
|
||||
|
||||
content_developer = USER_SP_TOKEN
|
||||
content_developer += content
|
||||
|
||||
if tools:
|
||||
content_developer += "\n\n" + render_tools(tools)
|
||||
if response_format:
|
||||
content_developer += "\n\n" + response_format_template.format(
|
||||
schema=to_json(response_format)
|
||||
)
|
||||
|
||||
prompt += user_msg_template.format(content=content_developer)
|
||||
|
||||
elif role == "user":
|
||||
prompt += USER_SP_TOKEN
|
||||
|
||||
# Handle content blocks (tool results mixed with text)
|
||||
content_blocks = msg.get("content_blocks")
|
||||
if content_blocks:
|
||||
parts = []
|
||||
for block in content_blocks:
|
||||
block_type = block.get("type")
|
||||
if block_type == "text":
|
||||
parts.append(block.get("text", ""))
|
||||
elif block_type == "tool_result":
|
||||
tool_content = block.get("content", "")
|
||||
if isinstance(tool_content, list):
|
||||
text_parts = []
|
||||
for b in tool_content:
|
||||
if b.get("type") == "text":
|
||||
text_parts.append(b.get("text", ""))
|
||||
else:
|
||||
text_parts.append(f"[Unsupported {b.get('type')}]")
|
||||
tool_content = "\n\n".join(text_parts)
|
||||
parts.append(tool_output_template.format(content=tool_content))
|
||||
else:
|
||||
parts.append(f"[Unsupported {block_type}]")
|
||||
prompt += "\n\n".join(parts)
|
||||
else:
|
||||
prompt += content or ""
|
||||
|
||||
elif role == "latest_reminder":
|
||||
prompt += LATEST_REMINDER_SP_TOKEN + latest_reminder_msg_template.format(
|
||||
content=content
|
||||
)
|
||||
|
||||
elif role == "tool":
|
||||
raise NotImplementedError(
|
||||
"deepseek_v4 merges tool messages into user; please preprocess with merge_tool_messages()"
|
||||
)
|
||||
|
||||
elif role == "assistant":
|
||||
thinking_part = ""
|
||||
tc_content = ""
|
||||
|
||||
if tool_calls:
|
||||
tc_list = [
|
||||
tool_call_template.format(
|
||||
dsml_token=dsml_token,
|
||||
name=tc.get("name"),
|
||||
arguments=encode_arguments_to_dsml(tc),
|
||||
)
|
||||
for tc in tool_calls
|
||||
]
|
||||
tc_content += "\n\n" + tool_calls_template.format(
|
||||
dsml_token=dsml_token,
|
||||
tool_calls="\n".join(tc_list),
|
||||
tc_block_name=tool_calls_block_name,
|
||||
)
|
||||
|
||||
summary_content = content or ""
|
||||
rc = reasoning_content or ""
|
||||
|
||||
# Check if previous message has a task - if so, this is a task output (no thinking)
|
||||
prev_has_task = index - 1 >= 0 and messages[index - 1].get("task") is not None
|
||||
|
||||
if thinking_mode == "thinking" and not prev_has_task:
|
||||
if not drop_thinking or index > last_user_idx:
|
||||
thinking_part = (
|
||||
thinking_template.format(reasoning_content=rc) + thinking_end_token
|
||||
)
|
||||
else:
|
||||
thinking_part = ""
|
||||
|
||||
if wo_eos:
|
||||
prompt += assistant_msg_wo_eos_template.format(
|
||||
reasoning=thinking_part,
|
||||
content=summary_content,
|
||||
tool_calls=tc_content,
|
||||
)
|
||||
else:
|
||||
prompt += assistant_msg_template.format(
|
||||
reasoning=thinking_part,
|
||||
content=summary_content,
|
||||
tool_calls=tc_content,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown role: {role}")
|
||||
|
||||
# Append transition tokens based on what follows
|
||||
if index + 1 < len(messages) and messages[index + 1].get("role") not in [
|
||||
"assistant",
|
||||
"latest_reminder",
|
||||
]:
|
||||
return prompt
|
||||
|
||||
task = messages[index].get("task")
|
||||
if task is not None:
|
||||
# Task special token for internal classification tasks
|
||||
assert (
|
||||
task in VALID_TASKS
|
||||
), f"Invalid task: '{task}'. Valid tasks are: {list(VALID_TASKS)}"
|
||||
task_sp_token = DS_TASK_SP_TOKENS[task]
|
||||
|
||||
if task != "action":
|
||||
# Non-action tasks: append task sp token directly after the message
|
||||
prompt += task_sp_token
|
||||
else:
|
||||
# Action task: append Assistant + thinking token + action sp token
|
||||
prompt += ASSISTANT_SP_TOKEN
|
||||
prompt += (
|
||||
thinking_end_token
|
||||
if thinking_mode != "thinking"
|
||||
else thinking_start_token
|
||||
)
|
||||
prompt += task_sp_token
|
||||
|
||||
elif messages[index].get("role") in ["user", "developer"]:
|
||||
# Normal generation: append Assistant + thinking token
|
||||
prompt += ASSISTANT_SP_TOKEN
|
||||
if not drop_thinking and thinking_mode == "thinking":
|
||||
prompt += thinking_start_token
|
||||
elif drop_thinking and thinking_mode == "thinking" and index >= last_user_idx:
|
||||
prompt += thinking_start_token
|
||||
else:
|
||||
prompt += thinking_end_token
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Preprocessing
|
||||
# ============================================================
|
||||
|
||||
|
||||
def merge_tool_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Merge tool messages into the preceding user message using content_blocks format.
|
||||
|
||||
DeepSeek-V4 does not have a standalone "tool" role; instead, tool results
|
||||
are encoded as <tool_result> blocks within user messages.
|
||||
|
||||
This function converts a standard OpenAI-format conversation (with separate
|
||||
"tool" role messages) into V4 format where tool results are merged into
|
||||
user messages.
|
||||
|
||||
Args:
|
||||
messages: List of message dicts in OpenAI format.
|
||||
|
||||
Returns:
|
||||
Processed message list with tool messages merged into user messages.
|
||||
"""
|
||||
merged: List[Dict[str, Any]] = []
|
||||
|
||||
for msg in messages:
|
||||
msg = copy.deepcopy(msg)
|
||||
role = msg.get("role")
|
||||
|
||||
if role == "tool":
|
||||
# Convert tool message to a user message with tool_result block
|
||||
tool_block = {
|
||||
"type": "tool_result",
|
||||
"tool_use_id": msg.get("tool_call_id", ""),
|
||||
"content": msg.get("content", ""),
|
||||
}
|
||||
# Merge into previous message if it's already a user (merged tool)
|
||||
if (
|
||||
merged
|
||||
and merged[-1].get("role") == "user"
|
||||
and "content_blocks" in merged[-1]
|
||||
):
|
||||
merged[-1]["content_blocks"].append(tool_block)
|
||||
else:
|
||||
merged.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content_blocks": [tool_block],
|
||||
}
|
||||
)
|
||||
elif role == "user":
|
||||
text_block = {"type": "text", "text": msg.get("content", "")}
|
||||
if (
|
||||
merged
|
||||
and merged[-1].get("role") == "user"
|
||||
and "content_blocks" in merged[-1]
|
||||
and merged[-1].get("task") is None
|
||||
):
|
||||
merged[-1]["content_blocks"].append(text_block)
|
||||
else:
|
||||
new_msg = {
|
||||
"role": "user",
|
||||
"content": msg.get("content", ""),
|
||||
"content_blocks": [text_block],
|
||||
}
|
||||
# Preserve extra fields (task, wo_eos, mask, etc.)
|
||||
for key in ("task", "wo_eos", "mask"):
|
||||
if key in msg:
|
||||
new_msg[key] = msg[key]
|
||||
merged.append(new_msg)
|
||||
else:
|
||||
merged.append(msg)
|
||||
|
||||
return merged
|
||||
|
||||
|
||||
def sort_tool_results_by_call_order(
|
||||
messages: List[Dict[str, Any]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Sort tool_result blocks within user messages by the order of tool_calls
|
||||
in the preceding assistant message.
|
||||
|
||||
Args:
|
||||
messages: Preprocessed message list (after merge_tool_messages).
|
||||
|
||||
Returns:
|
||||
Message list with sorted tool result blocks.
|
||||
"""
|
||||
last_tool_call_order: Dict[str, int] = {}
|
||||
|
||||
for msg in messages:
|
||||
role = msg.get("role")
|
||||
if role == "assistant" and msg.get("tool_calls"):
|
||||
last_tool_call_order = {}
|
||||
for idx, tc in enumerate(msg["tool_calls"]):
|
||||
tc_id = tc.get("id") or tc.get("function", {}).get("id", "")
|
||||
if tc_id:
|
||||
last_tool_call_order[tc_id] = idx
|
||||
|
||||
elif role == "user" and msg.get("content_blocks"):
|
||||
tool_blocks = [
|
||||
b for b in msg["content_blocks"] if b.get("type") == "tool_result"
|
||||
]
|
||||
if len(tool_blocks) > 1 and last_tool_call_order:
|
||||
sorted_blocks = sorted(
|
||||
tool_blocks,
|
||||
key=lambda b: last_tool_call_order.get(b.get("tool_use_id", ""), 0),
|
||||
)
|
||||
sorted_idx = 0
|
||||
new_blocks = []
|
||||
for block in msg["content_blocks"]:
|
||||
if block.get("type") == "tool_result":
|
||||
new_blocks.append(sorted_blocks[sorted_idx])
|
||||
sorted_idx += 1
|
||||
else:
|
||||
new_blocks.append(block)
|
||||
msg["content_blocks"] = new_blocks
|
||||
|
||||
return messages
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Main Encoding Function
|
||||
# ============================================================
|
||||
|
||||
|
||||
def encode_messages(
|
||||
messages: List[Dict[str, Any]],
|
||||
thinking_mode: str,
|
||||
context: Optional[List[Dict[str, Any]]] = None,
|
||||
drop_thinking: bool = True,
|
||||
add_default_bos_token: bool = True,
|
||||
reasoning_effort: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Encode a list of messages into the DeepSeek-V4 prompt format.
|
||||
|
||||
This is the main entry point for encoding conversations. It handles:
|
||||
- BOS token insertion
|
||||
- Thinking mode with optional reasoning content dropping
|
||||
- Tool message merging into user messages
|
||||
- Multi-turn conversation context
|
||||
|
||||
Args:
|
||||
messages: List of message dicts to encode.
|
||||
thinking_mode: Either "chat" or "thinking".
|
||||
context: Optional preceding context messages (already encoded prefix).
|
||||
drop_thinking: If True, drop reasoning_content from earlier assistant turns
|
||||
(only keep reasoning for messages after the last user message).
|
||||
add_default_bos_token: Whether to prepend BOS token at conversation start.
|
||||
reasoning_effort: Optional reasoning effort level ("max", "high", or None).
|
||||
|
||||
Returns:
|
||||
The encoded prompt string.
|
||||
"""
|
||||
context = context if context else []
|
||||
|
||||
# Preprocess: merge tool messages and sort tool results
|
||||
messages = merge_tool_messages(messages)
|
||||
messages = sort_tool_results_by_call_order(context + messages)[len(context) :]
|
||||
if context:
|
||||
context = merge_tool_messages(context)
|
||||
context = sort_tool_results_by_call_order(context)
|
||||
|
||||
full_messages = context + messages
|
||||
|
||||
prompt = bos_token if add_default_bos_token and len(context) == 0 else ""
|
||||
|
||||
# Resolve drop_thinking: if any message has tools defined, don't drop thinking
|
||||
effective_drop_thinking = drop_thinking
|
||||
if any(m.get("tools") for m in full_messages):
|
||||
effective_drop_thinking = False
|
||||
|
||||
if thinking_mode == "thinking" and effective_drop_thinking:
|
||||
full_messages = _drop_thinking_messages(full_messages)
|
||||
# After dropping, recalculate how many messages to render
|
||||
# (context may have shrunk too)
|
||||
num_to_render = len(full_messages) - len(_drop_thinking_messages(context))
|
||||
context_len = len(full_messages) - num_to_render
|
||||
else:
|
||||
num_to_render = len(messages)
|
||||
context_len = len(context)
|
||||
|
||||
for idx in range(num_to_render):
|
||||
prompt += render_message(
|
||||
idx + context_len,
|
||||
full_messages,
|
||||
thinking_mode=thinking_mode,
|
||||
drop_thinking=effective_drop_thinking,
|
||||
reasoning_effort=reasoning_effort,
|
||||
)
|
||||
|
||||
return prompt
|
||||
|
||||
|
||||
def _drop_thinking_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Drop reasoning_content and non-essential messages before the last user message.
|
||||
|
||||
Behavior:
|
||||
- Messages with role in ["user", "system", "tool", "latest_reminder"] are always kept.
|
||||
- Messages at or after the last user index are always kept.
|
||||
- Assistant messages before the last user get reasoning_content removed.
|
||||
- Developer messages before the last user are dropped entirely.
|
||||
"""
|
||||
last_user_idx = find_last_user_index(messages)
|
||||
result = []
|
||||
keep_roles = {"user", "system", "tool", "latest_reminder", "direct_search_results"}
|
||||
|
||||
for idx, msg in enumerate(messages):
|
||||
role = msg.get("role")
|
||||
if role in keep_roles or idx >= last_user_idx:
|
||||
result.append(msg)
|
||||
elif role == "assistant":
|
||||
msg = copy.copy(msg)
|
||||
msg.pop("reasoning_content", None)
|
||||
result.append(msg)
|
||||
# developer and other roles before last_user_idx are dropped
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Parsing (Decoding model output)
|
||||
# ============================================================
|
||||
|
||||
|
||||
def _read_until_stop(
|
||||
index: int, text: str, stop: List[str]
|
||||
) -> Tuple[int, str, Optional[str]]:
|
||||
"""
|
||||
Read text from index until one of the stop strings is found.
|
||||
|
||||
Returns:
|
||||
Tuple of (new_index, content_before_stop, matched_stop_string_or_None).
|
||||
"""
|
||||
min_pos = len(text)
|
||||
matched_stop = None
|
||||
|
||||
for s in stop:
|
||||
pos = text.find(s, index)
|
||||
if pos != -1 and pos < min_pos:
|
||||
min_pos = pos
|
||||
matched_stop = s
|
||||
|
||||
if matched_stop:
|
||||
content = text[index:min_pos]
|
||||
return min_pos + len(matched_stop), content, matched_stop
|
||||
else:
|
||||
content = text[index:]
|
||||
return len(text), content, None
|
||||
|
||||
|
||||
def parse_tool_calls(
|
||||
index: int, text: str
|
||||
) -> Tuple[int, Optional[str], List[Dict[str, str]]]:
|
||||
"""
|
||||
Parse DSML tool calls from text starting at the given index.
|
||||
|
||||
Args:
|
||||
index: Starting position in text.
|
||||
text: The full text to parse.
|
||||
|
||||
Returns:
|
||||
Tuple of (new_index, last_stop_token, list_of_tool_call_dicts).
|
||||
Each tool call dict has "name" and "arguments" keys.
|
||||
"""
|
||||
tool_calls: List[Dict[str, Any]] = []
|
||||
stop_token = None
|
||||
tool_calls_end_token = f"</{dsml_token}{tool_calls_block_name}>"
|
||||
|
||||
while index < len(text):
|
||||
index, _, stop_token = _read_until_stop(
|
||||
index, text, [f"<{dsml_token}invoke", tool_calls_end_token]
|
||||
)
|
||||
if _ != ">\n":
|
||||
raise ValueError(f"Tool call format error: expected '>\\n' but got '{_}'")
|
||||
|
||||
if stop_token == tool_calls_end_token:
|
||||
break
|
||||
|
||||
if stop_token is None:
|
||||
raise ValueError("Missing special token in tool calls")
|
||||
|
||||
index, tool_name_content, stop_token = _read_until_stop(
|
||||
index, text, [f"<{dsml_token}parameter", f"</{dsml_token}invoke"]
|
||||
)
|
||||
|
||||
p_tool_name = re.findall(
|
||||
r'^\s*name="(.*?)">\n$', tool_name_content, flags=re.DOTALL
|
||||
)
|
||||
if len(p_tool_name) != 1:
|
||||
raise ValueError(f"Tool name format error: '{tool_name_content}'")
|
||||
tool_name = p_tool_name[0]
|
||||
|
||||
tool_args: Dict[str, Tuple[str, str]] = {}
|
||||
while stop_token == f"<{dsml_token}parameter":
|
||||
index, param_content, stop_token = _read_until_stop(
|
||||
index, text, [f"/{dsml_token}parameter"]
|
||||
)
|
||||
|
||||
param_kv = re.findall(
|
||||
r'^ name="(.*?)" string="(true|false)">(.*?)<$',
|
||||
param_content,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
if len(param_kv) != 1:
|
||||
raise ValueError(f"Parameter format error: '{param_content}'")
|
||||
param_name, string, param_value = param_kv[0]
|
||||
|
||||
if param_name in tool_args:
|
||||
raise ValueError(f"Duplicate parameter name: '{param_name}'")
|
||||
tool_args[param_name] = (param_value, string)
|
||||
|
||||
index, content, stop_token = _read_until_stop(
|
||||
index, text, [f"<{dsml_token}parameter", f"</{dsml_token}invoke"]
|
||||
)
|
||||
if content != ">\n":
|
||||
raise ValueError(
|
||||
f"Parameter format error: expected '>\\n' but got '{content}'"
|
||||
)
|
||||
|
||||
tool_call = decode_dsml_to_arguments(tool_name=tool_name, tool_args=tool_args)
|
||||
tool_calls.append(tool_call)
|
||||
|
||||
return index, stop_token, tool_calls
|
||||
|
||||
|
||||
def parse_message_from_completion_text(text: str, thinking_mode: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Parse a model completion text into a structured assistant message.
|
||||
|
||||
This function takes the raw text output from the model (a single assistant turn)
|
||||
and extracts:
|
||||
- reasoning_content (thinking block)
|
||||
- content (summary/response)
|
||||
- tool_calls (if any)
|
||||
|
||||
NOTE: This function is designed to parse only correctly formatted strings and
|
||||
will raise ValueError for malformed output.
|
||||
|
||||
Args:
|
||||
text: The raw completion text (including EOS token).
|
||||
thinking_mode: Either "chat" or "thinking".
|
||||
|
||||
Returns:
|
||||
Dict with keys: "role", "content", "reasoning_content", "tool_calls".
|
||||
tool_calls are in OpenAI format.
|
||||
"""
|
||||
summary_content, reasoning_content, tool_calls = "", "", []
|
||||
index, stop_token = 0, None
|
||||
tool_calls_start_token = f"\n\n<{dsml_token}{tool_calls_block_name}"
|
||||
|
||||
is_thinking = thinking_mode == "thinking"
|
||||
is_tool_calling = False
|
||||
|
||||
if is_thinking:
|
||||
index, content_delta, stop_token = _read_until_stop(
|
||||
index, text, [thinking_end_token, tool_calls_start_token]
|
||||
)
|
||||
reasoning_content = content_delta
|
||||
assert (
|
||||
stop_token == thinking_end_token
|
||||
), "Invalid thinking format: missing </think>"
|
||||
|
||||
index, content_delta, stop_token = _read_until_stop(
|
||||
index, text, [eos_token, tool_calls_start_token]
|
||||
)
|
||||
summary_content = content_delta
|
||||
if stop_token == tool_calls_start_token:
|
||||
is_tool_calling = True
|
||||
else:
|
||||
assert stop_token == eos_token, "Invalid format: missing EOS token"
|
||||
|
||||
if is_tool_calling:
|
||||
index, stop_token, tool_calls = parse_tool_calls(index, text)
|
||||
|
||||
index, tool_ends_text, stop_token = _read_until_stop(index, text, [eos_token])
|
||||
assert not tool_ends_text, "Unexpected content after tool calls"
|
||||
|
||||
assert len(text) == index and stop_token in [
|
||||
eos_token,
|
||||
None,
|
||||
], "Unexpected content at end"
|
||||
|
||||
for sp_token in [
|
||||
bos_token,
|
||||
eos_token,
|
||||
thinking_start_token,
|
||||
thinking_end_token,
|
||||
dsml_token,
|
||||
]:
|
||||
assert (
|
||||
sp_token not in summary_content and sp_token not in reasoning_content
|
||||
), f"Unexpected special token '{sp_token}' in content"
|
||||
|
||||
return {
|
||||
"role": "assistant",
|
||||
"content": summary_content,
|
||||
"reasoning_content": reasoning_content,
|
||||
"tool_calls": tool_calls_to_openai_format(tool_calls),
|
||||
}
|
||||
@@ -17,7 +17,17 @@ import logging
|
||||
import time
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, NamedTuple, Optional, Tuple, TypeAlias, Union
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Tuple,
|
||||
TypeAlias,
|
||||
Union,
|
||||
get_args,
|
||||
)
|
||||
|
||||
from openai.types.responses import (
|
||||
ResponseFunctionToolCall,
|
||||
@@ -500,8 +510,14 @@ class ToolCall(BaseModel):
|
||||
function: FunctionResponse
|
||||
|
||||
|
||||
_GenericMessageRole = Literal[
|
||||
"system", "assistant", "tool", "function", "developer", "latest_reminder"
|
||||
]
|
||||
_GENERIC_MESSAGE_ROLES: Tuple[str, ...] = get_args(_GenericMessageRole)
|
||||
|
||||
|
||||
class ChatCompletionMessageGenericParam(BaseModel):
|
||||
role: Literal["system", "assistant", "tool", "function", "developer"]
|
||||
role: _GenericMessageRole
|
||||
content: Union[str, List[ChatCompletionMessageContentPart], None] = Field(
|
||||
default=None
|
||||
)
|
||||
@@ -516,10 +532,9 @@ class ChatCompletionMessageGenericParam(BaseModel):
|
||||
def _normalize_role(cls, v):
|
||||
if isinstance(v, str):
|
||||
v_lower = v.lower()
|
||||
if v_lower not in {"system", "assistant", "tool", "function", "developer"}:
|
||||
raise ValueError(
|
||||
"'role' must be one of 'system', 'developer', 'assistant', 'tool', or 'function' (case-insensitive)."
|
||||
)
|
||||
if v_lower not in _GENERIC_MESSAGE_ROLES:
|
||||
allowed = ", ".join(repr(r) for r in _GENERIC_MESSAGE_ROLES)
|
||||
raise ValueError(f"'role' must be one of {allowed} (case-insensitive).")
|
||||
return v_lower
|
||||
raise ValueError("'role' must be a string")
|
||||
|
||||
@@ -626,6 +641,15 @@ class ChatCompletionRequest(BaseModel):
|
||||
"in a response. 'none' defaults thinking and enable_thinking to false in "
|
||||
"chat_template_kwargs (unless explicitly overridden). Not supported in the harmony path.",
|
||||
)
|
||||
task: Optional[
|
||||
Literal["action", "query", "authority", "domain", "title", "read_url"]
|
||||
] = Field(
|
||||
default=None,
|
||||
description="DeepSeek-V4 quick instruction task. When set, the last "
|
||||
"user/developer message is treated as a single-shot classification prompt "
|
||||
"and the corresponding task special token (e.g. `<|domain|>`) is appended "
|
||||
"before generation. Only honored by the dsv4 chat encoder; ignored otherwise.",
|
||||
)
|
||||
|
||||
# Extra parameters for SRT backend only and will be ignored by OpenAI models.
|
||||
top_k: Optional[int] = None
|
||||
|
||||
@@ -15,7 +15,7 @@ from fastapi import Request
|
||||
from fastapi.responses import ORJSONResponse, StreamingResponse
|
||||
from jsonschema import Draft202012Validator, SchemaError
|
||||
|
||||
from sglang.srt.entrypoints.openai.encoding_dsv32 import encode_messages
|
||||
from sglang.srt.entrypoints.openai import encoding_dsv4, encoding_dsv32
|
||||
from sglang.srt.entrypoints.openai.protocol import (
|
||||
ChatCompletionRequest,
|
||||
ChatCompletionResponse,
|
||||
@@ -46,6 +46,7 @@ from sglang.srt.entrypoints.openai.utils import (
|
||||
should_include_usage,
|
||||
to_openai_style_logprobs,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.function_call.core_types import ToolCallItem
|
||||
from sglang.srt.function_call.function_call_parser import FunctionCallParser
|
||||
from sglang.srt.function_call.json_array_parser import JsonArrayParser
|
||||
@@ -232,7 +233,9 @@ class OpenAIServingChat(OpenAIServingBase):
|
||||
and self.tokenizer_manager.model_config.hf_config.model_type == "gemma4"
|
||||
)
|
||||
|
||||
self.use_dpsk_v32_encoding = self._use_dpsk_v32_encoding()
|
||||
# Which Python-based chat encoder (if any) bypasses apply_chat_template.
|
||||
# Values: "dsv32", "dsv4", or None.
|
||||
self.chat_encoding_spec = self._resolve_chat_encoding_spec()
|
||||
|
||||
def _handle_last_assistant_message(
|
||||
self,
|
||||
@@ -290,14 +293,25 @@ class OpenAIServingChat(OpenAIServingBase):
|
||||
encoded = encoded[1:]
|
||||
return prompt_ids + encoded
|
||||
|
||||
def _use_dpsk_v32_encoding(self) -> bool:
|
||||
def _resolve_chat_encoding_spec(self) -> Optional[str]:
|
||||
if self.tool_call_parser == "deepseekv4":
|
||||
return "dsv4"
|
||||
if self.tool_call_parser == "deepseekv32":
|
||||
return "dsv32"
|
||||
|
||||
architectures = self.tokenizer_manager.model_config.hf_config.architectures
|
||||
arch = architectures[0] if architectures else ""
|
||||
|
||||
if "DeepseekV4" in arch:
|
||||
return "dsv4"
|
||||
|
||||
has_chat_template = (
|
||||
self.tokenizer_manager.tokenizer is not None
|
||||
and self.tokenizer_manager.tokenizer.chat_template is not None
|
||||
)
|
||||
architectures = self.tokenizer_manager.model_config.hf_config.architectures
|
||||
is_dpsk_v32 = "DeepseekV3" in architectures[0] if architectures else False
|
||||
return not has_chat_template and is_dpsk_v32
|
||||
if "DeepseekV3" in arch and not has_chat_template:
|
||||
return "dsv32"
|
||||
return None
|
||||
|
||||
def _request_id_prefix(self) -> str:
|
||||
return "chatcmpl-"
|
||||
@@ -515,14 +529,22 @@ class OpenAIServingChat(OpenAIServingBase):
|
||||
|
||||
template_content_format = self.template_manager.jinja_template_content_format
|
||||
|
||||
if self.use_dpsk_v32_encoding:
|
||||
thinking_mode = (
|
||||
"thinking"
|
||||
if (request.chat_template_kwargs or {}).get("thinking")
|
||||
else "chat"
|
||||
if self.chat_encoding_spec is not None:
|
||||
# Per-request wins; env is fallback default for benchmark
|
||||
# workflows that can't pass per-request chat_template_kwargs.
|
||||
thinking_requested = (request.chat_template_kwargs or {}).get(
|
||||
"thinking", envs.SGLANG_DEFAULT_THINKING.get()
|
||||
)
|
||||
messages = request.messages
|
||||
messages = [msg.model_dump() for msg in messages]
|
||||
thinking_mode = "thinking" if thinking_requested else "chat"
|
||||
messages = [msg.model_dump() for msg in request.messages]
|
||||
|
||||
# dsv4/dsv32 are text-only and consume string content; flatten
|
||||
# OpenAI parts-list content here so the encoder sees a plain string.
|
||||
for i, msg in enumerate(messages):
|
||||
if isinstance(msg.get("content"), list):
|
||||
messages[i] = process_content_for_template_format(
|
||||
msg, "string", [], [], [], []
|
||||
)
|
||||
|
||||
for msg in messages:
|
||||
if msg.get("content") is None:
|
||||
@@ -534,7 +556,7 @@ class OpenAIServingChat(OpenAIServingBase):
|
||||
video_data,
|
||||
audio_data,
|
||||
modalities,
|
||||
use_dpsk_v32_encoding=self.use_dpsk_v32_encoding,
|
||||
use_dpsk_v32_encoding=self.chat_encoding_spec == "dsv32",
|
||||
)
|
||||
msg.update(processed_msg)
|
||||
|
||||
@@ -548,7 +570,32 @@ class OpenAIServingChat(OpenAIServingBase):
|
||||
messages.insert(0, {"role": "system", "content": ""})
|
||||
if request.tools:
|
||||
messages[0]["tools"] = [tool.model_dump() for tool in request.tools]
|
||||
real_input = encode_messages(messages, thinking_mode=thinking_mode)
|
||||
|
||||
if self.chat_encoding_spec == "dsv4":
|
||||
# V4 encoder only accepts "max" / "high" / None.
|
||||
# OpenAI protocol defaults to "medium" which V4 rejects; drop it.
|
||||
# Fallback: if request didn't set it, try env SGLANG_DSV4_REASONING_EFFORT.
|
||||
effort_source = request.reasoning_effort
|
||||
if effort_source is None:
|
||||
env_val = envs.SGLANG_DSV4_REASONING_EFFORT.get()
|
||||
if env_val:
|
||||
effort_source = env_val
|
||||
v4_reasoning_effort = (
|
||||
effort_source if effort_source in ("max", "high") else None
|
||||
)
|
||||
if request.task is not None:
|
||||
encoding_dsv4.attach_task_to_last_user_message(
|
||||
messages, request.task
|
||||
)
|
||||
real_input = encoding_dsv4.encode_messages(
|
||||
messages,
|
||||
thinking_mode=thinking_mode,
|
||||
reasoning_effort=v4_reasoning_effort,
|
||||
)
|
||||
else:
|
||||
real_input = encoding_dsv32.encode_messages(
|
||||
messages, thinking_mode=thinking_mode
|
||||
)
|
||||
prompt_ids = self.tokenizer_manager.tokenizer.encode(real_input)
|
||||
|
||||
# Append assistant prefix if continue_final_message is enabled
|
||||
|
||||
@@ -553,6 +553,64 @@ class Envs:
|
||||
# TokenizerManager
|
||||
SGLANG_REQUEST_STATE_WAIT_TIMEOUT = EnvInt(4)
|
||||
|
||||
SGLANG_DEFAULT_THINKING = EnvBool(False)
|
||||
|
||||
# ====================================================================
|
||||
# DeepSeek V4
|
||||
# ====================================================================
|
||||
|
||||
# Set False when using FP4-to-FP8 converted DeepSeek V4 checkpoint.
|
||||
SGLANG_DSV4_FP4_EXPERTS = EnvBool(True)
|
||||
# Default reasoning_effort for dsv4 chat encoder when request doesn't set it.
|
||||
# Accepts "", "max", "high" (empty string means unset); other values filtered to None.
|
||||
SGLANG_DSV4_REASONING_EFFORT = EnvStr("")
|
||||
|
||||
# CUDA kernels
|
||||
SGLANG_OPT_DEEPGEMM_HC_PRENORM = EnvBool(True)
|
||||
SGLANG_OPT_USE_TILELANG_MHC_PRE = EnvBool(True)
|
||||
SGLANG_OPT_USE_TILELANG_MHC_POST = EnvBool(True)
|
||||
SGLANG_OPT_USE_TILELANG_INDEXER = EnvBool(False)
|
||||
SGLANG_OPT_USE_JIT_INDEXER_METADATA = EnvBool(False)
|
||||
SGLANG_OPT_USE_ONLINE_COMPRESS = EnvBool(False)
|
||||
SGLANG_FP8_PAGED_MQA_LOGITS_TORCH = EnvBool(False)
|
||||
SGLANG_TOPK_TRANSFORM_512_TORCH = EnvBool(False)
|
||||
|
||||
# SWA radix cache
|
||||
SGLANG_OPT_CACHE_SWA_TRANSLATION = EnvBool(True)
|
||||
# TODO(DSV4): @ispobock this has bug on main branch when retract
|
||||
SGLANG_OPT_SWA_RADIX_CACHE_COMPACT = EnvBool(False)
|
||||
SGLANG_OPT_SWA_SPLIT_LEAF_ON_INSERT = EnvBool(False)
|
||||
SGLANG_OPT_SWA_RELEASE_LEAF_LOCK_AFTER_WINDOW = EnvBool(False)
|
||||
|
||||
# DeepGemm Mega MoE
|
||||
SGLANG_OPT_USE_DEEPGEMM_MEGA_MOE = EnvBool(False)
|
||||
SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK = EnvInt(1024)
|
||||
SGLANG_OPT_FIX_MEGA_MOE_MEMORY = EnvBool(False)
|
||||
|
||||
# TopK
|
||||
SGLANG_OPT_USE_FUSED_HASH_TOPK = EnvBool(True)
|
||||
SGLANG_OPT_USE_JIT_KERNEL_FUSED_TOPK = EnvBool(True)
|
||||
SGLANG_OPT_USE_TOPK_V2 = EnvBool(False)
|
||||
|
||||
# GEMM / kernel fusion
|
||||
SGLANG_OPT_FP8_WO_A_GEMM = EnvBool(False)
|
||||
SGLANG_OPT_BF16_FP32_GEMM_ALGO = EnvStr("cublas")
|
||||
SGLANG_OPT_USE_JIT_EP_ACTIVATION = EnvBool(True)
|
||||
SGLANG_OPT_USE_JIT_NORM = EnvBool(False)
|
||||
SGLANG_OPT_FUSE_WQA_WKV = EnvBool(True)
|
||||
SGLANG_OPT_SWIGLU_CLAMP_FUSION = EnvBool(True)
|
||||
|
||||
# Cache / overlap
|
||||
SGLANG_OPT_USE_FUSED_STORE_CACHE = EnvBool(True)
|
||||
SGLANG_OPT_USE_OVERLAP_STORE_CACHE = EnvBool(True)
|
||||
SGLANG_OPT_USE_MULTI_STREAM_OVERLAP = EnvBool(True)
|
||||
|
||||
# CUDA graph
|
||||
SGLANG_PREP_IN_CUDA_GRAPH = EnvBool(True)
|
||||
|
||||
# Distributed
|
||||
SGLANG_DSV4_FIX_TP_ATTN_A2A_SCATTER = EnvBool(True)
|
||||
|
||||
# Symmetric Memory
|
||||
SGLANG_SYMM_MEM_PREALLOC_GB_SIZE = EnvInt(-1)
|
||||
SGLANG_DEBUG_SYMM_MEM = EnvBool(False)
|
||||
@@ -611,6 +669,8 @@ def _convert_SGL_to_SGLANG():
|
||||
"SGLANG_ENABLE_TP_MEMORY_INBALANCE_CHECK",
|
||||
)
|
||||
_print_deprecated_env("SGLANG_PER_TOKEN_GROUP_QUANT_8BIT_V2")
|
||||
_print_deprecated_env("SGLANG_ENABLE_THINKING", "SGLANG_DEFAULT_THINKING")
|
||||
_print_deprecated_env("SGLANG_REASONING_EFFORT", "SGLANG_DSV4_REASONING_EFFORT")
|
||||
_print_deprecated_env(
|
||||
"SGLANG_USE_JIT_ALL_REDUCE", "SGLANG_OPT_USE_CUSTOM_ALL_REDUCE_V2"
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ from sglang.srt.environ import ToolStrictLevel, envs
|
||||
from sglang.srt.function_call.base_format_detector import BaseFormatDetector
|
||||
from sglang.srt.function_call.core_types import ToolCallItem
|
||||
from sglang.srt.function_call.deepseekv3_detector import DeepSeekV3Detector
|
||||
from sglang.srt.function_call.deepseekv4_detector import DeepSeekV4Detector
|
||||
from sglang.srt.function_call.deepseekv31_detector import DeepSeekV31Detector
|
||||
from sglang.srt.function_call.deepseekv32_detector import DeepSeekV32Detector
|
||||
from sglang.srt.function_call.gemma4_detector import Gemma4Detector
|
||||
@@ -55,6 +56,7 @@ class FunctionCallParser:
|
||||
"deepseekv3": DeepSeekV3Detector,
|
||||
"deepseekv31": DeepSeekV31Detector,
|
||||
"deepseekv32": DeepSeekV32Detector,
|
||||
"deepseekv4": DeepSeekV4Detector,
|
||||
"glm": Glm4MoeDetector,
|
||||
"glm45": Glm4MoeDetector,
|
||||
"glm47": Glm47MoeDetector,
|
||||
|
||||
@@ -92,6 +92,15 @@ def create_nsa_backend(runner):
|
||||
return NativeSparseAttnBackend(runner)
|
||||
|
||||
|
||||
@register_attention_backend("dsv4")
|
||||
def create_dsv4_backend(runner):
|
||||
from sglang.srt.layers.attention.deepseek_v4_backend import (
|
||||
DeepseekV4AttnBackend,
|
||||
)
|
||||
|
||||
return DeepseekV4AttnBackend(runner)
|
||||
|
||||
|
||||
@register_attention_backend("triton")
|
||||
def create_triton_backend(runner):
|
||||
assert not runner.model_config.is_encoder_decoder, (
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,379 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, List, Literal, NamedTuple, Optional, Union
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from sglang.jit_kernel.deepseek_v4 import (
|
||||
CompressorDecodePlan,
|
||||
CompressorPrefillPlan,
|
||||
compress_forward,
|
||||
compress_fused_norm_rope_inplace,
|
||||
linear_bf16_fp32,
|
||||
triton_create_paged_compress_data,
|
||||
)
|
||||
from sglang.srt.configs.deepseek_v4 import DeepSeekV4Config
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.attention.dsv4.quant_k_cache import (
|
||||
quant_to_nope_fp8_rope_bf16_pack_triton,
|
||||
)
|
||||
from sglang.srt.layers.attention.nsa.triton_kernel import act_quant
|
||||
from sglang.srt.layers.attention.nsa.utils import nsa_use_prefill_cp
|
||||
from sglang.srt.layers.dp_attention import get_attention_cp_size
|
||||
from sglang.srt.layers.layernorm import RMSNorm
|
||||
from sglang.srt.layers.linear import ReplicatedLinear
|
||||
from sglang.srt.layers.utils.cp_utils import cp_all_gather_rerange_output
|
||||
from sglang.srt.mem_cache.deepseek_v4_compress_state import CompressStatePool
|
||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
||||
from sglang.srt.utils import add_prefix
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.layers.attention.deepseek_v4_backend import DeepseekV4AttnBackend
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
|
||||
|
||||
class FusedCompressMetadata(NamedTuple):
|
||||
write_loc: torch.Tensor
|
||||
extra_data: Optional[torch.Tensor]
|
||||
plan: Union[CompressorDecodePlan, CompressorPrefillPlan]
|
||||
|
||||
def copy_(self, other: FusedCompressMetadata) -> None:
|
||||
from .metadata import maybe_copy_inplace
|
||||
|
||||
self.write_loc.copy_(other.write_loc)
|
||||
maybe_copy_inplace(self.extra_data, src=other.extra_data)
|
||||
self.plan.copy_(other.plan)
|
||||
|
||||
|
||||
class CompressorBackendMixin:
|
||||
def get_paged_compress_metadata(self, compress_ratio: int) -> FusedCompressMetadata:
|
||||
attr_name = f"c{compress_ratio}_compress_metadata"
|
||||
metadata = getattr(self.forward_metadata, attr_name)
|
||||
assert isinstance(metadata, FusedCompressMetadata)
|
||||
return metadata
|
||||
|
||||
def forward_compress(
|
||||
self,
|
||||
*,
|
||||
kv_score_buffer: torch.Tensor,
|
||||
kv_score_input: torch.Tensor,
|
||||
ape: torch.Tensor,
|
||||
head_dim: int,
|
||||
norm: RMSNorm,
|
||||
freqs_cis_cache: torch.Tensor,
|
||||
rotate: bool,
|
||||
forward_batch: ForwardBatch,
|
||||
compress_ratio: int,
|
||||
is_paged: bool = False,
|
||||
) -> torch.Tensor:
|
||||
from sglang.srt.layers.attention.nsa.nsa_indexer import rotate_activation
|
||||
|
||||
assert compress_ratio in (
|
||||
4,
|
||||
128,
|
||||
), f"DSV4 supports CSA(4x) and HCA(128x) only, got {compress_ratio=}"
|
||||
if is_paged:
|
||||
metadata = self.get_paged_compress_metadata(compress_ratio)
|
||||
coff = 2 if is_overlap_compress(compress_ratio) else 1
|
||||
if compress_ratio == 128 and envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get():
|
||||
kv_score_buffer = kv_score_buffer.view(-1, 1, head_dim * 3)
|
||||
else:
|
||||
last_dim = 2 * head_dim * coff
|
||||
assert kv_score_buffer.shape[-1] == last_dim
|
||||
kv_score_buffer = kv_score_buffer.view(-1, compress_ratio, last_dim)
|
||||
else:
|
||||
plan = make_compressor_plan(compress_ratio, forward_batch)
|
||||
metadata = (forward_batch.req_pool_indices.to(torch.int32), None, plan)
|
||||
indices, extra_data, plan = metadata
|
||||
|
||||
kv_compressed = compress_forward(
|
||||
kv_score_buffer=kv_score_buffer,
|
||||
kv_score_input=kv_score_input,
|
||||
ape=ape,
|
||||
indices=indices,
|
||||
plan=plan,
|
||||
compress_ratio=compress_ratio,
|
||||
head_dim=head_dim,
|
||||
extra_data=extra_data,
|
||||
)
|
||||
compress_fused_norm_rope_inplace(
|
||||
kv_compressed,
|
||||
norm.weight,
|
||||
norm.variance_epsilon,
|
||||
freqs_cis_cache,
|
||||
plan,
|
||||
)
|
||||
return rotate_activation(kv_compressed.bfloat16()) if rotate else kv_compressed
|
||||
|
||||
def forward_core_compressor(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
forward_batch: ForwardBatch,
|
||||
layer_id: int,
|
||||
compressor: Compressor,
|
||||
) -> None:
|
||||
if forward_batch.forward_mode.is_idle():
|
||||
return
|
||||
# PREP_IN_CG lazy upgrade: the concrete backend (DeepseekV4AttnBackend)
|
||||
# owns this helper. MQALayer._forward_prepare calls us before
|
||||
# attn_backend.forward(), so Raw -> DSV4Metadata must happen here too
|
||||
# (e.g. 1.6T layer 0 has compress_ratio=128 and needs cX_compress_metadata).
|
||||
self._maybe_upgrade_forward_metadata()
|
||||
token_to_kv_pool = forward_batch.token_to_kv_pool
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
||||
|
||||
new_compressed_kv = compressor(x, forward_batch)
|
||||
core_metadata = self.forward_metadata.core_metadata
|
||||
out_loc = (
|
||||
core_metadata.c4_out_loc
|
||||
if compressor.ratio == 4
|
||||
else core_metadata.c128_out_loc
|
||||
)
|
||||
if envs.SGLANG_OPT_USE_FUSED_STORE_CACHE.get():
|
||||
token_to_kv_pool.set_extra_key_buffer_fused(
|
||||
layer_id=layer_id,
|
||||
loc=out_loc,
|
||||
cache_k=new_compressed_kv,
|
||||
)
|
||||
else:
|
||||
pack = quant_to_nope_fp8_rope_bf16_pack_triton(new_compressed_kv.bfloat16())
|
||||
token_to_kv_pool.set_extra_key_buffer(layer_id, out_loc, pack)
|
||||
|
||||
def forward_indexer_compressor(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
forward_batch: ForwardBatch,
|
||||
layer_id: int,
|
||||
compressor: Compressor,
|
||||
) -> None:
|
||||
assert is_overlap_compress(compressor.ratio)
|
||||
# PREP_IN_CG lazy upgrade (see forward_core_compressor for rationale).
|
||||
self._maybe_upgrade_forward_metadata()
|
||||
token_to_kv_pool = forward_batch.token_to_kv_pool
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
||||
|
||||
new_compressed_kv = compressor(x, forward_batch)
|
||||
if envs.SGLANG_OPT_USE_FUSED_STORE_CACHE.get():
|
||||
token_to_kv_pool.set_index_k_fused(
|
||||
layer_id=layer_id,
|
||||
loc=self.forward_metadata.core_metadata.c4_out_loc,
|
||||
cache_k=new_compressed_kv,
|
||||
)
|
||||
else:
|
||||
new_compressed_kv_fp8, new_compressed_kv_scale = act_quant(
|
||||
new_compressed_kv
|
||||
)
|
||||
token_to_kv_pool.set_index_k_scale_buffer(
|
||||
layer_id=layer_id,
|
||||
loc=self.forward_metadata.core_metadata.c4_out_loc,
|
||||
index_k=new_compressed_kv_fp8,
|
||||
index_k_scale=new_compressed_kv_scale,
|
||||
)
|
||||
|
||||
|
||||
def is_overlap_compress(compress_ratio: int) -> bool:
|
||||
return compress_ratio == 4
|
||||
|
||||
|
||||
def make_compressor_plan(
|
||||
compress_ratio: Literal[4, 128],
|
||||
forward_batch: ForwardBatch,
|
||||
) -> Union[CompressorDecodePlan, CompressorPrefillPlan]:
|
||||
if forward_batch.forward_mode.is_decode():
|
||||
seq_lens_32 = forward_batch.seq_lens.to(torch.int32)
|
||||
return CompressorDecodePlan(compress_ratio, seq_lens_32)
|
||||
if forward_batch.forward_mode.is_prefill():
|
||||
assert not forward_batch.forward_mode.is_target_verify()
|
||||
extend_lens_list = forward_batch.extend_seq_lens_cpu
|
||||
seq_lens_cpu = forward_batch.seq_lens_cpu
|
||||
assert extend_lens_list is not None and seq_lens_cpu is not None
|
||||
return CompressorPrefillPlan.generate(
|
||||
compress_ratio=compress_ratio,
|
||||
num_q_tokens=sum(extend_lens_list),
|
||||
seq_lens=seq_lens_cpu,
|
||||
extend_lens=torch.tensor(extend_lens_list),
|
||||
device=forward_batch.seq_lens.device,
|
||||
)
|
||||
elif forward_batch.forward_mode.is_target_verify():
|
||||
raise NotImplementedError("target verify mode to be implemented")
|
||||
else:
|
||||
raise NotImplementedError(f"unsupported mode {forward_batch.forward_mode=}")
|
||||
|
||||
|
||||
def create_paged_compressor_data(
|
||||
compress_ratio: Literal[4, 128],
|
||||
*,
|
||||
is_prefill: bool,
|
||||
token_to_kv_pool: DeepSeekV4TokenToKVPool,
|
||||
req_to_token: torch.Tensor,
|
||||
req_pool_indices: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
extend_lens: Optional[torch.Tensor] = None,
|
||||
seq_lens_cpu: Optional[List[int]] = None,
|
||||
extend_lens_cpu: Optional[List[int]] = None,
|
||||
use_prefill_cuda_graph: bool = False,
|
||||
num_q_tokens: Optional[int] = None,
|
||||
) -> FusedCompressMetadata:
|
||||
swa_page_size = token_to_kv_pool.swa_page_size
|
||||
ring_size = token_to_kv_pool.get_ring_size(compress_ratio=compress_ratio)
|
||||
# assert ring_size % compress_ratio == 0
|
||||
|
||||
def clip_down(positions: torch.Tensor) -> torch.Tensor:
|
||||
return positions // compress_ratio * compress_ratio
|
||||
|
||||
def get_raw_loc(positions: torch.Tensor) -> torch.Tensor:
|
||||
positions = positions.masked_fill(positions < 0, 0)
|
||||
loc = req_to_token[req_pool_indices, positions]
|
||||
swa_loc = token_to_kv_pool.translate_loc_from_full_to_swa(loc)
|
||||
swa_pages = swa_loc // swa_page_size
|
||||
state_loc = swa_pages * ring_size + swa_loc % ring_size
|
||||
return (state_loc // compress_ratio).to(torch.int32)
|
||||
|
||||
is_overlap = is_overlap_compress(compress_ratio)
|
||||
|
||||
if is_prefill:
|
||||
assert extend_lens is not None
|
||||
write_loc, extra_data = triton_create_paged_compress_data(
|
||||
compress_ratio=compress_ratio,
|
||||
is_overlap=is_overlap,
|
||||
swa_page_size=swa_page_size,
|
||||
ring_size=ring_size,
|
||||
req_pool_indices=req_pool_indices,
|
||||
seq_lens=seq_lens,
|
||||
extend_seq_lens=extend_lens,
|
||||
req_to_token=req_to_token,
|
||||
full_to_swa_index_mapping=token_to_kv_pool.full_to_swa_index_mapping,
|
||||
)
|
||||
|
||||
plan_kwargs: dict
|
||||
if seq_lens_cpu is None:
|
||||
assert num_q_tokens is not None
|
||||
plan_kwargs = dict(
|
||||
num_q_tokens=num_q_tokens,
|
||||
seq_lens=seq_lens,
|
||||
extend_lens=extend_lens,
|
||||
)
|
||||
else:
|
||||
assert extend_lens_cpu is not None
|
||||
plan_kwargs = dict(
|
||||
num_q_tokens=sum(extend_lens_cpu),
|
||||
seq_lens=torch.tensor(seq_lens_cpu),
|
||||
extend_lens=torch.tensor(extend_lens_cpu),
|
||||
)
|
||||
plan = CompressorPrefillPlan.generate(
|
||||
compress_ratio=compress_ratio,
|
||||
device=seq_lens.device,
|
||||
use_cuda_graph=use_prefill_cuda_graph,
|
||||
**plan_kwargs,
|
||||
)
|
||||
else:
|
||||
write_positions = clip_down(seq_lens - 1)
|
||||
write_loc = get_raw_loc(write_positions)
|
||||
if is_overlap:
|
||||
write_overlap_loc = get_raw_loc(write_positions - compress_ratio)
|
||||
extra_data = write_overlap_loc.view(-1, 1)
|
||||
else:
|
||||
extra_data = None
|
||||
plan = CompressorDecodePlan(compress_ratio, seq_lens.to(torch.int32))
|
||||
|
||||
return FusedCompressMetadata(write_loc=write_loc, extra_data=extra_data, plan=plan)
|
||||
|
||||
|
||||
class Compressor(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: DeepSeekV4Config,
|
||||
layer_id: int,
|
||||
is_in_indexer: bool,
|
||||
freqs_cis: torch.Tensor,
|
||||
compress_ratio: Literal[0, 4, 128],
|
||||
head_dim: int,
|
||||
rotate: bool = False,
|
||||
prefix: str = "",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.layer_id = layer_id
|
||||
self.is_in_indexer = is_in_indexer
|
||||
self.dim = config.hidden_size
|
||||
self.head_dim = head_dim
|
||||
self.rope_head_dim = getattr(config, "qk_rope_head_dim", 64)
|
||||
assert compress_ratio != 0, "compress_ratio should not be 0"
|
||||
self.ratio = compress_ratio
|
||||
self.overlap = self.ratio == 4
|
||||
self.rotate = rotate
|
||||
coff = 1 + self.overlap
|
||||
|
||||
self.ape = nn.Parameter(
|
||||
torch.empty(self.ratio, coff * self.head_dim, dtype=torch.float32)
|
||||
)
|
||||
wkv_gate_dtype = torch.bfloat16
|
||||
self.wkv_gate = ReplicatedLinear(
|
||||
self.dim,
|
||||
2 * coff * self.head_dim,
|
||||
bias=False,
|
||||
quant_config=None,
|
||||
prefix=add_prefix("wkv_gate", prefix),
|
||||
params_dtype=wkv_gate_dtype,
|
||||
)
|
||||
self.norm = RMSNorm(
|
||||
self.head_dim, eps=config.rms_norm_eps, weight_dtype=torch.float32
|
||||
)
|
||||
self.freqs_cis = freqs_cis
|
||||
|
||||
self.ape_converted = False
|
||||
|
||||
def apply_ape_hotfix(self):
|
||||
assert not self.ape_converted
|
||||
self.ape_converted = True
|
||||
|
||||
if self.overlap:
|
||||
ape = torch.chunk(self.ape.data, 2, dim=-1)
|
||||
ape = torch.cat([ape[0], ape[1]], dim=0)
|
||||
self.ape.data.copy_(ape.view(self.ratio, -1))
|
||||
|
||||
def _get_state_pool(self, forward_batch: ForwardBatch) -> CompressStatePool:
|
||||
token_to_kv_pool = forward_batch.token_to_kv_pool
|
||||
assert isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
||||
if self.is_in_indexer:
|
||||
ret = token_to_kv_pool.get_indexer_compress_states(self.layer_id)
|
||||
else:
|
||||
ret = token_to_kv_pool.get_attention_compress_states(self.layer_id)
|
||||
|
||||
assert isinstance(ret, CompressStatePool)
|
||||
|
||||
return ret
|
||||
|
||||
def forward(self, x: torch.Tensor, forward_batch: ForwardBatch) -> torch.Tensor:
|
||||
if forward_batch.forward_mode.is_idle():
|
||||
assert x.shape[0] == 0
|
||||
return x.new_empty(0, self.head_dim)
|
||||
|
||||
kv_score = linear_bf16_fp32(x, self.wkv_gate.weight)
|
||||
if nsa_use_prefill_cp(forward_batch):
|
||||
kv_score = cp_all_gather_rerange_output(
|
||||
kv_score,
|
||||
get_attention_cp_size(),
|
||||
forward_batch,
|
||||
torch.cuda.current_stream(),
|
||||
)
|
||||
|
||||
backend = forward_batch.attn_backend
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(backend, DeepseekV4AttnBackend)
|
||||
kv_score_buffer = self._get_state_pool(forward_batch)
|
||||
kv_score_buffer = kv_score_buffer.kv_score_buffer.kv_score
|
||||
return backend.forward_compress(
|
||||
kv_score_buffer=kv_score_buffer,
|
||||
kv_score_input=kv_score,
|
||||
ape=self.ape.view(-1, self.head_dim),
|
||||
head_dim=self.head_dim,
|
||||
norm=self.norm,
|
||||
freqs_cis_cache=self.freqs_cis,
|
||||
rotate=self.rotate,
|
||||
compress_ratio=self.ratio,
|
||||
forward_batch=forward_batch,
|
||||
is_paged=True,
|
||||
)
|
||||
@@ -0,0 +1,257 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.srt.layers.quantization.fp8_kernel import is_fp8_fnuz
|
||||
|
||||
fp8_dtype = torch.float8_e4m3fnuz if is_fp8_fnuz() else torch.float8_e4m3fn
|
||||
|
||||
|
||||
@dataclass
|
||||
class NopeFp8RopeBf16Pack:
|
||||
k_nope_fp8: torch.Tensor
|
||||
k_rope_bf16: torch.Tensor
|
||||
scale_k_nope_ue8m0: torch.Tensor
|
||||
|
||||
def __post_init__(self):
|
||||
assert self.k_nope_fp8.shape[-1] == 448
|
||||
assert self.k_rope_bf16.shape[-1] == 64
|
||||
assert self.scale_k_nope_ue8m0.shape[-1] == 7
|
||||
|
||||
def slice_pack(self, _slice: Any) -> NopeFp8RopeBf16Pack:
|
||||
return NopeFp8RopeBf16Pack(
|
||||
k_nope_fp8=self.k_nope_fp8[_slice],
|
||||
k_rope_bf16=self.k_rope_bf16[_slice],
|
||||
scale_k_nope_ue8m0=self.scale_k_nope_ue8m0[_slice],
|
||||
)
|
||||
|
||||
|
||||
class SetKAndS:
|
||||
@classmethod
|
||||
def execute(cls, pool, buf, loc, nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack):
|
||||
cls.triton(pool, buf, loc, nope_fp8_rope_bf16_pack)
|
||||
|
||||
@classmethod
|
||||
def torch(cls, pool, buf, loc, nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack):
|
||||
_set_k_and_s_torch(buf, loc, nope_fp8_rope_bf16_pack, pool.page_size)
|
||||
|
||||
@classmethod
|
||||
def triton(cls, pool, buf, loc, nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack):
|
||||
_set_k_and_s_triton(buf, loc, nope_fp8_rope_bf16_pack, pool.page_size)
|
||||
|
||||
|
||||
def _set_k_and_s_triton(
|
||||
buf: torch.Tensor,
|
||||
loc: torch.Tensor,
|
||||
nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
|
||||
page_size: int,
|
||||
):
|
||||
num_pages, buf_numel_per_page = buf.shape
|
||||
(num_tokens_to_write,) = loc.shape
|
||||
|
||||
k_nope, k_rope, scale_k_nope = (
|
||||
nope_fp8_rope_bf16_pack.k_nope_fp8,
|
||||
nope_fp8_rope_bf16_pack.k_rope_bf16,
|
||||
nope_fp8_rope_bf16_pack.scale_k_nope_ue8m0,
|
||||
)
|
||||
|
||||
num_tokens_to_write_nope, nope_dim = k_nope.shape
|
||||
num_tokens_to_write_rope, rope_dim = k_rope.shape
|
||||
num_tokens_to_write_scale, scale_dim = scale_k_nope.shape
|
||||
|
||||
assert (
|
||||
num_tokens_to_write
|
||||
== num_tokens_to_write_nope
|
||||
== num_tokens_to_write_rope
|
||||
== num_tokens_to_write_scale
|
||||
)
|
||||
|
||||
assert buf.dtype == torch.uint8
|
||||
assert loc.dtype in [torch.int64, torch.int32], f"{loc.dtype=}"
|
||||
|
||||
assert k_nope.dtype == fp8_dtype
|
||||
assert k_rope.dtype == torch.bfloat16
|
||||
assert scale_k_nope.dtype == torch.uint8, f"{scale_k_nope.dtype=}"
|
||||
|
||||
assert buf.is_contiguous()
|
||||
assert loc.is_contiguous()
|
||||
assert k_nope.is_contiguous()
|
||||
assert k_rope.is_contiguous()
|
||||
assert scale_k_nope.is_contiguous()
|
||||
|
||||
buf_fp8 = buf.view(fp8_dtype)
|
||||
buf_bf16 = buf.view(torch.bfloat16)
|
||||
buf_uint8 = buf.view(torch.uint8)
|
||||
|
||||
nope_rope_bytes = nope_dim + rope_dim * 2
|
||||
s_offset_nbytes_in_page = page_size * (nope_dim + rope_dim * 2)
|
||||
|
||||
_set_k_and_s_triton_kernel[(num_tokens_to_write,)](
|
||||
buf_fp8,
|
||||
buf_bf16,
|
||||
buf_uint8,
|
||||
loc,
|
||||
k_nope,
|
||||
k_rope,
|
||||
scale_k_nope,
|
||||
k_nope.stride(0),
|
||||
k_rope.stride(0),
|
||||
scale_k_nope.stride(0),
|
||||
PAGE_SIZE=page_size,
|
||||
BUF_NUMEL_PER_PAGE=buf_numel_per_page,
|
||||
NUM_NOPE_ELEMS_PER_TOKEN=nope_dim,
|
||||
NUM_ROPE_ELEMS_PER_TOKEN=rope_dim,
|
||||
NUM_SCALE_ELEMS_PER_TOKEN=scale_dim,
|
||||
NUM_NOPE_ROPE_BYTES_PER_TOKEN=nope_rope_bytes,
|
||||
PADDED_SCALE_ELEMS_PER_TOKEN=scale_dim + 1,
|
||||
S_OFFSET_NBYTES_IN_PAGE=s_offset_nbytes_in_page,
|
||||
BLOCK_NOPE=512,
|
||||
BLOCK_ROPE=64,
|
||||
BLOCK_SCALE=8,
|
||||
)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _set_k_and_s_triton_kernel(
|
||||
buf_fp8_ptr,
|
||||
buf_bf16_ptr,
|
||||
buf_uint8_ptr,
|
||||
loc_ptr,
|
||||
k_nope_ptr,
|
||||
k_rope_ptr,
|
||||
scale_k_nope_ptr,
|
||||
k_nope_ptr_stride_0,
|
||||
k_rope_ptr_stride_0,
|
||||
scale_k_nope_ptr_stride_0,
|
||||
PAGE_SIZE: tl.constexpr,
|
||||
BUF_NUMEL_PER_PAGE: tl.constexpr,
|
||||
NUM_NOPE_ELEMS_PER_TOKEN: tl.constexpr,
|
||||
NUM_ROPE_ELEMS_PER_TOKEN: tl.constexpr,
|
||||
NUM_NOPE_ROPE_BYTES_PER_TOKEN: tl.constexpr,
|
||||
NUM_SCALE_ELEMS_PER_TOKEN: tl.constexpr,
|
||||
PADDED_SCALE_ELEMS_PER_TOKEN: tl.constexpr,
|
||||
S_OFFSET_NBYTES_IN_PAGE: tl.constexpr,
|
||||
BLOCK_NOPE: tl.constexpr,
|
||||
BLOCK_ROPE: tl.constexpr,
|
||||
BLOCK_SCALE: tl.constexpr,
|
||||
):
|
||||
token_id = tl.program_id(0)
|
||||
loc = tl.load(loc_ptr + token_id)
|
||||
|
||||
nope_range = tl.arange(0, BLOCK_NOPE)
|
||||
nope_mask = nope_range < NUM_NOPE_ELEMS_PER_TOKEN
|
||||
in_k_nope_offsets = token_id * k_nope_ptr_stride_0 + nope_range
|
||||
k_nope = tl.load(k_nope_ptr + in_k_nope_offsets, mask=nope_mask, other=0.0)
|
||||
|
||||
rope_range = tl.arange(0, BLOCK_ROPE)
|
||||
in_k_rope_offsets = token_id * k_rope_ptr_stride_0 + rope_range
|
||||
k_rope = tl.load(k_rope_ptr + in_k_rope_offsets)
|
||||
|
||||
scale_range = tl.arange(0, BLOCK_SCALE)
|
||||
scale_mask = scale_range < NUM_SCALE_ELEMS_PER_TOKEN
|
||||
in_scale_k_offsets = token_id * scale_k_nope_ptr_stride_0 + scale_range
|
||||
k_scale = tl.load(scale_k_nope_ptr + in_scale_k_offsets, mask=scale_mask, other=0)
|
||||
|
||||
loc_page_index = loc // PAGE_SIZE
|
||||
loc_token_offset_in_page = loc % PAGE_SIZE
|
||||
|
||||
out_k_nope_offsets = (
|
||||
loc_page_index * BUF_NUMEL_PER_PAGE
|
||||
+ loc_token_offset_in_page * NUM_NOPE_ROPE_BYTES_PER_TOKEN
|
||||
+ nope_range
|
||||
)
|
||||
|
||||
out_k_rope_offsets = (
|
||||
loc_page_index * BUF_NUMEL_PER_PAGE // 2
|
||||
+ loc_token_offset_in_page * (NUM_NOPE_ROPE_BYTES_PER_TOKEN // 2)
|
||||
+ NUM_NOPE_ELEMS_PER_TOKEN // 2
|
||||
+ rope_range
|
||||
)
|
||||
|
||||
out_s_offsets = (
|
||||
loc_page_index * BUF_NUMEL_PER_PAGE
|
||||
+ S_OFFSET_NBYTES_IN_PAGE
|
||||
+ loc_token_offset_in_page * PADDED_SCALE_ELEMS_PER_TOKEN
|
||||
+ scale_range
|
||||
)
|
||||
|
||||
tl.store(buf_fp8_ptr + out_k_nope_offsets, k_nope, mask=nope_mask)
|
||||
tl.store(buf_bf16_ptr + out_k_rope_offsets, k_rope)
|
||||
tl.store(buf_uint8_ptr + out_s_offsets, k_scale, mask=scale_mask)
|
||||
|
||||
|
||||
def _set_k_and_s_torch(
|
||||
buf: torch.Tensor,
|
||||
loc: torch.Tensor,
|
||||
nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
|
||||
page_size: int,
|
||||
):
|
||||
num_pages, buf_numel_per_page = buf.shape
|
||||
(num_tokens_to_write,) = loc.shape
|
||||
|
||||
k_nope, k_rope, scale_k_nope = (
|
||||
nope_fp8_rope_bf16_pack.k_nope_fp8,
|
||||
nope_fp8_rope_bf16_pack.k_rope_bf16,
|
||||
nope_fp8_rope_bf16_pack.scale_k_nope_ue8m0,
|
||||
)
|
||||
|
||||
num_tokens_to_write_nope, nope_dim = k_nope.shape
|
||||
num_tokens_to_write_rope, rope_dim = k_rope.shape
|
||||
num_tokens_to_write_scale, scale_dim = scale_k_nope.shape
|
||||
|
||||
assert (
|
||||
num_tokens_to_write
|
||||
== num_tokens_to_write_nope
|
||||
== num_tokens_to_write_rope
|
||||
== num_tokens_to_write_scale
|
||||
), f"{num_tokens_to_write=} {num_tokens_to_write_nope=} {num_tokens_to_write_rope=} {num_tokens_to_write_scale=}"
|
||||
|
||||
assert buf.dtype == torch.uint8
|
||||
assert loc.dtype in [
|
||||
torch.int64,
|
||||
torch.int32,
|
||||
], f"{loc.dtype=}"
|
||||
|
||||
assert k_nope.dtype == fp8_dtype
|
||||
assert k_rope.dtype == torch.bfloat16
|
||||
assert scale_k_nope.dtype == torch.uint8
|
||||
|
||||
assert buf.is_contiguous()
|
||||
assert loc.is_contiguous()
|
||||
assert k_nope.is_contiguous()
|
||||
assert k_rope.is_contiguous()
|
||||
assert scale_k_nope.is_contiguous()
|
||||
|
||||
buf_fp8 = buf.view(fp8_dtype).flatten()
|
||||
buf_bf16 = buf.view(torch.bfloat16).flatten()
|
||||
buf_scale = buf.view(torch.uint8).flatten()
|
||||
|
||||
loc_page_index = loc // page_size
|
||||
loc_token_offset_in_page = loc % page_size
|
||||
|
||||
s_offset_nbytes_in_page = page_size * (nope_dim + rope_dim * 2)
|
||||
|
||||
nope_offset = loc_page_index * buf_numel_per_page + loc_token_offset_in_page * (
|
||||
nope_dim + rope_dim * 2
|
||||
)
|
||||
|
||||
rope_offset = (
|
||||
loc_page_index * buf_numel_per_page // 2
|
||||
+ (loc_token_offset_in_page * (nope_dim + rope_dim * 2) + nope_dim) // 2
|
||||
)
|
||||
|
||||
s_offset = (
|
||||
loc_page_index * buf_numel_per_page
|
||||
+ s_offset_nbytes_in_page
|
||||
+ loc_token_offset_in_page * (scale_dim + 1)
|
||||
)
|
||||
|
||||
for i in range(num_tokens_to_write):
|
||||
buf_fp8[nope_offset[i] : nope_offset[i] + nope_dim] = k_nope[i]
|
||||
buf_bf16[rope_offset[i] : rope_offset[i] + rope_dim] = k_rope[i]
|
||||
buf_scale[s_offset[i] : s_offset[i] + scale_dim] = scale_k_nope[i]
|
||||
@@ -0,0 +1,562 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, List, Optional, Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.jit_kernel.deepseek_v4 import (
|
||||
fused_rope,
|
||||
topk_transform_512,
|
||||
topk_transform_512_v2,
|
||||
)
|
||||
from sglang.srt.configs.deepseek_v4 import DeepSeekV4Config
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.attention.dsv4.compressor import Compressor
|
||||
from sglang.srt.layers.attention.dsv4.metadata import PagedIndexerMetadata
|
||||
from sglang.srt.layers.attention.nsa.nsa_indexer import rotate_activation
|
||||
from sglang.srt.layers.attention.nsa.triton_kernel import act_quant
|
||||
from sglang.srt.layers.linear import ReplicatedLinear
|
||||
from sglang.srt.state_capturer.indexer_topk import get_global_indexer_capturer
|
||||
from sglang.srt.utils import add_prefix, is_hip
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.layers.attention.deepseek_v4_backend import DeepseekV4AttnBackend
|
||||
from sglang.srt.layers.attention.dsv4.compressor import (
|
||||
CompressorBackendMixin,
|
||||
)
|
||||
from sglang.srt.layers.quantization import QuantizationConfig
|
||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import DeepSeekV4TokenToKVPool
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
|
||||
|
||||
if is_hip():
|
||||
FP8_DTYPE = torch.float8_e4m3fnuz
|
||||
FP8_MAX = torch.finfo(FP8_DTYPE).max
|
||||
else:
|
||||
FP8_DTYPE = torch.float8_e4m3fn
|
||||
FP8_MAX = torch.finfo(FP8_DTYPE).max
|
||||
|
||||
|
||||
def fp8_paged_mqa_logits_torch(
|
||||
q_fp8: torch.Tensor,
|
||||
kvcache_fp8: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
page_table: torch.Tensor,
|
||||
deep_gemm_metadata: Any,
|
||||
max_seq_len: int,
|
||||
clean_logits: bool = True,
|
||||
) -> torch.Tensor:
|
||||
_ = deep_gemm_metadata
|
||||
batch_size, _, num_heads, head_dim = q_fp8.shape
|
||||
block_size = kvcache_fp8.shape[1]
|
||||
|
||||
assert head_dim == 128, "torch reference impl hardcodes DSV4 indexer head_dim=128"
|
||||
assert block_size == 64, "torch reference impl hardcodes block_size=64 cache layout"
|
||||
assert q_fp8.shape == (batch_size, 1, num_heads, head_dim)
|
||||
assert kvcache_fp8.shape[1:] == (block_size, 1, head_dim + 4)
|
||||
assert weight.shape == (batch_size, num_heads)
|
||||
assert seq_lens.shape == (batch_size,)
|
||||
assert page_table.shape[0] == batch_size
|
||||
assert clean_logits == False
|
||||
|
||||
logits = page_table.new_empty((batch_size, max_seq_len), dtype=torch.float32)
|
||||
for i in range(batch_size):
|
||||
q = q_fp8[i, 0]
|
||||
q = q.to(torch.float32)
|
||||
q_scale = weight[i]
|
||||
seq_len = int(seq_lens[i].item())
|
||||
assert seq_len <= max_seq_len
|
||||
num_pages = (seq_len + block_size - 1) // block_size
|
||||
padded_seq_len = num_pages * block_size
|
||||
pages = page_table[i, :num_pages]
|
||||
kvcache_fp8 = kvcache_fp8.view(-1, block_size * (head_dim + 4))
|
||||
kvcache = kvcache_fp8[pages]
|
||||
SCALE_OFFSET = block_size * head_dim
|
||||
kvcache_value = kvcache[..., :SCALE_OFFSET].view(dtype=FP8_DTYPE)
|
||||
kvcache_scale = kvcache[..., SCALE_OFFSET:].view(dtype=torch.float32)
|
||||
kvcache_value = kvcache_value.to(torch.float32)
|
||||
kvcache_scale = kvcache_scale.contiguous()
|
||||
kvcache_value = kvcache_value.view(padded_seq_len, head_dim)
|
||||
kvcache_scale = kvcache_scale.view(padded_seq_len)
|
||||
score = F.linear(kvcache_value, q)
|
||||
score = F.relu(score)
|
||||
score *= q_scale[None, :]
|
||||
score = score.sum(dim=1)
|
||||
score *= kvcache_scale
|
||||
logits[i, :seq_len] = score[:seq_len]
|
||||
|
||||
return logits
|
||||
|
||||
|
||||
def topk_transform_512_pytorch_vectorized(
|
||||
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:
|
||||
|
||||
TOPK = 512
|
||||
batch_size = scores.shape[0]
|
||||
max_seq_len = scores.shape[1]
|
||||
device = scores.device
|
||||
|
||||
page_bits = (page_size - 1).bit_length() if page_size > 1 else 0
|
||||
page_mask = page_size - 1
|
||||
|
||||
positions = (
|
||||
torch.arange(max_seq_len, device=device).unsqueeze(0).expand(batch_size, -1)
|
||||
)
|
||||
valid_mask = positions < seq_lens.unsqueeze(1)
|
||||
|
||||
masked_scores = scores.clone()
|
||||
masked_scores[~valid_mask] = float("-inf")
|
||||
|
||||
actual_k = min(TOPK, max_seq_len)
|
||||
_, raw_indices = torch.topk(
|
||||
masked_scores, k=actual_k, dim=1, largest=True, sorted=False
|
||||
)
|
||||
raw_indices = raw_indices.to(torch.int32)
|
||||
|
||||
if actual_k < TOPK:
|
||||
padding = torch.zeros(
|
||||
(batch_size, TOPK - actual_k), dtype=torch.int32, device=device
|
||||
)
|
||||
raw_indices = torch.cat([raw_indices, padding], dim=1)
|
||||
|
||||
batch_indices = (
|
||||
torch.arange(batch_size, device=device).unsqueeze(1).expand(-1, TOPK)
|
||||
)
|
||||
gathered_scores = scores[
|
||||
batch_indices.flatten(), raw_indices.clamp(min=0).flatten()
|
||||
].view(batch_size, TOPK)
|
||||
|
||||
valid_topk = gathered_scores != float("-inf")
|
||||
if actual_k < TOPK:
|
||||
pad_mask = torch.arange(TOPK, device=device).unsqueeze(0) >= actual_k
|
||||
valid_topk = valid_topk & ~pad_mask
|
||||
|
||||
needs_sequential = seq_lens <= TOPK
|
||||
if needs_sequential.any():
|
||||
sequential_indices = (
|
||||
torch.arange(TOPK, device=device, dtype=torch.int32)
|
||||
.unsqueeze(0)
|
||||
.expand(batch_size, -1)
|
||||
)
|
||||
sequential_valid = sequential_indices < seq_lens.unsqueeze(1)
|
||||
|
||||
raw_indices = torch.where(
|
||||
needs_sequential.unsqueeze(1).expand(-1, TOPK),
|
||||
torch.where(
|
||||
sequential_valid,
|
||||
sequential_indices,
|
||||
torch.tensor(-1, device=device, dtype=torch.int32),
|
||||
),
|
||||
raw_indices,
|
||||
)
|
||||
valid_topk = torch.where(
|
||||
needs_sequential.unsqueeze(1).expand(-1, TOPK), sequential_valid, valid_topk
|
||||
)
|
||||
|
||||
page_idx = raw_indices >> page_bits
|
||||
offset_in_page = raw_indices & page_mask
|
||||
|
||||
page_idx_clamped = torch.clamp(page_idx, min=0)
|
||||
physical_pages = torch.gather(page_tables, dim=1, index=page_idx_clamped.long())
|
||||
|
||||
page_indices = (physical_pages << page_bits) | offset_in_page
|
||||
page_indices = page_indices.to(torch.int32)
|
||||
|
||||
page_indices = torch.where(
|
||||
valid_topk, page_indices, torch.tensor(-1, device=device, dtype=torch.int32)
|
||||
)
|
||||
|
||||
out_page_indices.copy_(page_indices)
|
||||
|
||||
if out_raw_indices is not None:
|
||||
raw_indices = torch.where(
|
||||
valid_topk, raw_indices, torch.tensor(-1, device=device, dtype=torch.int32)
|
||||
)
|
||||
out_raw_indices.copy_(raw_indices)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fused_scale_kernel(
|
||||
weight_ptr,
|
||||
q_scale_ptr,
|
||||
out_ptr,
|
||||
numel,
|
||||
out_scale,
|
||||
BLOCK: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(0)
|
||||
offs = pid * BLOCK + tl.arange(0, BLOCK)
|
||||
mask = offs < numel
|
||||
|
||||
w = tl.load(weight_ptr + offs, mask=mask)
|
||||
qs = tl.load(q_scale_ptr + offs, mask=mask)
|
||||
|
||||
acc = w.to(tl.float32) * out_scale * qs.to(tl.float32)
|
||||
tl.store(out_ptr + offs, acc.to(out_ptr.dtype.element_ty), mask=mask)
|
||||
|
||||
|
||||
def fused_scale(
|
||||
weight: torch.Tensor,
|
||||
out_scale: float,
|
||||
q_scale: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
assert weight.is_contiguous() and q_scale.is_contiguous()
|
||||
B, H = weight.shape
|
||||
numel = B * H
|
||||
out_dtype = torch.promote_types(weight.dtype, q_scale.dtype)
|
||||
out = torch.empty((B, H, 1), device=weight.device, dtype=out_dtype)
|
||||
BLOCK = 1024
|
||||
grid = (triton.cdiv(numel, BLOCK),)
|
||||
_fused_scale_kernel[grid](
|
||||
weight,
|
||||
q_scale,
|
||||
out,
|
||||
numel,
|
||||
out_scale,
|
||||
BLOCK=BLOCK,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
class C4IndexerBackendMixin:
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.debug_use_external_c4_sparse_indices: bool = False
|
||||
|
||||
def _forward_prepare_multi_stream(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
q_lora: torch.Tensor,
|
||||
c4_indexer: C4Indexer,
|
||||
positions: torch.Tensor,
|
||||
forward_batch: ForwardBatch,
|
||||
token_to_kv_pool: DeepSeekV4TokenToKVPool,
|
||||
alt_streams: Optional[List[torch.cuda.Stream]] = None,
|
||||
q_lora_ready: Optional[torch.cuda.Event] = None,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(self, CompressorBackendMixin)
|
||||
|
||||
assert alt_streams is not None
|
||||
assert len(alt_streams) >= 2
|
||||
current_stream = torch.cuda.current_stream()
|
||||
stream_q = alt_streams[0]
|
||||
stream_weights = alt_streams[1]
|
||||
|
||||
stream_q.wait_stream(current_stream)
|
||||
stream_weights.wait_stream(current_stream)
|
||||
|
||||
self.forward_indexer_compressor(
|
||||
x=x,
|
||||
forward_batch=forward_batch,
|
||||
layer_id=c4_indexer.layer_id,
|
||||
compressor=c4_indexer.compressor,
|
||||
)
|
||||
c4_indexer_kv_cache = token_to_kv_pool.get_index_k_with_scale_buffer(
|
||||
layer_id=c4_indexer.layer_id,
|
||||
)
|
||||
|
||||
with torch.cuda.stream(stream_q):
|
||||
if q_lora_ready is not None:
|
||||
stream_q.wait_event(q_lora_ready)
|
||||
q = c4_indexer.compute_q(q_lora, positions=positions)
|
||||
q_fp8, q_scale = act_quant(q)
|
||||
q_scale_ready = stream_q.record_event()
|
||||
|
||||
with torch.cuda.stream(stream_weights):
|
||||
weights = c4_indexer.compute_weights(x, skip_scale=True)
|
||||
stream_weights.wait_event(q_scale_ready)
|
||||
weights = fused_scale(weights, c4_indexer.weight_scale, q_scale)
|
||||
|
||||
current_stream.wait_stream(stream_q)
|
||||
current_stream.wait_stream(stream_weights)
|
||||
|
||||
return q_fp8, weights, c4_indexer_kv_cache
|
||||
|
||||
def _forward_prepare_normal(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
q_lora: torch.Tensor,
|
||||
c4_indexer: C4Indexer,
|
||||
positions: torch.Tensor,
|
||||
forward_batch: ForwardBatch,
|
||||
token_to_kv_pool: DeepSeekV4TokenToKVPool,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(self, CompressorBackendMixin)
|
||||
|
||||
q = c4_indexer.compute_q(q_lora, positions=positions)
|
||||
q_fp8, q_scale = act_quant(q)
|
||||
weights = c4_indexer.compute_weights(x, skip_scale=True)
|
||||
weights = fused_scale(weights, c4_indexer.weight_scale, q_scale)
|
||||
self.forward_indexer_compressor(
|
||||
x=x,
|
||||
forward_batch=forward_batch,
|
||||
layer_id=c4_indexer.layer_id,
|
||||
compressor=c4_indexer.compressor,
|
||||
)
|
||||
c4_indexer_kv_cache = token_to_kv_pool.get_index_k_with_scale_buffer(
|
||||
layer_id=c4_indexer.layer_id,
|
||||
)
|
||||
return q_fp8, weights, c4_indexer_kv_cache
|
||||
|
||||
def forward_c4_indexer(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
q_lora: torch.Tensor,
|
||||
c4_indexer: C4Indexer,
|
||||
forward_batch: ForwardBatch,
|
||||
alt_streams: Optional[List[torch.cuda.Stream]] = None,
|
||||
enable_multi_stream: bool = False,
|
||||
q_lora_ready: Optional[torch.cuda.Event] = None,
|
||||
) -> None:
|
||||
if forward_batch.forward_mode.is_idle():
|
||||
return
|
||||
# PREP_IN_CG lazy upgrade: this runs from MQALayer._forward_prepare,
|
||||
# before attn_backend.forward() would trigger the upgrade.
|
||||
self._maybe_upgrade_forward_metadata()
|
||||
token_to_kv_pool = forward_batch.token_to_kv_pool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(token_to_kv_pool, DeepSeekV4TokenToKVPool)
|
||||
assert isinstance(self, CompressorBackendMixin)
|
||||
|
||||
metadata = self.forward_metadata
|
||||
indexer_metadata = metadata.indexer_metadata
|
||||
core_metadata = metadata.core_metadata
|
||||
|
||||
from sglang.srt.layers.attention.deepseek_v4_backend import (
|
||||
DSV4AttnMetadata,
|
||||
)
|
||||
|
||||
assert isinstance(core_metadata, DSV4AttnMetadata)
|
||||
assert isinstance(indexer_metadata, PagedIndexerMetadata)
|
||||
|
||||
if enable_multi_stream:
|
||||
q_fp8, weights, c4_indexer_kv_cache = self._forward_prepare_multi_stream(
|
||||
x=x,
|
||||
q_lora=q_lora,
|
||||
c4_indexer=c4_indexer,
|
||||
positions=core_metadata.positions,
|
||||
forward_batch=forward_batch,
|
||||
token_to_kv_pool=token_to_kv_pool,
|
||||
alt_streams=alt_streams,
|
||||
q_lora_ready=q_lora_ready,
|
||||
)
|
||||
else:
|
||||
assert q_lora_ready is None
|
||||
q_fp8, weights, c4_indexer_kv_cache = self._forward_prepare_normal(
|
||||
x=x,
|
||||
q_lora=q_lora,
|
||||
c4_indexer=c4_indexer,
|
||||
positions=core_metadata.positions,
|
||||
forward_batch=forward_batch,
|
||||
token_to_kv_pool=token_to_kv_pool,
|
||||
)
|
||||
|
||||
assert len(q_fp8.shape) == 3
|
||||
q_fp8 = q_fp8.unsqueeze(1)
|
||||
assert len(c4_indexer_kv_cache.shape) == 2
|
||||
block_kv = 64
|
||||
num_heads_kv = 1
|
||||
head_dim_with_sf = 132
|
||||
|
||||
c4_indexer_kv_cache = c4_indexer_kv_cache.view(
|
||||
c4_indexer_kv_cache.shape[0], block_kv, num_heads_kv, head_dim_with_sf
|
||||
)
|
||||
assert len(weights.shape) == 3
|
||||
weights = weights.squeeze(2)
|
||||
if envs.SGLANG_OPT_USE_TILELANG_INDEXER.get():
|
||||
from sglang.srt.layers.attention.dsv4.tilelang_kernel import (
|
||||
tilelang_fp8_paged_mqa_logits as fn,
|
||||
)
|
||||
elif envs.SGLANG_FP8_PAGED_MQA_LOGITS_TORCH.get():
|
||||
fn = fp8_paged_mqa_logits_torch
|
||||
else:
|
||||
from deep_gemm import fp8_paged_mqa_logits as fn
|
||||
|
||||
_c4sl = indexer_metadata.c4_seq_lens
|
||||
if _c4sl.dim() == 1:
|
||||
_c4sl = _c4sl.unsqueeze(-1)
|
||||
logits = fn(
|
||||
q_fp8,
|
||||
c4_indexer_kv_cache,
|
||||
weights,
|
||||
_c4sl,
|
||||
indexer_metadata.page_table,
|
||||
indexer_metadata.deep_gemm_metadata,
|
||||
indexer_metadata.max_c4_seq_len,
|
||||
False,
|
||||
)
|
||||
|
||||
assert indexer_metadata.page_table is core_metadata.page_table
|
||||
if self.debug_use_external_c4_sparse_indices:
|
||||
return
|
||||
|
||||
indexer_capturer = get_global_indexer_capturer()
|
||||
capture_enabled = indexer_capturer is not None
|
||||
|
||||
hisparse_coordinator = forward_batch.hisparse_coordinator
|
||||
hisparse_decode = (
|
||||
hisparse_coordinator is not None and forward_batch.forward_mode.is_decode()
|
||||
)
|
||||
|
||||
raw_indices = None
|
||||
if capture_enabled:
|
||||
raw_indices = torch.empty_like(core_metadata.c4_sparse_page_indices)
|
||||
elif hisparse_decode:
|
||||
raw_indices = hisparse_coordinator.raw_indices_buffer[
|
||||
: core_metadata.c4_sparse_page_indices.size(0)
|
||||
]
|
||||
|
||||
if envs.SGLANG_TOPK_TRANSFORM_512_TORCH.get():
|
||||
topk_transform_512_pytorch_vectorized(
|
||||
logits,
|
||||
indexer_metadata.c4_seq_lens,
|
||||
core_metadata.page_table,
|
||||
core_metadata.c4_sparse_page_indices,
|
||||
indexer_metadata.c4_page_size,
|
||||
raw_indices,
|
||||
)
|
||||
elif envs.SGLANG_OPT_USE_TOPK_V2.get() and raw_indices is None:
|
||||
topk_transform_512_v2(
|
||||
logits,
|
||||
indexer_metadata.c4_seq_lens,
|
||||
core_metadata.page_table,
|
||||
core_metadata.c4_sparse_page_indices,
|
||||
indexer_metadata.c4_page_size,
|
||||
indexer_metadata.topk_metadata,
|
||||
)
|
||||
else:
|
||||
topk_transform_512(
|
||||
logits,
|
||||
indexer_metadata.c4_seq_lens,
|
||||
core_metadata.page_table,
|
||||
core_metadata.c4_sparse_page_indices,
|
||||
indexer_metadata.c4_page_size,
|
||||
raw_indices,
|
||||
)
|
||||
if hisparse_coordinator is not None:
|
||||
if hisparse_decode:
|
||||
compress_layer_id = token_to_kv_pool.layer_mapping[
|
||||
c4_indexer.layer_id
|
||||
].compress_layer_id
|
||||
core_metadata.c4_sparse_page_indices = (
|
||||
hisparse_coordinator.swap_in_selected_pages(
|
||||
req_pool_indices=forward_batch.req_pool_indices,
|
||||
compressed_seq_lens=indexer_metadata.c4_seq_lens,
|
||||
top_k_result=raw_indices,
|
||||
layer_id=compress_layer_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
core_metadata.c4_sparse_page_indices = (
|
||||
token_to_kv_pool.c4_kv_pool.translate_loc_to_hisparse_device(
|
||||
core_metadata.c4_sparse_page_indices
|
||||
)
|
||||
)
|
||||
|
||||
if capture_enabled:
|
||||
compress_layer_id = token_to_kv_pool.layer_mapping[
|
||||
c4_indexer.layer_id
|
||||
].compress_layer_id
|
||||
indexer_capturer.capture(compress_layer_id, raw_indices)
|
||||
|
||||
|
||||
class C4Indexer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
config: DeepSeekV4Config,
|
||||
layer_id: int,
|
||||
freqs_cis: torch.Tensor,
|
||||
quant_config: Optional[QuantizationConfig] = None,
|
||||
prefix: str = "",
|
||||
alt_streams: Optional[List[torch.cuda.Stream]] = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.layer_id = layer_id
|
||||
self.dim = config.hidden_size
|
||||
self.n_heads = config.index_n_heads
|
||||
self.head_dim = config.index_head_dim
|
||||
self.rope_head_dim = config.qk_rope_head_dim
|
||||
self.q_lora_rank = config.q_lora_rank
|
||||
self.softmax_scale = self.head_dim**-0.5
|
||||
self.n_local_heads = self.n_heads
|
||||
self.wq_b = ReplicatedLinear(
|
||||
self.q_lora_rank,
|
||||
self.n_heads * self.head_dim,
|
||||
bias=False,
|
||||
quant_config=quant_config,
|
||||
params_dtype=torch.bfloat16,
|
||||
prefix=add_prefix("wq_b", prefix),
|
||||
)
|
||||
self.weights_proj = ReplicatedLinear(
|
||||
self.dim,
|
||||
self.n_heads,
|
||||
bias=False,
|
||||
quant_config=None,
|
||||
params_dtype=torch.bfloat16,
|
||||
prefix=add_prefix("weights_proj", prefix),
|
||||
)
|
||||
self.compressor = Compressor(
|
||||
config,
|
||||
self.layer_id,
|
||||
True,
|
||||
freqs_cis,
|
||||
compress_ratio=4,
|
||||
head_dim=self.head_dim,
|
||||
rotate=True,
|
||||
prefix=add_prefix("compressor", prefix),
|
||||
)
|
||||
self.freqs_cis = freqs_cis
|
||||
self.weight_scale: float = self.softmax_scale * self.n_heads**-0.5
|
||||
self.alt_streams = alt_streams
|
||||
|
||||
def compute_q(self, q_lora: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
|
||||
q, _ = self.wq_b(q_lora)
|
||||
q = q.view(-1, self.n_local_heads, self.head_dim)
|
||||
fused_rope(
|
||||
q[..., -self.rope_head_dim :],
|
||||
None,
|
||||
self.freqs_cis,
|
||||
positions=positions,
|
||||
)
|
||||
q = rotate_activation(q)
|
||||
return q
|
||||
|
||||
def compute_weights(self, x: torch.Tensor, skip_scale=False) -> torch.Tensor:
|
||||
out, _ = self.weights_proj(x)
|
||||
if not skip_scale:
|
||||
out = out * self.weight_scale
|
||||
return out
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
q_lora: torch.Tensor,
|
||||
forward_batch: ForwardBatch,
|
||||
enable_multi_stream: bool = False,
|
||||
q_lora_ready: Optional[torch.cuda.Event] = None,
|
||||
) -> None:
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(forward_batch.attn_backend, DeepseekV4AttnBackend)
|
||||
return forward_batch.attn_backend.forward_c4_indexer(
|
||||
x=x,
|
||||
q_lora=q_lora,
|
||||
forward_batch=forward_batch,
|
||||
c4_indexer=self,
|
||||
alt_streams=self.alt_streams,
|
||||
enable_multi_stream=enable_multi_stream,
|
||||
q_lora_ready=q_lora_ready,
|
||||
)
|
||||
@@ -0,0 +1,165 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from dataclasses import dataclass, field, fields
|
||||
from typing import TYPE_CHECKING, Any, List, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
"""
|
||||
Some comments on the common terms used in DeepSeekV4Backend:
|
||||
|
||||
topk_lengths:
|
||||
NOTE: TL;DR: topk_lengths == seq_lens
|
||||
The FlashMLA sparse decode kernel will attend to `k` tokens for each query.
|
||||
`topk_lengths` indicates how many tokens each query will attend to.
|
||||
This should be named as `seq_lens`, but we simply follow the naming convention.
|
||||
|
||||
page_table:
|
||||
The page table indicates which pages each request is assigned to.
|
||||
Each value in the page table is the page index in the TokenToKVPool.
|
||||
This page index is irrelevant to the actual `page_size`.
|
||||
|
||||
page_indices:
|
||||
The real indices used to index into the KV cache.
|
||||
This can be computed from the `page_table` and `page_size`.
|
||||
e.g. page_indices[i, j] = page_table[i, j // page_size] * page_size + (j % page_size)
|
||||
For sparse C4 top-512 attention, the indices will be selected from the C4 page indices.
|
||||
In implementation, we don't materialize the full C4 `page_indices`,
|
||||
but calculate them from `page_table` on-the-fly in the attention kernel.
|
||||
|
||||
positions:
|
||||
The position of the last token for each request.
|
||||
For compress token, the positions must be times of compress ratio.
|
||||
For example, for C4, raw_position=11 will trigger a compression,
|
||||
But the RoPE's position, during compression, must be 8 instead of 11.
|
||||
|
||||
Some other notes:
|
||||
c4_ / c128_: means "compressed by 4" / "compressed by 128".
|
||||
c4_page_size: page_size // 4
|
||||
c4_seq_lens: seq_lens // 4, but bounded by at least 1, due to flash_mla requirement.
|
||||
c4_sparse: means "compressed by 4" but only attend to top-512 tokens.
|
||||
all related length will be clipped to 512.
|
||||
"""
|
||||
|
||||
|
||||
def copy_metadata(
|
||||
*,
|
||||
src,
|
||||
dst,
|
||||
check_eq_fields: List[str],
|
||||
copy_fields: List[str],
|
||||
assign_fields: Optional[List[str]] = None,
|
||||
):
|
||||
assign_fields = assign_fields or []
|
||||
|
||||
for field_name in check_eq_fields:
|
||||
src_val = getattr(src, field_name)
|
||||
dst_val = getattr(dst, field_name)
|
||||
assert src_val == dst_val, f"{field_name=} {src_val=} {dst_val=}"
|
||||
|
||||
for field_name in copy_fields:
|
||||
src_val = getattr(src, field_name)
|
||||
dst_val = getattr(dst, field_name)
|
||||
if src_val is None and dst_val is None:
|
||||
continue
|
||||
assert dst_val is not None, f"{field_name=} {src_val=} {dst_val=}"
|
||||
if hasattr(dst_val, "copy_"):
|
||||
dst_val.copy_(src_val)
|
||||
else:
|
||||
warnings.warn(
|
||||
f"{field_name=} {type(dst_val)=} does not have copy_, use setattr"
|
||||
)
|
||||
setattr(dst, field_name, src_val)
|
||||
|
||||
for field_name in assign_fields:
|
||||
setattr(dst, field_name, getattr(src, field_name))
|
||||
|
||||
provided_fields = check_eq_fields + copy_fields + assign_fields
|
||||
provided_fields_unique = set(provided_fields)
|
||||
assert len(provided_fields) == len(
|
||||
provided_fields_unique
|
||||
), f"{provided_fields=} has dup"
|
||||
all_fields = {f.name for f in fields(src)}
|
||||
provided_fields = set(provided_fields)
|
||||
assert (
|
||||
provided_fields == all_fields
|
||||
), f"{provided_fields - all_fields=}, {all_fields - provided_fields=}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PagedIndexerMetadata:
|
||||
page_size: int
|
||||
page_table: torch.Tensor
|
||||
c4_seq_lens: torch.Tensor
|
||||
deep_gemm_metadata: Any = field(init=False, repr=False)
|
||||
topk_metadata: torch.Tensor = field(init=False, repr=False)
|
||||
|
||||
def __post_init__(self):
|
||||
if envs.SGLANG_FP8_PAGED_MQA_LOGITS_TORCH.get():
|
||||
self.deep_gemm_metadata = None
|
||||
else:
|
||||
import deep_gemm
|
||||
|
||||
if envs.SGLANG_OPT_USE_JIT_INDEXER_METADATA.get():
|
||||
from sglang.jit_kernel.deepseek_v4 import get_paged_mqa_logits_metadata
|
||||
else:
|
||||
from deep_gemm import get_paged_mqa_logits_metadata
|
||||
|
||||
_c4 = self.c4_seq_lens.to(torch.int32)
|
||||
if _c4.dim() == 1:
|
||||
_c4 = _c4.unsqueeze(-1)
|
||||
self.deep_gemm_metadata = get_paged_mqa_logits_metadata(
|
||||
_c4,
|
||||
self.c4_page_size,
|
||||
deep_gemm.get_num_sms(),
|
||||
)
|
||||
|
||||
assert isinstance(self.deep_gemm_metadata, torch.Tensor)
|
||||
|
||||
from sglang.jit_kernel.deepseek_v4 import plan_topk_v2
|
||||
|
||||
if envs.SGLANG_OPT_USE_TOPK_V2.get():
|
||||
self.topk_metadata = plan_topk_v2(self.c4_seq_lens)
|
||||
else:
|
||||
self.topk_metadata = torch.empty((0,))
|
||||
|
||||
assert self.page_size == 256, "the system hardcodes page_size=256"
|
||||
|
||||
@property
|
||||
def c4_page_size(self) -> int:
|
||||
return self.page_size // 4
|
||||
|
||||
@property
|
||||
def max_seq_len(self) -> int:
|
||||
return self.page_table.shape[1] * self.page_size
|
||||
|
||||
@property
|
||||
def max_c4_seq_len(self) -> int:
|
||||
return self.page_table.shape[1] * self.c4_page_size
|
||||
|
||||
def copy_(self, other: "PagedIndexerMetadata"):
|
||||
if is_hip():
|
||||
copy_fields = ["page_table", "c4_seq_lens"]
|
||||
else:
|
||||
copy_fields = ["page_table", "c4_seq_lens", "deep_gemm_metadata"]
|
||||
copy_fields += ["topk_metadata"]
|
||||
copy_metadata(
|
||||
src=other,
|
||||
dst=self,
|
||||
check_eq_fields=["page_size"],
|
||||
copy_fields=copy_fields,
|
||||
)
|
||||
|
||||
|
||||
def maybe_copy_inplace(dst, *, src) -> None:
|
||||
assert type(src) == type(dst)
|
||||
if dst is not None:
|
||||
dst.copy_(src)
|
||||
@@ -0,0 +1,200 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _init_compressed_attn_metadata_kernel(
|
||||
seq_lens_ptr,
|
||||
positions_ptr,
|
||||
raw_out_loc_ptr,
|
||||
page_table_ptr,
|
||||
c4_out_loc_ptr,
|
||||
c4_positions_ptr,
|
||||
c4_seq_lens_raw_ptr,
|
||||
c4_seq_lens_clamp1_ptr,
|
||||
c128_out_loc_ptr,
|
||||
c128_positions_ptr,
|
||||
c128_seq_lens_clamp1_ptr,
|
||||
c128_page_indices_ptr,
|
||||
bs,
|
||||
max_pages,
|
||||
page_size: tl.constexpr,
|
||||
c128_max_seq_len: tl.constexpr,
|
||||
c128_page_size: tl.constexpr,
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
COMPUTE_PAGE_INDICES: tl.constexpr,
|
||||
):
|
||||
batch_id = tl.program_id(0)
|
||||
if batch_id >= bs:
|
||||
return
|
||||
|
||||
seq_len = tl.load(seq_lens_ptr + batch_id)
|
||||
position = tl.load(positions_ptr + batch_id)
|
||||
raw_out_loc = tl.load(raw_out_loc_ptr + batch_id)
|
||||
|
||||
c4_should_compress = (seq_len % 4) == 0
|
||||
c4_out_loc = tl.where(c4_should_compress, raw_out_loc // 4, 0)
|
||||
c4_positions = position & (~3)
|
||||
c4_seq_lens_raw = seq_len // 4
|
||||
c4_seq_lens_clamp1 = tl.maximum(c4_seq_lens_raw, 1)
|
||||
|
||||
tl.store(c4_out_loc_ptr + batch_id, c4_out_loc)
|
||||
tl.store(c4_positions_ptr + batch_id, c4_positions)
|
||||
tl.store(c4_seq_lens_raw_ptr + batch_id, c4_seq_lens_raw)
|
||||
tl.store(c4_seq_lens_clamp1_ptr + batch_id, c4_seq_lens_clamp1)
|
||||
|
||||
c128_should_compress = (seq_len % 128) == 0
|
||||
c128_out_loc = tl.where(c128_should_compress, raw_out_loc // 128, 0)
|
||||
c128_positions = position & (~127)
|
||||
c128_seq_lens_raw = seq_len // 128
|
||||
c128_seq_lens_clamp1 = tl.maximum(c128_seq_lens_raw, 1)
|
||||
|
||||
tl.store(c128_out_loc_ptr + batch_id, c128_out_loc)
|
||||
tl.store(c128_positions_ptr + batch_id, c128_positions)
|
||||
tl.store(c128_seq_lens_clamp1_ptr + batch_id, c128_seq_lens_clamp1)
|
||||
|
||||
if COMPUTE_PAGE_INDICES:
|
||||
page_indices_base = batch_id * c128_max_seq_len
|
||||
for block_start in range(0, c128_max_seq_len, BLOCK_SIZE):
|
||||
offsets = block_start + tl.arange(0, BLOCK_SIZE)
|
||||
mask = offsets < c128_max_seq_len
|
||||
|
||||
page_idx = offsets // c128_page_size
|
||||
offset_in_page = offsets % c128_page_size
|
||||
|
||||
page_mask = mask & (page_idx < max_pages)
|
||||
page_table_vals = tl.load(
|
||||
page_table_ptr + batch_id * max_pages + page_idx,
|
||||
mask=page_mask,
|
||||
other=0,
|
||||
)
|
||||
|
||||
c_page_indices_vals = page_table_vals * c128_page_size + offset_in_page
|
||||
|
||||
valid_mask = offsets < c128_seq_lens_raw
|
||||
c_page_indices_vals = tl.where(valid_mask, c_page_indices_vals, -1)
|
||||
|
||||
tl.store(
|
||||
c128_page_indices_ptr + page_indices_base + offsets,
|
||||
c_page_indices_vals,
|
||||
mask=mask,
|
||||
)
|
||||
|
||||
|
||||
def _init_compressed_attn_metadata_triton(
|
||||
seq_lens: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
raw_out_loc: torch.Tensor,
|
||||
page_table: Optional[torch.Tensor] = None,
|
||||
page_size: int = 0,
|
||||
compute_page_indices: bool = True,
|
||||
) -> Tuple[
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
Optional[torch.Tensor],
|
||||
]:
|
||||
bs = seq_lens.shape[0]
|
||||
device = seq_lens.device
|
||||
|
||||
c4_out_loc = torch.empty(bs, dtype=torch.int32, device=device)
|
||||
c4_positions = torch.empty(bs, dtype=torch.int32, device=device)
|
||||
c4_seq_lens_raw = torch.empty(bs, dtype=torch.int32, device=device)
|
||||
c4_seq_lens_clamp1 = torch.empty(bs, dtype=torch.int32, device=device)
|
||||
|
||||
c128_out_loc = torch.empty(bs, dtype=torch.int32, device=device)
|
||||
c128_positions = torch.empty(bs, dtype=torch.int32, device=device)
|
||||
c128_seq_lens_clamp1 = torch.empty(bs, dtype=torch.int32, device=device)
|
||||
|
||||
if compute_page_indices:
|
||||
assert (
|
||||
page_table is not None
|
||||
), "page_table required when compute_page_indices=True"
|
||||
assert page_size > 0, "page_size required when compute_page_indices=True"
|
||||
max_pages = page_table.shape[1]
|
||||
c128_page_size = page_size // 128
|
||||
c128_max_seq_len = c128_page_size * max_pages
|
||||
c128_page_indices = torch.empty(
|
||||
bs, c128_max_seq_len, dtype=torch.int32, device=device
|
||||
)
|
||||
BLOCK_SIZE = triton.next_power_of_2(max(c128_page_size, 64))
|
||||
else:
|
||||
max_pages = 0
|
||||
c128_page_size = 1
|
||||
c128_max_seq_len = 0
|
||||
c128_page_indices = None
|
||||
BLOCK_SIZE = 64
|
||||
if page_table is None:
|
||||
page_table = torch.empty(0, dtype=torch.int32, device=device)
|
||||
|
||||
grid = (bs,)
|
||||
_init_compressed_attn_metadata_kernel[grid](
|
||||
seq_lens,
|
||||
positions,
|
||||
raw_out_loc,
|
||||
page_table,
|
||||
c4_out_loc,
|
||||
c4_positions,
|
||||
c4_seq_lens_raw,
|
||||
c4_seq_lens_clamp1,
|
||||
c128_out_loc,
|
||||
c128_positions,
|
||||
c128_seq_lens_clamp1,
|
||||
(
|
||||
c128_page_indices
|
||||
if c128_page_indices is not None
|
||||
else torch.empty(0, dtype=torch.int32, device=device)
|
||||
),
|
||||
bs,
|
||||
max_pages,
|
||||
page_size if page_size > 0 else 128,
|
||||
c128_max_seq_len,
|
||||
c128_page_size,
|
||||
BLOCK_SIZE,
|
||||
compute_page_indices,
|
||||
)
|
||||
|
||||
return (
|
||||
c4_out_loc,
|
||||
c4_positions,
|
||||
c4_seq_lens_raw,
|
||||
c4_seq_lens_clamp1,
|
||||
c128_out_loc,
|
||||
c128_positions,
|
||||
c128_seq_lens_clamp1,
|
||||
c128_page_indices,
|
||||
)
|
||||
|
||||
|
||||
def init_compression_metadata(
|
||||
seq_lens: torch.Tensor,
|
||||
positions: torch.Tensor,
|
||||
raw_out_loc: torch.Tensor,
|
||||
page_table: Optional[torch.Tensor] = None,
|
||||
page_size: int = 0,
|
||||
compute_page_indices: bool = True,
|
||||
) -> Tuple[
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
Optional[torch.Tensor],
|
||||
]:
|
||||
return _init_compressed_attn_metadata_triton(
|
||||
seq_lens,
|
||||
positions,
|
||||
raw_out_loc,
|
||||
page_table,
|
||||
page_size,
|
||||
compute_page_indices,
|
||||
)
|
||||
@@ -0,0 +1,120 @@
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.srt.layers.attention.dsv4.index_buf_accessor import NopeFp8RopeBf16Pack
|
||||
from sglang.srt.layers.quantization.fp8_kernel import is_fp8_fnuz
|
||||
|
||||
fp8_dtype = torch.float8_e4m3fnuz if is_fp8_fnuz() else torch.float8_e4m3fn
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _quant_k_cache_fused_kernel(
|
||||
k_bf16_ptr,
|
||||
k_nope_fp8_ptr,
|
||||
k_rope_bf16_ptr,
|
||||
scale_k_nope_uint8_ptr,
|
||||
k_bf16_stride_0,
|
||||
k_nope_fp8_stride_0,
|
||||
k_rope_bf16_stride_0,
|
||||
scale_stride_0,
|
||||
DIM_NOPE: tl.constexpr,
|
||||
DIM_ROPE: tl.constexpr,
|
||||
TILE_SIZE: tl.constexpr,
|
||||
NUM_TILES: tl.constexpr,
|
||||
FP8_MIN: tl.constexpr,
|
||||
FP8_MAX: tl.constexpr,
|
||||
EPS: tl.constexpr,
|
||||
):
|
||||
token_id = tl.program_id(0)
|
||||
tile_id = tl.program_id(1)
|
||||
|
||||
if tile_id == NUM_TILES:
|
||||
rope_range = tl.arange(0, TILE_SIZE)
|
||||
rope_mask = rope_range < DIM_ROPE
|
||||
|
||||
in_rope_offsets = token_id * k_bf16_stride_0 + DIM_NOPE + rope_range
|
||||
rope_data = tl.load(k_bf16_ptr + in_rope_offsets, mask=rope_mask, other=0.0)
|
||||
|
||||
out_rope_offsets = token_id * k_rope_bf16_stride_0 + rope_range
|
||||
tl.store(k_rope_bf16_ptr + out_rope_offsets, rope_data, mask=rope_mask)
|
||||
else:
|
||||
tile_range = tl.arange(0, TILE_SIZE)
|
||||
|
||||
in_tile_offsets = token_id * k_bf16_stride_0 + tile_id * TILE_SIZE + tile_range
|
||||
x_bf16 = tl.load(k_bf16_ptr + in_tile_offsets)
|
||||
x_fp32 = x_bf16.to(tl.float32)
|
||||
|
||||
abs_x = tl.abs(x_fp32)
|
||||
max_abs = tl.max(abs_x)
|
||||
max_abs_clamped = tl.maximum(max_abs, EPS)
|
||||
scale = max_abs_clamped / FP8_MAX
|
||||
|
||||
log2_scale = tl.log2(scale)
|
||||
ceil_log2 = tl.math.ceil(log2_scale)
|
||||
scale_pow2_fp32 = tl.exp2(ceil_log2)
|
||||
scale_inv = 1.0 / scale_pow2_fp32
|
||||
x_scaled = x_fp32 * scale_inv
|
||||
x_fp8 = tl.clamp(x_scaled, FP8_MIN, FP8_MAX).to(k_nope_fp8_ptr.dtype.element_ty)
|
||||
|
||||
out_fp8_offsets = (
|
||||
token_id * k_nope_fp8_stride_0 + tile_id * TILE_SIZE + tile_range
|
||||
)
|
||||
tl.store(k_nope_fp8_ptr + out_fp8_offsets, x_fp8)
|
||||
|
||||
exponent = ceil_log2.to(tl.int32)
|
||||
scale_uint8 = (exponent + 127).to(tl.uint8)
|
||||
|
||||
out_scale_offset = token_id * scale_stride_0 + tile_id
|
||||
tl.store(scale_k_nope_uint8_ptr + out_scale_offset, scale_uint8)
|
||||
|
||||
|
||||
def quant_to_nope_fp8_rope_bf16_pack_triton(
|
||||
k_bf16: torch.Tensor,
|
||||
) -> NopeFp8RopeBf16Pack:
|
||||
assert k_bf16.dtype == torch.bfloat16
|
||||
num_tokens, hidden_dim = k_bf16.shape
|
||||
assert hidden_dim == 512
|
||||
dim_nope = 448
|
||||
dim_rope = 64
|
||||
tile_size = 64
|
||||
num_tiles = dim_nope // tile_size
|
||||
|
||||
k_bf16 = k_bf16.contiguous()
|
||||
|
||||
k_nope_fp8 = torch.empty(
|
||||
(num_tokens, dim_nope), dtype=fp8_dtype, device=k_bf16.device
|
||||
)
|
||||
k_rope_bf16 = torch.empty(
|
||||
(num_tokens, dim_rope), dtype=torch.bfloat16, device=k_bf16.device
|
||||
)
|
||||
scale_k_nope_ue8m0 = torch.empty(
|
||||
(num_tokens, num_tiles), dtype=torch.uint8, device=k_bf16.device
|
||||
)
|
||||
|
||||
fp8_dtype_info = torch.finfo(fp8_dtype)
|
||||
|
||||
grid = (num_tokens, num_tiles + 1)
|
||||
_quant_k_cache_fused_kernel[grid](
|
||||
k_bf16,
|
||||
k_nope_fp8,
|
||||
k_rope_bf16,
|
||||
scale_k_nope_ue8m0,
|
||||
k_bf16.stride(0),
|
||||
k_nope_fp8.stride(0),
|
||||
k_rope_bf16.stride(0),
|
||||
scale_k_nope_ue8m0.stride(0),
|
||||
DIM_NOPE=dim_nope,
|
||||
DIM_ROPE=dim_rope,
|
||||
TILE_SIZE=tile_size,
|
||||
NUM_TILES=num_tiles,
|
||||
FP8_MIN=fp8_dtype_info.min,
|
||||
FP8_MAX=fp8_dtype_info.max,
|
||||
EPS=1e-8,
|
||||
)
|
||||
|
||||
return NopeFp8RopeBf16Pack(
|
||||
k_nope_fp8=k_nope_fp8,
|
||||
k_rope_bf16=k_rope_bf16,
|
||||
scale_k_nope_ue8m0=scale_k_nope_ue8m0,
|
||||
)
|
||||
@@ -0,0 +1,123 @@
|
||||
import functools
|
||||
from typing import Any
|
||||
|
||||
import tilelang
|
||||
import tilelang.language as T
|
||||
import torch
|
||||
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
if is_hip():
|
||||
FP8 = "float8_e5m2fnuz"
|
||||
FP8_ = torch.float8_e5m2
|
||||
else:
|
||||
FP8 = "float8_e4m3"
|
||||
FP8_ = torch.float8_e4m3fn
|
||||
FP32 = "float32"
|
||||
INT32 = "int32"
|
||||
|
||||
|
||||
@functools.cache
|
||||
def fp8_paged_mqa_logits_kernel(
|
||||
head_dim: int = 128,
|
||||
num_heads: int = 64,
|
||||
block_size: int = 64,
|
||||
clear_accum: bool = True,
|
||||
) -> Any:
|
||||
N = T.symbolic("batch_size")
|
||||
L = T.symbolic("max_table_length")
|
||||
S = T.symbolic("max_seq_len")
|
||||
C = T.symbolic("num_blocks")
|
||||
B = block_size
|
||||
D = head_dim
|
||||
H = num_heads
|
||||
d_0, d_1 = T.dynamic("d_0, d_1")
|
||||
|
||||
assert D % 4 == 0
|
||||
assert H % 4 == 0
|
||||
assert D == 128
|
||||
|
||||
@tilelang.jit
|
||||
def fp8_paged_mqa_logits(
|
||||
q: T.Tensor[(N, H, D), FP8],
|
||||
kvcache: T.StridedTensor[(C, B, D), (d_0, D, 1), FP8],
|
||||
kvcache_scale: T.StridedTensor[(C, B), (d_1, 1), FP32],
|
||||
weight: T.Tensor[(N, H), FP32],
|
||||
seq_lens: T.Tensor[(N,), INT32],
|
||||
page_table: T.Tensor[(N, L), INT32],
|
||||
o: T.Tensor[(N, S), FP32],
|
||||
) -> None:
|
||||
_ = N, L, S, C, D, H, B, d_0, d_1
|
||||
with T.Kernel(N) as bx:
|
||||
seq_len = seq_lens[bx]
|
||||
q_smem = T.alloc_shared((H, D), FP8)
|
||||
q_s_frag = T.alloc_fragment((H,), FP32)
|
||||
T.copy(q[bx, 0, 0], q_smem)
|
||||
T.copy(weight[bx, 0], q_s_frag)
|
||||
|
||||
for i in T.Pipelined(T.ceildiv(seq_len, B), num_stages=2):
|
||||
page = page_table[bx, i]
|
||||
k_smem = T.alloc_shared((B, D), FP8)
|
||||
k_s_frag = T.alloc_fragment((B,), FP32)
|
||||
T.copy(kvcache[page, 0, 0], k_smem)
|
||||
T.copy(kvcache_scale[page, 0], k_s_frag)
|
||||
|
||||
logits = T.alloc_fragment((B, H), FP32)
|
||||
if not clear_accum:
|
||||
T.fill(logits, 0.0)
|
||||
T.gemm(
|
||||
k_smem,
|
||||
q_smem,
|
||||
logits,
|
||||
transpose_A=False,
|
||||
transpose_B=True,
|
||||
clear_accum=clear_accum,
|
||||
)
|
||||
|
||||
for h, j in T.Parallel(H, B):
|
||||
logits[j, h] = T.max(logits[j, h], 0.0) * q_s_frag[h]
|
||||
logits_sum = T.alloc_fragment((B,), FP32)
|
||||
T.reduce_sum(logits, logits_sum, dim=1)
|
||||
for j in T.Parallel(B):
|
||||
logits_sum[j] *= k_s_frag[j]
|
||||
T.copy(logits_sum, o[bx, i * B])
|
||||
|
||||
return fp8_paged_mqa_logits
|
||||
|
||||
|
||||
def tilelang_fp8_paged_mqa_logits(
|
||||
q_fp8: torch.Tensor,
|
||||
kvcache_fp8: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
page_table: torch.Tensor,
|
||||
deep_gemm_metadata: Any,
|
||||
max_seq_len: int,
|
||||
clean_logits: bool = True,
|
||||
) -> torch.Tensor:
|
||||
_ = deep_gemm_metadata
|
||||
batch_size, _, num_heads, head_dim = q_fp8.shape
|
||||
block_size = kvcache_fp8.shape[1]
|
||||
assert head_dim == 128, "TODO"
|
||||
assert block_size == 64, "TODO"
|
||||
assert q_fp8.shape == (batch_size, 1, num_heads, head_dim)
|
||||
assert kvcache_fp8.shape[1:] == (block_size, 1, head_dim + 4)
|
||||
assert weight.shape == (batch_size, num_heads)
|
||||
assert seq_lens.shape == (batch_size,)
|
||||
assert page_table.shape[0] == batch_size
|
||||
assert clean_logits == False
|
||||
|
||||
logits = page_table.new_empty((batch_size, max_seq_len), dtype=torch.float32)
|
||||
kernel = fp8_paged_mqa_logits_kernel(
|
||||
head_dim=head_dim,
|
||||
num_heads=num_heads,
|
||||
block_size=block_size,
|
||||
clear_accum=clean_logits,
|
||||
)
|
||||
q_fp8 = q_fp8.view(batch_size, num_heads, head_dim)
|
||||
kvcache_fp8 = kvcache_fp8.view(-1, block_size * (head_dim + 4))
|
||||
kvcache = kvcache_fp8[..., : block_size * head_dim].view(dtype=FP8_)
|
||||
kvcache = kvcache.view(-1, block_size, head_dim)
|
||||
kvcache_scale = kvcache_fp8[..., block_size * head_dim :].view(dtype=torch.float32)
|
||||
kernel(q_fp8, kvcache, kvcache_scale, weight, seq_lens, page_table, logits)
|
||||
return logits
|
||||
@@ -32,6 +32,8 @@ def grouped_gemm_nt_f8f8bf16_masked(
|
||||
expected_m: int,
|
||||
overlap_args: Optional[Any] = None,
|
||||
max_block_n: int = 256,
|
||||
recipe_a: Optional[Tuple[int, int]] = None,
|
||||
recipe_b: Optional[Tuple[int, int]] = None,
|
||||
):
|
||||
num_groups, _, k = lhs[0].shape
|
||||
_, n, _ = rhs[0].shape
|
||||
@@ -50,12 +52,19 @@ def grouped_gemm_nt_f8f8bf16_masked(
|
||||
overlap_args.num_sms if overlap_args is not None else None
|
||||
):
|
||||
|
||||
fp4_kwargs = {}
|
||||
if recipe_a is not None:
|
||||
fp4_kwargs["recipe_a"] = recipe_a
|
||||
if recipe_b is not None:
|
||||
fp4_kwargs["recipe_b"] = recipe_b
|
||||
|
||||
return deep_gemm.fp8_m_grouped_gemm_nt_masked(
|
||||
lhs,
|
||||
rhs,
|
||||
out,
|
||||
masked_m,
|
||||
expected_m,
|
||||
**fp4_kwargs,
|
||||
**(
|
||||
dict(
|
||||
enable_overlap=True,
|
||||
@@ -82,6 +91,8 @@ def grouped_gemm_nt_f8f8bf16_contig(
|
||||
rhs: Tuple[torch.Tensor, torch.Tensor],
|
||||
out: torch.Tensor,
|
||||
m_indices: torch.Tensor,
|
||||
recipe_a: Optional[Tuple[int, int]] = None,
|
||||
recipe_b: Optional[Tuple[int, int]] = None,
|
||||
):
|
||||
m, k = lhs[0].shape
|
||||
num_groups, n, _ = rhs[0].shape
|
||||
@@ -93,8 +104,16 @@ def grouped_gemm_nt_f8f8bf16_contig(
|
||||
_sanity_check_input(lhs)
|
||||
_sanity_check_input(rhs)
|
||||
|
||||
fp4_kwargs = {}
|
||||
if recipe_a is not None:
|
||||
fp4_kwargs["recipe_a"] = recipe_a
|
||||
if recipe_b is not None:
|
||||
fp4_kwargs["recipe_b"] = recipe_b
|
||||
|
||||
with compile_utils.deep_gemm_execution_hook(m, n, k, num_groups, kernel_type):
|
||||
deep_gemm.m_grouped_fp8_gemm_nt_contiguous(lhs, rhs, out, m_indices)
|
||||
deep_gemm.m_grouped_fp8_gemm_nt_contiguous(
|
||||
lhs, rhs, out, m_indices, **fp4_kwargs
|
||||
)
|
||||
|
||||
|
||||
def gemm_nt_f8f8bf16(
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import math
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
import tilelang
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
tilelang.set_log_level("WARNING")
|
||||
|
||||
pass_configs = {
|
||||
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
|
||||
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
|
||||
}
|
||||
|
||||
FP8 = "float8_e4m3"
|
||||
BF16 = "bfloat16"
|
||||
FP32 = "float32"
|
||||
INT32 = "int32"
|
||||
|
||||
|
||||
@lru_cache(2)
|
||||
def precompute_freqs_cis(
|
||||
dim, seqlen, original_seq_len, base, factor, beta_fast, beta_slow
|
||||
) -> torch.Tensor:
|
||||
|
||||
def find_correction_dim(num_rotations, dim, base, max_seq_len):
|
||||
return (
|
||||
dim
|
||||
* math.log(max_seq_len / (num_rotations * 2 * math.pi))
|
||||
/ (2 * math.log(base))
|
||||
)
|
||||
|
||||
def find_correction_range(low_rot, high_rot, dim, base, max_seq_len):
|
||||
low = math.floor(find_correction_dim(low_rot, dim, base, max_seq_len))
|
||||
high = math.ceil(find_correction_dim(high_rot, dim, base, max_seq_len))
|
||||
return max(low, 0), min(high, dim - 1)
|
||||
|
||||
def linear_ramp_factor(min, max, dim):
|
||||
if min == max:
|
||||
max += 0.001
|
||||
linear_func = (torch.arange(dim, dtype=torch.float32) - min) / (max - min)
|
||||
ramp_func = torch.clamp(linear_func, 0, 1)
|
||||
return ramp_func
|
||||
|
||||
freqs = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
|
||||
if original_seq_len > 0:
|
||||
low, high = find_correction_range(
|
||||
beta_fast, beta_slow, dim, base, original_seq_len
|
||||
)
|
||||
smooth = 1 - linear_ramp_factor(low, high, dim // 2)
|
||||
freqs = freqs / factor * (1 - smooth) + freqs * smooth
|
||||
|
||||
t = torch.arange(seqlen)
|
||||
freqs = torch.outer(t, freqs)
|
||||
freqs_cis = torch.polar(torch.ones_like(freqs), freqs)
|
||||
return freqs_cis
|
||||
|
||||
|
||||
@triton.jit
|
||||
def apply_rotary_emb_triton_kernel(
|
||||
x_ptr,
|
||||
freqs_ptr,
|
||||
positions_ptr,
|
||||
rope_dim,
|
||||
stride_x_batch,
|
||||
stride_x_head,
|
||||
stride_x_dim,
|
||||
stride_freq_pos,
|
||||
stride_freq_dim,
|
||||
USE_POS: tl.constexpr,
|
||||
IS_INVERSE: tl.constexpr,
|
||||
IS_3D: tl.constexpr,
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
):
|
||||
pid_batch = tl.program_id(0)
|
||||
pid_head = tl.program_id(1)
|
||||
pid_dim = tl.program_id(2)
|
||||
|
||||
if USE_POS:
|
||||
position = tl.load(positions_ptr + pid_batch)
|
||||
else:
|
||||
position = pid_batch
|
||||
|
||||
if IS_3D:
|
||||
base_offset = pid_batch * stride_x_batch + pid_head * stride_x_head
|
||||
else:
|
||||
base_offset = pid_batch * stride_x_batch
|
||||
|
||||
offs_pair = pid_dim * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
|
||||
mask = offs_pair < (rope_dim // 2)
|
||||
|
||||
offs_x_real = base_offset + offs_pair * 2 * stride_x_dim
|
||||
offs_x_imag = base_offset + (offs_pair * 2 + 1) * stride_x_dim
|
||||
|
||||
x_real = tl.load(x_ptr + offs_x_real, mask=mask, other=0.0).to(tl.float32)
|
||||
x_imag = tl.load(x_ptr + offs_x_imag, mask=mask, other=0.0).to(tl.float32)
|
||||
|
||||
offs_freq_real = position * stride_freq_pos + offs_pair * 2 * stride_freq_dim
|
||||
offs_freq_imag = position * stride_freq_pos + (offs_pair * 2 + 1) * stride_freq_dim
|
||||
|
||||
freq_real = tl.load(freqs_ptr + offs_freq_real, mask=mask, other=0.0)
|
||||
freq_imag = tl.load(freqs_ptr + offs_freq_imag, mask=mask, other=0.0)
|
||||
|
||||
if IS_INVERSE:
|
||||
out_real = x_real * freq_real + x_imag * freq_imag
|
||||
out_imag = x_imag * freq_real - x_real * freq_imag
|
||||
else:
|
||||
out_real = x_real * freq_real - x_imag * freq_imag
|
||||
out_imag = x_real * freq_imag + x_imag * freq_real
|
||||
|
||||
tl.store(x_ptr + offs_x_real, out_real, mask=mask)
|
||||
tl.store(x_ptr + offs_x_imag, out_imag, mask=mask)
|
||||
|
||||
|
||||
def apply_rotary_emb_triton(
|
||||
x: torch.Tensor,
|
||||
freqs_cis: torch.Tensor,
|
||||
positions: Optional[torch.Tensor] = None,
|
||||
inverse: bool = False,
|
||||
) -> torch.Tensor:
|
||||
is_3d = x.ndim == 3
|
||||
|
||||
if is_3d:
|
||||
batch_size, n_heads, rope_dim = x.shape
|
||||
else:
|
||||
batch_size, rope_dim = x.shape
|
||||
n_heads = 1
|
||||
|
||||
freqs_real = torch.view_as_real(freqs_cis).flatten(-2)
|
||||
|
||||
BLOCK_SIZE = 128
|
||||
|
||||
num_blocks_dim = triton.cdiv(rope_dim // 2, BLOCK_SIZE)
|
||||
grid = (batch_size, n_heads if is_3d else 1, num_blocks_dim)
|
||||
|
||||
if positions is not None:
|
||||
assert positions.shape == (
|
||||
batch_size,
|
||||
), f"positions shape {positions.shape} != ({batch_size},)"
|
||||
|
||||
apply_rotary_emb_triton_kernel[grid](
|
||||
x,
|
||||
freqs_real,
|
||||
positions,
|
||||
rope_dim,
|
||||
x.stride(0),
|
||||
x.stride(1) if is_3d else 0,
|
||||
x.stride(-1),
|
||||
freqs_real.stride(0),
|
||||
freqs_real.stride(1),
|
||||
USE_POS=True,
|
||||
IS_INVERSE=inverse,
|
||||
IS_3D=is_3d,
|
||||
BLOCK_SIZE=BLOCK_SIZE,
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
freqs_real.shape[0] == batch_size
|
||||
), f"freqs_cis batch size {freqs_real.shape[0]} != x batch size {batch_size}"
|
||||
|
||||
apply_rotary_emb_triton_kernel[grid](
|
||||
x,
|
||||
freqs_real,
|
||||
None,
|
||||
rope_dim,
|
||||
x.stride(0),
|
||||
x.stride(1) if is_3d else 0,
|
||||
x.stride(-1),
|
||||
freqs_real.stride(0),
|
||||
freqs_real.stride(1),
|
||||
USE_POS=False,
|
||||
IS_INVERSE=inverse,
|
||||
IS_3D=is_3d,
|
||||
BLOCK_SIZE=BLOCK_SIZE,
|
||||
)
|
||||
|
||||
return x
|
||||
@@ -0,0 +1,643 @@
|
||||
import functools
|
||||
import math
|
||||
from typing import Tuple
|
||||
|
||||
import tilelang
|
||||
import tilelang.language as T
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.utils import is_arch_support_pdl
|
||||
from sglang.srt.layers.attention.nsa.utils import is_nsa_prefill_cp_round_robin_split
|
||||
from sglang.srt.layers.utils.common import strict_contiguous
|
||||
|
||||
tilelang.set_log_level("WARNING")
|
||||
|
||||
pass_configs = {
|
||||
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
|
||||
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
|
||||
}
|
||||
|
||||
FP8 = "float8_e4m3"
|
||||
BF16 = "bfloat16"
|
||||
FP32 = "float32"
|
||||
INT32 = "int32"
|
||||
|
||||
|
||||
@tilelang.jit(pass_configs=pass_configs)
|
||||
def hc_split_sinkhorn_kernel(hc: int, sinkhorn_iters: int, eps: float):
|
||||
n = T.symbolic("n")
|
||||
mix_hc = (2 + hc) * hc
|
||||
threads = 64
|
||||
|
||||
ENABLE_PDL = is_arch_support_pdl()
|
||||
|
||||
@T.prim_func
|
||||
def hc_split_sinkhorn_kernel_(
|
||||
mixes: T.Tensor[(n, mix_hc), FP32],
|
||||
hc_scale: T.Tensor[(3,), T.float32],
|
||||
hc_base: T.Tensor[(mix_hc,), T.float32],
|
||||
pre: T.Tensor[(n, hc), FP32],
|
||||
post: T.Tensor[(n, hc), FP32],
|
||||
comb: T.Tensor[(n, hc, hc), FP32],
|
||||
):
|
||||
with T.Kernel(n, threads=threads) as i:
|
||||
if ENABLE_PDL:
|
||||
T.pdl_sync()
|
||||
|
||||
mixes_shared = T.alloc_shared(mix_hc, FP32)
|
||||
comb_frag = T.alloc_fragment((hc, hc), FP32)
|
||||
T.copy(mixes[i, :], mixes_shared)
|
||||
|
||||
for j in T.Parallel(hc):
|
||||
pre[i, j] = T.sigmoid(mixes_shared[j] * hc_scale[0] + hc_base[j]) + eps
|
||||
for j in T.Parallel(hc):
|
||||
post[i, j] = 2 * T.sigmoid(
|
||||
mixes_shared[j + hc] * hc_scale[1] + hc_base[j + hc]
|
||||
)
|
||||
for j, k in T.Parallel(hc, hc):
|
||||
comb_frag[j, k] = (
|
||||
mixes_shared[j * hc + k + hc * 2] * hc_scale[2]
|
||||
+ hc_base[j * hc + k + hc * 2]
|
||||
)
|
||||
|
||||
row_sum = T.alloc_fragment(hc, FP32)
|
||||
col_sum = T.alloc_fragment(hc, FP32)
|
||||
|
||||
row_max = T.alloc_fragment(hc, FP32)
|
||||
T.reduce_max(comb_frag, row_max, dim=1)
|
||||
for j, k in T.Parallel(hc, hc):
|
||||
comb_frag[j, k] = T.exp(comb_frag[j, k] - row_max[j])
|
||||
T.reduce_sum(comb_frag, row_sum, dim=1)
|
||||
for j, k in T.Parallel(hc, hc):
|
||||
comb_frag[j, k] = comb_frag[j, k] / row_sum[j] + eps
|
||||
|
||||
T.reduce_sum(comb_frag, col_sum, dim=0)
|
||||
for j, k in T.Parallel(hc, hc):
|
||||
comb_frag[j, k] = comb_frag[j, k] / (col_sum[k] + eps)
|
||||
|
||||
for _ in T.serial(sinkhorn_iters - 1):
|
||||
T.reduce_sum(comb_frag, row_sum, dim=1)
|
||||
for j, k in T.Parallel(hc, hc):
|
||||
comb_frag[j, k] = comb_frag[j, k] / (row_sum[j] + eps)
|
||||
T.reduce_sum(comb_frag, col_sum, dim=0)
|
||||
for j, k in T.Parallel(hc, hc):
|
||||
comb_frag[j, k] = comb_frag[j, k] / (col_sum[k] + eps)
|
||||
|
||||
T.copy(comb_frag, comb[i, :, :])
|
||||
if ENABLE_PDL:
|
||||
T.pdl_trigger()
|
||||
|
||||
return hc_split_sinkhorn_kernel_
|
||||
|
||||
|
||||
def hc_split_sinkhorn(
|
||||
mixes: torch.Tensor,
|
||||
hc_scale: torch.Tensor,
|
||||
hc_base: torch.Tensor,
|
||||
hc_mult: int = 4,
|
||||
sinkhorn_iters: int = 20,
|
||||
eps: float = 1e-6,
|
||||
):
|
||||
b, s, _ = mixes.size()
|
||||
pre = mixes.new_empty(b, s, hc_mult)
|
||||
post = mixes.new_empty(b, s, hc_mult)
|
||||
comb = mixes.new_empty(b, s, hc_mult, hc_mult)
|
||||
kernel = hc_split_sinkhorn_kernel(hc_mult, sinkhorn_iters, eps)
|
||||
kernel(
|
||||
mixes.view(-1, (2 + hc_mult) * hc_mult),
|
||||
hc_scale,
|
||||
hc_base,
|
||||
pre.view(-1, hc_mult),
|
||||
post.view(-1, hc_mult),
|
||||
comb.view(-1, hc_mult, hc_mult),
|
||||
)
|
||||
return pre, post, comb
|
||||
|
||||
|
||||
@tilelang.jit(
|
||||
pass_configs={
|
||||
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
|
||||
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
|
||||
tilelang.PassConfigKey.TL_PTXAS_REGISTER_USAGE_LEVEL: 10,
|
||||
},
|
||||
)
|
||||
def mhc_pre_big_fuse_tilelang(
|
||||
gemm_out_mul,
|
||||
gemm_out_sqrsum,
|
||||
hc_scale,
|
||||
hc_base,
|
||||
residual,
|
||||
post_mix,
|
||||
comb_mix,
|
||||
layer_input,
|
||||
hidden_size: int,
|
||||
rms_eps: float,
|
||||
hc_pre_eps: float,
|
||||
hc_sinkhorn_eps: float,
|
||||
hc_post_mult_value: float,
|
||||
sinkhorn_repeat: int,
|
||||
n_splits: int = 16,
|
||||
hc_mult: int = 4,
|
||||
):
|
||||
num_tokens = T.dynamic("num_tokens")
|
||||
hc_mult3 = hc_mult * (2 + hc_mult)
|
||||
hidden_block = math.gcd(512, hidden_size)
|
||||
|
||||
gemm_out_mul: T.Tensor[[n_splits, num_tokens, hc_mult3], T.float32]
|
||||
gemm_out_sqrsum: T.Tensor[[n_splits, num_tokens], T.float32]
|
||||
hc_scale: T.Tensor[[3], T.float32]
|
||||
hc_base: T.Tensor[[hc_mult3], T.float32]
|
||||
residual: T.Tensor[[num_tokens, hc_mult, hidden_size], T.bfloat16]
|
||||
post_mix: T.Tensor[[num_tokens, hc_mult], T.float32]
|
||||
comb_mix: T.Tensor[[num_tokens, hc_mult * hc_mult], T.float32]
|
||||
layer_input: T.Tensor[[num_tokens, hidden_size], T.bfloat16]
|
||||
|
||||
ENABLE_PDL = is_arch_support_pdl()
|
||||
with T.Kernel(num_tokens, threads=96) as i:
|
||||
rms = T.alloc_fragment(1, T.float32)
|
||||
mixes = T.alloc_fragment(hc_mult3, T.float32)
|
||||
T.clear(mixes)
|
||||
rms[0] = 0
|
||||
|
||||
if ENABLE_PDL:
|
||||
T.pdl_sync()
|
||||
|
||||
for i_split in T.serial(n_splits):
|
||||
rms[0] += gemm_out_sqrsum[i_split, i]
|
||||
rms[0] = T.rsqrt(rms[0] / (hc_mult * hidden_size) + rms_eps)
|
||||
for j in T.Parallel(hc_mult3):
|
||||
mixes[j] = 0
|
||||
for i_split in T.serial(n_splits):
|
||||
mixes[j] += gemm_out_mul[i_split, i, j]
|
||||
mixes[j] *= rms[0]
|
||||
mixes_shared = T.alloc_shared(hc_mult3, T.float32)
|
||||
T.copy(mixes, mixes_shared)
|
||||
|
||||
if T.get_thread_binding() < 32:
|
||||
cm = T.alloc_fragment((hc_mult, hc_mult), T.float32)
|
||||
for j in T.Parallel(hc_mult):
|
||||
post_mix[i, j] = (
|
||||
T.sigmoid(
|
||||
mixes_shared[j + hc_mult] * hc_scale[1] + hc_base[j + hc_mult]
|
||||
)
|
||||
* hc_post_mult_value
|
||||
)
|
||||
for j, k in T.Parallel(hc_mult, hc_mult):
|
||||
cm[j, k] = (
|
||||
mixes_shared[j * hc_mult + k + hc_mult * 2] * hc_scale[2]
|
||||
+ hc_base[j * hc_mult + k + hc_mult * 2]
|
||||
)
|
||||
|
||||
row_sum = T.alloc_fragment(hc_mult, T.float32)
|
||||
col_sum = T.alloc_fragment(hc_mult, T.float32)
|
||||
|
||||
row_max = T.alloc_fragment(hc_mult, T.float32)
|
||||
T.reduce_max(cm, row_max, dim=1)
|
||||
for j, k in T.Parallel(hc_mult, hc_mult):
|
||||
cm[j, k] = T.exp(cm[j, k] - row_max[j])
|
||||
T.reduce_sum(cm, row_sum, dim=1)
|
||||
for j, k in T.Parallel(hc_mult, hc_mult):
|
||||
cm[j, k] = cm[j, k] / row_sum[j] + hc_sinkhorn_eps
|
||||
|
||||
T.reduce_sum(cm, col_sum, dim=0)
|
||||
for j, k in T.Parallel(hc_mult, hc_mult):
|
||||
cm[j, k] = cm[j, k] / (col_sum[k] + hc_sinkhorn_eps)
|
||||
|
||||
for _ in T.serial(sinkhorn_repeat - 1):
|
||||
T.reduce_sum(cm, row_sum, dim=1)
|
||||
for j, k in T.Parallel(hc_mult, hc_mult):
|
||||
cm[j, k] = cm[j, k] / (row_sum[j] + hc_sinkhorn_eps)
|
||||
|
||||
T.reduce_sum(cm, col_sum, dim=0)
|
||||
for j, k in T.Parallel(hc_mult, hc_mult):
|
||||
cm[j, k] = cm[j, k] / (col_sum[k] + hc_sinkhorn_eps)
|
||||
|
||||
for j, k in T.Parallel(hc_mult, hc_mult):
|
||||
comb_mix[i, j * hc_mult + k] = cm[j, k]
|
||||
else:
|
||||
pre_mix_shared = T.alloc_shared(hc_mult, T.float32)
|
||||
for j in T.Parallel(hc_mult):
|
||||
pre_mix_shared[j] = (
|
||||
T.sigmoid(
|
||||
mixes_shared[j] * hc_scale[0] + hc_base[j],
|
||||
)
|
||||
+ hc_pre_eps
|
||||
)
|
||||
for i0_h in T.Pipelined(hidden_size // hidden_block, num_stages=2):
|
||||
xs = T.alloc_shared((hc_mult, hidden_block), T.float32)
|
||||
xl = T.alloc_fragment((hc_mult, hidden_block), T.float32)
|
||||
T.copy(residual[i, 0, i0_h * hidden_block], xs)
|
||||
T.copy(xs, xl)
|
||||
|
||||
ol = T.alloc_fragment(hidden_block, T.float32)
|
||||
T.clear(ol)
|
||||
|
||||
for i_hc in T.serial(hc_mult):
|
||||
pre = pre_mix_shared[i_hc]
|
||||
for i1_h in T.Parallel(hidden_block):
|
||||
ol[i1_h] += pre * xl[i_hc, i1_h]
|
||||
|
||||
T.copy(ol, layer_input[i, i0_h * hidden_block])
|
||||
|
||||
if ENABLE_PDL:
|
||||
T.pdl_trigger()
|
||||
|
||||
|
||||
@tilelang.jit
|
||||
def mhc_pre_gemm_sqrsum_tilelang(
|
||||
x,
|
||||
fn,
|
||||
out,
|
||||
sqrsum,
|
||||
hc_mult3: int,
|
||||
hc_hidden_size: int,
|
||||
token_block: int = 32,
|
||||
hidden_block: int = 256,
|
||||
) -> tilelang.JITKernel:
|
||||
assert hc_mult3 <= 32
|
||||
num_tokens = T.dynamic("num_tokens")
|
||||
assert hc_hidden_size % hidden_block == 0
|
||||
|
||||
x: T.Tensor((num_tokens, hc_hidden_size), T.bfloat16)
|
||||
fn: T.Tensor((hc_mult3, hc_hidden_size), T.float32)
|
||||
out: T.Tensor((num_tokens, hc_mult3), T.float32)
|
||||
sqrsum: T.Tensor((num_tokens), T.float32)
|
||||
|
||||
ENABLE_PDL = is_arch_support_pdl()
|
||||
with T.Kernel(T.ceildiv(num_tokens, token_block)) as px:
|
||||
out_frag = T.alloc_fragment((token_block, 32), T.float32)
|
||||
sqrsum_part = T.alloc_fragment((token_block, 4), T.float32)
|
||||
T.clear(out_frag)
|
||||
T.clear(sqrsum_part)
|
||||
if ENABLE_PDL:
|
||||
T.pdl_sync()
|
||||
for pz in T.Pipelined(hc_hidden_size // hidden_block, num_stages=2):
|
||||
x_smem_16 = T.alloc_shared((token_block, hidden_block), T.bfloat16)
|
||||
fn_smem = T.alloc_shared((32, hidden_block), T.float32)
|
||||
|
||||
T.annotate_layout(
|
||||
{x_smem_16: tilelang.layout.make_swizzled_layout(x_smem_16)}
|
||||
)
|
||||
|
||||
T.copy(x[px * token_block, pz * hidden_block], x_smem_16)
|
||||
T.copy(fn[0, pz * hidden_block], fn_smem)
|
||||
|
||||
x_frag_16 = T.alloc_fragment((token_block, hidden_block), T.bfloat16)
|
||||
T.copy(x_smem_16, x_frag_16)
|
||||
x_frag = T.alloc_fragment((token_block, hidden_block), T.float32)
|
||||
T.copy(x_frag_16, x_frag)
|
||||
|
||||
for jj in T.serial(hidden_block // 4):
|
||||
for i, j in T.Parallel(token_block, 4):
|
||||
sqrsum_part[i, j] += x_frag[i, jj * 4 + j] * x_frag[i, jj * 4 + j]
|
||||
|
||||
T.gemm(
|
||||
x_frag,
|
||||
fn_smem,
|
||||
out_frag,
|
||||
transpose_A=False,
|
||||
transpose_B=True,
|
||||
wg_wait=0,
|
||||
clear_accum=False,
|
||||
)
|
||||
sqrsum_l = T.alloc_fragment(token_block, T.float32)
|
||||
T.reduce_sum(sqrsum_part, sqrsum_l)
|
||||
for i in T.Parallel(token_block):
|
||||
sqrsum[px * token_block + i] = sqrsum_l[i]
|
||||
for i, j in T.Parallel(token_block, 32):
|
||||
if j < hc_mult3:
|
||||
out[px * token_block + i, j] = out_frag[i, j]
|
||||
if ENABLE_PDL:
|
||||
T.pdl_trigger()
|
||||
|
||||
|
||||
@functools.cache
|
||||
def mhc_pre_gemm_sqrsum_splitk_kernel(
|
||||
hc_mult3: int,
|
||||
hc_hidden_size: int,
|
||||
split_k: int,
|
||||
token_block: int = 32,
|
||||
hidden_block: int = 256,
|
||||
threads: int = 128,
|
||||
) -> Tuple[tilelang.JITKernel, tilelang.JITKernel]:
|
||||
assert hc_mult3 <= 32
|
||||
assert hc_hidden_size % hidden_block == 0
|
||||
assert hc_hidden_size % split_k == 0
|
||||
split_size = hc_hidden_size // split_k
|
||||
assert split_size % hidden_block == 0
|
||||
|
||||
num_tokens = T.dynamic("num_tokens")
|
||||
|
||||
ENABLE_PDL = is_arch_support_pdl()
|
||||
|
||||
@tilelang.jit
|
||||
def mhc_pre_gemm_sqrsum_splitk_stage_0(
|
||||
x: T.Tensor[(num_tokens, hc_hidden_size), T.bfloat16],
|
||||
fn: T.Tensor[(hc_mult3, hc_hidden_size), T.float32],
|
||||
out_partial: T.Tensor[(split_k, num_tokens, 32), T.float32],
|
||||
sqrsum_partial: T.Tensor[(split_k, num_tokens), T.float32],
|
||||
):
|
||||
with T.Kernel(T.ceildiv(num_tokens, token_block), split_k, threads=threads) as (
|
||||
px,
|
||||
bz,
|
||||
):
|
||||
out_frag = T.alloc_fragment((token_block, 32), T.float32)
|
||||
sq_part4 = T.alloc_fragment((token_block, 4), T.float32)
|
||||
T.clear(out_frag)
|
||||
T.clear(sq_part4)
|
||||
|
||||
k_base = bz * split_size
|
||||
|
||||
if ENABLE_PDL:
|
||||
T.pdl_sync()
|
||||
|
||||
for pz in T.Pipelined(split_size // hidden_block, num_stages=2):
|
||||
x_smem = T.alloc_shared((token_block, hidden_block), T.bfloat16)
|
||||
fn_smem = T.alloc_shared((32, hidden_block), T.float32)
|
||||
|
||||
T.annotate_layout(
|
||||
{x_smem: tilelang.layout.make_swizzled_layout(x_smem)}
|
||||
)
|
||||
|
||||
T.copy(x[px * token_block, k_base + pz * hidden_block], x_smem)
|
||||
T.copy(fn[0, k_base + pz * hidden_block], fn_smem)
|
||||
|
||||
x_f16 = T.alloc_fragment((token_block, hidden_block), T.bfloat16)
|
||||
T.copy(x_smem, x_f16)
|
||||
x_f = T.alloc_fragment((token_block, hidden_block), T.float32)
|
||||
T.copy(x_f16, x_f)
|
||||
|
||||
for jj in T.serial(hidden_block // 4):
|
||||
for i, j in T.Parallel(token_block, 4):
|
||||
v = x_f[i, jj * 4 + j]
|
||||
sq_part4[i, j] += v * v
|
||||
|
||||
T.gemm(
|
||||
x_f,
|
||||
fn_smem,
|
||||
out_frag,
|
||||
transpose_A=False,
|
||||
transpose_B=True,
|
||||
wg_wait=0,
|
||||
clear_accum=False,
|
||||
)
|
||||
|
||||
sq_l = T.alloc_fragment((token_block,), T.float32)
|
||||
T.reduce_sum(sq_part4, sq_l)
|
||||
|
||||
for i in T.Parallel(token_block):
|
||||
t = px * token_block + i
|
||||
if t < num_tokens:
|
||||
sqrsum_partial[bz, t] = sq_l[i]
|
||||
|
||||
for i, j in T.Parallel(token_block, 32):
|
||||
t = px * token_block + i
|
||||
if t < num_tokens:
|
||||
out_partial[bz, t, j] = out_frag[i, j]
|
||||
|
||||
if ENABLE_PDL:
|
||||
T.pdl_trigger()
|
||||
|
||||
@tilelang.jit
|
||||
def mhc_pre_gemm_sqrsum_splitk_stage_1(
|
||||
out_partial: T.Tensor[(split_k, num_tokens, 32), T.float32],
|
||||
sqrsum_partial: T.Tensor[(split_k, num_tokens), T.float32],
|
||||
out: T.Tensor[(num_tokens, hc_mult3), T.float32],
|
||||
sqrsum: T.Tensor[(num_tokens,), T.float32],
|
||||
):
|
||||
warps_per_cta = threads // 32
|
||||
num_reduce = T.ceildiv(split_k, 32)
|
||||
with T.Kernel(T.ceildiv(num_tokens, warps_per_cta), threads=threads) as (px,):
|
||||
tx = T.get_thread_binding()
|
||||
warp = tx // 32
|
||||
lane = tx % 32
|
||||
t = px * warps_per_cta + warp
|
||||
s = T.alloc_local((1,), T.float32)
|
||||
acc = T.alloc_local((1,), T.float32)
|
||||
s[0] = 0
|
||||
acc[0] = 0
|
||||
if ENABLE_PDL:
|
||||
T.pdl_sync()
|
||||
|
||||
if t < num_tokens:
|
||||
for r in T.serial(num_reduce):
|
||||
bz = r * 32 + lane
|
||||
s[0] += T.if_then_else(bz < split_k, sqrsum_partial[bz, t], 0.0)
|
||||
sqrsum[t] = T.warp_reduce_sum(s[0])
|
||||
if lane < hc_mult3:
|
||||
for bz in T.serial(split_k):
|
||||
acc[0] += out_partial[bz, t, lane]
|
||||
out[t, lane] = acc[0]
|
||||
|
||||
if ENABLE_PDL:
|
||||
T.pdl_trigger()
|
||||
|
||||
return (
|
||||
mhc_pre_gemm_sqrsum_splitk_stage_0,
|
||||
mhc_pre_gemm_sqrsum_splitk_stage_1,
|
||||
)
|
||||
|
||||
|
||||
def mhc_pre(
|
||||
residual: torch.Tensor,
|
||||
fn: torch.Tensor,
|
||||
hc_scale: torch.Tensor,
|
||||
hc_base: torch.Tensor,
|
||||
rms_eps: float,
|
||||
hc_pre_eps: float,
|
||||
hc_sinkhorn_eps: float,
|
||||
hc_post_mult_value: float,
|
||||
sinkhorn_repeat: int,
|
||||
n_splits: int = 1,
|
||||
n_splits_pre: int = 32,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
|
||||
assert residual.dtype == torch.bfloat16
|
||||
assert fn.dtype == torch.float32
|
||||
assert hc_scale.dtype == torch.float32
|
||||
assert hc_base.dtype == torch.float32
|
||||
|
||||
hc_mult = residual.shape[-2]
|
||||
hidden_size = residual.shape[-1]
|
||||
hc_mult2 = hc_mult * hc_mult
|
||||
hc_mult3 = hc_mult * 2 + hc_mult2
|
||||
|
||||
hc_hidden_size = hc_mult * hidden_size
|
||||
assert fn.shape[0] == hc_mult3
|
||||
assert fn.shape[1] == hc_hidden_size
|
||||
assert hc_scale.shape == (3,)
|
||||
assert hc_base.shape == (hc_mult3,)
|
||||
|
||||
outer_shape = residual.shape[:-2]
|
||||
|
||||
residual_flat = residual.view(-1, hc_mult, hidden_size)
|
||||
num_tokens = residual_flat.shape[0]
|
||||
fn_flat = fn
|
||||
|
||||
post_mix = torch.empty(
|
||||
num_tokens, hc_mult, dtype=torch.float32, device=residual.device
|
||||
)
|
||||
comb_mix = torch.empty(
|
||||
num_tokens, hc_mult2, dtype=torch.float32, device=residual.device
|
||||
)
|
||||
layer_input = torch.empty(
|
||||
num_tokens, hidden_size, dtype=torch.bfloat16, device=residual.device
|
||||
)
|
||||
|
||||
gemm_out_mul = torch.empty(
|
||||
n_splits, num_tokens, hc_mult3, dtype=torch.float32, device=residual.device
|
||||
)
|
||||
gemm_out_sqrsum = torch.empty(
|
||||
n_splits, num_tokens, dtype=torch.float32, device=residual.device
|
||||
)
|
||||
|
||||
if num_tokens <= 2048:
|
||||
assert n_splits == 1
|
||||
if hc_hidden_size == 16384:
|
||||
hidden_block = 256
|
||||
elif hc_hidden_size == 28672:
|
||||
hidden_block = 128
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"mhc_pre splitk kernel only supports hc_hidden_size in {{16384, 28672}}, "
|
||||
f"got {hc_hidden_size}"
|
||||
)
|
||||
kernel_0, kernel_1 = mhc_pre_gemm_sqrsum_splitk_kernel(
|
||||
hc_mult3,
|
||||
hc_hidden_size,
|
||||
split_k=n_splits_pre,
|
||||
token_block=32,
|
||||
hidden_block=hidden_block,
|
||||
)
|
||||
partial_out = gemm_out_mul.new_empty(n_splits_pre, num_tokens, 32)
|
||||
partial_sqrsum = gemm_out_sqrsum.new_empty(n_splits_pre, num_tokens)
|
||||
kernel_0(
|
||||
residual_flat.view(num_tokens, hc_hidden_size),
|
||||
fn_flat,
|
||||
partial_out,
|
||||
partial_sqrsum,
|
||||
)
|
||||
kernel_1(
|
||||
partial_out,
|
||||
partial_sqrsum,
|
||||
gemm_out_mul.squeeze(0),
|
||||
gemm_out_sqrsum.squeeze(0),
|
||||
)
|
||||
del partial_out, partial_sqrsum
|
||||
else:
|
||||
assert (
|
||||
n_splits == 1
|
||||
), "The simple TileLang version gemm_sqrsum doesn't support split-k"
|
||||
mhc_pre_gemm_sqrsum_tilelang(
|
||||
residual_flat.view(num_tokens, hc_mult * hidden_size),
|
||||
fn_flat,
|
||||
gemm_out_mul.squeeze(0),
|
||||
gemm_out_sqrsum.squeeze(0),
|
||||
hc_mult3,
|
||||
hc_mult * hidden_size,
|
||||
)
|
||||
|
||||
mhc_pre_big_fuse_tilelang(
|
||||
gemm_out_mul,
|
||||
gemm_out_sqrsum,
|
||||
hc_scale,
|
||||
hc_base,
|
||||
residual_flat,
|
||||
post_mix,
|
||||
comb_mix,
|
||||
layer_input,
|
||||
hidden_size,
|
||||
rms_eps,
|
||||
hc_pre_eps,
|
||||
hc_sinkhorn_eps,
|
||||
hc_post_mult_value,
|
||||
sinkhorn_repeat,
|
||||
n_splits,
|
||||
hc_mult,
|
||||
)
|
||||
|
||||
post_mix = post_mix.view(*outer_shape, hc_mult, 1)
|
||||
comb_mix = comb_mix.view(*outer_shape, hc_mult, hc_mult)
|
||||
layer_input = layer_input.view(*outer_shape, hidden_size)
|
||||
|
||||
return post_mix, comb_mix, layer_input
|
||||
|
||||
|
||||
@tilelang.jit(
|
||||
pass_configs={
|
||||
tilelang.PassConfigKey.TL_DISABLE_WARP_SPECIALIZED: True,
|
||||
tilelang.PassConfigKey.TL_DISABLE_TMA_LOWER: True,
|
||||
tilelang.PassConfigKey.TL_PTXAS_REGISTER_USAGE_LEVEL: 10,
|
||||
},
|
||||
)
|
||||
def mhc_post_tilelang(
|
||||
a, b, c, d, x, hc: int, hidden: int, n_thr: int = 128, h_blk: int = 1024
|
||||
) -> tilelang.JITKernel:
|
||||
n = T.dynamic("num_tokens")
|
||||
h = hidden
|
||||
|
||||
h_blk = math.gcd(hidden, h_blk)
|
||||
a: T.Tensor((n, hc, hc), T.float32)
|
||||
b: T.Tensor((n, hc, h), T.bfloat16)
|
||||
c: T.Tensor((n, hc), T.float32)
|
||||
d: T.Tensor((n, h), T.bfloat16)
|
||||
x: T.Tensor((n, hc, h), T.bfloat16)
|
||||
|
||||
ENABLE_PDL = is_arch_support_pdl()
|
||||
with T.Kernel(n, threads=n_thr) as i_n:
|
||||
if ENABLE_PDL:
|
||||
T.pdl_sync()
|
||||
|
||||
x_shared = T.alloc_shared((hc, h_blk), T.bfloat16)
|
||||
b_shared = T.alloc_shared((hc, h_blk), T.bfloat16)
|
||||
d_shared = T.alloc_shared(h_blk, T.bfloat16)
|
||||
|
||||
x_local = T.alloc_fragment((hc, h_blk), T.float32)
|
||||
b_local = T.alloc_fragment((hc, h_blk), T.float32)
|
||||
d_local = T.alloc_fragment(h_blk, T.float32)
|
||||
|
||||
a_local = T.alloc_fragment((hc, hc), T.float32)
|
||||
c_local = T.alloc_fragment(hc, T.float32)
|
||||
T.copy(a[i_n, 0, 0], a_local)
|
||||
T.copy(c[i_n, 0], c_local)
|
||||
|
||||
for i0_h in T.Pipelined(T.ceildiv(h, h_blk), num_stages=2):
|
||||
T.copy(b[i_n, 0, i0_h * h_blk], b_shared)
|
||||
T.copy(d[i_n, i0_h * h_blk], d_shared)
|
||||
|
||||
T.copy(b_shared, b_local)
|
||||
T.copy(d_shared, d_local)
|
||||
for i_hco, i1_h in T.Parallel(hc, h_blk):
|
||||
x_local[i_hco, i1_h] = c_local[i_hco] * d_local[i1_h]
|
||||
for i_hci in T.serial(hc):
|
||||
x_local[i_hco, i1_h] += a_local[i_hci, i_hco] * b_local[i_hci, i1_h]
|
||||
T.copy(x_local, x_shared)
|
||||
|
||||
T.copy(x_shared, x[i_n, 0, i0_h * h_blk])
|
||||
|
||||
if ENABLE_PDL:
|
||||
T.pdl_trigger()
|
||||
|
||||
|
||||
def mhc_post(
|
||||
x: torch.Tensor,
|
||||
residual: torch.Tensor,
|
||||
post_layer_mix: torch.Tensor,
|
||||
comb_res_mix: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
if is_nsa_prefill_cp_round_robin_split():
|
||||
x = strict_contiguous(x)
|
||||
residual = strict_contiguous(residual)
|
||||
post_layer_mix = strict_contiguous(post_layer_mix)
|
||||
comb_res_mix = strict_contiguous(comb_res_mix)
|
||||
out = torch.empty_like(residual)
|
||||
mhc_post_tilelang(
|
||||
comb_res_mix,
|
||||
residual,
|
||||
post_layer_mix.squeeze(-1),
|
||||
x,
|
||||
out,
|
||||
residual.shape[-2],
|
||||
residual.shape[-1],
|
||||
)
|
||||
return out
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.srt.utils import is_cuda
|
||||
from sglang.srt.utils.custom_op import register_custom_op
|
||||
@@ -14,9 +15,16 @@ if _is_cuda:
|
||||
from sglang.jit_kernel.moe_wna16_marlin import moe_wna16_marlin_gemm
|
||||
|
||||
|
||||
def get_scalar_type(num_bits: int, has_zp: bool):
|
||||
def get_scalar_type(num_bits: int, has_zp: bool, scales: Optional[torch.Tensor] = None):
|
||||
from sgl_kernel.scalar_type import scalar_types
|
||||
|
||||
if (
|
||||
not has_zp
|
||||
and num_bits == 4
|
||||
and scales is not None
|
||||
and scales.dtype == torch.float8_e8m0fnu
|
||||
):
|
||||
return scalar_types.float4_e2m1f
|
||||
if has_zp:
|
||||
assert num_bits == 4
|
||||
return scalar_types.uint4
|
||||
@@ -24,6 +32,22 @@ def get_scalar_type(num_bits: int, has_zp: bool):
|
||||
return scalar_types.uint4b8 if num_bits == 4 else scalar_types.uint8b128
|
||||
|
||||
|
||||
def swiglu_limit_func(
|
||||
output: torch.Tensor,
|
||||
input: torch.Tensor, # first half is gate, second half is up
|
||||
swiglu_limit: float = 0.0,
|
||||
) -> None:
|
||||
d = input.shape[1] // 2
|
||||
gate = input[:, :d]
|
||||
up = input[:, d:]
|
||||
|
||||
if swiglu_limit > 0:
|
||||
gate = torch.clamp(gate, max=swiglu_limit)
|
||||
up = torch.clamp(up, min=-swiglu_limit, max=swiglu_limit)
|
||||
|
||||
output.copy_(F.silu(gate) * up)
|
||||
|
||||
|
||||
@register_custom_op(out_shape="hidden_states")
|
||||
def fused_marlin_moe(
|
||||
hidden_states: torch.Tensor,
|
||||
@@ -47,6 +71,7 @@ def fused_marlin_moe(
|
||||
is_k_full: bool = True,
|
||||
inplace: bool = False,
|
||||
routed_scaling_factor: Optional[float] = None,
|
||||
clamp_limit: Optional[float] = None,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
This function computes a Mixture of Experts (MoE) layer using two sets of
|
||||
@@ -86,12 +111,29 @@ def fused_marlin_moe(
|
||||
assert w1.is_contiguous(), "Expert weights1 must be contiguous"
|
||||
assert w2.is_contiguous(), "Expert weights2 must be contiguous"
|
||||
assert hidden_states.dtype in [torch.float16, torch.bfloat16]
|
||||
assert (
|
||||
hidden_states.dtype == w1_scale.dtype
|
||||
), f"moe_wna16_marlin_gemm assumes hidden_states.dtype ({hidden_states.dtype}) == w1_scale.dtype ({w1_scale.dtype})"
|
||||
assert (
|
||||
hidden_states.dtype == w2_scale.dtype
|
||||
), f"moe_wna16_marlin_gemm assumes hidden_states.dtype ({hidden_states.dtype}) == w2_scale.dtype ({w2_scale.dtype})"
|
||||
is_mxfp4_marlin = (
|
||||
num_bits == 4
|
||||
and w1_zeros is None
|
||||
and w2_zeros is None
|
||||
and w1_scale.dtype == torch.float8_e8m0fnu
|
||||
and w2_scale.dtype == torch.float8_e8m0fnu
|
||||
)
|
||||
if is_mxfp4_marlin:
|
||||
assert w1_scale.dtype == torch.float8_e8m0fnu, (
|
||||
"MXFP4 Marlin expects w1_scale to be torch.float8_e8m0fnu, "
|
||||
f"got {w1_scale.dtype}"
|
||||
)
|
||||
assert w2_scale.dtype == torch.float8_e8m0fnu, (
|
||||
"MXFP4 Marlin expects w2_scale to be torch.float8_e8m0fnu, "
|
||||
f"got {w2_scale.dtype}"
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
hidden_states.dtype == w1_scale.dtype
|
||||
), f"moe_wna16_marlin_gemm assumes hidden_states.dtype ({hidden_states.dtype}) == w1_scale.dtype ({w1_scale.dtype})"
|
||||
assert (
|
||||
hidden_states.dtype == w2_scale.dtype
|
||||
), f"moe_wna16_marlin_gemm assumes hidden_states.dtype ({hidden_states.dtype}) == w2_scale.dtype ({w2_scale.dtype})"
|
||||
assert num_bits in [4, 8]
|
||||
|
||||
M, K = hidden_states.shape
|
||||
@@ -122,8 +164,8 @@ def fused_marlin_moe(
|
||||
max_workspace_size, dtype=torch.int, device=device, requires_grad=False
|
||||
)
|
||||
|
||||
scalar_type1 = get_scalar_type(num_bits, w1_zeros is not None)
|
||||
scalar_type2 = get_scalar_type(num_bits, w2_zeros is not None)
|
||||
scalar_type1 = get_scalar_type(num_bits, w1_zeros is not None, w1_scale)
|
||||
scalar_type2 = get_scalar_type(num_bits, w2_zeros is not None, w2_scale)
|
||||
|
||||
intermediate_cache2 = torch.empty(
|
||||
(M * topk_ids.shape[1], N),
|
||||
@@ -143,7 +185,7 @@ def fused_marlin_moe(
|
||||
use_atomic_add = (
|
||||
hidden_states.dtype == torch.half
|
||||
or torch.cuda.get_device_capability(hidden_states.device)[0] >= 9
|
||||
)
|
||||
) and (not is_mxfp4_marlin)
|
||||
|
||||
intermediate_cache1 = moe_wna16_marlin_gemm(
|
||||
hidden_states,
|
||||
@@ -174,7 +216,14 @@ def fused_marlin_moe(
|
||||
is_zp_float=False,
|
||||
)
|
||||
|
||||
silu_and_mul(intermediate_cache1.view(-1, 2 * N), intermediate_cache2)
|
||||
if clamp_limit is not None:
|
||||
swiglu_limit_func(
|
||||
intermediate_cache2,
|
||||
intermediate_cache1.view(-1, 2 * N),
|
||||
clamp_limit,
|
||||
)
|
||||
else:
|
||||
silu_and_mul(intermediate_cache1.view(-1, 2 * N), intermediate_cache2)
|
||||
|
||||
if expert_map is not None:
|
||||
intermediate_cache3.zero_()
|
||||
@@ -210,12 +259,15 @@ def fused_marlin_moe(
|
||||
|
||||
output = hidden_states if inplace else torch.empty_like(hidden_states)
|
||||
|
||||
if routed_scaling_factor is None:
|
||||
routed_scaling_factor = 1.0
|
||||
if is_mxfp4_marlin:
|
||||
return torch.sum(intermediate_cache3, dim=1, out=output)
|
||||
else:
|
||||
if routed_scaling_factor is None:
|
||||
routed_scaling_factor = 1.0
|
||||
|
||||
moe_sum_reduce(
|
||||
intermediate_cache3,
|
||||
output,
|
||||
routed_scaling_factor,
|
||||
)
|
||||
return output
|
||||
moe_sum_reduce(
|
||||
intermediate_cache3,
|
||||
output,
|
||||
routed_scaling_factor,
|
||||
)
|
||||
return output
|
||||
|
||||
@@ -177,6 +177,7 @@ class FusedMoE(torch.nn.Module):
|
||||
routed_scaling_factor: Optional[float] = None,
|
||||
gemm1_alpha: Optional[float] = None,
|
||||
gemm1_clamp_limit: Optional[float] = None,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
use_weight_loader_fused: bool = False,
|
||||
with_bias=False,
|
||||
routing_method_type: Optional[RoutingMethodType] = None,
|
||||
@@ -262,6 +263,7 @@ class FusedMoE(torch.nn.Module):
|
||||
routed_scaling_factor=routed_scaling_factor,
|
||||
gemm1_alpha=gemm1_alpha,
|
||||
gemm1_clamp_limit=gemm1_clamp_limit,
|
||||
swiglu_limit=swiglu_limit,
|
||||
is_gated=is_gated,
|
||||
routing_method_type=routing_method_type,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.eplb.expert_location_dispatch import (
|
||||
ExpertLocationDispatchInfo,
|
||||
topk_ids_logical_to_physical,
|
||||
)
|
||||
from sglang.srt.layers.moe.topk import (
|
||||
StandardTopKOutput,
|
||||
_mask_topk_ids_padded_region,
|
||||
)
|
||||
from sglang.srt.utils import is_hip
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HashTopK(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
topk,
|
||||
num_experts,
|
||||
num_fused_shared_experts,
|
||||
vocab_size,
|
||||
scoring_func="sqrtsoftplus",
|
||||
routed_scaling_factor=1.5,
|
||||
apply_routed_scaling_factor_on_output=False,
|
||||
):
|
||||
super().__init__()
|
||||
self.num_experts = num_experts
|
||||
self.topk = topk
|
||||
self.routed_scaling_factor = routed_scaling_factor
|
||||
self.num_fused_shared_experts = num_fused_shared_experts
|
||||
self.score_func = scoring_func
|
||||
self.tid2eid = nn.Parameter(
|
||||
torch.empty(vocab_size, topk - num_fused_shared_experts, dtype=torch.int32),
|
||||
requires_grad=False,
|
||||
)
|
||||
|
||||
assert not apply_routed_scaling_factor_on_output, "not implemented"
|
||||
|
||||
def empty_topk_output(self, device: torch.device):
|
||||
topk = self.topk - self.num_fused_shared_experts
|
||||
topk_weights = torch.empty((0, topk), dtype=torch.float32, device=device)
|
||||
topk_ids = torch.full((0, topk), -1, dtype=torch.int32, device=device)
|
||||
router_logits = torch.empty((0, topk), dtype=torch.float32, device=device)
|
||||
return StandardTopKOutput(topk_weights, topk_ids, router_logits)
|
||||
|
||||
def _forward_torch(
|
||||
self, router_logits: torch.Tensor, input_ids: torch.Tensor
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
if self.score_func == "softmax":
|
||||
scores = router_logits.softmax(dim=-1)
|
||||
elif self.score_func == "sigmoid":
|
||||
scores = router_logits.sigmoid()
|
||||
else:
|
||||
scores = torch.nn.functional.softplus(router_logits).sqrt()
|
||||
|
||||
num_token = scores.shape[0]
|
||||
|
||||
topk_ids = torch.zeros(
|
||||
(num_token, self.topk), dtype=torch.int32, device=scores.device
|
||||
)
|
||||
topk_weights = torch.zeros(
|
||||
(num_token, self.topk), dtype=scores.dtype, device=scores.device
|
||||
)
|
||||
|
||||
if self.num_fused_shared_experts == 1:
|
||||
topk_ids[:, :-1] = self.tid2eid[input_ids]
|
||||
topk_weights[:, :-1] = scores.gather(1, topk_ids[:, :-1])
|
||||
|
||||
if self.score_func != "softmax":
|
||||
topk_weights[:, :-1] /= topk_weights[:, :-1].sum(dim=-1, keepdim=True)
|
||||
|
||||
topk_ids[:, -1] = torch.randint(
|
||||
low=self.num_experts,
|
||||
high=self.num_experts + self.num_fused_shared_experts,
|
||||
size=(num_token,),
|
||||
dtype=topk_ids.dtype,
|
||||
device=topk_ids.device,
|
||||
)
|
||||
|
||||
topk_weights[:, -1] = (
|
||||
topk_weights[:, :-1].sum(dim=-1) / self.routed_scaling_factor
|
||||
)
|
||||
else:
|
||||
topk_ids[:, :] = self.tid2eid[input_ids]
|
||||
topk_weights[:, :] = scores.gather(1, topk_ids[:, :])
|
||||
if self.score_func != "softmax":
|
||||
topk_weights[:, :] /= topk_weights[:, :].sum(dim=-1, keepdim=True)
|
||||
|
||||
return topk_weights, topk_ids
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
router_logits: torch.Tensor,
|
||||
input_ids: torch.Tensor,
|
||||
num_token_non_padded: Optional[torch.Tensor] = None,
|
||||
expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo] = None,
|
||||
):
|
||||
assert (
|
||||
input_ids.shape[0] == hidden_states.shape[0] == router_logits.shape[0]
|
||||
), f"{input_ids.shape=} {hidden_states.shape=} {router_logits.shape=}"
|
||||
|
||||
if envs.SGLANG_OPT_USE_FUSED_HASH_TOPK.get():
|
||||
from sglang.jit_kernel.deepseek_v4 import hash_topk
|
||||
|
||||
topk_weights, topk_ids = hash_topk(
|
||||
router_logits=router_logits,
|
||||
input_ids=input_ids,
|
||||
tid2eid=self.tid2eid,
|
||||
num_fused_shared_experts=self.num_fused_shared_experts,
|
||||
routed_scaling_factor=self.routed_scaling_factor,
|
||||
scoring_func=self.score_func,
|
||||
)
|
||||
else:
|
||||
topk_weights, topk_ids = self._forward_torch(router_logits, input_ids)
|
||||
|
||||
if is_hip():
|
||||
topk_weights = topk_weights.to(torch.float32)
|
||||
|
||||
topk_ids = topk_ids_logical_to_physical(topk_ids, expert_location_dispatch_info)
|
||||
_mask_topk_ids_padded_region(topk_ids, num_token_non_padded)
|
||||
topk_output = StandardTopKOutput(
|
||||
topk_weights=topk_weights, topk_ids=topk_ids, router_logits=router_logits
|
||||
)
|
||||
return topk_output
|
||||
@@ -0,0 +1,289 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Mega-MoE forward path and expert-weight prep shared by Deepseek V2/V4."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import nullcontext
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.deepseek_v4 import mega_moe_pre_dispatch
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.eplb.expert_location_dispatch import ExpertLocationDispatchInfo
|
||||
from sglang.srt.layers.dp_attention import get_dp_global_num_tokens
|
||||
from sglang.srt.model_executor.cuda_graph_runner import get_is_capture_mode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from deep_gemm import SymmBuffer
|
||||
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
from sglang.srt.models.deepseek_v2 import DeepseekV2MoE
|
||||
|
||||
|
||||
_MEGA_MOE_SYMM_BUFFER: dict = {}
|
||||
|
||||
|
||||
def _get_mega_moe_symm_buffer(
|
||||
group,
|
||||
num_experts: int,
|
||||
num_max_tokens_per_rank: int,
|
||||
num_topk: int,
|
||||
hidden: int,
|
||||
intermediate_hidden: int,
|
||||
) -> SymmBuffer:
|
||||
import deep_gemm
|
||||
|
||||
key = (
|
||||
id(group),
|
||||
num_max_tokens_per_rank,
|
||||
num_experts,
|
||||
num_topk,
|
||||
hidden,
|
||||
intermediate_hidden,
|
||||
)
|
||||
buf = _MEGA_MOE_SYMM_BUFFER.get(key)
|
||||
if buf is None:
|
||||
buf = deep_gemm.get_symm_buffer_for_mega_moe(
|
||||
group,
|
||||
num_experts,
|
||||
num_max_tokens_per_rank,
|
||||
num_topk,
|
||||
hidden,
|
||||
intermediate_hidden,
|
||||
use_fp8_dispatch=True,
|
||||
activation="swiglu",
|
||||
)
|
||||
_MEGA_MOE_SYMM_BUFFER[key] = buf
|
||||
return buf
|
||||
|
||||
|
||||
def should_use_mega_moe(moe: "DeepseekV2MoE", hidden_states: torch.Tensor) -> bool:
|
||||
if not envs.SGLANG_OPT_USE_DEEPGEMM_MEGA_MOE.get():
|
||||
return False
|
||||
if not getattr(moe.experts, "_mega_moe_weights_built", False):
|
||||
return False
|
||||
if get_is_capture_mode():
|
||||
return True
|
||||
|
||||
global_num_tokens = get_dp_global_num_tokens()
|
||||
if global_num_tokens:
|
||||
max_tokens_per_rank = max(global_num_tokens)
|
||||
else:
|
||||
max_tokens_per_rank = hidden_states.shape[0]
|
||||
cap = envs.SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK.get()
|
||||
return max_tokens_per_rank <= cap
|
||||
|
||||
|
||||
def forward_mega_moe(
|
||||
moe: "DeepseekV2MoE",
|
||||
hidden_states: torch.Tensor,
|
||||
forward_batch: Optional["ForwardBatch"] = None,
|
||||
input_ids_global: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
num_tokens = hidden_states.shape[0]
|
||||
|
||||
sbo_overlap_flag = (
|
||||
moe.alt_stream is not None
|
||||
and moe.num_fused_shared_experts == 0
|
||||
and num_tokens > 0
|
||||
and get_is_capture_mode()
|
||||
)
|
||||
|
||||
if sbo_overlap_flag:
|
||||
current_stream = torch.cuda.current_stream()
|
||||
moe.alt_stream.wait_stream(current_stream)
|
||||
shared_output = moe._forward_shared_experts(hidden_states)
|
||||
mega_stream_ctx = torch.cuda.stream(moe.alt_stream)
|
||||
else:
|
||||
shared_output = moe._forward_shared_experts(hidden_states)
|
||||
mega_stream_ctx = nullcontext()
|
||||
|
||||
with mega_stream_ctx:
|
||||
y = _run_mega_routed(
|
||||
moe, hidden_states, forward_batch, input_ids_global, num_tokens
|
||||
)
|
||||
|
||||
if sbo_overlap_flag:
|
||||
current_stream.wait_stream(moe.alt_stream)
|
||||
|
||||
if shared_output is not None:
|
||||
y.add_(shared_output)
|
||||
return y
|
||||
|
||||
|
||||
def _run_mega_routed(
|
||||
moe: "DeepseekV2MoE",
|
||||
hidden_states: torch.Tensor,
|
||||
forward_batch: Optional["ForwardBatch"],
|
||||
input_ids_global: Optional[torch.Tensor],
|
||||
num_tokens: int,
|
||||
) -> torch.Tensor:
|
||||
import deep_gemm
|
||||
|
||||
from sglang.srt.distributed.parallel_state import get_moe_ep_group
|
||||
|
||||
hidden_size = moe.config.hidden_size
|
||||
|
||||
if num_tokens > 0:
|
||||
router_logits = moe.gate(hidden_states, forward_batch=forward_batch)
|
||||
topk_kwargs = {"input_ids": input_ids_global} if moe.is_hash else {}
|
||||
topk_output = moe.topk(
|
||||
hidden_states,
|
||||
router_logits,
|
||||
num_token_non_padded=(
|
||||
forward_batch.num_token_non_padded
|
||||
if forward_batch is not None
|
||||
else None
|
||||
),
|
||||
expert_location_dispatch_info=ExpertLocationDispatchInfo.init_new(
|
||||
layer_id=moe.layer_id,
|
||||
),
|
||||
**topk_kwargs,
|
||||
)
|
||||
topk_ids = topk_output.topk_ids
|
||||
topk_weights = topk_output.topk_weights
|
||||
else:
|
||||
topk_ids = None
|
||||
topk_weights = None
|
||||
|
||||
ep_group = get_moe_ep_group().device_group
|
||||
num_experts = moe.experts.num_experts
|
||||
top_k = moe.config.num_experts_per_tok + moe.num_fused_shared_experts
|
||||
intermediate_size = moe.config.moe_intermediate_size
|
||||
num_max_tokens_per_rank = (
|
||||
envs.SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK.get()
|
||||
)
|
||||
assert num_tokens <= num_max_tokens_per_rank, (
|
||||
f"mega MoE: num_tokens={num_tokens} exceeds cap "
|
||||
f"SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK="
|
||||
f"{num_max_tokens_per_rank}; raise the env var or shrink "
|
||||
f"cuda_graph_max_bs / chunked_prefill_size accordingly"
|
||||
)
|
||||
|
||||
buf = _get_mega_moe_symm_buffer(
|
||||
ep_group,
|
||||
num_experts=num_experts,
|
||||
num_max_tokens_per_rank=num_max_tokens_per_rank,
|
||||
num_topk=top_k,
|
||||
hidden=hidden_size,
|
||||
intermediate_hidden=intermediate_size,
|
||||
)
|
||||
|
||||
if num_tokens > 0:
|
||||
topk_ids_in = topk_ids
|
||||
topk_weights_in = topk_weights
|
||||
else:
|
||||
topk_ids_in = hidden_states.new_empty((0, top_k), dtype=torch.int32)
|
||||
topk_weights_in = hidden_states.new_empty((0, top_k), dtype=torch.float32)
|
||||
mega_moe_pre_dispatch(
|
||||
hidden_states,
|
||||
topk_ids_in,
|
||||
topk_weights_in,
|
||||
buf.x,
|
||||
buf.x_sf,
|
||||
buf.topk_idx,
|
||||
buf.topk_weights,
|
||||
quant_group_size=32,
|
||||
)
|
||||
|
||||
# Allocate at least one row so y has a non-null CUDA data_ptr;
|
||||
# the DeepGEMM tvm-ffi binding rejects nullptr in convert_to_torch_tensor().
|
||||
y = torch.empty(
|
||||
(max(num_tokens, 1), hidden_size),
|
||||
dtype=torch.bfloat16,
|
||||
device=hidden_states.device,
|
||||
)
|
||||
swiglu_limit = getattr(moe.config, "swiglu_limit", None)
|
||||
deep_gemm.fp8_fp4_mega_moe(
|
||||
y,
|
||||
moe.experts.mega_l1_weights,
|
||||
moe.experts.mega_l2_weights,
|
||||
buf,
|
||||
recipe=(1, 1, 32),
|
||||
activation="swiglu",
|
||||
activation_clamp=swiglu_limit,
|
||||
fast_math=True,
|
||||
)
|
||||
y = y[:num_tokens]
|
||||
|
||||
if not moe.experts.should_fuse_routed_scaling_factor_in_topk:
|
||||
y.mul_(moe.routed_scaling_factor)
|
||||
return y
|
||||
|
||||
|
||||
def build_mega_moe_experts_weights(experts) -> None:
|
||||
from deep_gemm import (
|
||||
transform_sf_into_required_layout,
|
||||
transform_weights_for_mega_moe,
|
||||
)
|
||||
from deep_gemm.mega import _interleave_l1_weights, _transpose_sf_for_utccp
|
||||
|
||||
if getattr(experts, "_mega_moe_weights_built", False):
|
||||
return
|
||||
|
||||
w13 = experts.w13_weight.data
|
||||
w13_sf_fp32 = experts.w13_weight_scale_inv.data
|
||||
w2 = experts.w2_weight.data
|
||||
w2_sf_fp32 = experts.w2_weight_scale_inv.data
|
||||
|
||||
num_groups, n1, half_k1 = w13.shape
|
||||
k1 = half_k1 * 2
|
||||
_, n2, half_k2 = w2.shape
|
||||
k2 = half_k2 * 2
|
||||
|
||||
w13_sf = transform_sf_into_required_layout(
|
||||
w13_sf_fp32,
|
||||
mn=n1,
|
||||
k=k1,
|
||||
recipe=(1, 32),
|
||||
num_groups=num_groups,
|
||||
disable_ue8m0_cast=False,
|
||||
)
|
||||
w2_sf = transform_sf_into_required_layout(
|
||||
w2_sf_fp32,
|
||||
mn=n2,
|
||||
k=k2,
|
||||
recipe=(1, 32),
|
||||
num_groups=num_groups,
|
||||
disable_ue8m0_cast=False,
|
||||
)
|
||||
|
||||
if envs.SGLANG_OPT_FIX_MEGA_MOE_MEMORY.get():
|
||||
# Build the interleaved L1 weight + scale once; share the weight buffer
|
||||
# between `w13_weight.data` (normal deep-ep path) and `mega_l1_weights[0]`
|
||||
# (mega moe path). Mega moe additionally needs a UTCCP-transposed scale;
|
||||
# the deep-ep path consumes the non-transposed interleaved scale and a
|
||||
# swizzle-aware activation kernel. L2 weight is untouched by the mega
|
||||
# transform, so the existing `w2_weight.data` is shared directly.
|
||||
w13_interleaved, w13_sf_interleaved = _interleave_l1_weights((w13, w13_sf))
|
||||
w13_sf_utccp = _transpose_sf_for_utccp(w13_sf_interleaved)
|
||||
w2_sf_utccp = _transpose_sf_for_utccp(w2_sf)
|
||||
|
||||
experts.w13_weight.data = w13_interleaved
|
||||
experts.w13_weight_scale_inv.data = w13_sf_interleaved
|
||||
experts.w2_weight_scale_inv.data = w2_sf
|
||||
experts.w13_weight_scale_inv.format_ue8m0 = True
|
||||
experts.w2_weight_scale_inv.format_ue8m0 = True
|
||||
|
||||
experts.mega_l1_weights = (experts.w13_weight.data, w13_sf_utccp)
|
||||
experts.mega_l2_weights = (experts.w2_weight.data, w2_sf_utccp)
|
||||
else:
|
||||
l1_pair, l2_pair = transform_weights_for_mega_moe((w13, w13_sf), (w2, w2_sf))
|
||||
|
||||
experts.mega_l1_weights = l1_pair
|
||||
experts.mega_l2_weights = l2_pair
|
||||
|
||||
experts._mega_moe_weights_built = True
|
||||
@@ -48,6 +48,7 @@ class MoeRunnerConfig:
|
||||
routed_scaling_factor: Optional[float] = None
|
||||
gemm1_alpha: Optional[float] = None
|
||||
gemm1_clamp_limit: Optional[float] = None
|
||||
swiglu_limit: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, List, Optional
|
||||
from typing import TYPE_CHECKING, Any, List, Optional, Tuple
|
||||
|
||||
import einops
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.deepseek_v4 import silu_and_mul_masked_post_quant
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers import deep_gemm_wrapper
|
||||
from sglang.srt.layers.moe.moe_runner.base import (
|
||||
MoeQuantInfo,
|
||||
@@ -45,8 +48,11 @@ _is_cuda = is_cuda()
|
||||
_use_aiter = get_bool_env_var("SGLANG_USE_AITER") and _is_hip
|
||||
_is_musa = is_musa()
|
||||
|
||||
# Imported only for the SGLANG_OPT_FIX_MEGA_MOE_MEMORY=False fallback path.
|
||||
if not (_is_npu or _is_hip) and _is_cuda:
|
||||
from sglang.jit_kernel.activation import silu_and_mul
|
||||
from sglang.jit_kernel.activation import silu_and_mul as _legacy_silu_and_mul
|
||||
else:
|
||||
_legacy_silu_and_mul = None
|
||||
|
||||
|
||||
_MASKED_GEMM_FAST_ACT = get_bool_env_var("SGLANG_MASKED_GEMM_FAST_ACT")
|
||||
@@ -109,6 +115,8 @@ class DeepGemmMoeQuantInfo(MoeQuantInfo):
|
||||
w13_scale: Optional[torch.Tensor] = None
|
||||
w2_scale: Optional[torch.Tensor] = None
|
||||
block_shape: Optional[List[int]] = None
|
||||
# DSV4 mxfp4 layout flag; selects recipe_a=(1,128)/recipe_b=(1,32) downstream.
|
||||
is_fp4_experts: bool = False
|
||||
|
||||
|
||||
class DeepGemmRunnerCore(MoeRunnerCore):
|
||||
@@ -116,6 +124,13 @@ class DeepGemmRunnerCore(MoeRunnerCore):
|
||||
super().__init__(config)
|
||||
assert self.config.activation == "silu"
|
||||
assert self.config.is_gated
|
||||
self.swiglu_limit = self.config.swiglu_limit
|
||||
self.use_swizzle = False
|
||||
if envs.SGLANG_OPT_FIX_MEGA_MOE_MEMORY.get():
|
||||
assert envs.SGLANG_OPT_SWIGLU_CLAMP_FUSION.get()
|
||||
assert envs.SGLANG_OPT_USE_JIT_EP_ACTIVATION.get()
|
||||
assert envs.SGLANG_OPT_USE_DEEPGEMM_MEGA_MOE.get()
|
||||
self.use_swizzle = True
|
||||
|
||||
def run(
|
||||
self,
|
||||
@@ -140,9 +155,10 @@ class DeepGemmRunnerCore(MoeRunnerCore):
|
||||
quant_info: DeepGemmMoeQuantInfo,
|
||||
running_state: dict,
|
||||
) -> torch.Tensor:
|
||||
from sglang.jit_kernel.deepseek_v4 import silu_and_mul_contig_post_quant
|
||||
from sglang.srt.layers.moe.ep_moe.kernels import tma_align_input_scale
|
||||
from sglang.srt.layers.quantization.fp8_kernel import (
|
||||
sglang_per_token_group_quant_fp8,
|
||||
create_per_token_group_quant_fp8_output_scale,
|
||||
)
|
||||
|
||||
hidden_states = runner_input.hidden_states
|
||||
@@ -157,6 +173,10 @@ class DeepGemmRunnerCore(MoeRunnerCore):
|
||||
K = hidden_states_shape[1]
|
||||
scale_block_size = 128
|
||||
|
||||
recipe_a, recipe_b = (
|
||||
((1, 128), (1, 32)) if quant_info.is_fp4_experts else (None, None)
|
||||
)
|
||||
|
||||
w13_weight_fp8 = (
|
||||
quant_info.w13_weight,
|
||||
quant_info.w13_scale,
|
||||
@@ -176,30 +196,69 @@ class DeepGemmRunnerCore(MoeRunnerCore):
|
||||
w13_weight_fp8,
|
||||
gateup_output,
|
||||
m_indices,
|
||||
recipe_a=recipe_a,
|
||||
recipe_b=recipe_b,
|
||||
)
|
||||
|
||||
dispose_tensor(hidden_states)
|
||||
dispose_tensor(hidden_states_scale)
|
||||
|
||||
down_input = torch.empty(
|
||||
(
|
||||
all_tokens,
|
||||
N // 2,
|
||||
),
|
||||
device=gateup_output.device,
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
silu_and_mul(gateup_output.view(-1, N), down_input)
|
||||
del gateup_output
|
||||
if envs.SGLANG_OPT_FIX_MEGA_MOE_MEMORY.get():
|
||||
swiglu_limit_arg: Optional[float] = self.swiglu_limit
|
||||
|
||||
down_input_fp8, down_input_scale = sglang_per_token_group_quant_fp8(
|
||||
down_input,
|
||||
scale_block_size,
|
||||
column_major_scales=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
scale_tma_aligned=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
)
|
||||
del down_input
|
||||
down_input_fp8 = torch.empty(
|
||||
(all_tokens, N // 2),
|
||||
device=gateup_output.device,
|
||||
dtype=torch.float8_e4m3fn,
|
||||
)
|
||||
down_input_scale = create_per_token_group_quant_fp8_output_scale(
|
||||
x_shape=(all_tokens, N // 2),
|
||||
device=gateup_output.device,
|
||||
group_size=scale_block_size,
|
||||
column_major_scales=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
scale_tma_aligned=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
)
|
||||
silu_and_mul_contig_post_quant(
|
||||
input=gateup_output,
|
||||
output=down_input_fp8,
|
||||
output_scale=down_input_scale,
|
||||
quant_group_size=scale_block_size,
|
||||
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
transposed=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
swiglu_limit=swiglu_limit_arg,
|
||||
swizzle=self.use_swizzle,
|
||||
)
|
||||
del gateup_output
|
||||
else:
|
||||
# Hacky byte-equal fallback that reproduces the optimize-branch
|
||||
# code path exactly: bf16 silu_and_mul then a separate per-token
|
||||
# group fp8 quant. Kept behind the mega-moe-memory flag.
|
||||
from sglang.srt.layers.quantization.fp8_kernel import (
|
||||
sglang_per_token_group_quant_fp8,
|
||||
)
|
||||
|
||||
if self.swiglu_limit is not None:
|
||||
gateup_output = _apply_swiglu_limit(
|
||||
gateup_output, swiglu_limit=self.swiglu_limit
|
||||
)
|
||||
|
||||
down_input = torch.empty(
|
||||
(all_tokens, N // 2),
|
||||
device=gateup_output.device,
|
||||
dtype=torch.bfloat16,
|
||||
)
|
||||
_legacy_silu_and_mul(gateup_output.view(-1, N), down_input)
|
||||
del gateup_output
|
||||
|
||||
down_input_fp8, down_input_scale = sglang_per_token_group_quant_fp8(
|
||||
down_input,
|
||||
scale_block_size,
|
||||
column_major_scales=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
scale_tma_aligned=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
)
|
||||
del down_input
|
||||
|
||||
down_output = torch.empty(
|
||||
(all_tokens, K),
|
||||
@@ -214,6 +273,8 @@ class DeepGemmRunnerCore(MoeRunnerCore):
|
||||
w2_weight_fp8,
|
||||
down_output,
|
||||
m_indices,
|
||||
recipe_a=recipe_a,
|
||||
recipe_b=recipe_b,
|
||||
)
|
||||
|
||||
return down_output
|
||||
@@ -225,12 +286,6 @@ class DeepGemmRunnerCore(MoeRunnerCore):
|
||||
running_state: dict,
|
||||
) -> torch.Tensor:
|
||||
from sglang.srt.layers import deep_gemm_wrapper
|
||||
from sglang.srt.layers.moe.ep_moe.kernels import (
|
||||
silu_and_mul_masked_post_quant_fwd,
|
||||
)
|
||||
from sglang.srt.layers.quantization.fp8_kernel import (
|
||||
sglang_per_token_group_quant_8bit,
|
||||
)
|
||||
|
||||
hidden_states = runner_input.hidden_states
|
||||
hidden_states_scale = runner_input.hidden_states_scale
|
||||
@@ -242,6 +297,10 @@ class DeepGemmRunnerCore(MoeRunnerCore):
|
||||
w13_scale = quant_info.w13_scale
|
||||
w2_scale = quant_info.w2_scale
|
||||
|
||||
recipe_a, recipe_b = (
|
||||
((1, 128), (1, 32)) if quant_info.is_fp4_experts else (None, None)
|
||||
)
|
||||
|
||||
hidden_states_device = running_state["hidden_states_device"]
|
||||
|
||||
# GroupGemm-0
|
||||
@@ -270,51 +329,45 @@ class DeepGemmRunnerCore(MoeRunnerCore):
|
||||
gateup_output,
|
||||
masked_m,
|
||||
expected_m,
|
||||
recipe_a=recipe_a,
|
||||
recipe_b=recipe_b,
|
||||
)
|
||||
dispose_tensor(hidden_states)
|
||||
dispose_tensor(hidden_states_scale)
|
||||
|
||||
swiglu_limit_arg: Optional[float] = None
|
||||
if self.swiglu_limit is not None:
|
||||
# DeepSeek V4: clamped swiglu requires JIT EP activation; the
|
||||
# FAST_ACT fused-quant path doesn't carry a swiglu_limit arg.
|
||||
assert (
|
||||
not _MASKED_GEMM_FAST_ACT
|
||||
), "DeepSeek V4 does not support SGLANG_MASKED_GEMM_FAST_ACT"
|
||||
assert (
|
||||
envs.SGLANG_OPT_USE_JIT_EP_ACTIVATION.get()
|
||||
), "DeepSeek V4 requires SGLANG_OPT_USE_JIT_EP_ACTIVATION=True"
|
||||
|
||||
if envs.SGLANG_OPT_SWIGLU_CLAMP_FUSION.get():
|
||||
swiglu_limit_arg = self.swiglu_limit
|
||||
else:
|
||||
gateup_output = einops.rearrange(
|
||||
gateup_output, "grp tok hidden -> (grp tok) hidden"
|
||||
)
|
||||
gateup_output = _apply_swiglu_limit(
|
||||
gateup_output, swiglu_limit=self.swiglu_limit
|
||||
)
|
||||
gateup_output = einops.rearrange(
|
||||
gateup_output, "(grp tok) hidden -> grp tok hidden", grp=num_groups
|
||||
)
|
||||
|
||||
# Act
|
||||
scale_block_size = 128
|
||||
if _MASKED_GEMM_FAST_ACT:
|
||||
down_input, down_input_scale = sglang_per_token_group_quant_8bit(
|
||||
x=gateup_output,
|
||||
dst_dtype=torch.float8_e4m3fn,
|
||||
group_size=scale_block_size,
|
||||
masked_m=masked_m,
|
||||
column_major_scales=True,
|
||||
scale_tma_aligned=True,
|
||||
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
fuse_silu_and_mul=True,
|
||||
enable_v2=True,
|
||||
)
|
||||
else:
|
||||
down_input = torch.empty(
|
||||
(
|
||||
gateup_output.shape[0],
|
||||
gateup_output.shape[1],
|
||||
gateup_output.shape[2] // 2,
|
||||
),
|
||||
device=hidden_states_device,
|
||||
dtype=torch.float8_e4m3fn,
|
||||
)
|
||||
down_input_scale = torch.empty(
|
||||
(
|
||||
gateup_output.shape[0],
|
||||
gateup_output.shape[1],
|
||||
gateup_output.shape[2] // 2 // scale_block_size,
|
||||
),
|
||||
device=hidden_states_device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
silu_and_mul_masked_post_quant_fwd(
|
||||
gateup_output,
|
||||
down_input,
|
||||
down_input_scale,
|
||||
scale_block_size,
|
||||
masked_m,
|
||||
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
)
|
||||
down_input, down_input_scale = _varlen_deep_gemm_silu_mul_quant(
|
||||
gateup_output,
|
||||
masked_m,
|
||||
group_size=128,
|
||||
topk=self.config.top_k,
|
||||
swiglu_limit=swiglu_limit_arg,
|
||||
swizzle=self.use_swizzle,
|
||||
)
|
||||
del gateup_output
|
||||
|
||||
# GroupGemm-1
|
||||
@@ -348,6 +401,8 @@ class DeepGemmRunnerCore(MoeRunnerCore):
|
||||
down_output,
|
||||
masked_m,
|
||||
expected_m,
|
||||
recipe_a=recipe_a,
|
||||
recipe_b=recipe_b,
|
||||
**gemm_overlap_args_dict,
|
||||
)
|
||||
meta_overlap_args = running_state.get("meta_overlap_args", None)
|
||||
@@ -616,3 +671,113 @@ def post_permute_deep_gemm_to_deepep_normal(
|
||||
topk_ids=running_state["topk_ids"],
|
||||
topk_weights=running_state["topk_weights"],
|
||||
)
|
||||
|
||||
|
||||
def _varlen_deep_gemm_silu_mul_quant(
|
||||
gateup_output: torch.Tensor,
|
||||
masked_m: Optional[torch.Tensor],
|
||||
group_size: int,
|
||||
topk: int,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
swizzle: bool = False,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
from sglang.srt.layers.moe.ep_moe.kernels import silu_and_mul_masked_post_quant_fwd
|
||||
from sglang.srt.layers.quantization.fp8_kernel import (
|
||||
sglang_per_token_group_quant_8bit,
|
||||
)
|
||||
|
||||
if _MASKED_GEMM_FAST_ACT:
|
||||
assert not swizzle, (
|
||||
"SGLANG_OPT_FIX_MEGA_MOE_MEMORY is incompatible with "
|
||||
"SGLANG_MASKED_GEMM_FAST_ACT (swizzled layout only supported by JIT act)"
|
||||
)
|
||||
assert (
|
||||
swiglu_limit is None
|
||||
), "swiglu_limit (DeepSeek V4) is not supported together with SGLANG_MASKED_GEMM_FAST_ACT"
|
||||
return sglang_per_token_group_quant_8bit(
|
||||
x=gateup_output,
|
||||
dst_dtype=torch.float8_e4m3fn,
|
||||
group_size=group_size,
|
||||
masked_m=masked_m,
|
||||
column_major_scales=True,
|
||||
scale_tma_aligned=True,
|
||||
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
fuse_silu_and_mul=True,
|
||||
enable_v2=True,
|
||||
)
|
||||
|
||||
assert masked_m is not None
|
||||
hidden_states_device = gateup_output.device
|
||||
E, N, D_2 = gateup_output.shape
|
||||
D = D_2 // 2
|
||||
del D_2
|
||||
G = D // group_size
|
||||
down_input = torch.empty(
|
||||
(E, N, D),
|
||||
device=hidden_states_device,
|
||||
dtype=torch.float8_e4m3fn,
|
||||
)
|
||||
|
||||
if envs.SGLANG_OPT_USE_JIT_EP_ACTIVATION.get():
|
||||
assert N % 4 == 0 and G % 4 == 0
|
||||
packed_ue8m0 = deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0
|
||||
down_input_scale = torch.empty(
|
||||
(E, G // 4, N) if packed_ue8m0 else (E, N, G),
|
||||
device=hidden_states_device,
|
||||
dtype=torch.int32 if packed_ue8m0 else torch.float32,
|
||||
)
|
||||
silu_and_mul_masked_post_quant(
|
||||
gateup_output,
|
||||
down_input,
|
||||
down_input_scale,
|
||||
group_size,
|
||||
masked_m,
|
||||
scale_ue8m0=packed_ue8m0,
|
||||
topk=topk,
|
||||
transposed=packed_ue8m0,
|
||||
swiglu_limit=swiglu_limit,
|
||||
swizzle=swizzle,
|
||||
)
|
||||
if packed_ue8m0:
|
||||
down_input_scale = down_input_scale.transpose(-1, -2)
|
||||
else:
|
||||
assert (
|
||||
swiglu_limit is None
|
||||
), "swiglu_limit (DeepSeek V4) requires SGLANG_OPT_USE_JIT_EP_ACTIVATION=True"
|
||||
assert (
|
||||
not swizzle
|
||||
), "SGLANG_OPT_FIX_MEGA_MOE_MEMORY requires SGLANG_OPT_USE_JIT_EP_ACTIVATION=True"
|
||||
down_input_scale = torch.empty(
|
||||
(E, N, G),
|
||||
device=hidden_states_device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
silu_and_mul_masked_post_quant_fwd(
|
||||
gateup_output,
|
||||
down_input,
|
||||
down_input_scale,
|
||||
group_size,
|
||||
masked_m,
|
||||
scale_ue8m0=deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0,
|
||||
)
|
||||
return down_input, down_input_scale
|
||||
|
||||
|
||||
def _apply_swiglu_limit(
|
||||
gateup_output: torch.Tensor, swiglu_limit: float
|
||||
) -> torch.Tensor:
|
||||
assert swiglu_limit == 10
|
||||
|
||||
num_tokens, hidden_size_x2 = gateup_output.shape
|
||||
assert gateup_output.dtype == torch.bfloat16
|
||||
|
||||
gate, up = torch.chunk(gateup_output, chunks=2, dim=-1)
|
||||
assert gate.shape == (num_tokens, hidden_size_x2 // 2)
|
||||
assert up.shape == (num_tokens, hidden_size_x2 // 2)
|
||||
|
||||
up = torch.clamp(up, min=-swiglu_limit, max=swiglu_limit)
|
||||
gate = torch.clamp(gate, max=swiglu_limit)
|
||||
|
||||
out = torch.cat([gate, up], dim=-1)
|
||||
assert out.shape == (num_tokens, hidden_size_x2)
|
||||
return out
|
||||
|
||||
@@ -97,8 +97,26 @@ def fused_experts_none_to_marlin(
|
||||
hidden_states.device, max_blocks_per_sm=4
|
||||
)
|
||||
|
||||
marlin_hidden_states = hidden_states
|
||||
# Avoid aliasing the MoE input buffer until Marlin output semantics are
|
||||
# fully validated across shared-expert and overlap paths.
|
||||
marlin_inplace = False
|
||||
if (
|
||||
quant_info.weight_bits == 4
|
||||
and quant_info.w13_qzeros is None
|
||||
and quant_info.w2_qzeros is None
|
||||
and quant_info.w13_scales.dtype == torch.float8_e8m0fnu
|
||||
and quant_info.w2_scales.dtype == torch.float8_e8m0fnu
|
||||
and hidden_states.dtype == torch.float16
|
||||
):
|
||||
# MXFP4(E8M0) Marlin kernels are only numerically valid on the bf16
|
||||
# activation path. The fp16 + E8M0 path is intentionally not generated
|
||||
# in sgl-kernel, so upcast activations here and cast the result back.
|
||||
marlin_hidden_states = hidden_states.to(torch.bfloat16)
|
||||
marlin_inplace = False
|
||||
|
||||
output = fused_marlin_moe(
|
||||
hidden_states=hidden_states,
|
||||
hidden_states=marlin_hidden_states,
|
||||
w1=quant_info.w13_qweight,
|
||||
w2=quant_info.w2_qweight,
|
||||
w1_scale=quant_info.w13_scales,
|
||||
@@ -116,8 +134,9 @@ def fused_experts_none_to_marlin(
|
||||
workspace=MARLIN_MOE_WORKSPACE,
|
||||
num_bits=quant_info.weight_bits,
|
||||
is_k_full=quant_info.is_k_full,
|
||||
inplace=runner_config.inplace,
|
||||
inplace=marlin_inplace,
|
||||
routed_scaling_factor=runner_config.routed_scaling_factor,
|
||||
clamp_limit=runner_config.swiglu_limit,
|
||||
).to(hidden_states.dtype)
|
||||
|
||||
return StandardCombineInput(
|
||||
|
||||
@@ -126,6 +126,7 @@ class TritonRunnerCore(MoeRunnerCore):
|
||||
gemm1_limit=self.config.gemm1_clamp_limit,
|
||||
filter_expert=filter_expert,
|
||||
hooks=hooks,
|
||||
swiglu_limit=self.config.swiglu_limit,
|
||||
)
|
||||
|
||||
return TritonRunnerOutput(hidden_states=out)
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"1": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"2": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"4": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"8": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"16": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"24": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"32": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"48": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"64": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"96": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"128": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"256": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"512": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"1024": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"1536": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 5
|
||||
},
|
||||
"2048": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"3072": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 64,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"4096": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 8,
|
||||
"num_stages": 4
|
||||
}
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"1": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"2": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"4": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"8": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"16": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"24": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"32": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"48": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"64": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"96": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"128": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 1,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"256": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"512": {
|
||||
"BLOCK_SIZE_M": 16,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 32,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"1024": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"1536": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"2048": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 64,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 3
|
||||
},
|
||||
"3072": {
|
||||
"BLOCK_SIZE_M": 64,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 4,
|
||||
"num_stages": 4
|
||||
},
|
||||
"4096": {
|
||||
"BLOCK_SIZE_M": 128,
|
||||
"BLOCK_SIZE_N": 128,
|
||||
"BLOCK_SIZE_K": 128,
|
||||
"GROUP_SIZE_M": 16,
|
||||
"num_warps": 8,
|
||||
"num_stages": 4
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import torch
|
||||
import torch.nn.functional as F
|
||||
import triton.language as tl
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.moe.moe_runner import MoeRunnerConfig
|
||||
from sglang.srt.layers.moe.utils import get_moe_padding_size
|
||||
from sglang.srt.server_args import get_global_server_args
|
||||
@@ -28,6 +29,7 @@ from sglang.srt.utils.custom_op import register_custom_op
|
||||
|
||||
from .fused_moe_triton_config import get_config_dtype_str, try_get_optimal_moe_config
|
||||
from .fused_moe_triton_kernels import (
|
||||
act_and_mul_triton,
|
||||
invoke_fused_moe_kernel,
|
||||
moe_sum_reduce_triton,
|
||||
support_tensor_descriptor,
|
||||
@@ -112,6 +114,7 @@ def inplace_fused_experts(
|
||||
gemm1_alpha: Optional[float] = None,
|
||||
gemm1_limit: Optional[float] = None,
|
||||
filter_expert: bool = True,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
) -> None:
|
||||
fused_experts_impl(
|
||||
hidden_states,
|
||||
@@ -142,6 +145,7 @@ def inplace_fused_experts(
|
||||
gemm1_alpha,
|
||||
gemm1_limit,
|
||||
filter_expert,
|
||||
swiglu_limit=swiglu_limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -174,6 +178,7 @@ def outplace_fused_experts(
|
||||
gemm1_alpha: Optional[float] = None,
|
||||
gemm1_limit: Optional[float] = None,
|
||||
filter_expert: bool = True,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
) -> torch.Tensor:
|
||||
return fused_experts_impl(
|
||||
hidden_states,
|
||||
@@ -204,6 +209,7 @@ def outplace_fused_experts(
|
||||
gemm1_alpha=gemm1_alpha,
|
||||
gemm1_limit=gemm1_limit,
|
||||
filter_expert=filter_expert,
|
||||
swiglu_limit=swiglu_limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -262,6 +268,7 @@ def fused_experts(
|
||||
moe_runner_config.gemm1_alpha,
|
||||
moe_runner_config.gemm1_clamp_limit,
|
||||
filter_expert,
|
||||
swiglu_limit=moe_runner_config.swiglu_limit,
|
||||
)
|
||||
return hidden_states
|
||||
else:
|
||||
@@ -293,6 +300,7 @@ def fused_experts(
|
||||
gemm1_alpha=moe_runner_config.gemm1_alpha,
|
||||
gemm1_limit=moe_runner_config.gemm1_clamp_limit,
|
||||
filter_expert=filter_expert,
|
||||
swiglu_limit=moe_runner_config.swiglu_limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -425,6 +433,7 @@ def _fused_moe_kernel_sequence(
|
||||
gemm1_limit: Optional[float],
|
||||
filter_expert: bool,
|
||||
hooks: Optional[Any] = None,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
) -> torch.Tensor:
|
||||
"""Run the MoE kernel/activation/kernel/combine sequence in a single shot.
|
||||
|
||||
@@ -519,6 +528,7 @@ def _fused_moe_kernel_sequence(
|
||||
if activation == "silu" and is_gated:
|
||||
# - gemm1_alpha != None: GPT-OSS-style swiglu(alpha, limit)
|
||||
# - gemm1_alpha == None and gemm1_limit != None: silu+clamp+mul(limit-only)
|
||||
# - swiglu_limit != None: DeepSeek V4 swiglu clamp + silu_and_mul (CUDA/HIP only)
|
||||
if gemm1_alpha is not None:
|
||||
assert gemm1_limit is not None
|
||||
intermediate_cache2 = swiglu_gpt_oss_sigmoid_alpha(
|
||||
@@ -528,6 +538,55 @@ def _fused_moe_kernel_sequence(
|
||||
intermediate_cache2 = _swiglu_silu_clamp_mul(
|
||||
intermediate_cache1.view(-1, N), gemm1_limit
|
||||
)
|
||||
elif swiglu_limit is not None:
|
||||
# DeepSeek V4: swiglu clamp before silu_and_mul.
|
||||
# Two paths gated by SGLANG_OPT_SWIGLU_CLAMP_FUSION:
|
||||
# fusion=True: clamp fused into act_and_mul_triton or silu_and_mul_clamp
|
||||
# fusion=False: explicit clamp_ on intermediate_cache1 (path checker)
|
||||
assert swiglu_limit == 10
|
||||
assert intermediate_cache1.shape == (total_tokens, N)
|
||||
assert _is_cuda or _is_hip, "DeepSeek V4 only supports CUDA/HIP downstream"
|
||||
|
||||
swiglu_limit_for_triton: Optional[float] = None
|
||||
swiglu_limit_for_silu_and_mul_clamp: Optional[float] = None
|
||||
|
||||
if envs.SGLANG_OPT_SWIGLU_CLAMP_FUSION.get():
|
||||
if filter_expert:
|
||||
swiglu_limit_for_triton = swiglu_limit
|
||||
else:
|
||||
assert (
|
||||
_is_cuda
|
||||
), "fused silu_and_mul_clamp kernel is CUDA-only; HIP must disable SWIGLU_CLAMP_FUSION"
|
||||
swiglu_limit_for_silu_and_mul_clamp = swiglu_limit
|
||||
else:
|
||||
half = N // 2
|
||||
intermediate_cache1[:, :half].clamp_(max=swiglu_limit)
|
||||
intermediate_cache1[:, half:].clamp_(
|
||||
min=-swiglu_limit, max=swiglu_limit
|
||||
)
|
||||
|
||||
if not filter_expert:
|
||||
if swiglu_limit_for_silu_and_mul_clamp is not None:
|
||||
from sglang.jit_kernel.deepseek_v4 import silu_and_mul_clamp
|
||||
|
||||
silu_and_mul_clamp(
|
||||
intermediate_cache1.view(-1, N),
|
||||
intermediate_cache2,
|
||||
swiglu_limit_for_silu_and_mul_clamp,
|
||||
)
|
||||
else:
|
||||
silu_and_mul(intermediate_cache1.view(-1, N), intermediate_cache2)
|
||||
else:
|
||||
act_and_mul_triton(
|
||||
intermediate_cache1.view(-1, N),
|
||||
intermediate_cache2,
|
||||
config,
|
||||
topk_ids,
|
||||
expert_ids,
|
||||
down_moe_use_tma,
|
||||
activation,
|
||||
swiglu_limit=swiglu_limit_for_triton,
|
||||
)
|
||||
elif _is_cuda or _is_hip or _is_xpu:
|
||||
if filter_expert and _is_cuda:
|
||||
# HIP/XPU fall through to the unfiltered path: the down kernel
|
||||
@@ -753,6 +812,7 @@ def fused_experts_impl(
|
||||
gemm1_alpha: Optional[float] = None,
|
||||
gemm1_limit: Optional[float] = None,
|
||||
filter_expert: bool = True,
|
||||
swiglu_limit: Optional[float] = None,
|
||||
):
|
||||
padded_size = padding_size
|
||||
if not (use_fp8_w8a8 or use_int8_w8a8) or block_shape is not None or _use_aiter:
|
||||
@@ -827,6 +887,7 @@ def fused_experts_impl(
|
||||
gemm1_limit=gemm1_limit,
|
||||
filter_expert=filter_expert,
|
||||
hooks=None,
|
||||
swiglu_limit=swiglu_limit,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -930,6 +930,124 @@ def invoke_fused_moe_kernel(
|
||||
)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def tanh(x):
|
||||
return 2 * tl.sigmoid(2 * x) - 1
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _apply_activation(x, ACTIVATION_TYPE: tl.constexpr):
|
||||
"""
|
||||
Apply activation function based on compile-time constant.
|
||||
|
||||
Args:
|
||||
x: Input tensor (converted to float32 inside)
|
||||
ACTIVATION_TYPE: Compile-time constant string ("silu" or "gelu")
|
||||
|
||||
Returns:
|
||||
Activated output in the same dtype as input
|
||||
"""
|
||||
x = x.to(tl.float32)
|
||||
if ACTIVATION_TYPE == "silu":
|
||||
return x * tl.sigmoid(x)
|
||||
elif ACTIVATION_TYPE == "gelu":
|
||||
kAlpha = 0.7978845608028654
|
||||
return 0.5 * x * (1 + tanh(kAlpha * (x + 0.044715 * x * x * x)))
|
||||
else:
|
||||
raise ValueError(f"Unsupported activation: {ACTIVATION_TYPE}")
|
||||
|
||||
|
||||
@triton.jit
|
||||
def act_and_mul_kernel(
|
||||
gateup_output,
|
||||
down_input,
|
||||
hidden_size,
|
||||
expert_ids_ptr,
|
||||
expert_step: tl.constexpr,
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
ACTIVATION_TYPE: tl.constexpr,
|
||||
SWIGLU_LIMIT: tl.constexpr = 0.0,
|
||||
HAS_SWIGLU_LIMIT: tl.constexpr = False,
|
||||
):
|
||||
"""
|
||||
Unified activation and multiply kernel that handles both sorted and unsorted routing,
|
||||
and both SiLU and GELU activations using compile-time constants.
|
||||
"""
|
||||
InDtype = gateup_output.dtype.element_ty
|
||||
OutDtype = down_input.dtype.element_ty
|
||||
|
||||
half_hidden_size = hidden_size // 2
|
||||
pid = tl.program_id(0)
|
||||
|
||||
expert_id = tl.load(expert_ids_ptr + pid // expert_step)
|
||||
|
||||
if expert_id == -1:
|
||||
return
|
||||
|
||||
gateup_output_ptr = gateup_output + pid * hidden_size
|
||||
down_input_ptr = down_input + pid * half_hidden_size
|
||||
gate_output_ptr = gateup_output_ptr
|
||||
up_output_ptr = gateup_output_ptr + half_hidden_size
|
||||
|
||||
for start_offset in tl.range(0, half_hidden_size, BLOCK_SIZE):
|
||||
offset = start_offset + tl.arange(0, BLOCK_SIZE)
|
||||
mask = offset < half_hidden_size
|
||||
|
||||
gate_output = tl.load(gate_output_ptr + offset, mask=mask)
|
||||
up_output = tl.load(up_output_ptr + offset, mask=mask)
|
||||
|
||||
if HAS_SWIGLU_LIMIT:
|
||||
gate_output = tl.minimum(gate_output, SWIGLU_LIMIT)
|
||||
up_output = tl.maximum(tl.minimum(up_output, SWIGLU_LIMIT), -SWIGLU_LIMIT)
|
||||
|
||||
gate_output_activated = _apply_activation(gate_output, ACTIVATION_TYPE)
|
||||
gate_output_activated = gate_output_activated.to(InDtype)
|
||||
|
||||
act_mul_output = gate_output_activated * up_output
|
||||
act_mul_output = act_mul_output.to(OutDtype)
|
||||
tl.store(down_input_ptr + offset, act_mul_output, mask=mask)
|
||||
|
||||
|
||||
def act_and_mul_triton(
|
||||
gateup_output: torch.Tensor,
|
||||
down_input: torch.Tensor,
|
||||
config: Dict[str, Any],
|
||||
topk_ids: Optional[torch.Tensor] = None,
|
||||
expert_ids: Optional[torch.Tensor] = None,
|
||||
down_moe_use_tma: bool = False,
|
||||
activation: str = "silu",
|
||||
swiglu_limit: Optional[float] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Args:
|
||||
gateup_output: Input tensor containing gate and up outputs concatenated
|
||||
down_input: Output tensor for the result
|
||||
config: Configuration dictionary with BLOCK_SIZE_M and BLOCK_SIZE_N
|
||||
topk_ids: Expert IDs for unsorted routing (used when down_moe_use_tma=False)
|
||||
expert_ids: Expert IDs for sorted routing (used when down_moe_use_tma=True)
|
||||
down_moe_use_tma: Whether to use sorted routing layout
|
||||
activation: Activation type ("silu" or "gelu")
|
||||
swiglu_limit: if not None, clamp gate to [-inf, L] and up to [-L, L] before activation
|
||||
(compiles a separate kernel variant via tl.constexpr).
|
||||
"""
|
||||
grid = (down_input.shape[0],)
|
||||
hidden_size = gateup_output.shape[1]
|
||||
expert_ids_row = topk_ids.view(-1) if not down_moe_use_tma else expert_ids
|
||||
expert_step = 1 if not down_moe_use_tma else config["BLOCK_SIZE_M"]
|
||||
has_swiglu_limit = swiglu_limit is not None
|
||||
act_and_mul_kernel[grid](
|
||||
gateup_output,
|
||||
down_input,
|
||||
hidden_size,
|
||||
expert_ids_row,
|
||||
expert_step,
|
||||
BLOCK_SIZE=512,
|
||||
ACTIVATION_TYPE=activation,
|
||||
SWIGLU_LIMIT=float(swiglu_limit) if has_swiglu_limit else 0.0,
|
||||
HAS_SWIGLU_LIMIT=has_swiglu_limit,
|
||||
)
|
||||
|
||||
|
||||
# _moe_sum_reduce_kernel kernel modified from https://github.com/ModelTC/lightllm/blob/main/lightllm/common/fused_moe/moe_sum_reduce.py
|
||||
@triton.jit
|
||||
def _moe_sum_reduce_kernel(
|
||||
|
||||
@@ -88,15 +88,16 @@ class StandardDispatcher(BaseDispatcher):
|
||||
self.moe_ep_size = get_moe_expert_parallel_world_size()
|
||||
backend = get_moe_runner_backend()
|
||||
self.enable_flashinfer_cutlass_moe = backend.is_flashinfer_cutlass()
|
||||
# FlashInfer CUTLASS and CuteDSL handle EP internally with global expert IDs.
|
||||
# Skip local expert mapping so topk_ids stay in global space.
|
||||
self.enable_flashinfer_mxfp4_moe = backend.is_flashinfer_mxfp4()
|
||||
self.enable_flashinfer_trtllm_routed_moe = backend.is_flashinfer_trtllm_routed()
|
||||
# Skip local expert mapping when the backend handles EP with global expert IDs:
|
||||
# - cutlass / cutedsl / trtllm_routed handle EP internally
|
||||
# - mxfp4 dispatcher mapping is already global
|
||||
self.skip_local_expert_mapping = (
|
||||
backend.is_flashinfer_cutlass()
|
||||
or backend.is_flashinfer_cutedsl()
|
||||
or backend.is_flashinfer_trtllm_routed()
|
||||
)
|
||||
self.enable_flashinfer_trtllm_routed_moe = (
|
||||
get_moe_runner_backend().is_flashinfer_trtllm_routed()
|
||||
or self.enable_flashinfer_mxfp4_moe
|
||||
)
|
||||
self.num_experts = moe_runner_config.num_experts
|
||||
self.num_local_experts = moe_runner_config.num_local_experts
|
||||
@@ -189,7 +190,7 @@ class StandardDispatcher(BaseDispatcher):
|
||||
)
|
||||
)
|
||||
|
||||
if self.local_expert_mapping is not None:
|
||||
if self.local_expert_mapping is not None and not self.skip_local_expert_mapping:
|
||||
if _use_aiter:
|
||||
self.expert_mask_gpu = (
|
||||
(
|
||||
|
||||
@@ -36,6 +36,7 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from sglang.jit_kernel.deepseek_v4 import mask_topk_ids
|
||||
from sglang.srt.distributed import (
|
||||
get_moe_expert_parallel_rank,
|
||||
get_moe_expert_parallel_world_size,
|
||||
@@ -44,6 +45,7 @@ from sglang.srt.distributed import (
|
||||
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
|
||||
use_symmetric_memory,
|
||||
)
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.eplb import expert_location_dispatch
|
||||
from sglang.srt.eplb.expert_distribution import get_global_expert_distribution_recorder
|
||||
from sglang.srt.eplb.expert_location_dispatch import (
|
||||
@@ -272,6 +274,7 @@ class TopK(MultiPlatformOp):
|
||||
apply_routed_scaling_factor_on_output: Optional[bool] = False,
|
||||
output_format: Optional[TopKOutputFormat] = None,
|
||||
fused_shared_experts_scaling_factor: Optional[float] = None,
|
||||
is_fp4_experts: bool = False,
|
||||
):
|
||||
# NOTE: scoring_func is not used for now, but we keep it for future use
|
||||
# see https://github.com/sgl-project/sglang/pull/4505 for more details
|
||||
@@ -281,6 +284,9 @@ class TopK(MultiPlatformOp):
|
||||
assert num_expert_group is not None and topk_group is not None
|
||||
|
||||
self.layer_id = layer_id
|
||||
# flashinfer_mxfp4 backend only: True -> STANDARD (Mxfp4FlashinferTrtllmMoEMethod
|
||||
# consumes), False -> BYPASSED (flashinfer's own mxfp4 kernel). No-op otherwise.
|
||||
self.is_fp4_experts = is_fp4_experts
|
||||
self.topk_config = TopKConfig(
|
||||
top_k=top_k,
|
||||
use_grouped_topk=use_grouped_topk,
|
||||
@@ -327,9 +333,8 @@ class TopK(MultiPlatformOp):
|
||||
output_format = self.topk_config.output_format
|
||||
elif get_moe_runner_backend().is_triton_kernels():
|
||||
output_format = TopKOutputFormat.TRITON_KERNEL
|
||||
elif (
|
||||
get_moe_runner_backend().is_flashinfer_trtllm()
|
||||
or get_moe_runner_backend().is_flashinfer_mxfp4()
|
||||
elif get_moe_runner_backend().is_flashinfer_trtllm() or (
|
||||
get_moe_runner_backend().is_flashinfer_mxfp4() and not self.is_fp4_experts
|
||||
):
|
||||
output_format = TopKOutputFormat.BYPASSED
|
||||
else:
|
||||
@@ -699,6 +704,101 @@ def kimi_k2_biased_topk_impl(
|
||||
return topk_weights, topk_ids
|
||||
|
||||
|
||||
@torch.compile(dynamic=True, backend=get_compiler_backend(), disable=_is_npu)
|
||||
def biased_topk_impl(
|
||||
hidden_states: torch.Tensor,
|
||||
gating_output: torch.Tensor,
|
||||
correction_bias: torch.Tensor,
|
||||
topk: int,
|
||||
renormalize: bool,
|
||||
scoring_func: str = "sigmoid",
|
||||
num_fused_shared_experts: int = 0,
|
||||
routed_scaling_factor: Optional[float] = None,
|
||||
num_token_non_padded: Optional[torch.Tensor] = None,
|
||||
expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo] = None,
|
||||
apply_routed_scaling_factor_on_output: Optional[bool] = False,
|
||||
):
|
||||
assert hidden_states.shape[0] == gating_output.shape[0], "Number of tokens mismatch"
|
||||
|
||||
if scoring_func == "sigmoid":
|
||||
scores = gating_output.sigmoid()
|
||||
elif scoring_func == "sqrtsoftplus":
|
||||
scores = torch.nn.functional.softplus(gating_output).sqrt()
|
||||
|
||||
num_token = scores.shape[0]
|
||||
num_experts = scores.shape[1]
|
||||
|
||||
scores_for_choice = scores.view(num_token, -1) + correction_bias.unsqueeze(0)
|
||||
_, topk_ids = torch.topk(
|
||||
scores_for_choice,
|
||||
k=topk,
|
||||
dim=-1,
|
||||
sorted=(True if num_fused_shared_experts > 0 else False),
|
||||
)
|
||||
topk_weights = scores.gather(1, topk_ids)
|
||||
|
||||
if num_fused_shared_experts:
|
||||
topk_ids[:, -1] = torch.randint(
|
||||
low=num_experts,
|
||||
high=num_experts + num_fused_shared_experts,
|
||||
size=(topk_ids.size(0),),
|
||||
dtype=topk_ids.dtype,
|
||||
device=topk_ids.device,
|
||||
)
|
||||
if routed_scaling_factor is not None:
|
||||
topk_weights[:, -1] = (
|
||||
topk_weights[:, :-1].sum(dim=-1) / routed_scaling_factor
|
||||
)
|
||||
|
||||
if renormalize:
|
||||
topk_weights_sum = (
|
||||
topk_weights.sum(dim=-1, keepdim=True)
|
||||
if num_fused_shared_experts == 0
|
||||
else topk_weights[:, :-1].sum(dim=-1, keepdim=True)
|
||||
)
|
||||
topk_weights = topk_weights / topk_weights_sum
|
||||
if apply_routed_scaling_factor_on_output:
|
||||
topk_weights *= routed_scaling_factor
|
||||
|
||||
topk_weights, topk_ids = topk_weights.to(torch.float32), topk_ids.to(torch.int32)
|
||||
topk_ids = topk_ids_logical_to_physical(topk_ids, expert_location_dispatch_info)
|
||||
_mask_topk_ids_padded_region(topk_ids, num_token_non_padded)
|
||||
return topk_weights, topk_ids
|
||||
|
||||
|
||||
def biased_topk_jit_kernel_impl(
|
||||
hidden_states: torch.Tensor,
|
||||
gating_output: torch.Tensor,
|
||||
correction_bias: torch.Tensor,
|
||||
topk: int,
|
||||
renormalize: bool,
|
||||
scoring_func: str = "sigmoid",
|
||||
num_fused_shared_experts: int = 0,
|
||||
routed_scaling_factor: Optional[float] = None,
|
||||
num_token_non_padded: Optional[torch.Tensor] = None,
|
||||
expert_location_dispatch_info: Optional[ExpertLocationDispatchInfo] = None,
|
||||
apply_routed_scaling_factor_on_output: Optional[bool] = False,
|
||||
):
|
||||
assert hidden_states.shape[0] == gating_output.shape[0], "Number of tokens mismatch"
|
||||
|
||||
from sglang.jit_kernel.moe_fused_gate import moe_fused_gate
|
||||
|
||||
topk_weights, topk_ids = moe_fused_gate(
|
||||
gating_output,
|
||||
correction_bias,
|
||||
topk=topk,
|
||||
scoring_func=scoring_func,
|
||||
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,
|
||||
)
|
||||
topk_weights, topk_ids = topk_weights.to(torch.float32), topk_ids.to(torch.int32)
|
||||
topk_ids = topk_ids_logical_to_physical(topk_ids, expert_location_dispatch_info)
|
||||
_mask_topk_ids_padded_region(topk_ids, num_token_non_padded)
|
||||
return topk_weights, topk_ids
|
||||
|
||||
|
||||
@torch.compile(dynamic=True, backend=get_compiler_backend(), disable=_is_npu)
|
||||
def biased_grouped_topk_impl(
|
||||
hidden_states: torch.Tensor,
|
||||
@@ -779,11 +879,15 @@ def is_power_of_two(n):
|
||||
def _mask_topk_ids_padded_region(
|
||||
topk_ids: torch.Tensor,
|
||||
num_token_non_padded: Optional[torch.Tensor] = None,
|
||||
):
|
||||
) -> None:
|
||||
if num_token_non_padded is None:
|
||||
return
|
||||
indices = torch.arange(0, topk_ids.shape[0], device=topk_ids.device)
|
||||
topk_ids[indices >= num_token_non_padded, :] = -1
|
||||
# TODO: let the kernel support other dtypes
|
||||
if _is_cuda and topk_ids.dtype == torch.int32:
|
||||
mask_topk_ids(topk_ids, num_token_non_padded)
|
||||
else:
|
||||
indices = torch.arange(0, topk_ids.shape[0], device=topk_ids.device)
|
||||
topk_ids[indices >= num_token_non_padded, :] = -1
|
||||
|
||||
|
||||
@torch.compile(dynamic=True, backend=get_compiler_backend())
|
||||
@@ -1209,7 +1313,27 @@ def select_experts(
|
||||
)
|
||||
elif custom_routing_function is None:
|
||||
assert not apply_routed_scaling_factor_on_output, "Not implemented"
|
||||
if (
|
||||
if scoring_func == "sqrtsoftplus":
|
||||
_biased_topk = (
|
||||
biased_topk_jit_kernel_impl
|
||||
if envs.SGLANG_OPT_USE_JIT_KERNEL_FUSED_TOPK.get()
|
||||
else biased_topk_impl
|
||||
)
|
||||
|
||||
topk_weights, topk_ids = _biased_topk(
|
||||
hidden_states=hidden_states,
|
||||
gating_output=router_logits,
|
||||
correction_bias=correction_bias,
|
||||
topk=num_routed_topk if _use_aiter else top_k,
|
||||
renormalize=renormalize,
|
||||
scoring_func=scoring_func,
|
||||
num_fused_shared_experts=num_fused_shared_experts,
|
||||
routed_scaling_factor=routed_scaling_factor,
|
||||
num_token_non_padded=num_token_non_padded,
|
||||
expert_location_dispatch_info=expert_location_dispatch_info,
|
||||
apply_routed_scaling_factor_on_output=apply_routed_scaling_factor_on_output,
|
||||
)
|
||||
elif (
|
||||
get_moe_runner_backend().is_flashinfer_trtllm_routed()
|
||||
and scoring_func == "softmax"
|
||||
and correction_bias is None
|
||||
|
||||
@@ -34,6 +34,7 @@ def _dtype_rank(dtype: torch.dtype) -> Optional[int]:
|
||||
torch.float8_e4m3fnuz,
|
||||
torch.float8_e5m2,
|
||||
torch.float8_e5m2fnuz,
|
||||
torch.float8_e8m0fnu,
|
||||
):
|
||||
return 0
|
||||
if dtype in (torch.float16, torch.bfloat16):
|
||||
@@ -70,6 +71,8 @@ def copy_with_check(target: torch.Tensor, loaded_weight: torch.Tensor):
|
||||
raise ValueError(
|
||||
f"Downcasting not allowed: {target.dtype=}, {loaded_weight.dtype=}"
|
||||
)
|
||||
if loaded_rank == torch.float8_e8m0fnu:
|
||||
assert target_rank in {torch.float8_e8m0fnu, torch.float32}
|
||||
|
||||
target.copy_(loaded_weight)
|
||||
|
||||
|
||||
@@ -136,8 +136,12 @@ class Fp8Config(QuantizationConfig):
|
||||
weight_block_size: List[int] = None,
|
||||
packed_modules_mapping: Optional[Dict[str, List[str]]] = None,
|
||||
use_mxfp8: bool = False,
|
||||
is_fp4_experts: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
# DSV4 mxfp4-packed (True) vs converted FP8 (False); injected by
|
||||
# model_loader from ModelConfig. Default False off the DSV4 path.
|
||||
self.is_fp4_experts = is_fp4_experts
|
||||
self.is_checkpoint_fp8_serialized = is_checkpoint_fp8_serialized
|
||||
if is_checkpoint_fp8_serialized:
|
||||
log_info_on_rank0(logger, "Detected fp8 checkpoint.")
|
||||
@@ -247,7 +251,23 @@ class Fp8Config(QuantizationConfig):
|
||||
return UnquantizedFusedMoEMethod(
|
||||
layer.use_triton_kernels, layer.use_flashinfer_trtllm_moe
|
||||
)
|
||||
return Fp8MoEMethod(self)
|
||||
|
||||
fp8_method = Fp8MoEMethod(self)
|
||||
|
||||
if self.is_fp4_experts and get_moe_runner_backend().is_marlin():
|
||||
from sglang.srt.layers.quantization.mxfp4_marlin_moe import (
|
||||
Mxfp4MarlinMoEMethod,
|
||||
)
|
||||
|
||||
return Mxfp4MarlinMoEMethod(fp8_method, prefix=prefix)
|
||||
|
||||
if self.is_fp4_experts and get_moe_runner_backend().is_flashinfer_mxfp4():
|
||||
from sglang.srt.layers.quantization.mxfp4_flashinfer_trtllm_moe import (
|
||||
Mxfp4FlashinferTrtllmMoEMethod,
|
||||
)
|
||||
|
||||
return Mxfp4FlashinferTrtllmMoEMethod(fp8_method, prefix=prefix)
|
||||
return fp8_method
|
||||
elif isinstance(layer, RadixAttention):
|
||||
return Fp8KVCacheMethod(self)
|
||||
return None
|
||||
@@ -796,6 +816,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
|
||||
self.block_quant = (
|
||||
self.use_mxfp8 or self.quant_config.weight_block_size is not None
|
||||
)
|
||||
self.is_fp4_expert = self.quant_config.is_fp4_experts
|
||||
self.with_bias = False
|
||||
if get_moe_runner_backend().is_cutlass():
|
||||
assert (
|
||||
@@ -873,7 +894,26 @@ class Fp8MoEMethod(FusedMoEMethodBase):
|
||||
)
|
||||
|
||||
# WEIGHTS
|
||||
if _is_hip and _use_hip_int4:
|
||||
if self.is_fp4_expert:
|
||||
w13_weight = torch.nn.Parameter(
|
||||
torch.empty(
|
||||
num_experts,
|
||||
2 * intermediate_size_per_partition,
|
||||
hidden_size // 2,
|
||||
dtype=torch.int8,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
w2_weight = torch.nn.Parameter(
|
||||
torch.empty(
|
||||
num_experts,
|
||||
hidden_size,
|
||||
intermediate_size_per_partition // 2,
|
||||
dtype=torch.int8,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
elif _is_hip and _use_hip_int4:
|
||||
# INT4 MoE weight - INT32 packed
|
||||
w13_weight = torch.nn.Parameter(
|
||||
torch.empty(
|
||||
@@ -945,7 +985,29 @@ class Fp8MoEMethod(FusedMoEMethodBase):
|
||||
set_weight_attrs(w2_weight_bias, extra_weight_attrs)
|
||||
|
||||
# WEIGHT_SCALES
|
||||
if self.block_quant:
|
||||
if self.is_fp4_expert:
|
||||
fp4_block_k = 32
|
||||
w13_weight_scale = torch.nn.Parameter(
|
||||
torch.ones(
|
||||
num_experts,
|
||||
2 * intermediate_size_per_partition,
|
||||
hidden_size // fp4_block_k,
|
||||
dtype=torch.float32,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
w2_weight_scale = torch.nn.Parameter(
|
||||
torch.ones(
|
||||
num_experts,
|
||||
hidden_size,
|
||||
intermediate_size_per_partition // fp4_block_k,
|
||||
dtype=torch.float32,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.register_parameter("w13_weight_scale_inv", w13_weight_scale)
|
||||
layer.register_parameter("w2_weight_scale_inv", w2_weight_scale)
|
||||
elif self.block_quant:
|
||||
scale_dtype = torch.uint8 if self.use_mxfp8 else torch.float32
|
||||
scale_init = torch.zeros if scale_dtype == torch.uint8 else torch.ones
|
||||
w13_weight_scale = torch.nn.Parameter(
|
||||
@@ -1102,6 +1164,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
|
||||
)
|
||||
else:
|
||||
# For fp8 moe run with deepgemm, the expert weights and scales need be requantized to ue8m0
|
||||
from sglang.srt.layers import deep_gemm_wrapper
|
||||
from sglang.srt.layers.moe.ep_moe.layer import DeepEPMoE
|
||||
from sglang.srt.model_loader.utils import (
|
||||
should_deepgemm_weight_requant_ue8m0,
|
||||
@@ -1110,8 +1173,46 @@ class Fp8MoEMethod(FusedMoEMethodBase):
|
||||
# Check if MoE will actually use DeepGEMM runner
|
||||
will_use_deepgemm = self.is_deepgemm_moe_runner_backend_enabled()
|
||||
|
||||
if self.is_fp4_expert:
|
||||
if get_moe_runner_backend().is_marlin():
|
||||
layer.w13_weight.data = layer.w13_weight.data.view(torch.int8)
|
||||
layer.w2_weight.data = layer.w2_weight.data.view(torch.int8)
|
||||
return
|
||||
|
||||
layer.w13_weight.data = layer.w13_weight.data.view(torch.int8)
|
||||
layer.w2_weight.data = layer.w2_weight.data.view(torch.int8)
|
||||
|
||||
if envs.SGLANG_OPT_USE_DEEPGEMM_MEGA_MOE.get():
|
||||
from sglang.srt.layers.moe.mega_moe import (
|
||||
build_mega_moe_experts_weights,
|
||||
)
|
||||
|
||||
build_mega_moe_experts_weights(layer)
|
||||
return
|
||||
|
||||
if deep_gemm_wrapper.DEEPGEMM_SCALE_UE8M0 and will_use_deepgemm:
|
||||
from deep_gemm import transform_sf_into_required_layout
|
||||
|
||||
for scale_param, weight_param in [
|
||||
(layer.w13_weight_scale_inv, layer.w13_weight),
|
||||
(layer.w2_weight_scale_inv, layer.w2_weight),
|
||||
]:
|
||||
num_experts, n, _ = scale_param.data.shape
|
||||
k = weight_param.shape[2] * 2
|
||||
scale_param.data = transform_sf_into_required_layout(
|
||||
scale_param.data,
|
||||
mn=n,
|
||||
k=k,
|
||||
recipe=(1, 32),
|
||||
num_groups=num_experts,
|
||||
disable_ue8m0_cast=False,
|
||||
)
|
||||
layer.w13_weight_scale_inv.format_ue8m0 = True
|
||||
layer.w2_weight_scale_inv.format_ue8m0 = True
|
||||
|
||||
if (
|
||||
should_deepgemm_weight_requant_ue8m0(
|
||||
not self.is_fp4_expert
|
||||
and should_deepgemm_weight_requant_ue8m0(
|
||||
weight_block_size=getattr(
|
||||
self.quant_config, "weight_block_size", None
|
||||
),
|
||||
@@ -1690,6 +1791,7 @@ class Fp8MoEMethod(FusedMoEMethodBase):
|
||||
w13_scale=w13_scale,
|
||||
w2_scale=w2_scale,
|
||||
block_shape=block_shape,
|
||||
is_fp4_experts=self.is_fp4_expert,
|
||||
)
|
||||
elif (
|
||||
self.runner.runner_backend.is_flashinfer_trtllm()
|
||||
|
||||
@@ -547,6 +547,51 @@ def sglang_per_token_group_quant_fp8(
|
||||
return x_q, x_s
|
||||
|
||||
|
||||
def sglang_per_token_group_quant_fp8_ue8m0(
|
||||
x: torch.Tensor,
|
||||
group_size: int,
|
||||
eps: float = 1e-10,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
assert (
|
||||
x.shape[-1] % group_size == 0
|
||||
), f"hidden ({x.shape[-1]}) must be divisible by group_size ({group_size})"
|
||||
assert x.is_contiguous(), "x must be contiguous"
|
||||
assert enable_sgl_per_token_group_quant_8bit, (
|
||||
"sgl_per_token_group_quant_8bit is required (v2 kernel supports "
|
||||
"group_size in {16, 32, 64, 128})"
|
||||
)
|
||||
|
||||
*x_batch, x_q_mn, x_q_k = x.shape
|
||||
x_q = torch.empty(x.shape, device=x.device, dtype=fp8_dtype)
|
||||
|
||||
x_s_mn = x_q_mn
|
||||
x_s_k = x_q_k // group_size
|
||||
aligned_mn = ceil_align(x_s_mn, 4)
|
||||
aligned_k = ceil_align(x_s_k, 4)
|
||||
x_s = torch.empty(
|
||||
(*x_batch, aligned_k // 4, aligned_mn),
|
||||
device=x.device,
|
||||
dtype=torch.int,
|
||||
).transpose(-1, -2)[..., :x_s_mn, :]
|
||||
|
||||
if x.shape[0] > 0:
|
||||
sgl_per_token_group_quant_8bit(
|
||||
x,
|
||||
x_q,
|
||||
x_s,
|
||||
group_size,
|
||||
eps,
|
||||
fp8_min,
|
||||
fp8_max,
|
||||
True, # scale_ue8m0
|
||||
False, # fuse_silu_and_mul
|
||||
None, # masked_m
|
||||
enable_v2=True,
|
||||
)
|
||||
|
||||
return x_q, x_s
|
||||
|
||||
|
||||
# TODO maybe unify int8 and fp8 code later
|
||||
def sglang_per_token_group_quant_8bit(
|
||||
x: torch.Tensor,
|
||||
@@ -1015,8 +1060,25 @@ def get_w8a8_block_fp8_configs(
|
||||
logger,
|
||||
f"Using configuration from {config_file_path} for W8A8 Block FP8 kernel.",
|
||||
)
|
||||
# If a configuration has been found, return it
|
||||
return {int(key): val for key, val in json.load(f).items()}
|
||||
raw = {int(key): val for key, val in json.load(f).items()}
|
||||
|
||||
sanitized = {}
|
||||
clamped_ms = []
|
||||
for m_key, cfg in raw.items():
|
||||
if cfg["BLOCK_SIZE_K"] < block_k:
|
||||
clamped_ms.append((m_key, cfg["BLOCK_SIZE_K"]))
|
||||
cfg = {**cfg, "BLOCK_SIZE_K": block_k}
|
||||
sanitized[m_key] = cfg
|
||||
if clamped_ms:
|
||||
logger.warning(
|
||||
"Clamped BLOCK_SIZE_K up to %d in tuned config %s for entries %s "
|
||||
"(scale stepping requires BLOCK_SIZE_K >= block_k).",
|
||||
block_k,
|
||||
json_file_name,
|
||||
clamped_ms,
|
||||
)
|
||||
|
||||
return sanitized
|
||||
|
||||
# If no optimized configuration is available, we will use the default
|
||||
# configuration
|
||||
|
||||
@@ -40,6 +40,8 @@ class BaseKVCacheMethod(QuantizeMethodBase):
|
||||
layer.v_scale = torch.nn.Parameter(
|
||||
torch.tensor(-1.0, dtype=torch.float32), requires_grad=False
|
||||
)
|
||||
layer.k_scale._skip_weight_check = True
|
||||
layer.v_scale._skip_weight_check = True
|
||||
|
||||
def apply(self, layer: torch.nn.Module) -> torch.Tensor:
|
||||
raise RuntimeError(f"{self.__class__.__name__}.apply should not be called.")
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.quantization.marlin_utils import (
|
||||
marlin_make_workspace,
|
||||
marlin_permute_bias,
|
||||
marlin_permute_scales,
|
||||
)
|
||||
from sglang.srt.utils import is_cuda
|
||||
|
||||
_is_cuda = is_cuda()
|
||||
|
||||
if _is_cuda:
|
||||
from sglang.jit_kernel.gptq_marlin_repack import gptq_marlin_repack
|
||||
|
||||
|
||||
def mxfp4_marlin_process_scales(
|
||||
marlin_scales: torch.Tensor,
|
||||
input_dtype: torch.dtype | None = None,
|
||||
) -> torch.Tensor:
|
||||
if input_dtype is None or input_dtype.itemsize == 2:
|
||||
marlin_scales = marlin_scales.view(-1, 4)[:, [0, 2, 1, 3]].view(
|
||||
marlin_scales.size(0), -1
|
||||
)
|
||||
marlin_scales = marlin_scales.to(torch.float8_e8m0fnu)
|
||||
if input_dtype == torch.float8_e4m3fn:
|
||||
marlin_scales = marlin_scales.view(torch.uint8)
|
||||
assert marlin_scales.max() <= 249
|
||||
# exponent_bias (fp4->fp8) = 2 ** 3 - 2 ** 1 = 6
|
||||
marlin_scales = marlin_scales + 6
|
||||
marlin_scales = marlin_scales.view(torch.float8_e8m0fnu)
|
||||
return marlin_scales
|
||||
|
||||
|
||||
def _normalize_scale_tensor(
|
||||
scales: torch.Tensor, target_dtype: torch.dtype
|
||||
) -> torch.Tensor:
|
||||
# The kernel consumes E8M0 exponents. Regardless of the placeholder dtype
|
||||
# the loader used, we want the *numerical* value 2**e in ``target_dtype``.
|
||||
# float32/bfloat16/float16 containers hold the numerical 2**e directly
|
||||
# (they were filled via a dtype-promoting copy from uint8/e8m0).
|
||||
# uint8/int8 containers hold the raw E8M0 byte and must be reinterpreted.
|
||||
if scales.dtype == torch.float8_e8m0fnu:
|
||||
return scales.to(target_dtype)
|
||||
if scales.dtype == torch.uint8:
|
||||
return scales.view(torch.float8_e8m0fnu).to(target_dtype)
|
||||
if scales.dtype == torch.int8:
|
||||
return scales.view(torch.uint8).view(torch.float8_e8m0fnu).to(target_dtype)
|
||||
if scales.dtype in (torch.float32, torch.bfloat16, torch.float16):
|
||||
return scales.to(target_dtype)
|
||||
raise TypeError(f"Unsupported MXFP4 scale dtype for Marlin: {scales.dtype}")
|
||||
|
||||
|
||||
def prepare_moe_mxfp4_layer_for_marlin(layer: torch.nn.Module) -> None:
|
||||
group_size = 32
|
||||
w13 = layer.w13_weight.data
|
||||
w2 = layer.w2_weight.data
|
||||
w13_scale = layer.w13_weight_scale_inv.data
|
||||
w2_scale = layer.w2_weight_scale_inv.data
|
||||
w13_bias = getattr(layer, "w13_bias", None)
|
||||
w2_bias = getattr(layer, "w2_bias", None)
|
||||
|
||||
num_experts = w13.shape[0]
|
||||
intermediate_size = w13.shape[1] // 2
|
||||
hidden_size = w13.shape[2] * 2
|
||||
param_dtype = getattr(
|
||||
layer,
|
||||
"orig_dtype",
|
||||
w13_bias.dtype if w13_bias is not None else torch.bfloat16,
|
||||
)
|
||||
|
||||
device = w13.device
|
||||
layer.workspace = marlin_make_workspace(device, 4)
|
||||
perm = torch.empty(0, dtype=torch.int, device=device)
|
||||
|
||||
def _repack_weight(weight: torch.Tensor, is_w13: bool) -> torch.Tensor:
|
||||
if is_w13:
|
||||
size_n, size_k = intermediate_size * 2, hidden_size
|
||||
else:
|
||||
size_n, size_k = hidden_size, intermediate_size
|
||||
assert weight.shape == (num_experts, size_n, size_k // 2)
|
||||
|
||||
tensor_list = []
|
||||
for i in range(num_experts):
|
||||
qweight = weight[i].view(torch.int32).T.contiguous()
|
||||
marlin_qweight = gptq_marlin_repack(
|
||||
b_q_weight=qweight,
|
||||
perm=perm,
|
||||
size_k=size_k,
|
||||
size_n=size_n,
|
||||
num_bits=4,
|
||||
)
|
||||
tensor_list.append(marlin_qweight)
|
||||
return torch.stack(tensor_list)
|
||||
|
||||
def _permute_scales(scales: torch.Tensor, is_w13: bool) -> torch.Tensor:
|
||||
scales = _normalize_scale_tensor(scales, param_dtype)
|
||||
|
||||
if is_w13:
|
||||
size_n, size_k = intermediate_size * 2, hidden_size
|
||||
else:
|
||||
size_n, size_k = hidden_size, intermediate_size
|
||||
|
||||
tensor_list = []
|
||||
for i in range(num_experts):
|
||||
scale = scales[i].T.contiguous()
|
||||
marlin_scales = marlin_permute_scales(
|
||||
s=scale,
|
||||
size_k=size_k,
|
||||
size_n=size_n,
|
||||
group_size=group_size,
|
||||
)
|
||||
tensor_list.append(
|
||||
mxfp4_marlin_process_scales(
|
||||
marlin_scales,
|
||||
input_dtype=param_dtype,
|
||||
)
|
||||
)
|
||||
return torch.stack(tensor_list)
|
||||
|
||||
def _permute_bias(bias: torch.Tensor | None) -> torch.Tensor | None:
|
||||
if bias is None:
|
||||
return None
|
||||
tensor_list = []
|
||||
for i in range(num_experts):
|
||||
tensor_list.append(marlin_permute_bias(bias[i].to(param_dtype)))
|
||||
return torch.stack(tensor_list)
|
||||
|
||||
w13_marlin = _repack_weight(w13, True)
|
||||
w2_marlin = _repack_weight(w2, False)
|
||||
w13_scale_marlin = _permute_scales(w13_scale, True)
|
||||
w2_scale_marlin = _permute_scales(w2_scale, False)
|
||||
|
||||
layer.w13_weight = torch.nn.Parameter(w13_marlin, requires_grad=False)
|
||||
layer.w2_weight = torch.nn.Parameter(w2_marlin, requires_grad=False)
|
||||
layer.w13_weight_scale_inv = torch.nn.Parameter(
|
||||
w13_scale_marlin, requires_grad=False
|
||||
)
|
||||
layer.w2_weight_scale_inv = torch.nn.Parameter(w2_scale_marlin, requires_grad=False)
|
||||
|
||||
if w13_bias is not None:
|
||||
layer.w13_bias = torch.nn.Parameter(
|
||||
_permute_bias(w13_bias), requires_grad=False
|
||||
)
|
||||
if w2_bias is not None:
|
||||
layer.w2_bias = torch.nn.Parameter(_permute_bias(w2_bias), requires_grad=False)
|
||||
@@ -0,0 +1,461 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
from torch.nn import Module
|
||||
from torch.nn.parameter import Parameter
|
||||
|
||||
from sglang.srt.distributed import get_tp_group
|
||||
from sglang.srt.distributed.device_communicators.pynccl_allocator import (
|
||||
use_symmetric_memory,
|
||||
)
|
||||
from sglang.srt.layers.dp_attention import is_allocation_symmetric
|
||||
from sglang.srt.layers.moe.utils import RoutingMethodType
|
||||
from sglang.srt.server_args import get_global_server_args
|
||||
from sglang.srt.utils import (
|
||||
is_flashinfer_available,
|
||||
log_info_on_rank0,
|
||||
set_weight_attrs,
|
||||
)
|
||||
from sglang.srt.utils.common import next_power_of_2
|
||||
|
||||
if is_flashinfer_available():
|
||||
from flashinfer import mxfp8_quantize, shuffle_matrix_a, shuffle_matrix_sf_a
|
||||
from flashinfer.fp4_quantization import block_scale_interleave
|
||||
from flashinfer.fused_moe import trtllm_fp4_block_scale_routed_moe
|
||||
from flashinfer.fused_moe.core import (
|
||||
_maybe_get_cached_w3_w1_permute_indices,
|
||||
get_w2_permute_indices_with_cache,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.layers.moe.token_dispatcher import CombineInput, DispatchOutput
|
||||
|
||||
from sglang.srt.utils.common import get_bool_env_var
|
||||
|
||||
_USE_OFFICIAL_SHUFFLE = get_bool_env_var(
|
||||
"SGLANG_MXFP4_USE_OFFICIAL_SHUFFLE", default="true"
|
||||
)
|
||||
|
||||
|
||||
class PackTopkIds:
|
||||
|
||||
@classmethod
|
||||
def execute(
|
||||
cls, topk_ids: torch.Tensor, topk_weights: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
return cls.triton(topk_ids, topk_weights)
|
||||
|
||||
@classmethod
|
||||
def vanilla(
|
||||
cls, topk_ids: torch.Tensor, topk_weights: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
weight_bits = (
|
||||
topk_weights.to(torch.bfloat16).view(torch.int16).to(torch.int32) & 0xFFFF
|
||||
)
|
||||
return (topk_ids.to(torch.int32) << 16) | weight_bits
|
||||
|
||||
@classmethod
|
||||
def triton(cls, topk_ids: torch.Tensor, topk_weights: torch.Tensor) -> torch.Tensor:
|
||||
assert (
|
||||
topk_ids.shape == topk_weights.shape
|
||||
), f"shape mismatch: {topk_ids.shape=} vs {topk_weights.shape=}"
|
||||
assert topk_ids.ndim >= 1, f"expected >=1D, got {topk_ids.shape=}"
|
||||
|
||||
assert (
|
||||
topk_ids.dtype == torch.int32
|
||||
), f"topk_ids must be int32, got {topk_ids.dtype}"
|
||||
assert (
|
||||
topk_weights.dtype == torch.float32
|
||||
), f"topk_weights must be float32, got {topk_weights.dtype}"
|
||||
|
||||
assert topk_ids.is_contiguous(), "topk_ids must be contiguous"
|
||||
assert topk_weights.is_contiguous(), "topk_weights must be contiguous"
|
||||
|
||||
out = torch.empty_like(topk_ids, dtype=torch.int32)
|
||||
numel = out.numel()
|
||||
if numel == 0:
|
||||
return out
|
||||
|
||||
BLOCK_SIZE = 1024
|
||||
grid = (triton.cdiv(numel, BLOCK_SIZE),)
|
||||
_pack_topk_ids_triton_kernel[grid](
|
||||
topk_ids,
|
||||
topk_weights,
|
||||
out,
|
||||
numel,
|
||||
BLOCK_SIZE=BLOCK_SIZE,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _pack_topk_ids_triton_kernel(
|
||||
topk_ids_ptr,
|
||||
topk_weights_ptr,
|
||||
out_ptr,
|
||||
numel,
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(0)
|
||||
offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
|
||||
mask = offsets < numel
|
||||
|
||||
ids = tl.load(topk_ids_ptr + offsets, mask=mask, other=0)
|
||||
w = tl.load(topk_weights_ptr + offsets, mask=mask, other=0.0)
|
||||
|
||||
w_bf16 = w.to(tl.bfloat16)
|
||||
w_i16 = w_bf16.to(tl.int16, bitcast=True)
|
||||
w_i32 = w_i16.to(tl.int32) & 0xFFFF
|
||||
|
||||
ids_i32 = ids.to(tl.int32)
|
||||
packed = (ids_i32 << 16) | w_i32
|
||||
|
||||
tl.store(out_ptr + offsets, packed, mask=mask)
|
||||
|
||||
|
||||
class Mxfp4FlashinferTrtllmMoEMethod:
|
||||
|
||||
def __init__(self, fp8_method, prefix: str):
|
||||
self._fp8 = fp8_method
|
||||
self.prefix = prefix
|
||||
self.flashinfer_mxfp4_moe_precision = (
|
||||
get_global_server_args().flashinfer_mxfp4_moe_precision
|
||||
)
|
||||
|
||||
def create_moe_runner(self, layer, moe_runner_config):
|
||||
self.moe_runner_config = moe_runner_config
|
||||
|
||||
swiglu_limit = moe_runner_config.swiglu_limit
|
||||
assert (
|
||||
swiglu_limit is not None
|
||||
), f"swiglu_limit must be non-None for DeepSeek V4 (got {swiglu_limit!r})"
|
||||
self._gemm1_clamp_limit_tensor = (
|
||||
torch.full(
|
||||
(layer.num_local_experts,),
|
||||
swiglu_limit,
|
||||
dtype=torch.float32,
|
||||
device=layer.w13_weight.device,
|
||||
)
|
||||
if swiglu_limit is not None
|
||||
else None
|
||||
)
|
||||
|
||||
def create_weights(
|
||||
self,
|
||||
layer,
|
||||
num_experts: int,
|
||||
hidden_size: int,
|
||||
intermediate_size_per_partition: int,
|
||||
params_dtype,
|
||||
**extra_weight_attrs,
|
||||
):
|
||||
from sglang.srt.layers.moe.fused_moe_triton import FusedMoeWeightScaleSupported
|
||||
|
||||
fp4_block_k = 32
|
||||
|
||||
w13_weight = Parameter(
|
||||
torch.empty(
|
||||
num_experts,
|
||||
2 * intermediate_size_per_partition,
|
||||
hidden_size // 2,
|
||||
dtype=torch.int8,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
w2_weight = Parameter(
|
||||
torch.empty(
|
||||
num_experts,
|
||||
hidden_size,
|
||||
intermediate_size_per_partition // 2,
|
||||
dtype=torch.int8,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.register_parameter("w13_weight", w13_weight)
|
||||
set_weight_attrs(w13_weight, extra_weight_attrs)
|
||||
layer.register_parameter("w2_weight", w2_weight)
|
||||
set_weight_attrs(w2_weight, extra_weight_attrs)
|
||||
|
||||
w13_weight_scale = Parameter(
|
||||
torch.ones(
|
||||
num_experts,
|
||||
2 * intermediate_size_per_partition,
|
||||
hidden_size // fp4_block_k,
|
||||
dtype=torch.float32,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
w2_weight_scale = Parameter(
|
||||
torch.ones(
|
||||
num_experts,
|
||||
hidden_size,
|
||||
intermediate_size_per_partition // fp4_block_k,
|
||||
dtype=torch.float32,
|
||||
),
|
||||
requires_grad=False,
|
||||
)
|
||||
w13_weight_scale.format_ue8m0 = False
|
||||
w2_weight_scale.format_ue8m0 = False
|
||||
scale_attrs = dict(extra_weight_attrs)
|
||||
scale_attrs["quant_method"] = FusedMoeWeightScaleSupported.BLOCK.value
|
||||
layer.register_parameter("w13_weight_scale_inv", w13_weight_scale)
|
||||
set_weight_attrs(w13_weight_scale, scale_attrs)
|
||||
layer.register_parameter("w2_weight_scale_inv", w2_weight_scale)
|
||||
set_weight_attrs(w2_weight_scale, scale_attrs)
|
||||
|
||||
def process_weights_after_loading(self, layer: Module) -> None:
|
||||
from sglang.srt.layers.quantization.utils import reorder_w1w3_to_w3w1
|
||||
|
||||
self._fp8.process_weights_after_loading(layer)
|
||||
|
||||
if getattr(layer, "_mega_moe_weights_built", False):
|
||||
return
|
||||
|
||||
w13_w, w13_s = reorder_w1w3_to_w3w1(
|
||||
layer.w13_weight.data, layer.w13_weight_scale_inv.data
|
||||
)
|
||||
layer.w13_weight = Parameter(w13_w, requires_grad=False)
|
||||
layer.w13_weight_scale_inv = Parameter(w13_s, requires_grad=False)
|
||||
|
||||
log_info_on_rank0(
|
||||
logger,
|
||||
f"Shuffling FP4 expert weights for TRT-LLM MxFP4 kernel "
|
||||
f"(layer: {self.prefix})...",
|
||||
)
|
||||
|
||||
w13 = layer.w13_weight.data
|
||||
w2 = layer.w2_weight.data
|
||||
w13_scale = layer.w13_weight_scale_inv.data
|
||||
w2_scale = layer.w2_weight_scale_inv.data
|
||||
num_experts = w13.shape[0]
|
||||
|
||||
if w13_scale.dtype == torch.float32:
|
||||
w13_scale = w13_scale.to(torch.float8_e8m0fnu)
|
||||
w2_scale = w2_scale.to(torch.float8_e8m0fnu)
|
||||
|
||||
epilogue_tile_m = 128
|
||||
g1_w, g1_s, g2_w, g2_s = [], [], [], []
|
||||
if _USE_OFFICIAL_SHUFFLE:
|
||||
cache: dict = {}
|
||||
for i in range(num_experts):
|
||||
w13_u8 = w13[i].view(torch.uint8)
|
||||
w13_s_u8 = w13_scale[i].view(torch.uint8)
|
||||
w2_u8 = w2[i].view(torch.uint8)
|
||||
w2_s_u8 = w2_scale[i].view(torch.uint8)
|
||||
|
||||
perm = _maybe_get_cached_w3_w1_permute_indices(
|
||||
cache,
|
||||
w13_u8,
|
||||
epilogue_tile_m,
|
||||
)
|
||||
g1_w.append(w13_u8[perm.to(w13_u8.device)].contiguous())
|
||||
perm_sf = _maybe_get_cached_w3_w1_permute_indices(
|
||||
cache,
|
||||
w13_s_u8,
|
||||
epilogue_tile_m,
|
||||
num_elts_per_sf=16,
|
||||
)
|
||||
g1_s.append(
|
||||
block_scale_interleave(
|
||||
w13_s_u8[perm_sf.to(w13_s_u8.device)].contiguous()
|
||||
)
|
||||
)
|
||||
|
||||
perm = get_w2_permute_indices_with_cache(
|
||||
cache,
|
||||
w2_u8,
|
||||
epilogue_tile_m,
|
||||
)
|
||||
g2_w.append(w2_u8[perm.to(w2_u8.device)].contiguous())
|
||||
perm_sf = get_w2_permute_indices_with_cache(
|
||||
cache,
|
||||
w2_s_u8,
|
||||
epilogue_tile_m,
|
||||
num_elts_per_sf=16,
|
||||
)
|
||||
g2_s.append(
|
||||
block_scale_interleave(
|
||||
w2_s_u8[perm_sf.to(w2_s_u8.device)].contiguous()
|
||||
)
|
||||
)
|
||||
else:
|
||||
for i in range(num_experts):
|
||||
g1_w.append(shuffle_matrix_a(w13[i].view(torch.uint8), epilogue_tile_m))
|
||||
g1_s.append(
|
||||
shuffle_matrix_sf_a(w13_scale[i].view(torch.uint8), epilogue_tile_m)
|
||||
)
|
||||
g2_w.append(shuffle_matrix_a(w2[i].view(torch.uint8), epilogue_tile_m))
|
||||
g2_s.append(
|
||||
shuffle_matrix_sf_a(w2_scale[i].view(torch.uint8), epilogue_tile_m)
|
||||
)
|
||||
|
||||
layer.w13_weight = Parameter(torch.stack(g1_w), requires_grad=False)
|
||||
layer.w13_weight_scale_inv = Parameter(
|
||||
torch.stack(g1_s)
|
||||
.view(torch.float8_e4m3fn)
|
||||
.reshape(num_experts, w13.shape[1], -1),
|
||||
requires_grad=False,
|
||||
)
|
||||
layer.w2_weight = Parameter(torch.stack(g2_w), requires_grad=False)
|
||||
layer.w2_weight_scale_inv = Parameter(
|
||||
torch.stack(g2_s)
|
||||
.view(torch.float8_e4m3fn)
|
||||
.reshape(num_experts, w2.shape[1], -1),
|
||||
requires_grad=False,
|
||||
)
|
||||
|
||||
self._register_static_scale_ones(layer)
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
def _register_static_scale_ones(self, layer: Module) -> None:
|
||||
device = layer.w13_weight.device
|
||||
for name in (
|
||||
"output1_scale_scalar",
|
||||
"output1_scale_gate_scalar",
|
||||
"output2_scale_scalar",
|
||||
):
|
||||
layer.register_buffer(
|
||||
name,
|
||||
torch.ones(layer.num_local_experts, device=device, dtype=torch.float32),
|
||||
persistent=False,
|
||||
)
|
||||
|
||||
def apply(
|
||||
self,
|
||||
layer: Module,
|
||||
dispatch_output: DispatchOutput,
|
||||
) -> CombineInput:
|
||||
from sglang.srt.layers.moe.token_dispatcher import StandardCombineInput
|
||||
from sglang.srt.layers.moe.topk import TopKOutputChecker
|
||||
|
||||
hidden_states = dispatch_output.hidden_states
|
||||
topk_output = dispatch_output.topk_output
|
||||
|
||||
w13 = layer.w13_weight
|
||||
w2 = layer.w2_weight
|
||||
w13_scale = layer.w13_weight_scale_inv
|
||||
w2_scale = layer.w2_weight_scale_inv
|
||||
|
||||
intermediate_size = w2.shape[2] * 2 if w2.dtype == torch.uint8 else w2.shape[2]
|
||||
hidden_size = w13.shape[2] * 2 if w13.dtype == torch.uint8 else w13.shape[2]
|
||||
|
||||
num_local_experts = layer.num_local_experts
|
||||
if w13_scale.dim() == 2:
|
||||
w13_scale = w13_scale.reshape(num_local_experts, 2 * intermediate_size, -1)
|
||||
if w2_scale.dim() == 2:
|
||||
w2_scale = w2_scale.reshape(num_local_experts, hidden_size, -1)
|
||||
|
||||
if TopKOutputChecker.format_is_standard(topk_output):
|
||||
topk_ids = topk_output.topk_ids
|
||||
topk_weights = topk_output.topk_weights
|
||||
elif TopKOutputChecker.format_is_bypassed(topk_output):
|
||||
raise NotImplementedError(
|
||||
"the old code in this branch is WRONG. e.g. it does not consider HashTopK, and may miss args"
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported topk output format: {topk_output.format}")
|
||||
|
||||
packed_topk = PackTopkIds.execute(topk_ids, topk_weights)
|
||||
|
||||
precision = self.flashinfer_mxfp4_moe_precision
|
||||
if precision == "bf16":
|
||||
assert hidden_states.dtype == torch.bfloat16
|
||||
x_quant = hidden_states
|
||||
x_scale = None
|
||||
origin_dim = x_quant.shape[-1]
|
||||
if hidden_size != origin_dim:
|
||||
x_quant = torch.nn.functional.pad(
|
||||
x_quant,
|
||||
(0, hidden_size - origin_dim),
|
||||
mode="constant",
|
||||
value=0.0,
|
||||
)
|
||||
elif precision == "default":
|
||||
x_quant, x_scale = mxfp8_quantize(
|
||||
hidden_states, False, alignment=hidden_size
|
||||
)
|
||||
x_scale = x_scale.view(torch.float8_e4m3fn).reshape(
|
||||
*hidden_states.shape[:-1], -1
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(f"Unsupported mxfp4 moe precision: {precision}")
|
||||
|
||||
with use_symmetric_memory(
|
||||
get_tp_group(), disabled=not is_allocation_symmetric()
|
||||
):
|
||||
num_tokens = x_quant.shape[0]
|
||||
out_hidden_size = (
|
||||
x_quant.shape[-1] * 2
|
||||
if x_quant.dtype == torch.uint8
|
||||
else x_quant.shape[-1]
|
||||
)
|
||||
symm_output = torch.empty(
|
||||
num_tokens, out_hidden_size, dtype=torch.bfloat16, device=x_quant.device
|
||||
)
|
||||
|
||||
output = trtllm_fp4_block_scale_routed_moe(
|
||||
topk_ids=packed_topk,
|
||||
routing_bias=None,
|
||||
hidden_states=x_quant,
|
||||
hidden_states_scale=x_scale,
|
||||
gemm1_weights=w13,
|
||||
gemm1_weights_scale=w13_scale,
|
||||
gemm1_bias=None,
|
||||
gemm1_alpha=None,
|
||||
gemm1_beta=None,
|
||||
gemm1_clamp_limit=self._gemm1_clamp_limit_tensor,
|
||||
gemm2_weights=w2,
|
||||
gemm2_weights_scale=w2_scale,
|
||||
gemm2_bias=None,
|
||||
output1_scale_scalar=layer.output1_scale_scalar,
|
||||
output1_scale_gate_scalar=layer.output1_scale_gate_scalar,
|
||||
output2_scale_scalar=layer.output2_scale_scalar,
|
||||
num_experts=layer.num_experts,
|
||||
top_k=packed_topk.shape[1],
|
||||
n_group=1,
|
||||
topk_group=1,
|
||||
intermediate_size=intermediate_size,
|
||||
local_expert_offset=layer.moe_ep_rank * layer.num_local_experts,
|
||||
local_num_experts=num_local_experts,
|
||||
routed_scaling_factor=1.0,
|
||||
routing_method_type=int(RoutingMethodType.TopK),
|
||||
do_finalize=True,
|
||||
tune_max_num_tokens=next_power_of_2(x_quant.shape[0]),
|
||||
output=symm_output,
|
||||
)[0]
|
||||
|
||||
return StandardCombineInput(hidden_states=output)
|
||||
|
||||
|
||||
def maybe_fuse_routed_scale_and_shared_add(
|
||||
experts,
|
||||
routed: torch.Tensor,
|
||||
shared: torch.Tensor | None,
|
||||
routed_scaling_factor: float,
|
||||
) -> torch.Tensor:
|
||||
# When MxFP4 fusion is on, the upstream `routed *= scale` is skipped and
|
||||
# the scaling is folded into the shared-add via `shared.add_(routed,
|
||||
# alpha=scale)`. With no shared output, the missing scale is applied
|
||||
# in-place. Otherwise `routed` is already scale-final and we just add
|
||||
# `shared` (or pass through if there is none).
|
||||
from sglang.srt.layers.quantization.mxfp4_marlin_moe import (
|
||||
Mxfp4MarlinMoEMethod,
|
||||
)
|
||||
|
||||
fused = isinstance(
|
||||
experts.quant_method, (Mxfp4FlashinferTrtllmMoEMethod, Mxfp4MarlinMoEMethod)
|
||||
)
|
||||
if fused:
|
||||
if shared is not None:
|
||||
return shared.add_(routed, alpha=routed_scaling_factor)
|
||||
return routed.mul_(routed_scaling_factor)
|
||||
if shared is not None:
|
||||
routed += shared
|
||||
return routed
|
||||
@@ -0,0 +1,114 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
from torch.nn import Module
|
||||
|
||||
from sglang.srt.layers.moe.moe_runner.marlin import MarlinMoeQuantInfo
|
||||
from sglang.srt.layers.moe.utils import MoeRunnerBackend
|
||||
from sglang.srt.utils import log_info_on_rank0
|
||||
from sglang.srt.utils.common import is_sm90_supported
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.layers.moe.token_dispatcher import CombineInput, DispatchOutput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Mxfp4MarlinMoEMethod:
|
||||
"""MXFP4 (E8M0 scales) MoE quantization method using the Marlin backend."""
|
||||
|
||||
def __init__(self, fp8_method, prefix: str):
|
||||
self._fp8 = fp8_method
|
||||
self.prefix = prefix
|
||||
|
||||
def create_moe_runner(self, layer, moe_runner_config):
|
||||
from sglang.srt.layers.moe.moe_runner import MoeRunner
|
||||
|
||||
self.runner = MoeRunner(MoeRunnerBackend.MARLIN, moe_runner_config)
|
||||
|
||||
def create_weights(
|
||||
self,
|
||||
layer: Module,
|
||||
num_experts: int,
|
||||
hidden_size: int,
|
||||
intermediate_size_per_partition: int,
|
||||
params_dtype: torch.dtype,
|
||||
**extra_weight_attrs,
|
||||
):
|
||||
# Delegate to the underlying FP8 method for weight creation —
|
||||
# the raw weight shapes are the same; only post-loading processing differs.
|
||||
self._fp8.create_weights(
|
||||
layer,
|
||||
num_experts,
|
||||
hidden_size,
|
||||
intermediate_size_per_partition,
|
||||
params_dtype,
|
||||
**extra_weight_attrs,
|
||||
)
|
||||
|
||||
def process_weights_after_loading(self, layer: Module) -> None:
|
||||
from sglang.srt.layers.quantization.marlin_utils import (
|
||||
check_moe_marlin_supports_layer,
|
||||
)
|
||||
from sglang.srt.layers.quantization.marlin_utils_fp4 import (
|
||||
prepare_moe_mxfp4_layer_for_marlin,
|
||||
)
|
||||
|
||||
# Let the FP8 base method handle ROCm normalization, etc.
|
||||
self._fp8.process_weights_after_loading(layer)
|
||||
|
||||
if getattr(layer, "_mega_moe_weights_built", False):
|
||||
return
|
||||
|
||||
if not is_sm90_supported():
|
||||
raise RuntimeError(
|
||||
"DeepSeekV4 MXFP4 Marlin fallback requires Hopper/SM90 or above."
|
||||
)
|
||||
if not check_moe_marlin_supports_layer(layer, 32):
|
||||
raise RuntimeError(
|
||||
"Current DeepSeekV4 MoE layer does not satisfy Marlin constraints."
|
||||
)
|
||||
|
||||
# NOTE: the Marlin MoE runner consumes w13 in the checkpoint's
|
||||
# native ``[w1; w3]`` order -- see ``silu_and_mul`` in
|
||||
# fused_marlin_moe.py which expects ``gate = intermediate[:, :N]``
|
||||
# (first half) and ``up = intermediate[:, N:]`` (second half).
|
||||
# Unlike the flashinfer trtllm_fp4 kernel (which wants [w3, w1]),
|
||||
# we must *not* call ``reorder_w1w3_to_w3w1`` here.
|
||||
|
||||
log_info_on_rank0(
|
||||
logger,
|
||||
f"Preparing DeepSeekV4 MXFP4 experts for Marlin backend "
|
||||
f"(layer: {self.prefix})...",
|
||||
)
|
||||
prepare_moe_mxfp4_layer_for_marlin(layer)
|
||||
layer._dsv4_mxfp4_backend = "marlin"
|
||||
|
||||
def apply(
|
||||
self,
|
||||
layer: Module,
|
||||
dispatch_output: DispatchOutput,
|
||||
) -> CombineInput:
|
||||
from sglang.srt.layers.moe.token_dispatcher.standard import StandardCombineInput
|
||||
from sglang.srt.layers.moe.topk import TopKOutputChecker
|
||||
|
||||
topk_output = dispatch_output.topk_output
|
||||
if not TopKOutputChecker.format_is_standard(topk_output):
|
||||
raise ValueError(f"Unsupported topk output format: {topk_output.format}")
|
||||
|
||||
quant_info = MarlinMoeQuantInfo(
|
||||
w13_qweight=layer.w13_weight,
|
||||
w2_qweight=layer.w2_weight,
|
||||
w13_scales=layer.w13_weight_scale_inv,
|
||||
w2_scales=layer.w2_weight_scale_inv,
|
||||
w13_g_idx_sort_indices=None,
|
||||
w2_g_idx_sort_indices=None,
|
||||
weight_bits=4,
|
||||
is_k_full=True,
|
||||
)
|
||||
runner_output = self.runner.run(dispatch_output, quant_info=quant_info)
|
||||
|
||||
return StandardCombineInput(hidden_states=runner_output.hidden_states)
|
||||
@@ -38,6 +38,21 @@ def pad_or_narrow_weight(
|
||||
)
|
||||
|
||||
|
||||
def is_strict_contiguous(x: torch.Tensor) -> bool:
|
||||
expected_stride = 1
|
||||
for size, stride in zip(reversed(x.shape), reversed(x.stride())):
|
||||
if stride != expected_stride:
|
||||
return False
|
||||
expected_stride *= size
|
||||
return True
|
||||
|
||||
|
||||
def strict_contiguous(x: torch.Tensor) -> torch.Tensor:
|
||||
if is_strict_contiguous(x):
|
||||
return x
|
||||
return x.clone(memory_format=torch.contiguous_format)
|
||||
|
||||
|
||||
def copy_or_rebind_param(
|
||||
module: torch.nn.Module, name: str, new_value: torch.Tensor
|
||||
) -> None:
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# to be combined with the sparse coordinator class and sparse algorithm family
|
||||
|
||||
import logging
|
||||
from typing import List, NamedTuple
|
||||
from typing import List, NamedTuple, Union
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.managers.schedule_batch import Req
|
||||
from sglang.srt.mem_cache.hisparse_memory_pool import (
|
||||
DeepSeekV4HiSparseTokenToKVPoolAllocator,
|
||||
DeepSeekV4SingleKVPoolHost,
|
||||
HiSparseNSATokenToKVPool,
|
||||
HiSparseTokenToKVPoolAllocator,
|
||||
)
|
||||
@@ -15,7 +17,10 @@ from sglang.srt.utils import get_device_module
|
||||
|
||||
device_module = get_device_module()
|
||||
|
||||
from sglang.jit_kernel.hisparse import load_cache_to_device_buffer_mla
|
||||
from sglang.jit_kernel.hisparse import (
|
||||
load_cache_to_device_buffer_dsv4_mla,
|
||||
load_cache_to_device_buffer_mla,
|
||||
)
|
||||
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,11 +43,14 @@ class HiSparseCoordinator:
|
||||
def __init__(
|
||||
self,
|
||||
req_to_token_pool: ReqToTokenPool,
|
||||
token_to_kv_pool_allocator: HiSparseTokenToKVPoolAllocator,
|
||||
token_to_kv_pool_allocator: Union[
|
||||
HiSparseTokenToKVPoolAllocator,
|
||||
DeepSeekV4HiSparseTokenToKVPoolAllocator,
|
||||
],
|
||||
top_k: int,
|
||||
device_buffer_size: int,
|
||||
device: str,
|
||||
tp_group: torch.distributed.ProcessGroup,
|
||||
tp_group,
|
||||
host_to_device_ratio: int = 2,
|
||||
):
|
||||
self.req_to_token_pool = req_to_token_pool
|
||||
@@ -50,21 +58,43 @@ class HiSparseCoordinator:
|
||||
self.top_k = top_k
|
||||
self.device_buffer_size = device_buffer_size
|
||||
self.device = device
|
||||
self.compress_ratio = self.token_to_kv_pool_allocator.compress_ratio
|
||||
|
||||
self.mem_pool_device: HiSparseNSATokenToKVPool = (
|
||||
self.token_to_kv_pool_allocator.get_kvcache()
|
||||
)
|
||||
self.mem_pool_host = MLATokenToKVPoolHost(
|
||||
device_pool=self.mem_pool_device,
|
||||
host_to_device_ratio=host_to_device_ratio,
|
||||
host_size=0,
|
||||
page_size=1, # for simplicity, we set page size to 1 to enable backup one token at a time
|
||||
layout="layer_first",
|
||||
override_kv_cache_dim=self.mem_pool_device.kv_cache_dim,
|
||||
self.is_dsv4_hisparse = isinstance(
|
||||
self.token_to_kv_pool_allocator, DeepSeekV4HiSparseTokenToKVPoolAllocator
|
||||
)
|
||||
if self.is_dsv4_hisparse:
|
||||
self.mem_pool_device = self.token_to_kv_pool_allocator.hisparse_kvcache
|
||||
host_size = self.token_to_kv_pool_allocator.size_full // self.compress_ratio
|
||||
self.mem_pool_host = DeepSeekV4SingleKVPoolHost(
|
||||
self.mem_pool_device, host_size, 1
|
||||
)
|
||||
self.item_size_bytes = (
|
||||
self.mem_pool_host.kv_cache_total_dim
|
||||
* self.mem_pool_host.dtype.itemsize
|
||||
)
|
||||
else:
|
||||
assert isinstance(
|
||||
self.token_to_kv_pool_allocator, HiSparseTokenToKVPoolAllocator
|
||||
)
|
||||
self.mem_pool_device: HiSparseNSATokenToKVPool = (
|
||||
self.token_to_kv_pool_allocator.get_kvcache()
|
||||
)
|
||||
self.mem_pool_host = MLATokenToKVPoolHost(
|
||||
device_pool=self.mem_pool_device,
|
||||
host_to_device_ratio=host_to_device_ratio,
|
||||
host_size=0,
|
||||
page_size=1,
|
||||
layout="layer_first",
|
||||
override_kv_cache_dim=self.mem_pool_device.kv_cache_dim,
|
||||
)
|
||||
self.item_size_bytes = self.mem_pool_host.token_stride_size
|
||||
|
||||
max_num_reqs = req_to_token_pool.req_to_token.shape[0]
|
||||
max_num_req_slots = req_to_token_pool.req_to_token.shape[0]
|
||||
max_context_len = req_to_token_pool.max_context_len
|
||||
max_compressed_context_len = (
|
||||
max_context_len + self.compress_ratio - 1
|
||||
) // self.compress_ratio
|
||||
|
||||
# to have an extra page for new tokens
|
||||
self.padded_buffer_size = (
|
||||
@@ -72,13 +102,15 @@ class HiSparseCoordinator:
|
||||
)
|
||||
|
||||
self.req_to_device_buffer = torch.zeros(
|
||||
(max_num_reqs, self.padded_buffer_size), dtype=torch.int64, device=device
|
||||
(max_num_req_slots, self.padded_buffer_size),
|
||||
dtype=torch.int64,
|
||||
device=device,
|
||||
)
|
||||
self.req_device_buffer_size = torch.zeros(
|
||||
max_num_reqs, dtype=torch.int64, device="cpu"
|
||||
max_num_req_slots, dtype=torch.int64, device="cpu"
|
||||
)
|
||||
self.req_to_host_pool = torch.full(
|
||||
(max_num_reqs, max_context_len),
|
||||
(max_num_req_slots, max_compressed_context_len),
|
||||
-1,
|
||||
dtype=torch.int64,
|
||||
device=device,
|
||||
@@ -97,13 +129,13 @@ class HiSparseCoordinator:
|
||||
# initialize data structures for swap-in kernel
|
||||
layer_num = self.mem_pool_device.layer_num
|
||||
self.req_device_buffer_tokens = torch.full(
|
||||
(layer_num, max_num_reqs, self.padded_buffer_size),
|
||||
(layer_num, max_num_req_slots, self.padded_buffer_size),
|
||||
-1,
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
)
|
||||
self.req_device_buffer_token_locs = torch.full(
|
||||
(layer_num, max_num_reqs, self.padded_buffer_size),
|
||||
(layer_num, max_num_req_slots, self.padded_buffer_size),
|
||||
-1,
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
@@ -113,13 +145,19 @@ class HiSparseCoordinator:
|
||||
)
|
||||
self.lru_slots = (
|
||||
self._lru_init.view(1, 1, -1)
|
||||
.repeat(layer_num, max_num_reqs, 1)
|
||||
.repeat(layer_num, max_num_req_slots, 1)
|
||||
.contiguous()
|
||||
)
|
||||
self._device_buffer_arange_i32 = torch.arange(
|
||||
self.device_buffer_size, dtype=torch.int32, device=device
|
||||
)
|
||||
|
||||
# Pre-allocated output buffer for swap_in_selected_pages (CUDA-graph safe)
|
||||
self.top_k_device_locs_buffer = torch.full(
|
||||
(max_num_reqs, self.top_k), -1, dtype=torch.int32, device=device
|
||||
(max_num_req_slots, self.top_k), -1, dtype=torch.int32, device=device
|
||||
)
|
||||
self.raw_indices_buffer = torch.full(
|
||||
(max_num_req_slots, self.top_k), -1, dtype=torch.int32, device=device
|
||||
)
|
||||
# Scalar tensor: number of real (non-padded) requests in the batch.
|
||||
# Updated before each graph replay so padded blocks early-return.
|
||||
@@ -127,7 +165,7 @@ class HiSparseCoordinator:
|
||||
|
||||
# CPU flag: True means "skip backup on the next decode step" because
|
||||
# staging already backed up all prefill tokens. Cleared after one step.
|
||||
self._skip_first_backup = [False] * max_num_reqs
|
||||
self._skip_first_backup = [False] * max_num_req_slots
|
||||
|
||||
def set_decode_producer_stream(self, stream) -> None:
|
||||
self.decode_producer_stream = stream
|
||||
@@ -151,11 +189,14 @@ class HiSparseCoordinator:
|
||||
|
||||
def admit_request_into_staging(self, req: Req) -> None:
|
||||
req.hisparse_staging = True
|
||||
logical_indices = self.req_to_token_pool.req_to_token[
|
||||
|
||||
full_kv_indices = self.req_to_token_pool.req_to_token[
|
||||
req.req_pool_idx, : len(req.fill_ids)
|
||||
]
|
||||
device_indices = self.mem_pool_device._translate_loc_to_hisparse_device(
|
||||
logical_indices
|
||||
].to(dtype=torch.int64, copy=True)
|
||||
device_indices = (
|
||||
self.mem_pool_device.translate_loc_from_full_to_hisparse_device(
|
||||
full_kv_indices
|
||||
)
|
||||
)
|
||||
|
||||
prefill_len = len(device_indices)
|
||||
@@ -178,7 +219,10 @@ class HiSparseCoordinator:
|
||||
with device_module.stream(self.write_staging_stream):
|
||||
start_event.wait(self.write_staging_stream)
|
||||
self.mem_pool_host.backup_from_device_all_layer(
|
||||
self.mem_pool_device, host_indices, device_indices, io_backend="kernel"
|
||||
self.mem_pool_device,
|
||||
host_indices,
|
||||
device_indices,
|
||||
io_backend="kernel",
|
||||
)
|
||||
finish_event.record()
|
||||
if host_indices.is_cuda:
|
||||
@@ -201,6 +245,12 @@ class HiSparseCoordinator:
|
||||
buffer. In the staging path this is correct (prefill filled the buffer),
|
||||
but here the buffer is empty.
|
||||
"""
|
||||
if self.is_dsv4_hisparse:
|
||||
# TODO(dsv4): wire PD direct-to-host. Needs (a) load_to_device_per_layer
|
||||
raise NotImplementedError(
|
||||
"PD direct-to-host admission is not supported for dsv4 hisparse yet."
|
||||
)
|
||||
|
||||
self.alloc_device_buffer(req)
|
||||
|
||||
if req.kv_allocated_len <= self.device_buffer_size:
|
||||
@@ -211,12 +261,12 @@ class HiSparseCoordinator:
|
||||
self._preload_to_device_buffer(req)
|
||||
else:
|
||||
# Long sequence: reset device_buffer_tokens to -1 so the kernel
|
||||
# sees all slots as empty → every top-k lookup is a miss → host load.
|
||||
# sees all slots as empty -> every top-k lookup is a miss -> host load.
|
||||
self.req_device_buffer_tokens[
|
||||
:, req.req_pool_idx, : self.device_buffer_size
|
||||
] = -1
|
||||
|
||||
req.staging = False
|
||||
req.hisparse_staging = False
|
||||
self._skip_first_backup[req.req_pool_idx] = True
|
||||
logger.debug("HiSparse: admitting request %s directly", req.rid)
|
||||
|
||||
@@ -236,74 +286,52 @@ class HiSparseCoordinator:
|
||||
)
|
||||
|
||||
def alloc_device_buffer(self, req: Req) -> None:
|
||||
allocated_indices = self.req_to_token_pool.req_to_token[
|
||||
req.req_pool_idx, : req.kv_allocated_len
|
||||
]
|
||||
page_size = self.mem_pool_device.page_size
|
||||
# Allocate only enough for current tokens (page-aligned).
|
||||
# When prefill already fills device_buffer_size, include the reserved page.
|
||||
alloc_size = min(
|
||||
((req.kv_allocated_len + page_size - 1) // page_size) * page_size,
|
||||
self.device_buffer_size,
|
||||
)
|
||||
if alloc_size == self.device_buffer_size:
|
||||
if self.is_dsv4_hisparse:
|
||||
allocated_len = len(req.fill_ids)
|
||||
alloc_size = self.padded_buffer_size
|
||||
else:
|
||||
allocated_len = req.kv_allocated_len
|
||||
page_size = self.mem_pool_device.page_size
|
||||
# Allocate only enough for current tokens (page-aligned).
|
||||
# When prefill already fills device_buffer_size, include the reserved page.
|
||||
alloc_size = min(
|
||||
((allocated_len + page_size - 1) // page_size) * page_size,
|
||||
self.device_buffer_size,
|
||||
)
|
||||
if alloc_size == self.device_buffer_size:
|
||||
alloc_size = self.padded_buffer_size
|
||||
|
||||
compressed_logical_indices = (
|
||||
self.mem_pool_device.translate_loc_from_full_to_compressed(
|
||||
self.req_to_token_pool.req_to_token[req.req_pool_idx, :allocated_len]
|
||||
)
|
||||
)
|
||||
compressed_len = len(compressed_logical_indices)
|
||||
|
||||
buffer_indices = self.token_to_kv_pool_allocator.alloc_device_buffer(
|
||||
allocated_indices,
|
||||
alloc_size,
|
||||
compressed_logical_indices, alloc_size
|
||||
)
|
||||
if buffer_indices is None:
|
||||
logger.error(
|
||||
"HiSparse: alloc_device_buffer failed for req %s "
|
||||
"(kv_allocated_len=%d, alloc_size=%d)",
|
||||
"(compressed_len=%d, alloc_size=%d)",
|
||||
req.rid,
|
||||
req.kv_allocated_len,
|
||||
compressed_len,
|
||||
alloc_size,
|
||||
)
|
||||
raise RuntimeError("HiSparse alloc_device_buffer returned None")
|
||||
|
||||
buffer_indices = buffer_indices.to(torch.int32)
|
||||
self.req_to_device_buffer[req.req_pool_idx, :alloc_size] = buffer_indices
|
||||
self.req_device_buffer_size[req.req_pool_idx] = alloc_size
|
||||
|
||||
self.req_device_buffer_tokens[
|
||||
:, req.req_pool_idx, : self.device_buffer_size
|
||||
] = torch.arange(self.device_buffer_size, device=self.device)
|
||||
] = self._device_buffer_arange_i32
|
||||
self.req_device_buffer_token_locs[:, req.req_pool_idx, :alloc_size] = (
|
||||
buffer_indices[:alloc_size]
|
||||
)
|
||||
|
||||
def has_ongoing_staging(self) -> bool:
|
||||
return len(self.ack_staging_queue) > 0
|
||||
|
||||
def collect_ready_reqs(self) -> List[Req]:
|
||||
ready_reqs = []
|
||||
if len(self.ack_staging_queue) == 0:
|
||||
return ready_reqs
|
||||
|
||||
finish_count = 0
|
||||
for _, finish_event, _ in self.ack_staging_queue:
|
||||
if not finish_event.query():
|
||||
break
|
||||
finish_count += 1
|
||||
queue_size = torch.tensor(finish_count, dtype=torch.int, device="cpu")
|
||||
if self.tp_world_size > 1:
|
||||
# synchronize TP workers to make sure the same update to scheduler
|
||||
torch.distributed.all_reduce(
|
||||
queue_size,
|
||||
op=torch.distributed.ReduceOp.MIN,
|
||||
group=self.tp_group,
|
||||
)
|
||||
finish_count = int(queue_size.item())
|
||||
while finish_count > 0:
|
||||
_, _, req = self.ack_staging_queue.pop(0)
|
||||
# prepare device buffer and update req
|
||||
self.alloc_device_buffer(req)
|
||||
req.hisparse_staging = False
|
||||
self._skip_first_backup[req.req_pool_idx] = True
|
||||
finish_count -= 1
|
||||
ready_reqs.append(req)
|
||||
return ready_reqs
|
||||
|
||||
def _grow_device_buffers(
|
||||
self,
|
||||
seq_lens: torch.Tensor,
|
||||
@@ -377,6 +405,38 @@ class HiSparseCoordinator:
|
||||
reserved_positions = (seq_lens - 1).clamp(max=self.device_buffer_size)
|
||||
return self.req_to_device_buffer[req_pool_indices, reserved_positions]
|
||||
|
||||
def has_ongoing_staging(self) -> bool:
|
||||
return len(self.ack_staging_queue) > 0
|
||||
|
||||
def collect_ready_reqs(self) -> List[Req]:
|
||||
ready_reqs: List[Req] = []
|
||||
if len(self.ack_staging_queue) == 0:
|
||||
return ready_reqs
|
||||
|
||||
finish_count = 0
|
||||
for _, finish_event, _ in self.ack_staging_queue:
|
||||
if not finish_event.query():
|
||||
break
|
||||
finish_count += 1
|
||||
queue_size = torch.tensor(finish_count, dtype=torch.int, device="cpu")
|
||||
if self.tp_world_size > 1:
|
||||
# synchronize TP workers to make sure the same update to scheduler
|
||||
torch.distributed.all_reduce(
|
||||
queue_size,
|
||||
op=torch.distributed.ReduceOp.MIN,
|
||||
group=self.tp_group,
|
||||
)
|
||||
finish_count = int(queue_size.item())
|
||||
while finish_count > 0:
|
||||
_, _, req = self.ack_staging_queue.pop(0)
|
||||
# prepare device buffer and update req
|
||||
self.alloc_device_buffer(req)
|
||||
self._skip_first_backup[req.req_pool_idx] = True
|
||||
req.hisparse_staging = False
|
||||
finish_count -= 1
|
||||
ready_reqs.append(req)
|
||||
return ready_reqs
|
||||
|
||||
def map_last_loc_to_buffer(
|
||||
self,
|
||||
seq_lens: torch.Tensor,
|
||||
@@ -389,17 +449,52 @@ class HiSparseCoordinator:
|
||||
self._eager_backup_previous_token(
|
||||
seq_lens, req_pool_indices, seq_lens_cpu, req_pool_indices_cpu
|
||||
)
|
||||
# Grow device buffers if needed and resolve the latest-token slot.
|
||||
reserved_buffer_loc = self._grow_device_buffers(
|
||||
seq_lens, req_pool_indices, seq_lens_cpu, req_pool_indices_cpu
|
||||
|
||||
if not self.is_dsv4_hisparse:
|
||||
# Grow device buffers if needed and resolve the latest-token slot.
|
||||
reserved_buffer_loc = self._grow_device_buffers(
|
||||
seq_lens, req_pool_indices, seq_lens_cpu, req_pool_indices_cpu
|
||||
)
|
||||
self.req_device_buffer_token_locs[
|
||||
:, req_pool_indices, self.device_buffer_size
|
||||
] = reserved_buffer_loc.to(torch.int32)
|
||||
|
||||
# No need to clear prior mappings: the only consumer of the mapping
|
||||
# for past tokens is the swap-in kernel, and it goes through
|
||||
# top_k_device_locs returned by swap_in_selected_pages -- not via
|
||||
# mapping[old_out_cache_loc] -- so stale entries are harmless.
|
||||
compressed_locs = self.token_to_kv_pool_allocator.get_last_loc_compressed(
|
||||
out_cache_loc
|
||||
)
|
||||
self.mem_pool_device.full_to_hisparse_device_index_mapping[
|
||||
compressed_locs
|
||||
] = reserved_buffer_loc
|
||||
return
|
||||
|
||||
active_reqs = seq_lens % self.compress_ratio == 0
|
||||
if not torch.any(active_reqs):
|
||||
return
|
||||
|
||||
active_seq_lens = seq_lens[active_reqs]
|
||||
active_out_cache_loc = out_cache_loc[active_reqs]
|
||||
active_req_pool_indices = req_pool_indices[active_reqs]
|
||||
|
||||
compressed_seq_lens = active_seq_lens // self.compress_ratio
|
||||
reserved_positions = (compressed_seq_lens - 1).clamp(
|
||||
max=self.device_buffer_size
|
||||
)
|
||||
reserved_buffer_loc = self.req_to_device_buffer[
|
||||
active_req_pool_indices, reserved_positions
|
||||
]
|
||||
|
||||
self.req_device_buffer_token_locs[
|
||||
:, req_pool_indices, self.device_buffer_size
|
||||
:, active_req_pool_indices, self.device_buffer_size
|
||||
] = reserved_buffer_loc.to(torch.int32)
|
||||
|
||||
# todo, clear the prior mapping as well
|
||||
self.mem_pool_device.full_to_hisparse_device_index_mapping[out_cache_loc] = (
|
||||
compressed_locs = self.token_to_kv_pool_allocator.get_last_loc_compressed(
|
||||
active_out_cache_loc
|
||||
)
|
||||
self.mem_pool_device.full_to_hisparse_device_index_mapping[compressed_locs] = (
|
||||
reserved_buffer_loc
|
||||
)
|
||||
|
||||
@@ -410,23 +505,29 @@ class HiSparseCoordinator:
|
||||
seq_lens_cpu: torch.Tensor,
|
||||
req_pool_indices_cpu: torch.Tensor,
|
||||
) -> None:
|
||||
"""Back up the previous decode token to host memory.
|
||||
"""Back up the previous compressed token to host memory.
|
||||
|
||||
Every decode step, the token written in the *previous* step must be
|
||||
backed up to host so the swap-in kernel can later recover it.
|
||||
Each newly produced compressed token (one per `compress_ratio` decode
|
||||
steps) must be backed up to host so the swap-in kernel can later
|
||||
recover it.
|
||||
|
||||
The only exception is the first decode step right after staging: all
|
||||
prefill tokens were already backed up during staging, so there is nothing new to save yet.
|
||||
Two cases are skipped:
|
||||
- The first decode step right after staging: all prefill tokens were
|
||||
already backed up during staging, so there is nothing new to save.
|
||||
- Steps where `(seq_len - 1) % compress_ratio != 0`: no new compressed
|
||||
token was produced this step.
|
||||
"""
|
||||
# Build the list of batch positions that need a host backup.
|
||||
# Skip the first decode step after staging (prefill already backed up).
|
||||
# Skip the first decode step after staging (prefill already backed up),
|
||||
# and skip non-aligned steps that did not produce a new compressed token.
|
||||
backup_indices = []
|
||||
for i in range(len(seq_lens_cpu)):
|
||||
req_idx = int(req_pool_indices_cpu[i])
|
||||
if self._skip_first_backup[req_idx]:
|
||||
self._skip_first_backup[req_idx] = False
|
||||
continue
|
||||
backup_indices.append(i)
|
||||
if (int(seq_lens_cpu[i]) - 1) % self.compress_ratio == 0:
|
||||
backup_indices.append(i)
|
||||
|
||||
if not backup_indices:
|
||||
return
|
||||
@@ -434,13 +535,18 @@ class HiSparseCoordinator:
|
||||
backup_indices_gpu = torch.tensor(
|
||||
backup_indices, dtype=torch.int64, device=self.device
|
||||
)
|
||||
# The previous token's position and its device buffer slot:
|
||||
# - short seq: slot = seq_len - 2 (within the regular buffer)
|
||||
# - long seq: slot = device_buffer_size (the reserved slot)
|
||||
actual_token_pos = seq_lens[backup_indices_gpu] - 2
|
||||
buffer_slot = actual_token_pos.clamp(max=self.device_buffer_size)
|
||||
|
||||
backup_req_indices = req_pool_indices[backup_indices_gpu]
|
||||
|
||||
# The previous compressed token's position and its device buffer slot:
|
||||
# compressed_pos = (seq_len - 1) // compress_ratio - 1
|
||||
# - short: slot = compressed_pos (within the regular buffer)
|
||||
# - long: slot = device_buffer_size (the reserved slot)
|
||||
prev_seq_lens = seq_lens[backup_indices_gpu] - 1
|
||||
compressed_prev_seq_lens = prev_seq_lens // self.compress_ratio
|
||||
actual_compressed_pos = compressed_prev_seq_lens - 1
|
||||
|
||||
buffer_slot = actual_compressed_pos.clamp(max=self.device_buffer_size)
|
||||
|
||||
device_locs = self.req_to_device_buffer[backup_req_indices, buffer_slot]
|
||||
|
||||
host_locs = self.mem_pool_host.alloc(len(device_locs))
|
||||
@@ -453,11 +559,9 @@ class HiSparseCoordinator:
|
||||
f"HiSparse host mem pool alloc failed for {len(device_locs)} decode backup tokens"
|
||||
)
|
||||
host_locs = host_locs.to(device=self.device)
|
||||
self.req_to_host_pool[backup_req_indices, actual_token_pos] = host_locs
|
||||
self.req_to_host_pool[backup_req_indices, actual_compressed_pos] = host_locs
|
||||
|
||||
if self._has_pending_backup:
|
||||
self._backup_done_event.wait(device_module.current_stream())
|
||||
self._has_pending_backup = False
|
||||
self.wait_for_pending_backup()
|
||||
schedule_stream = device_module.current_stream()
|
||||
with device_module.stream(self.decode_backup_stream):
|
||||
self.decode_backup_stream.wait_stream(schedule_stream)
|
||||
@@ -474,8 +578,8 @@ class HiSparseCoordinator:
|
||||
host_locs.record_stream(self.decode_backup_stream)
|
||||
if backup_req_indices.is_cuda:
|
||||
backup_req_indices.record_stream(self.decode_backup_stream)
|
||||
if actual_token_pos.is_cuda:
|
||||
actual_token_pos.record_stream(self.decode_backup_stream)
|
||||
if actual_compressed_pos.is_cuda:
|
||||
actual_compressed_pos.record_stream(self.decode_backup_stream)
|
||||
if device_locs.is_cuda:
|
||||
device_locs.record_stream(self.decode_backup_stream)
|
||||
self._has_pending_backup = True
|
||||
@@ -486,20 +590,6 @@ class HiSparseCoordinator:
|
||||
self._backup_done_event.wait(device_module.current_stream())
|
||||
self._has_pending_backup = False
|
||||
|
||||
def get_front_topk_tokens(
|
||||
self,
|
||||
req_pool_indices: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
top_k_indices = self.req_to_device_buffer[req_pool_indices, : self.top_k].to(
|
||||
torch.int32
|
||||
)
|
||||
topk_col_indices = torch.arange(self.top_k, device=self.device).unsqueeze(0)
|
||||
# Mask out positions beyond each request's seq_len
|
||||
mask = topk_col_indices >= seq_lens.unsqueeze(1)
|
||||
top_k_indices[mask] = -1
|
||||
return top_k_indices
|
||||
|
||||
def naive_load_topk(
|
||||
self,
|
||||
req_pool_indices: torch.Tensor,
|
||||
@@ -512,6 +602,10 @@ class HiSparseCoordinator:
|
||||
This is a naive per-request loop implementation for debugging/validation.
|
||||
Production code uses swap_in_selected_pages (JIT CUDA kernel) instead.
|
||||
|
||||
Note: dsv4 hisparse is not supported — DeepSeekV4SingleKVPoolHost has no
|
||||
load_to_device_per_layer and indices live in compressed space. Currently
|
||||
only used as a kernel oracle in test_hisparse_unit.py (non-dsv4 path).
|
||||
|
||||
Args:
|
||||
req_pool_indices: Pool indices for each request. Shape: (num_reqs,)
|
||||
seq_lens: Sequence lengths for each request. Shape: (num_reqs,)
|
||||
@@ -521,6 +615,9 @@ class HiSparseCoordinator:
|
||||
Returns:
|
||||
Device KV cache indices for the selected tokens. Shape: (num_reqs, top_k)
|
||||
"""
|
||||
assert (
|
||||
not self.is_dsv4_hisparse
|
||||
), "naive_load_topk is not implemented for dsv4 hisparse"
|
||||
num_reqs = req_pool_indices.size(0)
|
||||
top_k_indices = torch.full(
|
||||
(num_reqs, self.top_k), -1, dtype=torch.int32, device=self.device
|
||||
@@ -586,7 +683,7 @@ class HiSparseCoordinator:
|
||||
return top_k_indices
|
||||
|
||||
def abort_staging_request(self, req: Req) -> None:
|
||||
"""Remove a request from the staging queue and free its host resources.
|
||||
"""Remove a request from the staging queue and free its host + device resources.
|
||||
|
||||
Must be called when aborting a request that has been admitted into staging
|
||||
but has not yet completed (i.e. req.hisparse_staging is True).
|
||||
@@ -598,8 +695,15 @@ class HiSparseCoordinator:
|
||||
# Wait for any in-flight staging DMA to complete before freeing
|
||||
self.write_staging_stream.synchronize()
|
||||
|
||||
prefill_len = len(req.fill_ids)
|
||||
allocated_locs = self.req_to_token_pool.req_to_token[
|
||||
req.req_pool_idx, :prefill_len
|
||||
]
|
||||
self.token_to_kv_pool_allocator.free_hisparse(allocated_locs)
|
||||
|
||||
# Free host memory that was allocated during admit_request_into_staging
|
||||
host_indices = self.req_to_host_pool[req.req_pool_idx, : req.kv_allocated_len]
|
||||
compressed_len = prefill_len // self.compress_ratio
|
||||
host_indices = self.req_to_host_pool[req.req_pool_idx, :compressed_len]
|
||||
host_indices = host_indices[host_indices >= 0]
|
||||
if host_indices.numel() > 0:
|
||||
self.mem_pool_host.free(host_indices)
|
||||
@@ -617,26 +721,38 @@ class HiSparseCoordinator:
|
||||
# release resources only after the execution of a potential overlapped batch
|
||||
if self.decode_producer_stream is not None:
|
||||
device_module.current_stream().wait_stream(self.decode_producer_stream)
|
||||
if self._has_pending_backup:
|
||||
self._backup_done_event.wait(device_module.current_stream())
|
||||
self._has_pending_backup = False
|
||||
self.wait_for_pending_backup()
|
||||
|
||||
# release memory — only free actually-allocated buffer indices
|
||||
# Use kv_allocated_len (not seqlen): under speculative decoding the
|
||||
# allocator can over-allocate beyond the committed seqlen, and those
|
||||
# extra slots may carry stale mapping entries pointing at buffer slots
|
||||
# we just freed via free_hisparse_indices(all_hi). If left set, the
|
||||
# subsequent release_kv_cache -> allocator.free -> free_hisparse path
|
||||
# re-frees them (double-free into the page allocator's free list).
|
||||
allocated_len = req.kv_allocated_len
|
||||
compressed_len = allocated_len // self.compress_ratio
|
||||
|
||||
# release memory -- only free actually-allocated buffer indices
|
||||
current_cap = int(self.req_device_buffer_size[req.req_pool_idx])
|
||||
buffer_indices = self.req_to_device_buffer[req.req_pool_idx, :current_cap]
|
||||
self.token_to_kv_pool_allocator.free_hisparse_indices(buffer_indices)
|
||||
if current_cap > 0:
|
||||
side_buf_hi = self.req_to_device_buffer[req.req_pool_idx, :current_cap]
|
||||
all_hi = torch.unique(side_buf_hi[side_buf_hi > 0])
|
||||
if all_hi.numel() > 0:
|
||||
self.token_to_kv_pool_allocator.free_hisparse_indices(all_hi)
|
||||
|
||||
allocated_locs = self.req_to_token_pool.req_to_token[
|
||||
req.req_pool_idx, : req.kv_allocated_len
|
||||
req.req_pool_idx, :allocated_len
|
||||
]
|
||||
self.token_to_kv_pool_allocator.full_to_hisparse_device_index_mapping[
|
||||
compressed_locs = self.mem_pool_device.translate_loc_from_full_to_compressed(
|
||||
allocated_locs
|
||||
] = 0
|
||||
)
|
||||
self.mem_pool_device.full_to_hisparse_device_index_mapping[compressed_locs] = 0
|
||||
|
||||
host_indices = self.req_to_host_pool[req.req_pool_idx, : req.kv_allocated_len]
|
||||
host_indices = self.req_to_host_pool[req.req_pool_idx, :compressed_len]
|
||||
host_indices = host_indices[host_indices >= 0]
|
||||
if host_indices.numel() > 0:
|
||||
self.mem_pool_host.free(host_indices)
|
||||
|
||||
# clear req info
|
||||
self.req_device_buffer_tokens[:, req.req_pool_idx, :] = -1
|
||||
self.req_device_buffer_token_locs[:, req.req_pool_idx, :] = -1
|
||||
@@ -649,31 +765,24 @@ class HiSparseCoordinator:
|
||||
def swap_in_selected_pages(
|
||||
self,
|
||||
req_pool_indices: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
compressed_seq_lens: torch.Tensor,
|
||||
top_k_result: torch.Tensor,
|
||||
layer_id: int,
|
||||
) -> torch.Tensor:
|
||||
"""Swap selected top-k tokens into device memory and return their indices."""
|
||||
# The CUDA kernel expects req_pool_indices as int64 and seq_lens as int32 or int64.
|
||||
if req_pool_indices.dtype != torch.int64:
|
||||
raise ValueError(
|
||||
f"req_pool_indices dtype {req_pool_indices.dtype} is not int64 as expected"
|
||||
)
|
||||
if seq_lens.dtype not in (torch.int32, torch.int64):
|
||||
raise ValueError(
|
||||
f"seq_lens dtype {seq_lens.dtype} is not int32 or int64 as expected"
|
||||
)
|
||||
if top_k_result.dtype != torch.int32:
|
||||
raise ValueError(
|
||||
f"top_k_result dtype {top_k_result.dtype} is not int32 as expected"
|
||||
)
|
||||
|
||||
num_reqs = req_pool_indices.size(0)
|
||||
|
||||
top_k_indices = self.top_k_device_locs_buffer[:num_reqs]
|
||||
top_k_indices.fill_(-1)
|
||||
|
||||
# todo, adjustable for performance
|
||||
block_size = 1024
|
||||
load_cache_to_device_buffer_mla(
|
||||
swap_in_fn = (
|
||||
load_cache_to_device_buffer_dsv4_mla
|
||||
if self.is_dsv4_hisparse
|
||||
else load_cache_to_device_buffer_mla
|
||||
)
|
||||
swap_in_fn(
|
||||
top_k_tokens=top_k_result,
|
||||
device_buffer_tokens=self.req_device_buffer_tokens[layer_id],
|
||||
host_cache_locs=self.req_to_host_pool,
|
||||
@@ -682,9 +791,9 @@ class HiSparseCoordinator:
|
||||
device_buffer=self.mem_pool_device.kv_buffer[layer_id],
|
||||
top_k_device_locs=top_k_indices,
|
||||
req_pool_indices=req_pool_indices,
|
||||
seq_lens=seq_lens,
|
||||
seq_lens=compressed_seq_lens,
|
||||
lru_slots=self.lru_slots[layer_id],
|
||||
item_size_bytes=self.mem_pool_host.token_stride_size,
|
||||
item_size_bytes=self.item_size_bytes,
|
||||
num_top_k=self.top_k,
|
||||
hot_buffer_size=self.device_buffer_size,
|
||||
page_size=1,
|
||||
|
||||
@@ -742,6 +742,8 @@ class Req(ReqDllmMixin):
|
||||
self.storage_hit_length = 0
|
||||
# The node to lock until for swa radix tree lock ref
|
||||
self.swa_uuid_for_lock: Optional[int] = None
|
||||
# Whether the prefill-time SWA tree lock has been released early
|
||||
self.swa_prefix_lock_released: bool = False
|
||||
# The prefix length that is inserted into the tree cache
|
||||
self.cache_protected_len: int = 0
|
||||
|
||||
@@ -1239,6 +1241,7 @@ class Req(ReqDllmMixin):
|
||||
self.last_node = None
|
||||
self.cache_protected_len = 0
|
||||
self.swa_uuid_for_lock = None
|
||||
self.swa_prefix_lock_released = False
|
||||
self.extend_input_len = 0
|
||||
self.is_retracted = True
|
||||
self.retracted_stain = True
|
||||
@@ -1524,7 +1527,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
|
||||
if isinstance(token_to_kv_pool_allocator, SWATokenToKVPoolAllocator):
|
||||
is_hybrid_swa = True
|
||||
|
||||
return cls(
|
||||
batch = cls(
|
||||
reqs=reqs,
|
||||
req_to_token_pool=req_to_token_pool,
|
||||
token_to_kv_pool_allocator=token_to_kv_pool_allocator,
|
||||
@@ -1544,6 +1547,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
|
||||
chunked_req=chunked_req,
|
||||
dllm_config=dllm_config,
|
||||
)
|
||||
return batch
|
||||
|
||||
def batch_size(self):
|
||||
return len(self.reqs)
|
||||
@@ -2223,7 +2227,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
|
||||
def release_req(self, idx: int, remaing_req_count: int, server_args: ServerArgs):
|
||||
req = self.reqs[idx]
|
||||
|
||||
if self.hisparse_coordinator is not None:
|
||||
if self.hisparse_coordinator is not None and not req.finished():
|
||||
self.hisparse_coordinator.retract_req(req)
|
||||
|
||||
if server_args.disaggregation_mode == "decode":
|
||||
@@ -2634,6 +2638,11 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
|
||||
sliding_window_size = self.tree_cache.sliding_window_size
|
||||
server_args = get_global_server_args()
|
||||
|
||||
release_leaf_lock = (
|
||||
envs.SGLANG_OPT_SWA_RELEASE_LEAF_LOCK_AFTER_WINDOW.get()
|
||||
and hasattr(self.tree_cache, "dec_swa_lock_only")
|
||||
)
|
||||
|
||||
# Eviction_interval: trade-off between SWA token waste and eviction overhead
|
||||
page_size = self.tree_cache.page_size
|
||||
eviction_interval = max(
|
||||
@@ -2651,6 +2660,22 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
|
||||
# 2. Evict swa every eviction_interval tokens to reduce the overhead.
|
||||
if req.decode_batch_idx % eviction_interval == 1:
|
||||
self._evict_swa(req, req.seqlen - 1)
|
||||
|
||||
# Once the decode position has moved past the sliding window,
|
||||
# the SWA portion of the prefill-time tree lock is no longer
|
||||
# needed by this request. Convert it from protected to
|
||||
# evictable so SWA LRU can reclaim it under pressure.
|
||||
if (
|
||||
release_leaf_lock
|
||||
and not req.swa_prefix_lock_released
|
||||
and req.swa_uuid_for_lock is not None
|
||||
and req.last_node is not None
|
||||
and req.decode_batch_idx >= sliding_window_size
|
||||
):
|
||||
self.tree_cache.dec_swa_lock_only(
|
||||
req.last_node, req.swa_uuid_for_lock
|
||||
)
|
||||
req.swa_prefix_lock_released = True
|
||||
elif self.forward_mode.is_extend() and self.tree_cache.is_chunk_cache():
|
||||
pre_len = self.prefix_lens[idx]
|
||||
if self.enable_overlap:
|
||||
@@ -2680,7 +2705,8 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
|
||||
# Subtract an extra page_size so the eviction frontier never reaches the
|
||||
# radix tree insert boundary (page_floor(seq_len)). This keeps at least one
|
||||
# page of non-evicted SWA KV for the tree to store as a non-tombstone node,
|
||||
# preserving cache reuse in multi-turn scenarios.
|
||||
# preserving cache reuse in multi-turn scenarios. Without this, leaf nodes
|
||||
# may become tombstoned, causing SWA memory leak.
|
||||
# See also: _insert_helper case 3 in swa_radix_cache.py (defensive counterpart).
|
||||
new_swa_evicted_seqlen = max(
|
||||
req.swa_evicted_seqlen,
|
||||
|
||||
@@ -43,6 +43,9 @@ from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
InsertParams,
|
||||
MatchPrefixParams,
|
||||
)
|
||||
from sglang.srt.mem_cache.hisparse_memory_pool import (
|
||||
DeepSeekV4HiSparseTokenToKVPoolAllocator,
|
||||
)
|
||||
from sglang.srt.mem_cache.radix_cache import RadixCache, RadixKey, TreeNode
|
||||
from sglang.srt.mem_cache.swa_memory_pool import SWATokenToKVPoolAllocator
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
@@ -444,8 +447,11 @@ class PrefillAdder:
|
||||
]
|
||||
)
|
||||
|
||||
# DeepSeek V4 HiSparse wraps an SWATokenToKVPoolAllocator internally and
|
||||
# exposes the full SWA allocator interface.
|
||||
self.is_hybrid_swa = isinstance(
|
||||
self.token_to_kv_pool_allocator, SWATokenToKVPoolAllocator
|
||||
self.token_to_kv_pool_allocator,
|
||||
(SWATokenToKVPoolAllocator, DeepSeekV4HiSparseTokenToKVPoolAllocator),
|
||||
)
|
||||
self.is_hybrid_ssm_cache = self.tree_cache.supports_mamba()
|
||||
|
||||
@@ -753,6 +759,13 @@ class PrefillAdder:
|
||||
return AddReqResult.NO_TOKEN
|
||||
tokens_freed += tokens_occupied
|
||||
|
||||
if (self.prefill_delayer_single_pass is not None) and (
|
||||
not self.prefill_delayer_single_pass.negotiate_should_allow_prefill(
|
||||
local_prefillable=True
|
||||
)
|
||||
):
|
||||
return AddReqResult.OTHER
|
||||
|
||||
if self.dllm_config is not None:
|
||||
if self.rem_dllm_tokens <= 0:
|
||||
return AddReqResult.OTHER
|
||||
@@ -906,6 +919,13 @@ class PrefillAdder:
|
||||
trunc_len // truncation_align_size
|
||||
)
|
||||
|
||||
now_input_len = trunc_len + len(req.prefix_indices)
|
||||
now_input_len = now_input_len // self.page_size * self.page_size
|
||||
trunc_len = now_input_len - len(req.prefix_indices)
|
||||
|
||||
if trunc_len <= 0:
|
||||
return AddReqResult.OTHER
|
||||
|
||||
# Chunked prefill
|
||||
req.set_extend_input_len(trunc_len)
|
||||
req.fill_ids = req.fill_ids[: len(req.prefix_indices) + trunc_len]
|
||||
|
||||
@@ -1192,6 +1192,11 @@ class Scheduler(
|
||||
|
||||
# todo: should we fix this when enabling mtp or it doesn't matter since we only enable mtp in decode node thus we don't transfer draft kvs between P and D?
|
||||
draft_token_to_kv_pool, model_config = self._get_draft_kv_pool()
|
||||
# Default to the target model_config so the MetadataBuffers branches
|
||||
# below can always access it; overridden by the draft model_config
|
||||
# when this node runs a spec module.
|
||||
if model_config is None:
|
||||
model_config = self.model_config
|
||||
|
||||
if (
|
||||
self.disaggregation_mode == DisaggregationMode.DECODE
|
||||
@@ -2419,17 +2424,14 @@ class Scheduler(
|
||||
batch.seq_lens_cpu = torch.tensor(seq_lens, dtype=torch.int64)
|
||||
batch.orig_seq_lens = torch.tensor(seq_lens, dtype=torch.int32, device=device)
|
||||
batch.seq_lens_sum = sum(seq_lens)
|
||||
# output_ids = last generated token, used as input_ids by prepare_for_decode
|
||||
batch.output_ids = torch.tensor(
|
||||
[r.output_ids[-1] for r in reqs], dtype=torch.int64, device=device
|
||||
)
|
||||
|
||||
# Set logprob fields if any request needs them
|
||||
if batch.return_logprob:
|
||||
batch.top_logprobs_nums = [r.top_logprobs_num for r in reqs]
|
||||
batch.token_ids_logprobs = [list(r.origin_input_ids) for r in reqs]
|
||||
|
||||
# Build sampling info from scratch for these requests
|
||||
batch.sampling_info = SamplingBatchInfo.from_schedule_batch(
|
||||
batch, self.model_config.vocab_size
|
||||
)
|
||||
@@ -3512,8 +3514,6 @@ class Scheduler(
|
||||
self.send_to_tokenizer.send_output(AbortReq(rid=req.rid), req)
|
||||
# For disaggregation decode mode, the request in the waiting queue has KV cache allocated.
|
||||
if self.disaggregation_mode == DisaggregationMode.DECODE:
|
||||
if self.enable_hisparse:
|
||||
self.hisparse_coordinator.request_finished(req)
|
||||
release_kv_cache(req, self.tree_cache)
|
||||
# For disaggregation prefill mode, free the metadata buffer index
|
||||
if self.disaggregation_mode == DisaggregationMode.PREFILL:
|
||||
|
||||
@@ -179,11 +179,12 @@ class SchedulerRuntimeCheckerMixin:
|
||||
if self.is_hybrid_swa:
|
||||
pool_stats = self._get_swa_token_info()
|
||||
elif self.is_hybrid_ssm:
|
||||
return self._get_mamba_token_info()
|
||||
elif self.enable_hisparse:
|
||||
return self._get_hisparse_token_info()
|
||||
pool_stats = self._get_mamba_token_info()
|
||||
else:
|
||||
return self._get_token_info()
|
||||
pool_stats = self._get_token_info()
|
||||
|
||||
if self.enable_hisparse:
|
||||
pool_stats = self._get_hisparse_token_info(pool_stats)
|
||||
|
||||
# swa + ssm can coexist: overlay mamba fields onto swa stats
|
||||
if self.is_hybrid_ssm:
|
||||
@@ -208,8 +209,7 @@ class SchedulerRuntimeCheckerMixin:
|
||||
full_evictable_size=evictable_size,
|
||||
)
|
||||
|
||||
def _get_hisparse_token_info(self: Scheduler) -> PoolStats:
|
||||
pool_stats = self._get_token_info()
|
||||
def _get_hisparse_token_info(self: Scheduler, pool_stats: PoolStats) -> PoolStats:
|
||||
if self.enable_hisparse and self.hisparse_coordinator is not None:
|
||||
h = self.hisparse_coordinator.get_token_stats()
|
||||
return dataclasses.replace(
|
||||
@@ -266,6 +266,13 @@ class SchedulerRuntimeCheckerMixin:
|
||||
swa_num_used = self.swa_tokens_per_layer - (
|
||||
swa_available_size + swa_evictable_size
|
||||
)
|
||||
# FIXME(hisparse): host-backup transiently over-releases the device pool
|
||||
# counter, producing negative full_num_used / swa_num_used. We clamp to 0
|
||||
# to keep token_usage / leak checks sane, but the underlying accounting
|
||||
# bug should be fixed so the clamp can go away.
|
||||
if self.enable_hisparse:
|
||||
full_num_used = max(0, full_num_used)
|
||||
swa_num_used = max(0, swa_num_used)
|
||||
full_token_usage = full_num_used / self.full_tokens_per_layer
|
||||
swa_token_usage = swa_num_used / self.swa_tokens_per_layer
|
||||
|
||||
@@ -546,11 +553,13 @@ class SchedulerRuntimeCheckerMixin:
|
||||
if not self.is_fully_idle():
|
||||
return
|
||||
|
||||
# memory leak check
|
||||
has_leak, messages = self._check_all_pools(self.get_pool_stats())
|
||||
if has_leak:
|
||||
self._report_leak("pool", "\n".join(messages))
|
||||
self._check_req_pool()
|
||||
# memory leak check (skipped for hisparse — pool counters intentionally
|
||||
# diverge during host-backup, see _get_swa_token_info clamp).
|
||||
if not self.enable_hisparse:
|
||||
has_leak, messages = self._check_all_pools(self.get_pool_stats())
|
||||
if has_leak:
|
||||
self._report_leak("pool", "\n".join(messages))
|
||||
self._check_req_pool()
|
||||
|
||||
# tree cache sanity check
|
||||
self._check_tree_cache()
|
||||
|
||||
@@ -41,6 +41,7 @@ from sglang.srt.managers.scheduler import GenerationBatchResult
|
||||
from sglang.srt.mem_cache.allocator import BaseTokenToKVPoolAllocator
|
||||
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
|
||||
from sglang.srt.model_executor.pool_configurator import MemoryPoolConfig
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
from sglang.srt.utils import MultiprocessingSerializer, broadcast_pyobj, set_random_seed
|
||||
from sglang.srt.utils.hf_transformers_utils import (
|
||||
@@ -249,9 +250,10 @@ class TpModelWorker(BaseTpWorker):
|
||||
self.is_multi_layer_eagle = is_multi_layer_eagle
|
||||
self.req_to_token_pool = req_to_token_pool
|
||||
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
|
||||
self.memory_pool_config = memory_pool_config
|
||||
self.attn_cp_rank = attn_cp_rank
|
||||
self.moe_dp_rank = moe_dp_rank
|
||||
# Draft worker: target's resolved MemoryPoolConfig (forwarded to ModelRunner).
|
||||
self.memory_pool_config = memory_pool_config
|
||||
|
||||
# MTP model runners
|
||||
self.model_runner_list: List[ModelRunner] = []
|
||||
|
||||
@@ -55,6 +55,10 @@ class BaseTokenToKVPoolAllocator(abc.ABC):
|
||||
self.is_not_in_free_group = True
|
||||
self.free_group = []
|
||||
|
||||
@property
|
||||
def size_full(self):
|
||||
return self.size
|
||||
|
||||
def debug_print(self) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import abc
|
||||
from typing import List, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.mem_cache.memory_pool import KVCache
|
||||
|
||||
|
||||
class BaseSWAKVPool(KVCache):
|
||||
"""ABC for SWA-like KV pools.
|
||||
|
||||
Subclasses expose a `swa_kv_pool` sub-pool plus a full -> swa index
|
||||
mapping. Used by `SWATokenToKVPoolAllocator` and the disagg paths to
|
||||
handle SWA state separately from the full KV state.
|
||||
"""
|
||||
|
||||
swa_kv_pool: KVCache
|
||||
|
||||
@abc.abstractmethod
|
||||
def register_mapping(self, full_to_swa_index_mapping: torch.Tensor) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def translate_loc_from_full_to_swa(self, kv_indices: torch.Tensor) -> torch.Tensor:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_swa_loc(self, loc: torch.Tensor) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_state_buf_infos(self) -> Tuple[List[int], List[int], List[int]]:
|
||||
raise NotImplementedError()
|
||||
@@ -19,6 +19,9 @@ from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
MatchPrefixParams,
|
||||
MatchResult,
|
||||
)
|
||||
from sglang.srt.mem_cache.hisparse_memory_pool import (
|
||||
DeepSeekV4HiSparseTokenToKVPoolAllocator,
|
||||
)
|
||||
from sglang.srt.mem_cache.swa_memory_pool import SWATokenToKVPoolAllocator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -110,7 +113,14 @@ class SWAChunkCache(ChunkCache):
|
||||
"""ChunkCache with support for sliding window attention."""
|
||||
|
||||
def __init__(self, params: CacheInitParams):
|
||||
assert isinstance(params.token_to_kv_pool_allocator, SWATokenToKVPoolAllocator)
|
||||
# DeepSeek V4 HiSparse wraps SWATokenToKVPoolAllocator and exposes the same API.
|
||||
assert isinstance(
|
||||
params.token_to_kv_pool_allocator,
|
||||
(
|
||||
SWATokenToKVPoolAllocator,
|
||||
DeepSeekV4HiSparseTokenToKVPoolAllocator,
|
||||
),
|
||||
)
|
||||
super().__init__(params)
|
||||
|
||||
self.sliding_window_size = params.sliding_window_size
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from contextlib import nullcontext
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE
|
||||
from sglang.srt.mem_cache.utils import maybe_init_custom_mem_pool
|
||||
from sglang.srt.utils.torch_memory_saver_adapter import TorchMemorySaverAdapter
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class KVAndScore:
|
||||
kv_score: torch.Tensor
|
||||
|
||||
@property
|
||||
def kv(self) -> torch.Tensor:
|
||||
return self.kv_score[..., : self._item_size]
|
||||
|
||||
@property
|
||||
def score(self) -> torch.Tensor:
|
||||
return self.kv_score[..., self._item_size :]
|
||||
|
||||
def __post_init__(self):
|
||||
self._item_size = self.kv_score.shape[-1] // 2
|
||||
|
||||
def __getitem__(self, index) -> KVAndScore:
|
||||
return KVAndScore(self.kv_score[index])
|
||||
|
||||
def clear(self):
|
||||
self.kv.zero_()
|
||||
self.score.fill_(float("-inf"))
|
||||
|
||||
|
||||
class CompressStatePool:
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
ring_size: int,
|
||||
overlap: bool,
|
||||
head_dim: int,
|
||||
dtype: torch.dtype,
|
||||
device: str,
|
||||
enable_memory_saver: bool,
|
||||
ratio: int,
|
||||
online: bool = False,
|
||||
):
|
||||
self.ring_size = ring_size
|
||||
|
||||
if online:
|
||||
assert ring_size == 1, "online compress requires ring_size=1"
|
||||
self._size = size + self.ring_size + 1
|
||||
last_dim = 3 * head_dim
|
||||
else:
|
||||
self._size = size + self.ring_size + 1
|
||||
self._size = (self._size + ratio - 1) // ratio * ratio
|
||||
last_dim = 2 * (1 + overlap) * head_dim
|
||||
|
||||
self.memory_saver_adapter = TorchMemorySaverAdapter.create(
|
||||
enable=enable_memory_saver
|
||||
)
|
||||
self.enable_custom_mem_pool, self.custom_mem_pool, _ = (
|
||||
maybe_init_custom_mem_pool(device=device)
|
||||
)
|
||||
|
||||
with self.memory_saver_adapter.region(GPU_MEMORY_TYPE_KV_CACHE):
|
||||
with (
|
||||
torch.cuda.use_mem_pool(self.custom_mem_pool)
|
||||
if self.custom_mem_pool
|
||||
else nullcontext()
|
||||
):
|
||||
self.kv_score_buffer = KVAndScore(
|
||||
torch.empty(
|
||||
(self._size, last_dim),
|
||||
dtype=dtype,
|
||||
device=device,
|
||||
)
|
||||
)
|
||||
if not online:
|
||||
self.kv_score_buffer[-1].clear()
|
||||
@@ -0,0 +1,738 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from contextlib import nullcontext
|
||||
from typing import List, Literal, NamedTuple, Optional, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.deepseek_v4 import fused_store_cache
|
||||
from sglang.srt.constants import GPU_MEMORY_TYPE_KV_CACHE
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.layers.attention.dsv4 import (
|
||||
index_buf_accessor as dsv4_index_buf_accessor,
|
||||
)
|
||||
from sglang.srt.layers.attention.dsv4.index_buf_accessor import NopeFp8RopeBf16Pack
|
||||
from sglang.srt.layers.attention.nsa import index_buf_accessor
|
||||
from sglang.srt.mem_cache.base_swa_memory_pool import BaseSWAKVPool
|
||||
from sglang.srt.mem_cache.deepseek_v4_compress_state import CompressStatePool
|
||||
from sglang.srt.mem_cache.memory_pool import KVCache
|
||||
from sglang.srt.server_args import get_global_server_args
|
||||
from sglang.srt.utils import ceil_div
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ONLINE_C128 = envs.SGLANG_OPT_USE_ONLINE_COMPRESS.get()
|
||||
|
||||
|
||||
def get_compress_state_ring_size(
|
||||
compress_ratio: int, is_speculative: bool = False
|
||||
) -> int:
|
||||
assert compress_ratio in [4, 128], f"Unsupported {compress_ratio = }"
|
||||
# Online c128 keeps a single (max, sum, kv) state per index instead of a
|
||||
# 128-slot ring buffer of raw tokens, so ring_size collapses to 1. Online
|
||||
# is incompatible with speculative decode for now.
|
||||
if compress_ratio == 128 and ONLINE_C128:
|
||||
assert not is_speculative, "online c128 does not support MTP"
|
||||
return 1
|
||||
if is_speculative:
|
||||
return 16 if compress_ratio == 4 else 256
|
||||
else:
|
||||
return 8 if compress_ratio == 4 else 128
|
||||
|
||||
|
||||
class DeepSeekV4SingleKVPool(KVCache):
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
page_size: int,
|
||||
dtype: torch.dtype,
|
||||
qk_nope_head_dim: int,
|
||||
qk_rope_head_dim: int,
|
||||
layer_num: int,
|
||||
device: str,
|
||||
enable_memory_saver: bool,
|
||||
start_layer: Optional[int] = None,
|
||||
end_layer: Optional[int] = None,
|
||||
):
|
||||
super().__init__(
|
||||
size,
|
||||
page_size,
|
||||
dtype,
|
||||
layer_num,
|
||||
device,
|
||||
enable_memory_saver,
|
||||
start_layer,
|
||||
end_layer,
|
||||
)
|
||||
self.qk_nope_head_dim = qk_nope_head_dim
|
||||
self.qk_rope_head_dim = qk_rope_head_dim
|
||||
|
||||
self.scale_pad = 1
|
||||
self.quantize_block_size = 64
|
||||
self.rope_storage_dtype = torch.bfloat16
|
||||
self.k_with_scale_buffer_dtype = torch.int8
|
||||
self._create_buffers()
|
||||
|
||||
def _create_buffers(self):
|
||||
with self.memory_saver_adapter.region(GPU_MEMORY_TYPE_KV_CACHE):
|
||||
with (
|
||||
torch.cuda.use_mem_pool(self.custom_mem_pool)
|
||||
if self.custom_mem_pool
|
||||
else nullcontext()
|
||||
):
|
||||
self.kv_buffer = [
|
||||
self.create_buffer(
|
||||
num_pages=(self.size + self.page_size + 1) // self.page_size,
|
||||
)
|
||||
for _ in range(self.layer_num)
|
||||
]
|
||||
|
||||
def get_bytes_per_token(self) -> int:
|
||||
dim_per_token = (
|
||||
self.qk_nope_head_dim
|
||||
+ self.qk_rope_head_dim * self.rope_storage_dtype.itemsize
|
||||
+ self.qk_nope_head_dim // self.quantize_block_size
|
||||
+ self.scale_pad
|
||||
)
|
||||
return dim_per_token
|
||||
|
||||
def create_buffer(self, *, num_pages: int):
|
||||
bytes_per_token = self.get_bytes_per_token()
|
||||
self.kv_cache_total_dim = bytes_per_token
|
||||
bytes_per_page_non_padded = self.page_size * bytes_per_token
|
||||
self.bytes_per_page_padded = ceil_div(bytes_per_page_non_padded, 576) * 576
|
||||
|
||||
assert bytes_per_token == 448 + 64 * 2 + 8, (
|
||||
"DSV4 KV layout: qk_nope_head_dim FP8 (448) + qk_rope_head_dim BF16 "
|
||||
"(64*2) + nope FP8 scales + scale_pad = 584 bytes/token"
|
||||
)
|
||||
assert self.store_dtype == torch.uint8
|
||||
|
||||
return torch.zeros(
|
||||
num_pages,
|
||||
self.bytes_per_page_padded,
|
||||
dtype=self.store_dtype,
|
||||
device=self.device,
|
||||
)
|
||||
|
||||
def set_key_buffer(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
|
||||
):
|
||||
dsv4_index_buf_accessor.SetKAndS.execute(
|
||||
pool=self,
|
||||
buf=self.kv_buffer[layer_id],
|
||||
loc=loc,
|
||||
nope_fp8_rope_bf16_pack=cache_nope_fp8_rope_bf16_pack,
|
||||
)
|
||||
|
||||
def set_key_buffer_fused(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
) -> None:
|
||||
return fused_store_cache(
|
||||
input=cache_k,
|
||||
cache=self.kv_buffer[layer_id],
|
||||
indices=loc,
|
||||
page_size=self.page_size,
|
||||
type="flashmla",
|
||||
)
|
||||
|
||||
def get_key_buffer(self, layer_id: int):
|
||||
return self.kv_buffer[layer_id]
|
||||
|
||||
def set_kv_buffer(self, *args, **kwargs) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_value_buffer(self, layer_id: int) -> torch.Tensor:
|
||||
raise NotImplementedError("Use get_key_buffer instead.")
|
||||
|
||||
def get_kv_buffer(self, layer_id: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
raise NotImplementedError("Use get_key_buffer instead.")
|
||||
|
||||
|
||||
class HiSparseC4DevicePool(DeepSeekV4SingleKVPool):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
page_size: int,
|
||||
dtype: torch.dtype,
|
||||
qk_nope_head_dim: int,
|
||||
qk_rope_head_dim: int,
|
||||
layer_num: int,
|
||||
device: str,
|
||||
enable_memory_saver: bool,
|
||||
start_layer: int | None = None,
|
||||
end_layer: int | None = None,
|
||||
):
|
||||
super().__init__(
|
||||
size,
|
||||
page_size,
|
||||
dtype,
|
||||
qk_nope_head_dim,
|
||||
qk_rope_head_dim,
|
||||
layer_num,
|
||||
device,
|
||||
enable_memory_saver,
|
||||
start_layer,
|
||||
end_layer,
|
||||
)
|
||||
|
||||
self.data_ptrs = torch.tensor(
|
||||
[x.data_ptr() for x in self.kv_buffer],
|
||||
dtype=torch.uint64,
|
||||
device=self.device,
|
||||
)
|
||||
self.compress_ratio = 4
|
||||
|
||||
def register_mapping(self, full_to_hisparse_device_index_mapping: torch.Tensor):
|
||||
self.full_to_hisparse_device_index_mapping = (
|
||||
full_to_hisparse_device_index_mapping
|
||||
)
|
||||
|
||||
def translate_loc_from_full_to_compressed(self, full_indices: torch.Tensor):
|
||||
mask = (full_indices + 1) % self.compress_ratio == 0
|
||||
compressed_indices = full_indices[mask] // self.compress_ratio
|
||||
return compressed_indices
|
||||
|
||||
def translate_loc_to_hisparse_device(self, compressed_indices: torch.Tensor):
|
||||
return self.full_to_hisparse_device_index_mapping[compressed_indices].to(
|
||||
torch.int32
|
||||
)
|
||||
|
||||
def _translate_loc_to_hisparse_device(self, compressed_indices: torch.Tensor):
|
||||
return self.full_to_hisparse_device_index_mapping[compressed_indices]
|
||||
|
||||
def translate_loc_from_full_to_hisparse_device(self, full_indices: torch.Tensor):
|
||||
return self._translate_loc_to_hisparse_device(
|
||||
self.translate_loc_from_full_to_compressed(full_indices)
|
||||
)
|
||||
|
||||
def set_key_buffer(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_nope_fp8_rope_bf16_pack,
|
||||
):
|
||||
loc = self.translate_loc_to_hisparse_device(loc)
|
||||
super().set_key_buffer(layer_id, loc, cache_nope_fp8_rope_bf16_pack)
|
||||
|
||||
def set_key_buffer_fused(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
) -> None:
|
||||
loc = self.translate_loc_to_hisparse_device(loc)
|
||||
return super().set_key_buffer_fused(layer_id, loc, cache_k)
|
||||
|
||||
def get_cpu_copy(self, indices, mamba_indices=None):
|
||||
raise NotImplementedError("HiSparseC4DevicePool does not support get_cpu_copy")
|
||||
|
||||
def load_cpu_copy(self, kv_cache_cpu, indices, mamba_indices=None):
|
||||
raise NotImplementedError("HiSparseC4DevicePool does not support load_cpu_copy")
|
||||
|
||||
|
||||
class DeepSeekV4IndexerPool(KVCache):
|
||||
quant_block_size = 128
|
||||
index_k_with_scale_buffer_dtype = torch.uint8
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
page_size: int,
|
||||
dtype: torch.dtype,
|
||||
index_head_dim: int,
|
||||
layer_num: int,
|
||||
device: str,
|
||||
enable_memory_saver: bool,
|
||||
start_layer: Optional[int] = None,
|
||||
end_layer: Optional[int] = None,
|
||||
):
|
||||
super().__init__(
|
||||
size,
|
||||
page_size,
|
||||
dtype,
|
||||
layer_num,
|
||||
device,
|
||||
enable_memory_saver,
|
||||
start_layer,
|
||||
end_layer,
|
||||
)
|
||||
self.index_head_dim = index_head_dim
|
||||
|
||||
self._create_buffer()
|
||||
|
||||
def _create_buffer(self):
|
||||
num_scales_per_token = self.index_head_dim // self.quant_block_size
|
||||
page_bytes = self.page_size * self.index_head_dim
|
||||
page_bytes += self.page_size * num_scales_per_token * 4
|
||||
with self.memory_saver_adapter.region(GPU_MEMORY_TYPE_KV_CACHE):
|
||||
with (
|
||||
torch.cuda.use_mem_pool(self.custom_mem_pool)
|
||||
if self.custom_mem_pool
|
||||
else nullcontext()
|
||||
):
|
||||
self.index_k_with_scale_buffer = [
|
||||
torch.zeros(
|
||||
(self.size + self.page_size + 1) // self.page_size,
|
||||
page_bytes,
|
||||
dtype=self.index_k_with_scale_buffer_dtype,
|
||||
device=self.device,
|
||||
)
|
||||
for _ in range(self.layer_num)
|
||||
]
|
||||
|
||||
def get_kv_buffer(self, layer_id: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_key_buffer(self, layer_id: int) -> torch.Tensor:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_value_buffer(self, layer_id: int) -> torch.Tensor:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_kv_buffer(self, *args, **kwargs) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_index_k_with_scale_buffer(self, layer_id: int) -> torch.Tensor:
|
||||
return self.index_k_with_scale_buffer[layer_id]
|
||||
|
||||
def get_index_k_scale_buffer(
|
||||
self,
|
||||
layer_id: int,
|
||||
seq_len: int,
|
||||
page_indices: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
buf = self.index_k_with_scale_buffer[layer_id]
|
||||
return index_buf_accessor.GetKAndS.execute(
|
||||
self, buf, seq_len=seq_len, page_indices=page_indices
|
||||
)
|
||||
|
||||
def set_index_k_scale_buffer(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
index_k: torch.Tensor,
|
||||
index_k_scale: torch.Tensor,
|
||||
) -> None:
|
||||
buf = self.index_k_with_scale_buffer[layer_id - self.start_layer]
|
||||
index_buf_accessor.SetKAndS.execute(
|
||||
pool=self, buf=buf, loc=loc, index_k=index_k, index_k_scale=index_k_scale
|
||||
)
|
||||
|
||||
def set_index_fused(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
) -> None:
|
||||
return fused_store_cache(
|
||||
input=cache_k,
|
||||
cache=self.index_k_with_scale_buffer[layer_id - self.start_layer],
|
||||
indices=loc,
|
||||
page_size=self.page_size,
|
||||
type="indexer",
|
||||
)
|
||||
|
||||
|
||||
class DeepSeekV4LayerItem(NamedTuple):
|
||||
compress_ratio: Literal[0, 4, 128]
|
||||
compress_layer_id: int
|
||||
compress_kv_pool: Optional[DeepSeekV4SingleKVPool] = None
|
||||
|
||||
|
||||
class DeepSeekV4TokenToKVPool(BaseSWAKVPool):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
max_num_reqs: int,
|
||||
swa_size: int,
|
||||
c4_size: int,
|
||||
c128_size: int,
|
||||
c4_state_pool_size: int,
|
||||
c128_state_pool_size: int,
|
||||
page_size: int,
|
||||
swa_page_size: int,
|
||||
dtype: torch.dtype,
|
||||
state_dtype: torch.dtype,
|
||||
qk_nope_head_dim: int,
|
||||
qk_rope_head_dim: int,
|
||||
indexer_head_dim: int,
|
||||
layer_num: int,
|
||||
device: str,
|
||||
enable_memory_saver: bool,
|
||||
compression_ratios: List[int],
|
||||
start_layer: Optional[int] = None,
|
||||
end_layer: Optional[int] = None,
|
||||
enable_hisparse: bool = False,
|
||||
):
|
||||
super().__init__(
|
||||
swa_size,
|
||||
page_size,
|
||||
dtype,
|
||||
layer_num,
|
||||
device,
|
||||
enable_memory_saver,
|
||||
start_layer,
|
||||
end_layer,
|
||||
)
|
||||
c4_logical_size = c128_size * 32
|
||||
|
||||
logger.info(
|
||||
"Initialize DeepSeekV4TokenToKVPool with "
|
||||
f"{max_num_reqs=} {swa_size=} {c4_size=} "
|
||||
f"{c4_logical_size=} {c128_size=} "
|
||||
f"{c4_state_pool_size=} {c128_state_pool_size=}"
|
||||
)
|
||||
|
||||
self.max_num_reqs = max_num_reqs
|
||||
self.c4_size = c4_size
|
||||
self.c4_logical_size = c4_logical_size
|
||||
self.c128_size = c128_size
|
||||
self.c4_state_pool_size = c4_state_pool_size
|
||||
self.c128_state_pool_size = c128_state_pool_size
|
||||
self.state_dtype = state_dtype
|
||||
self.compression_ratios = compression_ratios
|
||||
|
||||
assert page_size % swa_page_size == 0
|
||||
|
||||
self.swa_size = swa_size
|
||||
self.swa_window_size = swa_page_size
|
||||
self.swa_page_size = swa_page_size
|
||||
self.scale_pad = 1
|
||||
|
||||
self.qk_nope_head_dim = qk_nope_head_dim
|
||||
self.qk_rope_head_dim = qk_rope_head_dim
|
||||
self.indexer_head_dim = indexer_head_dim
|
||||
|
||||
c4_layer_num = sum(1 for r in compression_ratios if r == 4)
|
||||
c128_layer_num = sum(1 for r in compression_ratios if r == 128)
|
||||
c4_page_size = page_size // 4
|
||||
c128_page_size = page_size // 128
|
||||
self.swa_kv_pool = DeepSeekV4SingleKVPool(
|
||||
swa_size,
|
||||
swa_page_size,
|
||||
dtype,
|
||||
qk_nope_head_dim,
|
||||
qk_rope_head_dim,
|
||||
layer_num,
|
||||
device,
|
||||
enable_memory_saver,
|
||||
)
|
||||
|
||||
c4_kv_pool_type = DeepSeekV4SingleKVPool
|
||||
if enable_hisparse:
|
||||
c4_kv_pool_type = HiSparseC4DevicePool
|
||||
self.c4_kv_pool = c4_kv_pool_type(
|
||||
c4_size,
|
||||
c4_page_size,
|
||||
dtype,
|
||||
qk_nope_head_dim,
|
||||
qk_rope_head_dim,
|
||||
c4_layer_num,
|
||||
device,
|
||||
enable_memory_saver,
|
||||
)
|
||||
|
||||
self.c128_kv_pool = DeepSeekV4SingleKVPool(
|
||||
c128_size,
|
||||
c128_page_size,
|
||||
dtype,
|
||||
qk_nope_head_dim,
|
||||
qk_rope_head_dim,
|
||||
c128_layer_num,
|
||||
device,
|
||||
enable_memory_saver,
|
||||
)
|
||||
|
||||
self.c4_indexer_kv_pool = DeepSeekV4IndexerPool(
|
||||
self.c4_logical_size,
|
||||
c4_page_size,
|
||||
dtype,
|
||||
indexer_head_dim,
|
||||
c4_layer_num,
|
||||
device,
|
||||
enable_memory_saver,
|
||||
)
|
||||
|
||||
self._init_compressed_layer_mapping()
|
||||
|
||||
self._init_paged_compress_states(enable_memory_saver)
|
||||
|
||||
self._should_cache_swa = envs.SGLANG_OPT_CACHE_SWA_TRANSLATION.get()
|
||||
|
||||
def register_mapping(self, full_to_swa_index_mapping: torch.Tensor):
|
||||
self.full_to_swa_index_mapping = full_to_swa_index_mapping
|
||||
|
||||
def get_ring_size(self, compress_ratio: int) -> int:
|
||||
server_args = get_global_server_args()
|
||||
is_speculative = server_args.speculative_algorithm is not None
|
||||
return get_compress_state_ring_size(compress_ratio, is_speculative)
|
||||
|
||||
def translate_loc_from_full_to_swa(self, kv_indices: torch.Tensor):
|
||||
assert self.full_to_swa_index_mapping is not None
|
||||
|
||||
return self.full_to_swa_index_mapping[kv_indices].to(torch.int32)
|
||||
|
||||
def set_swa_loc(self, loc: torch.Tensor) -> None:
|
||||
# No-op: SWAKVPool's set_swa_loc precomputes SWA-translated loc once per
|
||||
# forward batch for set_kv_buffer to read via self.swa_loc. DSV4 has its
|
||||
# own equivalent cache via `_should_cache_swa + cached_loc` (in
|
||||
# set_swa_key_buffer_radix_fused), so we ignore main's precomputed loc.
|
||||
pass
|
||||
|
||||
def get_contiguous_buf_infos(self) -> Tuple[List[int], List[int], List[int]]:
|
||||
data_ptrs: List[int] = []
|
||||
data_lens: List[int] = []
|
||||
item_lens: List[int] = []
|
||||
|
||||
for bufs in [
|
||||
self.c4_kv_pool.kv_buffer,
|
||||
self.c4_indexer_kv_pool.index_k_with_scale_buffer,
|
||||
self.c128_kv_pool.kv_buffer,
|
||||
]:
|
||||
for buf in bufs:
|
||||
assert buf.ndim == 2, f"expected 2D buffer, got {buf.ndim}D"
|
||||
data_ptrs.append(buf.data_ptr())
|
||||
data_lens.append(buf.nbytes)
|
||||
item_lens.append(buf[0].nbytes)
|
||||
|
||||
return data_ptrs, data_lens, item_lens
|
||||
|
||||
def get_state_buf_infos(self) -> Tuple[List[int], List[int], List[int]]:
|
||||
data_ptrs: List[int] = []
|
||||
data_lens: List[int] = []
|
||||
item_lens: List[int] = []
|
||||
|
||||
for buf in self.swa_kv_pool.kv_buffer:
|
||||
assert buf.ndim == 2, f"expected 2D buffer, got {buf.ndim}D"
|
||||
data_ptrs.append(buf.data_ptr())
|
||||
data_lens.append(buf.nbytes)
|
||||
item_lens.append(buf[0].nbytes)
|
||||
|
||||
for pools in [
|
||||
self.compress_state_pools,
|
||||
self.indexer_compress_state_pools,
|
||||
]:
|
||||
for pool in pools:
|
||||
if pool is None:
|
||||
continue
|
||||
t = pool.kv_score_buffer.kv_score
|
||||
assert t.ndim == 2, f"expected 2D buffer, got {t.ndim}D"
|
||||
data_ptrs.append(t.data_ptr())
|
||||
data_lens.append(t.nbytes)
|
||||
item_lens.append(t[0].nbytes * pool.ring_size)
|
||||
|
||||
return data_ptrs, data_lens, item_lens
|
||||
|
||||
def _init_paged_compress_states(self, enable_memory_saver: bool):
|
||||
c4_state_pool_size = self.c4_state_pool_size
|
||||
c128_state_pool_size = self.c128_state_pool_size
|
||||
self.compress_state_pools: List[CompressStatePool] = []
|
||||
self.indexer_compress_state_pools: List[CompressStatePool] = []
|
||||
|
||||
for ratio in self.compression_ratios:
|
||||
overlap = ratio == 4
|
||||
compress_state_pool = indexer_compress_state_pool = None
|
||||
size = c4_state_pool_size if ratio == 4 else c128_state_pool_size
|
||||
ring_size = self.get_ring_size(ratio) if ratio != 0 else 0
|
||||
if ratio != 0:
|
||||
compress_state_pool = CompressStatePool(
|
||||
size=size,
|
||||
ring_size=ring_size,
|
||||
overlap=overlap,
|
||||
head_dim=self.qk_nope_head_dim + self.qk_rope_head_dim,
|
||||
dtype=self.state_dtype,
|
||||
device=self.device,
|
||||
enable_memory_saver=enable_memory_saver,
|
||||
ratio=ratio,
|
||||
online=(ratio == 128 and ONLINE_C128),
|
||||
)
|
||||
|
||||
if ratio == 4:
|
||||
indexer_compress_state_pool = CompressStatePool(
|
||||
size=size,
|
||||
ring_size=ring_size,
|
||||
overlap=overlap,
|
||||
head_dim=self.indexer_head_dim,
|
||||
device=self.device,
|
||||
dtype=self.state_dtype,
|
||||
enable_memory_saver=enable_memory_saver,
|
||||
ratio=ratio,
|
||||
)
|
||||
|
||||
self.compress_state_pools.append(compress_state_pool)
|
||||
self.indexer_compress_state_pools.append(indexer_compress_state_pool)
|
||||
|
||||
def _init_compressed_layer_mapping(self):
|
||||
c1_cnt, c4_cnt, c128_cnt = 0, 0, 0
|
||||
self.layer_mapping: List[DeepSeekV4LayerItem] = []
|
||||
|
||||
for ratio in self.compression_ratios:
|
||||
if ratio == 0:
|
||||
self.layer_mapping.append(
|
||||
DeepSeekV4LayerItem(
|
||||
compress_ratio=0,
|
||||
compress_layer_id=c1_cnt,
|
||||
)
|
||||
)
|
||||
c1_cnt += 1
|
||||
elif ratio == 4:
|
||||
self.layer_mapping.append(
|
||||
DeepSeekV4LayerItem(
|
||||
compress_ratio=4,
|
||||
compress_layer_id=c4_cnt,
|
||||
compress_kv_pool=self.c4_kv_pool,
|
||||
)
|
||||
)
|
||||
c4_cnt += 1
|
||||
elif ratio == 128:
|
||||
self.layer_mapping.append(
|
||||
DeepSeekV4LayerItem(
|
||||
compress_ratio=128,
|
||||
compress_layer_id=c128_cnt,
|
||||
compress_kv_pool=self.c128_kv_pool,
|
||||
)
|
||||
)
|
||||
c128_cnt += 1
|
||||
else:
|
||||
raise ValueError(f"Unsupported compression ratio: {ratio}")
|
||||
|
||||
def get_attention_compress_states(self, layer_id: int) -> CompressStatePool:
|
||||
compress_state_pool = self.compress_state_pools[layer_id]
|
||||
assert (
|
||||
compress_state_pool is not None
|
||||
), "Only c4/c128 layers have attention states."
|
||||
return compress_state_pool
|
||||
|
||||
def get_indexer_compress_states(self, layer_id: int) -> CompressStatePool:
|
||||
indexer_compress_state_pool = self.indexer_compress_state_pools[layer_id]
|
||||
assert (
|
||||
indexer_compress_state_pool is not None
|
||||
), "Only c4 layers have indexer states."
|
||||
return indexer_compress_state_pool
|
||||
|
||||
def get_swa_key_buffer(self, layer_id: int) -> torch.Tensor:
|
||||
return self.swa_kv_pool.get_key_buffer(layer_id)
|
||||
|
||||
def set_swa_key_buffer(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
|
||||
) -> None:
|
||||
self.swa_kv_pool.set_key_buffer(layer_id, loc, cache_nope_fp8_rope_bf16_pack)
|
||||
|
||||
def get_extra_key_buffer(self, layer_id: int) -> torch.Tensor | None:
|
||||
_, compress_layer_id, compress_kv_pool = self.layer_mapping[layer_id]
|
||||
assert compress_kv_pool is not None
|
||||
return compress_kv_pool.get_key_buffer(compress_layer_id)
|
||||
|
||||
def set_extra_key_buffer(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
|
||||
) -> None:
|
||||
_, compress_layer_id, compress_kv_pool = self.layer_mapping[layer_id]
|
||||
assert compress_kv_pool is not None
|
||||
compress_kv_pool.set_key_buffer(
|
||||
compress_layer_id, loc, cache_nope_fp8_rope_bf16_pack
|
||||
)
|
||||
|
||||
def get_index_k_with_scale_buffer(self, layer_id: int) -> torch.Tensor:
|
||||
compress_ratio, compress_layer_id, _ = self.layer_mapping[layer_id]
|
||||
assert compress_ratio == 4, f"only c4 has indexer, got {compress_ratio = }"
|
||||
return self.c4_indexer_kv_pool.get_index_k_with_scale_buffer(compress_layer_id)
|
||||
|
||||
def get_index_k_scale_buffer(
|
||||
self,
|
||||
layer_id: int,
|
||||
seq_len: int,
|
||||
page_indices: torch.Tensor,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
compress_ratio, compress_layer_id, _ = self.layer_mapping[layer_id]
|
||||
assert compress_ratio == 4, f"only c4 has indexer, got {compress_ratio = }"
|
||||
return self.c4_indexer_kv_pool.get_index_k_scale_buffer(
|
||||
compress_layer_id, seq_len, page_indices
|
||||
)
|
||||
|
||||
def set_index_k_scale_buffer(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
index_k: torch.Tensor,
|
||||
index_k_scale: torch.Tensor,
|
||||
) -> None:
|
||||
compress_ratio, compress_layer_id, _ = self.layer_mapping[layer_id]
|
||||
assert compress_ratio == 4, f"only c4 has indexer, got {compress_ratio = }"
|
||||
self.c4_indexer_kv_pool.set_index_k_scale_buffer(
|
||||
compress_layer_id, loc, index_k, index_k_scale
|
||||
)
|
||||
|
||||
def get_key_buffer(self, layer_id: int) -> torch.Tensor:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_value_buffer(self, layer_id: int) -> torch.Tensor:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_kv_buffer(self, layer_id: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_kv_buffer(self, *args, **kwargs) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_swa_key_buffer_radix(
|
||||
self,
|
||||
layer_id: int,
|
||||
raw_loc: torch.Tensor,
|
||||
cache_nope_fp8_rope_bf16_pack: NopeFp8RopeBf16Pack,
|
||||
) -> None:
|
||||
swa_loc = self.translate_loc_from_full_to_swa(raw_loc)
|
||||
self.swa_kv_pool.set_key_buffer(
|
||||
layer_id, swa_loc, cache_nope_fp8_rope_bf16_pack
|
||||
)
|
||||
|
||||
def get_swa_key_buffer_radix(self, layer_id: int) -> torch.Tensor:
|
||||
return self.swa_kv_pool.get_key_buffer(layer_id)
|
||||
|
||||
def set_swa_key_buffer_radix_fused(
|
||||
self,
|
||||
layer_id: int,
|
||||
raw_loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
) -> None:
|
||||
if self._should_cache_swa:
|
||||
if layer_id == 0:
|
||||
self.cached_loc = self.translate_loc_from_full_to_swa(raw_loc)
|
||||
swa_loc = self.cached_loc
|
||||
else:
|
||||
swa_loc = self.translate_loc_from_full_to_swa(raw_loc)
|
||||
return self.swa_kv_pool.set_key_buffer_fused(layer_id, swa_loc, cache_k)
|
||||
|
||||
def set_extra_key_buffer_fused(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
) -> None:
|
||||
_, compress_layer_id, compress_kv_pool = self.layer_mapping[layer_id]
|
||||
assert compress_kv_pool is not None
|
||||
return compress_kv_pool.set_key_buffer_fused(compress_layer_id, loc, cache_k)
|
||||
|
||||
def set_index_k_fused(
|
||||
self,
|
||||
layer_id: int,
|
||||
loc: torch.Tensor,
|
||||
cache_k: torch.Tensor,
|
||||
) -> None:
|
||||
compress_ratio, compress_layer_id, _ = self.layer_mapping[layer_id]
|
||||
assert compress_ratio == 4, f"only c4 has indexer, got {compress_ratio = }"
|
||||
return self.c4_indexer_kv_pool.set_index_fused(compress_layer_id, loc, cache_k)
|
||||
@@ -1,8 +1,10 @@
|
||||
# mapping on device memory, host memory and memory allocator
|
||||
|
||||
import logging
|
||||
import weakref
|
||||
from typing import Optional
|
||||
|
||||
import psutil
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.radix_attention import RadixAttention
|
||||
@@ -10,10 +12,16 @@ from sglang.srt.mem_cache.allocator import (
|
||||
BaseTokenToKVPoolAllocator,
|
||||
PagedTokenToKVPoolAllocator,
|
||||
)
|
||||
from sglang.srt.mem_cache.deepseek_v4_memory_pool import (
|
||||
DeepSeekV4TokenToKVPool,
|
||||
HiSparseC4DevicePool,
|
||||
)
|
||||
from sglang.srt.mem_cache.memory_pool import NSATokenToKVPool
|
||||
from sglang.srt.utils import is_cuda, is_hip
|
||||
from sglang.srt.utils.common import get_num_new_pages
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# sgl_kernel.kvcacheio is only available in CUDA/ROCm sgl-kernel builds (not XPU/MPS/NPU/CPU).
|
||||
_is_cuda = is_cuda()
|
||||
_is_hip = is_hip()
|
||||
@@ -75,6 +83,12 @@ class HiSparseNSATokenToKVPool(NSATokenToKVPool):
|
||||
def _translate_loc_to_hisparse_device(self, compressed_indices: torch.Tensor):
|
||||
return self.full_to_hisparse_device_index_mapping[compressed_indices]
|
||||
|
||||
def translate_loc_from_full_to_hisparse_device(self, full_indices: torch.Tensor):
|
||||
return self._translate_loc_to_hisparse_device(full_indices)
|
||||
|
||||
def translate_loc_from_full_to_compressed(self, full_indices: torch.Tensor):
|
||||
return full_indices
|
||||
|
||||
def set_kv_buffer(
|
||||
self,
|
||||
layer: RadixAttention,
|
||||
@@ -128,13 +142,14 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
page_size: int,
|
||||
dtype: torch.dtype,
|
||||
device: torch.device,
|
||||
kvcache: NSATokenToKVPool,
|
||||
kvcache: HiSparseNSATokenToKVPool,
|
||||
need_sort: bool,
|
||||
host_to_device_ratio: int = 2,
|
||||
):
|
||||
self._kvcache = kvcache
|
||||
self._size_full = size * host_to_device_ratio
|
||||
self._size_hisparse = size
|
||||
self.compress_ratio = 1
|
||||
self.dtype = dtype
|
||||
self.device = device
|
||||
self.page_size = page_size
|
||||
@@ -148,7 +163,6 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
kvcache,
|
||||
need_sort,
|
||||
)
|
||||
|
||||
self.hisparse_attn_allocator = PagedTokenToKVPoolAllocator(
|
||||
self._size_hisparse,
|
||||
self.page_size,
|
||||
@@ -157,7 +171,6 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
kvcache,
|
||||
need_sort,
|
||||
)
|
||||
|
||||
self.full_to_hisparse_device_index_mapping = torch.cat(
|
||||
[
|
||||
torch.zeros(
|
||||
@@ -174,7 +187,6 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
self.is_not_in_free_group = True
|
||||
self.free_group = []
|
||||
self.clear()
|
||||
|
||||
self._kvcache.register_mapping(
|
||||
weakref.proxy(self.full_to_hisparse_device_index_mapping)
|
||||
)
|
||||
@@ -183,15 +195,23 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
def size_full(self) -> int:
|
||||
return self._size_full
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return self._size_full
|
||||
|
||||
def available_size(self) -> int:
|
||||
return min(
|
||||
self.logical_attn_allocator.available_size(),
|
||||
self.hisparse_attn_allocator.available_size(),
|
||||
)
|
||||
|
||||
def get_kvcache(self):
|
||||
return self._kvcache
|
||||
|
||||
def alloc(self, need_size: int):
|
||||
raise NotImplementedError(
|
||||
"Page size = 1 is not supported in HiSparse allocator"
|
||||
"HiSparse allocator does not support direct token allocation; "
|
||||
"use alloc_extend or alloc_decode instead."
|
||||
)
|
||||
|
||||
def alloc_logical_only(
|
||||
@@ -260,9 +280,11 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
self.hisparse_attn_allocator.is_not_in_free_group = True
|
||||
self.hisparse_attn_allocator.free(buffer_indices[buffer_indices > 0])
|
||||
|
||||
def get_last_loc_compressed(self, last_locs: torch.Tensor):
|
||||
return last_locs
|
||||
|
||||
def get_last_loc_hisparse_device(self, last_locs: torch.Tensor):
|
||||
hisparse_last_locs = self._kvcache._translate_loc_to_hisparse_device(last_locs)
|
||||
return hisparse_last_locs
|
||||
return self._kvcache._translate_loc_to_hisparse_device(last_locs)
|
||||
|
||||
def alloc_extend(
|
||||
self,
|
||||
@@ -312,9 +334,7 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
assert (
|
||||
hisparse_indices is not None
|
||||
), "Hisparse allocation failed in alloc_extend"
|
||||
|
||||
self.full_to_hisparse_device_index_mapping[logical_indices] = hisparse_indices
|
||||
|
||||
return logical_indices
|
||||
|
||||
def alloc_decode(
|
||||
@@ -323,36 +343,10 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
seq_lens_cpu: torch.Tensor,
|
||||
last_loc: torch.Tensor, # last_loc for full layers
|
||||
):
|
||||
logical_indices = self.logical_attn_allocator.alloc_decode(
|
||||
return self.logical_attn_allocator.alloc_decode(
|
||||
seq_lens, seq_lens_cpu, last_loc
|
||||
)
|
||||
|
||||
return logical_indices
|
||||
|
||||
def alloc_decode_debug(
|
||||
self,
|
||||
seq_lens: torch.Tensor,
|
||||
seq_lens_cpu: torch.Tensor,
|
||||
last_loc: torch.Tensor, # last_loc for full layers
|
||||
):
|
||||
logical_indices = self.logical_attn_allocator.alloc_decode(
|
||||
seq_lens, seq_lens_cpu, last_loc
|
||||
)
|
||||
|
||||
hisparse_last_loc = self.get_last_loc_hisparse_device(last_loc)
|
||||
hisparse_indices = self.hisparse_attn_allocator.alloc_decode(
|
||||
seq_lens,
|
||||
seq_lens_cpu,
|
||||
hisparse_last_loc,
|
||||
)
|
||||
|
||||
if logical_indices is None or hisparse_indices is None:
|
||||
return None
|
||||
|
||||
self.full_to_hisparse_device_index_mapping[logical_indices] = hisparse_indices
|
||||
|
||||
return logical_indices
|
||||
|
||||
def free_hisparse(self, free_indices: torch.Tensor):
|
||||
hisparse_indices = self._kvcache._translate_loc_to_hisparse_device(free_indices)
|
||||
hisparse_indices = hisparse_indices[hisparse_indices > 0]
|
||||
@@ -362,7 +356,6 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
def clear(self):
|
||||
self.logical_attn_allocator.clear()
|
||||
self.hisparse_attn_allocator.clear()
|
||||
|
||||
# Note: the last item is -1, we don't clear it, see the comment in __init__
|
||||
self.full_to_hisparse_device_index_mapping[:-1].fill_(0)
|
||||
self.is_not_in_free_group = True
|
||||
@@ -377,7 +370,6 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
def free(self, free_index: torch.Tensor):
|
||||
if free_index.numel() == 0:
|
||||
return
|
||||
|
||||
if self.is_not_in_free_group:
|
||||
self.logical_attn_allocator.free(free_index)
|
||||
self.free_hisparse(free_index)
|
||||
@@ -391,3 +383,396 @@ class HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
self.hisparse_attn_allocator.available_size()
|
||||
<= self.hisparse_attn_allocator.size
|
||||
)
|
||||
|
||||
|
||||
class DeepSeekV4SingleKVPoolHost:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_pool: HiSparseC4DevicePool,
|
||||
host_size: int,
|
||||
page_size: int,
|
||||
pin_memory: bool = True,
|
||||
device: str = "cpu",
|
||||
):
|
||||
|
||||
assert host_size > 0, "Host size must be specified and greater than 0"
|
||||
assert page_size == 1, "Host page size must be 1 for DeepSeekV4SingleKVPoolHost"
|
||||
|
||||
self.device_pool = device_pool
|
||||
self.size = host_size
|
||||
self.page_size = page_size
|
||||
self.num_pages = (self.size + self.page_size - 1) // self.page_size
|
||||
self.pin_memory = pin_memory
|
||||
self.device = device
|
||||
|
||||
self.dtype = device_pool.store_dtype
|
||||
self.layer_num = device_pool.layer_num
|
||||
self.kv_cache_total_dim = device_pool.kv_cache_total_dim
|
||||
|
||||
self.kv_buffer = self.init_kv_buffer()
|
||||
self.data_refs = [self.kv_buffer[i] for i in range(self.layer_num)]
|
||||
self.data_ptrs = torch.tensor(
|
||||
[x.data_ptr() for x in self.data_refs],
|
||||
dtype=torch.uint64,
|
||||
device=self.device_pool.device,
|
||||
)
|
||||
self.clear()
|
||||
|
||||
def clear(self):
|
||||
self.free_slots = torch.arange(
|
||||
1, self.num_pages + 1, dtype=torch.int64, device="cpu"
|
||||
)
|
||||
|
||||
def init_kv_buffer(self):
|
||||
dims = (self.layer_num, self.size + self.page_size, self.kv_cache_total_dim)
|
||||
requested_bytes = (
|
||||
self.layer_num
|
||||
* (self.size + self.page_size)
|
||||
* self.kv_cache_total_dim
|
||||
* self.dtype.itemsize
|
||||
)
|
||||
host_mem = psutil.virtual_memory()
|
||||
# preserve at least 10GB for other usage
|
||||
ten_gb = 10 * (1024**3)
|
||||
available_bytes = host_mem.available - ten_gb
|
||||
if requested_bytes > available_bytes:
|
||||
raise ValueError(
|
||||
f"Not enough host memory available. Requesting "
|
||||
f"{requested_bytes / 1e9:.2f} GB but only have "
|
||||
f"{available_bytes / 1e9:.2f} GB free. Please reduce the "
|
||||
f"size of the hierarchical cache."
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"Allocating {requested_bytes / 1e9:.2f} GB host memory for hierarchical KV cache."
|
||||
)
|
||||
|
||||
host_pool = torch.empty(dims, dtype=self.dtype, device=self.device)
|
||||
assert self.pin_memory, "DeepSeekV4SingleKVPoolHost requires pin_memory=True"
|
||||
if self.pin_memory:
|
||||
torch.cuda.cudart().cudaHostRegister(
|
||||
host_pool.data_ptr(), host_pool.numel() * host_pool.element_size(), 0
|
||||
)
|
||||
return host_pool
|
||||
|
||||
def backup_from_device_all_layer(
|
||||
self, device_pool, host_indices, device_indices, io_backend="kernel"
|
||||
):
|
||||
if io_backend != "kernel":
|
||||
raise ValueError(f"Unsupported IO backend: {io_backend}")
|
||||
|
||||
from sglang.jit_kernel.deepseek_v4 import hisparse_offload_to_host
|
||||
|
||||
if host_indices.device != device_indices.device:
|
||||
host_indices = host_indices.to(device=device_indices.device)
|
||||
host_indices_i64 = (
|
||||
host_indices.to(torch.int64)
|
||||
if host_indices.dtype != torch.int64
|
||||
else host_indices
|
||||
)
|
||||
device_indices_i64 = (
|
||||
device_indices.to(torch.int64)
|
||||
if device_indices.dtype != torch.int64
|
||||
else device_indices
|
||||
)
|
||||
hisparse_offload_to_host(
|
||||
gpu_ptrs=device_pool.data_ptrs,
|
||||
cpu_ptrs=self.data_ptrs,
|
||||
gpu_indices=device_indices_i64,
|
||||
cpu_indices=host_indices_i64,
|
||||
)
|
||||
|
||||
def available_size(self):
|
||||
return len(self.free_slots)
|
||||
|
||||
def alloc(self, need_size: int) -> Optional[torch.Tensor]:
|
||||
if need_size > self.available_size():
|
||||
return None
|
||||
|
||||
select_index = self.free_slots[:need_size]
|
||||
self.free_slots = self.free_slots[need_size:]
|
||||
|
||||
return select_index
|
||||
|
||||
def free(self, indices: torch.Tensor) -> int:
|
||||
self.free_slots = torch.cat([self.free_slots, indices.cpu()])
|
||||
return len(indices)
|
||||
|
||||
|
||||
class DeepSeekV4HiSparseTokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
logical_attn_allocator: BaseTokenToKVPoolAllocator,
|
||||
):
|
||||
assert isinstance(logical_attn_allocator._kvcache, DeepSeekV4TokenToKVPool)
|
||||
assert isinstance(
|
||||
logical_attn_allocator._kvcache.c4_kv_pool, HiSparseC4DevicePool
|
||||
)
|
||||
self.compress_ratio = 4
|
||||
|
||||
self.hisparse_kvcache = logical_attn_allocator._kvcache.c4_kv_pool
|
||||
self._size_full = logical_attn_allocator.size_full
|
||||
self._size_hisparse = self.hisparse_kvcache.size
|
||||
|
||||
self.dtype = self.hisparse_kvcache.dtype
|
||||
self.device = self.hisparse_kvcache.device
|
||||
self.page_size = self.hisparse_kvcache.page_size
|
||||
|
||||
self.logical_attn_allocator = logical_attn_allocator
|
||||
self._kvcache = logical_attn_allocator._kvcache
|
||||
self.hisparse_attn_allocator = PagedTokenToKVPoolAllocator(
|
||||
self._size_hisparse,
|
||||
self.page_size,
|
||||
self.dtype,
|
||||
self.device,
|
||||
self.hisparse_kvcache,
|
||||
logical_attn_allocator.need_sort,
|
||||
)
|
||||
|
||||
self.full_to_hisparse_device_index_mapping = torch.cat(
|
||||
[
|
||||
torch.zeros(
|
||||
self._kvcache.c4_logical_size + self.page_size,
|
||||
dtype=torch.int64,
|
||||
device=self.device,
|
||||
),
|
||||
torch.tensor([-1], dtype=torch.int64, device=self.device),
|
||||
]
|
||||
)
|
||||
|
||||
self.need_sort = logical_attn_allocator.need_sort
|
||||
self.free_pages = None
|
||||
self.release_pages = None
|
||||
self.is_not_in_free_group = True
|
||||
self.free_group = []
|
||||
self.clear()
|
||||
|
||||
self.hisparse_kvcache.register_mapping(
|
||||
weakref.proxy(self.full_to_hisparse_device_index_mapping)
|
||||
)
|
||||
|
||||
@property
|
||||
def size_full(self) -> int:
|
||||
return self._size_full
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
return self.logical_attn_allocator.size
|
||||
|
||||
@property
|
||||
def size_swa(self) -> int:
|
||||
return self.logical_attn_allocator.size_swa
|
||||
|
||||
@property
|
||||
def full_to_swa_index_mapping(self):
|
||||
return self.logical_attn_allocator.full_to_swa_index_mapping
|
||||
|
||||
def debug_print(self) -> str:
|
||||
msg = self.logical_attn_allocator.debug_print()
|
||||
msg += (
|
||||
f"#hisparse-available-size: "
|
||||
f"{self.hisparse_attn_allocator.available_size()}, "
|
||||
)
|
||||
return msg
|
||||
|
||||
def get_kvcache(self):
|
||||
return self._kvcache
|
||||
|
||||
def translate_loc_from_full_to_swa(self, kv_indices: torch.Tensor):
|
||||
return self.logical_attn_allocator.translate_loc_from_full_to_swa(kv_indices)
|
||||
|
||||
def full_available_size(self):
|
||||
return min(
|
||||
self.logical_attn_allocator.full_available_size(),
|
||||
self.hisparse_attn_allocator.available_size() * self.compress_ratio,
|
||||
)
|
||||
|
||||
def swa_available_size(self):
|
||||
return self.logical_attn_allocator.swa_available_size()
|
||||
|
||||
def free_swa(self, free_indices: torch.Tensor):
|
||||
self.logical_attn_allocator.free_swa(free_indices)
|
||||
|
||||
def available_size(self) -> int:
|
||||
return min(
|
||||
self.logical_attn_allocator.available_size(),
|
||||
self.hisparse_attn_allocator.available_size() * self.compress_ratio,
|
||||
)
|
||||
|
||||
def alloc(self, need_size: int):
|
||||
raise NotImplementedError(
|
||||
"DeepSeek V4 HiSparse allocator does not support direct token allocation; "
|
||||
"use alloc_extend or alloc_decode instead."
|
||||
)
|
||||
|
||||
def alloc_device_buffer(self, allocated_indices, need_size: int):
|
||||
assert need_size % self.page_size == 0
|
||||
hisparse_indices = self.full_to_hisparse_device_index_mapping[allocated_indices]
|
||||
self.full_to_hisparse_device_index_mapping[allocated_indices] = 0
|
||||
|
||||
device_buffer_size = need_size - self.page_size
|
||||
P = len(hisparse_indices)
|
||||
if P > device_buffer_size + 1:
|
||||
newest_src = hisparse_indices[P - 1].clone()
|
||||
old_at_dbs = hisparse_indices[device_buffer_size].clone()
|
||||
hisparse_indices[device_buffer_size] = newest_src
|
||||
hisparse_indices[P - 1] = old_at_dbs
|
||||
|
||||
if len(hisparse_indices) >= need_size:
|
||||
buffer_indices = hisparse_indices[:need_size]
|
||||
surplus = hisparse_indices[need_size:]
|
||||
if surplus.numel() > 0:
|
||||
buffer_pages = torch.unique(buffer_indices // self.page_size)
|
||||
surplus_pages = torch.unique(surplus // self.page_size)
|
||||
pure_surplus = surplus_pages[~torch.isin(surplus_pages, buffer_pages)]
|
||||
if pure_surplus.numel() > 0:
|
||||
self.hisparse_attn_allocator.is_not_in_free_group = True
|
||||
self.hisparse_attn_allocator.free(pure_surplus * self.page_size)
|
||||
else:
|
||||
page_residual_length = len(hisparse_indices) % self.page_size
|
||||
if page_residual_length != 0:
|
||||
hisparse_indices = torch.cat(
|
||||
[
|
||||
hisparse_indices,
|
||||
torch.arange(
|
||||
hisparse_indices[-1] + 1,
|
||||
hisparse_indices[-1]
|
||||
+ self.page_size
|
||||
- page_residual_length
|
||||
+ 1,
|
||||
device=self.device,
|
||||
),
|
||||
]
|
||||
)
|
||||
extra_indices = self.hisparse_attn_allocator.alloc(
|
||||
need_size - len(hisparse_indices)
|
||||
)
|
||||
assert (
|
||||
extra_indices is not None
|
||||
), "Hisparse allocation failed in alloc_device_buffer"
|
||||
buffer_indices = torch.cat([hisparse_indices, extra_indices])
|
||||
return buffer_indices
|
||||
|
||||
def free_hisparse_indices(self, buffer_indices: torch.Tensor):
|
||||
self.hisparse_attn_allocator.is_not_in_free_group = True
|
||||
self.hisparse_attn_allocator.free(buffer_indices[buffer_indices > 0])
|
||||
|
||||
def get_last_loc_compressed(self, last_locs: torch.Tensor):
|
||||
return (last_locs - 3) // self.compress_ratio
|
||||
|
||||
def get_last_loc_hisparse_device(self, last_locs: torch.Tensor):
|
||||
return self.hisparse_kvcache._translate_loc_to_hisparse_device(
|
||||
self.get_last_loc_compressed(last_locs)
|
||||
)
|
||||
|
||||
def alloc_extend(
|
||||
self,
|
||||
prefix_lens: torch.Tensor,
|
||||
prefix_lens_cpu: torch.Tensor,
|
||||
seq_lens: torch.Tensor,
|
||||
seq_lens_cpu: torch.Tensor,
|
||||
last_loc: torch.Tensor,
|
||||
extend_num_tokens: int,
|
||||
):
|
||||
assert self.page_size > 1
|
||||
|
||||
num_new_pages_logical = get_num_new_pages(
|
||||
seq_lens=seq_lens_cpu, page_size=self.page_size, prefix_lens=prefix_lens_cpu
|
||||
)
|
||||
num_new_pages_hisparse = get_num_new_pages(
|
||||
seq_lens=seq_lens_cpu // self.compress_ratio,
|
||||
page_size=self.page_size,
|
||||
prefix_lens=prefix_lens_cpu // self.compress_ratio,
|
||||
)
|
||||
if (
|
||||
num_new_pages_logical
|
||||
> self.logical_attn_allocator.available_size() // self.page_size
|
||||
):
|
||||
return None
|
||||
if (
|
||||
num_new_pages_hisparse
|
||||
> self.hisparse_attn_allocator.available_size() // self.page_size
|
||||
):
|
||||
return None
|
||||
|
||||
logical_indices = self.logical_attn_allocator.alloc_extend(
|
||||
prefix_lens,
|
||||
prefix_lens_cpu,
|
||||
seq_lens,
|
||||
seq_lens_cpu,
|
||||
last_loc,
|
||||
extend_num_tokens,
|
||||
)
|
||||
assert logical_indices is not None, "Logical allocation failed in alloc_extend"
|
||||
|
||||
compressed_logical_indices = (
|
||||
self.hisparse_kvcache.translate_loc_from_full_to_compressed(logical_indices)
|
||||
)
|
||||
hisparse_last_loc = self.get_last_loc_hisparse_device(last_loc)
|
||||
hisparse_indices = self.hisparse_attn_allocator.alloc_extend(
|
||||
prefix_lens // self.compress_ratio,
|
||||
prefix_lens_cpu // self.compress_ratio,
|
||||
seq_lens // self.compress_ratio,
|
||||
seq_lens_cpu // self.compress_ratio,
|
||||
hisparse_last_loc,
|
||||
len(compressed_logical_indices),
|
||||
)
|
||||
assert (
|
||||
hisparse_indices is not None
|
||||
), "Hisparse allocation failed in alloc_extend"
|
||||
|
||||
self.full_to_hisparse_device_index_mapping[compressed_logical_indices] = (
|
||||
hisparse_indices.to(torch.int64)
|
||||
)
|
||||
return logical_indices
|
||||
|
||||
def alloc_decode(
|
||||
self,
|
||||
seq_lens: torch.Tensor,
|
||||
seq_lens_cpu: torch.Tensor,
|
||||
last_loc: torch.Tensor,
|
||||
):
|
||||
return self.logical_attn_allocator.alloc_decode(
|
||||
seq_lens, seq_lens_cpu, last_loc
|
||||
)
|
||||
|
||||
def free_compressed(self, compressed_indices: torch.Tensor):
|
||||
hisparse_indices = self.hisparse_kvcache.translate_loc_to_hisparse_device(
|
||||
compressed_indices
|
||||
)
|
||||
hisparse_indices = hisparse_indices[hisparse_indices > 0]
|
||||
self.free_hisparse_indices(hisparse_indices)
|
||||
self.full_to_hisparse_device_index_mapping[compressed_indices] = 0
|
||||
|
||||
def free_hisparse(self, free_indices: torch.Tensor):
|
||||
compressed_indices = (
|
||||
self.hisparse_kvcache.translate_loc_from_full_to_compressed(free_indices)
|
||||
)
|
||||
self.free_compressed(compressed_indices)
|
||||
|
||||
def clear(self):
|
||||
self.logical_attn_allocator.clear()
|
||||
self.hisparse_attn_allocator.clear()
|
||||
|
||||
self.full_to_hisparse_device_index_mapping[:-1].fill_(0)
|
||||
self.is_not_in_free_group = True
|
||||
self.free_group = []
|
||||
|
||||
def free(self, free_index: torch.Tensor):
|
||||
if free_index.numel() == 0:
|
||||
return
|
||||
|
||||
if self.is_not_in_free_group:
|
||||
self.logical_attn_allocator.free(free_index)
|
||||
else:
|
||||
self.free_group.append(free_index)
|
||||
assert (
|
||||
self.logical_attn_allocator.available_size()
|
||||
<= self.logical_attn_allocator.size
|
||||
)
|
||||
assert (
|
||||
self.hisparse_attn_allocator.available_size()
|
||||
<= self.hisparse_attn_allocator.size
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ from sglang.srt.mem_cache.allocator import (
|
||||
PagedTokenToKVPoolAllocator,
|
||||
TokenToKVPoolAllocator,
|
||||
)
|
||||
from sglang.srt.mem_cache.base_swa_memory_pool import BaseSWAKVPool
|
||||
from sglang.srt.mem_cache.memory_pool import KVCache, MHATokenToKVPool
|
||||
from sglang.srt.mem_cache.utils import maybe_init_custom_mem_pool
|
||||
from sglang.srt.utils import is_npu
|
||||
@@ -25,7 +26,7 @@ logger = logging.getLogger(__name__)
|
||||
GB = 1024 * 1024 * 1024
|
||||
|
||||
|
||||
class SWAKVPool(KVCache):
|
||||
class SWAKVPool(BaseSWAKVPool):
|
||||
"""KV cache with separate pools for full and SWA attention layers."""
|
||||
|
||||
def __init__(
|
||||
@@ -253,29 +254,32 @@ class SWATokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
page_size: int,
|
||||
dtype: torch.dtype,
|
||||
device: str,
|
||||
kvcache: SWAKVPool,
|
||||
kvcache: BaseSWAKVPool,
|
||||
need_sort: bool,
|
||||
):
|
||||
assert isinstance(kvcache, SWAKVPool)
|
||||
assert isinstance(kvcache, BaseSWAKVPool)
|
||||
self._size_full = size
|
||||
self._size_swa = size_swa
|
||||
self.dtype = dtype
|
||||
self.device = device
|
||||
self.page_size = page_size
|
||||
|
||||
full_kv_pool = getattr(kvcache, "full_kv_pool", None)
|
||||
swa_kv_pool = getattr(kvcache, "swa_kv_pool", None)
|
||||
|
||||
if page_size == 1:
|
||||
self.full_attn_allocator = TokenToKVPoolAllocator(
|
||||
size,
|
||||
dtype,
|
||||
device,
|
||||
kvcache.full_kv_pool,
|
||||
full_kv_pool,
|
||||
need_sort,
|
||||
)
|
||||
self.swa_attn_allocator = TokenToKVPoolAllocator(
|
||||
size_swa,
|
||||
dtype,
|
||||
device,
|
||||
kvcache.swa_kv_pool,
|
||||
swa_kv_pool,
|
||||
need_sort,
|
||||
)
|
||||
else:
|
||||
@@ -288,7 +292,7 @@ class SWATokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
page_size,
|
||||
dtype,
|
||||
device,
|
||||
kvcache.full_kv_pool,
|
||||
full_kv_pool,
|
||||
need_sort,
|
||||
)
|
||||
self.swa_attn_allocator = PagedTokenToKVPoolAllocatorClass(
|
||||
@@ -296,7 +300,7 @@ class SWATokenToKVPoolAllocator(BaseTokenToKVPoolAllocator):
|
||||
page_size,
|
||||
dtype,
|
||||
device,
|
||||
kvcache.swa_kv_pool,
|
||||
swa_kv_pool,
|
||||
need_sort,
|
||||
)
|
||||
# Note: append one more item of value -1 in the end so -1 maps to -1.
|
||||
|
||||
@@ -27,6 +27,7 @@ from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
import torch
|
||||
from numpy import float64
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||
BasePrefixCache,
|
||||
DecLockRefParams,
|
||||
@@ -471,8 +472,11 @@ class SWARadixCache(BasePrefixCache):
|
||||
|
||||
# Remove req slot release the cache lock
|
||||
self.dec_lock_ref(
|
||||
req.last_node, DecLockRefParams(swa_uuid_for_lock=req.swa_uuid_for_lock)
|
||||
req.last_node,
|
||||
DecLockRefParams(swa_uuid_for_lock=req.swa_uuid_for_lock),
|
||||
skip_swa=req.swa_prefix_lock_released,
|
||||
)
|
||||
req.swa_prefix_lock_released = False
|
||||
|
||||
def cache_unfinished_req(self, req: Req, chunked=False) -> None:
|
||||
"""Cache request when it is unfinished."""
|
||||
@@ -524,8 +528,11 @@ class SWARadixCache(BasePrefixCache):
|
||||
req.cache_protected_len = len(new_indices)
|
||||
|
||||
self.dec_lock_ref(
|
||||
req.last_node, DecLockRefParams(swa_uuid_for_lock=req.swa_uuid_for_lock)
|
||||
req.last_node,
|
||||
DecLockRefParams(swa_uuid_for_lock=req.swa_uuid_for_lock),
|
||||
skip_swa=req.swa_prefix_lock_released,
|
||||
)
|
||||
req.swa_prefix_lock_released = False
|
||||
result = self.inc_lock_ref(new_last_node)
|
||||
swa_uuid_for_lock = result.swa_uuid_for_lock
|
||||
|
||||
@@ -568,12 +575,15 @@ class SWARadixCache(BasePrefixCache):
|
||||
# 1. free node kv indices, evict full and swa tokens
|
||||
self.token_to_kv_pool_allocator.free(x.value)
|
||||
full_num_evicted += len(x.value)
|
||||
swa_num_evicted += len(x.value)
|
||||
# Tombstoned leaves had their SWA freed earlier in `dec_swa_lock_only`
|
||||
if not x.swa_tombstone:
|
||||
swa_num_evicted += len(x.value)
|
||||
|
||||
# 2. get the next leaf, update the lru lists
|
||||
x_next = self.full_lru_list.get_prev_leaf_no_lock(x)
|
||||
self.full_lru_list.remove_node(x)
|
||||
self.swa_lru_list.remove_node(x)
|
||||
if not x.swa_tombstone:
|
||||
self.swa_lru_list.remove_node(x)
|
||||
|
||||
# 3. delete the leaf node
|
||||
self._delete_leaf(x)
|
||||
@@ -610,6 +620,18 @@ class SWARadixCache(BasePrefixCache):
|
||||
|
||||
# 3. tombstone the node
|
||||
self._tombstone_internal_node(x)
|
||||
elif x.full_lock_ref > 0:
|
||||
# Leaf still holds a full-side lock (can happen when the
|
||||
# SWA leaf-lock early-release optimization revived a
|
||||
# tombstoned leaf. Treat it like an internal tombstone.
|
||||
self.token_to_kv_pool_allocator.free_swa(x.value)
|
||||
swa_num_evicted += len(x.value)
|
||||
|
||||
x_next = self.swa_lru_list.get_prev_no_lock(x)
|
||||
self.swa_lru_list.remove_node(x)
|
||||
|
||||
self.swa_evictable_size_ -= len(x.value)
|
||||
x.swa_tombstone = True
|
||||
else:
|
||||
assert (
|
||||
x.full_lock_ref == 0
|
||||
@@ -679,20 +701,26 @@ class SWARadixCache(BasePrefixCache):
|
||||
return IncLockRefResult(swa_uuid_for_lock=swa_uuid_for_lock)
|
||||
|
||||
def dec_lock_ref(
|
||||
self, node: TreeNode, params: Optional[DecLockRefParams] = None
|
||||
self,
|
||||
node: TreeNode,
|
||||
params: Optional[DecLockRefParams] = None,
|
||||
skip_swa: bool = False,
|
||||
) -> DecLockRefResult:
|
||||
"""
|
||||
Decrement the lock reference count for the node.
|
||||
It unlocks the full_lock_ref for nodes between the [last node, root), exclusive.
|
||||
It unlocks the swa_lock_ref for nodes between the [last node, swa_uuid_for_lock], inclusive.
|
||||
If swa_uuid_for_lock is None, it unlocks to the root, exclusive.
|
||||
|
||||
If skip_swa is True, only the full_lock_ref is decremented; the SWA lock is
|
||||
assumed to have been released already (e.g. via `dec_swa_lock_only`).
|
||||
"""
|
||||
swa_uuid_for_lock = params.swa_uuid_for_lock if params is not None else None
|
||||
|
||||
if self.disable:
|
||||
return DecLockRefResult()
|
||||
|
||||
dec_lock_swa = True
|
||||
dec_lock_swa = not skip_swa
|
||||
while node != self.root_node:
|
||||
assert (
|
||||
node.full_lock_ref > 0
|
||||
@@ -721,6 +749,61 @@ class SWARadixCache(BasePrefixCache):
|
||||
|
||||
return DecLockRefResult()
|
||||
|
||||
def dec_swa_lock_only(
|
||||
self, node: TreeNode, swa_uuid_for_lock: Optional[int] = None
|
||||
):
|
||||
"""
|
||||
Decrement only the swa_lock_ref (and swa_protected_size_) along the chain
|
||||
[node, swa_uuid_for_lock], inclusive. The full_lock_ref is left untouched
|
||||
so the caller's full-cache protection is preserved.
|
||||
|
||||
Used to early-release the SWA portion of a request's tree lock once the
|
||||
request's decode position has advanced past the sliding window, so the
|
||||
protected window can be reclaimed.
|
||||
|
||||
For internal nodes, the standard protected -> evictable transition is
|
||||
applied (node stays in swa_lru_list and may be evicted by SWA LRU later).
|
||||
For leaf nodes, since `swa_lru_list` cannot contain a leaf with
|
||||
`full_lock_ref > 0` (SWA-eviction would also delete the still-referenced
|
||||
leaf), we instead free the SWA pool slots immediately and mark the leaf
|
||||
as `swa_tombstone=True`. The full kv stays alive until the full-side
|
||||
lock drops; future prefix-matches stop before this tombstoned leaf.
|
||||
|
||||
Caller must ensure this is invoked at most once per (node, swa_uuid_for_lock)
|
||||
pair (track via e.g. `Req.swa_prefix_lock_released`). When the request
|
||||
finally releases its full lock via `dec_lock_ref`, pass `skip_swa=True`
|
||||
to avoid touching SWA state again.
|
||||
"""
|
||||
if self.disable:
|
||||
return
|
||||
|
||||
while node != self.root_node:
|
||||
assert (
|
||||
not node.swa_tombstone
|
||||
), f"dec_swa_lock_only on swa_tombstone node, {node.id=}"
|
||||
assert (
|
||||
node.swa_lock_ref > 0
|
||||
), f"dec_swa_lock_only on node with {node.swa_lock_ref=}, {node.id=}"
|
||||
|
||||
if node.swa_lock_ref == 1:
|
||||
self.swa_protected_size_ -= len(node.value)
|
||||
if len(node.children) == 0:
|
||||
# Leaf: free SWA pool slots and tombstone, and remove from
|
||||
# swa_lru_list so SWA-eviction won't pick this tombstoned
|
||||
# leaf (which still holds full_lock_ref > 0). The full kv
|
||||
# stays alive until the request releases its full lock.
|
||||
self.token_to_kv_pool_allocator.free_swa(node.value)
|
||||
self.swa_lru_list.remove_node(node)
|
||||
node.swa_tombstone = True
|
||||
else:
|
||||
# Internal: standard protected -> evictable.
|
||||
self.swa_evictable_size_ += len(node.value)
|
||||
node.swa_lock_ref -= 1
|
||||
|
||||
if swa_uuid_for_lock and node.swa_uuid == swa_uuid_for_lock:
|
||||
break
|
||||
node = node.parent
|
||||
|
||||
def sanity_check(self):
|
||||
self.full_lru_list.sanity_check(self)
|
||||
self.swa_lru_list.sanity_check(self)
|
||||
@@ -789,9 +872,13 @@ class SWARadixCache(BasePrefixCache):
|
||||
match_len_since_tombstone = float("inf")
|
||||
best_value_len = 0
|
||||
best_last_node = node
|
||||
enable_compact = envs.SGLANG_OPT_SWA_RADIX_CACHE_COMPACT.get()
|
||||
while len(key) > 0 and child_key in node.children.keys():
|
||||
child = node.children[child_key]
|
||||
|
||||
if enable_compact:
|
||||
self._compact_single_child_chain(child)
|
||||
|
||||
if child.swa_tombstone:
|
||||
# update best_value_len and best_last_node if needed
|
||||
if match_len_since_tombstone >= self.sliding_window_size:
|
||||
@@ -871,6 +958,84 @@ class SWARadixCache(BasePrefixCache):
|
||||
last_host_node=last_node,
|
||||
)
|
||||
|
||||
def _compact_single_child_chain(self, node: TreeNode) -> None:
|
||||
# FIXME(ispobock): drifts retract pool accounting (commit 6348cb506);
|
||||
# also overwrites active swa_uuid when window > page_size. Off by
|
||||
# default via SGLANG_OPT_SWA_RADIX_CACHE_COMPACT.
|
||||
while len(node.children) == 1:
|
||||
child = next(iter(node.children.values()))
|
||||
if len(child.children) == 0:
|
||||
break
|
||||
sum_gc_full_lock_ref = sum(
|
||||
gc.full_lock_ref for gc in child.children.values()
|
||||
)
|
||||
if child.full_lock_ref > sum_gc_full_lock_ref:
|
||||
break
|
||||
if (
|
||||
child.swa_tombstone != node.swa_tombstone
|
||||
or child.full_lock_ref != node.full_lock_ref
|
||||
or child.swa_lock_ref != node.swa_lock_ref
|
||||
):
|
||||
break
|
||||
|
||||
# Preserve is_bigram: main #23106 made bigram an O(1) flag on RadixKey;
|
||||
# the constructor defaults to False, so concat without explicit flag
|
||||
# silently demotes EAGLE/MTP bigram keys → match() returns 0 →
|
||||
# _split_node assert.
|
||||
node.key = RadixKey(
|
||||
node.key.token_ids + child.key.token_ids,
|
||||
node.key.extra_key,
|
||||
is_bigram=node.key.is_bigram,
|
||||
)
|
||||
node.value = torch.cat([node.value, child.value])
|
||||
node.children = child.children
|
||||
for grandchild in node.children.values():
|
||||
grandchild.parent = node
|
||||
|
||||
if child.swa_uuid is not None:
|
||||
node.swa_uuid = child.swa_uuid
|
||||
|
||||
self.full_lru_list.remove_node(child)
|
||||
if not child.swa_tombstone:
|
||||
self.swa_lru_list.remove_node(child)
|
||||
|
||||
def _maybe_split_leaf_for_swa_lock(self, leaf: TreeNode) -> TreeNode:
|
||||
"""``inc_lock_ref`` protects ``len(leaf.value)`` SWA tokens for the
|
||||
leaf even though SWA only actually needs the last
|
||||
``sliding_window_size`` tokens. With chunked prefill, leaves can be
|
||||
thousands of tokens long, which inflates ``swa_protected_size_`` by
|
||||
~``chunked_prefill_size / sliding_window_size`` and causes premature
|
||||
SWA pool exhaustion / retract thrashing.
|
||||
"""
|
||||
if (
|
||||
leaf is self.root_node
|
||||
or leaf.swa_lock_ref > 0
|
||||
or leaf.swa_tombstone
|
||||
or len(leaf.value) == 0
|
||||
):
|
||||
return leaf
|
||||
|
||||
# Smallest page-aligned size that still covers the sliding window.
|
||||
tail_size = (
|
||||
(self.sliding_window_size + self.page_size - 1)
|
||||
// self.page_size
|
||||
* self.page_size
|
||||
)
|
||||
if len(leaf.value) <= tail_size:
|
||||
return leaf
|
||||
|
||||
split_at = len(leaf.value) - tail_size
|
||||
|
||||
if split_at <= 0 or split_at >= len(leaf.value):
|
||||
return leaf
|
||||
if self.page_size > 1 and (
|
||||
split_at % self.page_size != 0 or len(leaf.value) % self.page_size != 0
|
||||
):
|
||||
return leaf
|
||||
|
||||
self._split_node(leaf.key, leaf, split_at)
|
||||
return leaf
|
||||
|
||||
def _split_node(self, key: RadixKey, child: TreeNode, split_len: int) -> TreeNode:
|
||||
# new_node -> child
|
||||
new_node = TreeNode()
|
||||
@@ -1026,7 +1191,13 @@ class SWARadixCache(BasePrefixCache):
|
||||
key = key[swa_tombstone_len:]
|
||||
value = value[swa_tombstone_len:]
|
||||
|
||||
self._add_new_node(node, key, value, swa_tombstone=False)
|
||||
new_leaf = self._add_new_node(node, key, value, swa_tombstone=False)
|
||||
|
||||
if envs.SGLANG_OPT_SWA_SPLIT_LEAF_ON_INSERT.get():
|
||||
# Cap the leaf at one (page-aligned) sliding window so a future
|
||||
# inc_lock_ref only protects `sliding_window_size` tokens of SWA pool.
|
||||
self._maybe_split_leaf_for_swa_lock(new_leaf)
|
||||
|
||||
return total_prefix_length
|
||||
|
||||
def _add_new_node(
|
||||
@@ -1074,15 +1245,15 @@ class SWARadixCache(BasePrefixCache):
|
||||
return node, full_num_evicted
|
||||
|
||||
def _delete_leaf(self, node: TreeNode) -> None:
|
||||
assert (
|
||||
not node.swa_tombstone
|
||||
), f"Invariant violated: leaf node is a tombstone, {node.id=}"
|
||||
assert len(node.children) == 0, f"leaf node has children, {node.id=}"
|
||||
key = node.key.child_key(self.page_size)
|
||||
v = node.parent.children.pop(key, None)
|
||||
assert v == node, f"parent does not have child key, {key}"
|
||||
self.full_evictable_size_ -= len(node.key)
|
||||
self.swa_evictable_size_ -= len(node.key)
|
||||
# Tombstoned leaves were never (re-)added to swa_lru_list and were
|
||||
# already removed from swa_evictable_size_ when they were tombstoned.
|
||||
if not node.swa_tombstone:
|
||||
self.swa_evictable_size_ -= len(node.key)
|
||||
|
||||
def _tombstone_internal_node(self, node: TreeNode) -> None:
|
||||
assert len(node.children) != 0, f"Cannot tombstone a leaf node, {node.id=}"
|
||||
|
||||
@@ -394,6 +394,12 @@ def get_is_capture_mode():
|
||||
return is_capture_mode
|
||||
|
||||
|
||||
def compile_in_capture_mode(func):
|
||||
if get_is_capture_mode():
|
||||
return torch.compile(func)
|
||||
return func
|
||||
|
||||
|
||||
def get_capture_lora_variant() -> Optional[str]:
|
||||
"""Return the lora variant being captured, or None if not in dual capture."""
|
||||
return _capture_lora_variant
|
||||
@@ -1163,11 +1169,13 @@ class CudaGraphRunner:
|
||||
self.device_module.synchronize()
|
||||
self.model_runner.tp_group.barrier()
|
||||
run_once()
|
||||
attn_backend.on_after_cuda_graph_warmup()
|
||||
|
||||
if get_global_graph_memory_pool() is None:
|
||||
set_global_graph_memory_pool(self.device_module.graph_pool_handle())
|
||||
# Set graph pool id globally to be able to use symmetric memory
|
||||
set_graph_pool_id(get_global_graph_memory_pool())
|
||||
|
||||
out = self._capture_graph(
|
||||
graph, get_global_graph_memory_pool(), stream, run_once
|
||||
)
|
||||
@@ -1269,6 +1277,9 @@ class CudaGraphRunner:
|
||||
attn_backend = self.model_runner.decode_attn_backend_group[stream_idx]
|
||||
else:
|
||||
attn_backend = self.attn_backend
|
||||
# FIXME: implicit channel for backends (dsv4) that need forward_batch
|
||||
# in replay metadata prep. Should become a real param on the interface.
|
||||
attn_backend._replay_forward_batch = forward_batch
|
||||
attn_backend.init_forward_metadata_replay_cuda_graph(
|
||||
bs,
|
||||
buffers.req_pool_indices[:bs],
|
||||
@@ -1279,6 +1290,7 @@ class CudaGraphRunner:
|
||||
forward_batch.spec_info,
|
||||
seq_lens_cpu=buffers.seq_lens_cpu[:bs],
|
||||
)
|
||||
attn_backend._replay_forward_batch = None
|
||||
|
||||
# Store fields
|
||||
self.raw_bs = raw_bs
|
||||
@@ -1326,6 +1338,7 @@ class CudaGraphRunner:
|
||||
)
|
||||
with ctx:
|
||||
self.graphs[graph_key].replay()
|
||||
|
||||
output = self.output_buffers[graph_key]
|
||||
|
||||
if isinstance(output, LogitsProcessorOutput):
|
||||
|
||||
@@ -346,6 +346,10 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
):
|
||||
# Parse args
|
||||
self.mem_fraction_static = mem_fraction_static
|
||||
# Set on target by `_resolve_memory_pool_config`; passed in for draft
|
||||
# workers so they reuse target's resolved sizes (replaces legacy
|
||||
# `server_args._draft_pool_config` mutation hack).
|
||||
self.memory_pool_config = memory_pool_config
|
||||
self.device = server_args.device
|
||||
self.gpu_id = gpu_id
|
||||
self.tp_rank = tp_rank
|
||||
@@ -364,7 +368,6 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
self.dist_port = nccl_port
|
||||
self.server_args = server_args
|
||||
self.is_draft_worker = is_draft_worker
|
||||
self.memory_pool_config = memory_pool_config
|
||||
self.is_generation = model_config.is_generation
|
||||
self.device_timer = None
|
||||
self.is_multimodal = model_config.is_multimodal
|
||||
@@ -378,7 +381,9 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
self.req_to_token_pool = req_to_token_pool
|
||||
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
|
||||
self.is_hybrid_swa = model_config.is_hybrid_swa
|
||||
self.is_hybrid_swa_compress = model_config.is_hybrid_swa_compress
|
||||
self.is_hybrid_swa_compress = getattr(
|
||||
model_config, "is_hybrid_swa_compress", False
|
||||
)
|
||||
self.use_mla_backend = self.model_config.attention_arch == AttentionArch.MLA
|
||||
self.attention_chunk_size = model_config.attention_chunk_size
|
||||
rope_scaling = getattr(
|
||||
@@ -555,6 +560,9 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
self._model_update_group = {}
|
||||
self._weights_send_group = {}
|
||||
|
||||
if not hasattr(self, "hisparse_coordinator"):
|
||||
self.hisparse_coordinator = None
|
||||
|
||||
def _build_model_config(
|
||||
self, server_args, model_path=None, model_revision=None, is_draft_model=False
|
||||
):
|
||||
@@ -738,26 +746,6 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
# Init ngram embedding token table
|
||||
self.maybe_init_ngram_embedding()
|
||||
|
||||
# Init hisparse coordinator (must happen before CUDA graph capture)
|
||||
if self.enable_hisparse:
|
||||
from sglang.srt.managers.hisparse_coordinator import HiSparseCoordinator
|
||||
from sglang.srt.mem_cache.sparsity import parse_hisparse_config
|
||||
|
||||
hisparse_cfg = parse_hisparse_config(self.server_args)
|
||||
self.hisparse_coordinator = HiSparseCoordinator(
|
||||
req_to_token_pool=self.req_to_token_pool,
|
||||
token_to_kv_pool_allocator=self.token_to_kv_pool_allocator,
|
||||
top_k=hisparse_cfg.top_k,
|
||||
device_buffer_size=hisparse_cfg.device_buffer_size,
|
||||
device=self.device,
|
||||
tp_group=(
|
||||
self.attention_tp_group.cpu_group
|
||||
if self.server_args.enable_dp_attention
|
||||
else self.tp_group.cpu_group
|
||||
),
|
||||
host_to_device_ratio=hisparse_cfg.host_to_device_ratio,
|
||||
)
|
||||
|
||||
# Init routed experts capturer
|
||||
self.init_routed_experts_capturer()
|
||||
|
||||
@@ -772,6 +760,28 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
self.init_cublas()
|
||||
self.init_attention_backend()
|
||||
self.kernel_warmup()
|
||||
# Init hisparse coordinator (must happen before CUDA graph capture)
|
||||
if self.enable_hisparse:
|
||||
from sglang.srt.managers.hisparse_coordinator import HiSparseCoordinator
|
||||
from sglang.srt.mem_cache.sparsity import parse_hisparse_config
|
||||
|
||||
hisparse_cfg = parse_hisparse_config(self.server_args)
|
||||
hisparse_top_k = getattr(
|
||||
self.model_config.hf_text_config, "index_topk", hisparse_cfg.top_k
|
||||
)
|
||||
self.hisparse_coordinator = HiSparseCoordinator(
|
||||
req_to_token_pool=self.req_to_token_pool,
|
||||
token_to_kv_pool_allocator=self.token_to_kv_pool_allocator,
|
||||
top_k=hisparse_top_k,
|
||||
device_buffer_size=hisparse_cfg.device_buffer_size,
|
||||
device=self.device,
|
||||
tp_group=(
|
||||
self.attention_tp_group.cpu_group
|
||||
if self.server_args.enable_dp_attention
|
||||
else self.tp_group.cpu_group
|
||||
),
|
||||
host_to_device_ratio=hisparse_cfg.host_to_device_ratio,
|
||||
)
|
||||
self._pre_initialize_flashinfer_allreduce_workspace()
|
||||
self.init_device_graphs()
|
||||
elif self.device in ["npu", "cpu"]:
|
||||
@@ -801,6 +811,9 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
if not self.is_hybrid_swa:
|
||||
return
|
||||
|
||||
if self.model_config.is_deepseek_v4_arch:
|
||||
return
|
||||
|
||||
full_attention_layer_ids = [
|
||||
layer_idx
|
||||
for layer_idx in range(self.start_layer, self.end_layer + 1)
|
||||
@@ -3313,10 +3326,12 @@ class ModelRunner(ModelRunnerKVCacheMixin):
|
||||
|
||||
# Hisparse coordinator
|
||||
if (
|
||||
self.hisparse_coordinator is not None
|
||||
and forward_batch.forward_mode.is_decode()
|
||||
forward_batch.forward_mode.is_decode()
|
||||
and self.hisparse_coordinator is not None
|
||||
):
|
||||
forward_batch.hisparse_coordinator = self.hisparse_coordinator
|
||||
self.hisparse_coordinator.wait_for_pending_backup()
|
||||
self.hisparse_coordinator.num_real_reqs.fill_(forward_batch.batch_size)
|
||||
|
||||
# Replay cuda graph if applicable
|
||||
if can_run_graph:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user