Add Inkling model support (#31681)

Co-authored-by: Chunan Zeng <zcnrex@gmail.com>
Co-authored-by: Ke Bao <ispobaoke@gmail.com>
Co-authored-by: Yanbin Jiang <jybsuper@gmail.com>
Co-authored-by: Yuhao Yang <47235274+yhyang201@users.noreply.github.com>
Co-authored-by: Qiaolin Yu <qiaolin.yu@radixark.ai>
Co-authored-by: Zhichen Zeng <zczeng@uw.edu>
Co-authored-by: Aurick Qiao <aurick@thinkingmachines.ai>
Co-authored-by: Joseph <jk@thinkingmachines.ai>
This commit is contained in:
Cheng Wan
2026-07-19 22:57:37 -07:00
committed by GitHub
co-authored by Chunan Zeng Ke Bao Yanbin Jiang Yuhao Yang Qiaolin Yu Zhichen Zeng Aurick Qiao Joseph
parent 829e9ce9d5
commit 02236fa38c
279 changed files with 74334 additions and 931 deletions
+8
View File
@@ -33,6 +33,7 @@ dependencies = [
"flash-attn-4==4.0.0b15",
"flashinfer_python[cu13]==0.6.14", # keep it aligned with jit-cache version in Dockerfile
"gguf",
"helion==0.2.6",
"humming-kernels[cu13]==0.1.10",
"interegular",
"IPython",
@@ -42,6 +43,7 @@ dependencies = [
"modelscope",
"msgspec",
"ninja",
"numba==0.65.1",
"numpy",
"nvidia-cutlass-dsl[cu13]==4.5.2",
"nvidia-mathdx==25.6.0",
@@ -228,5 +230,11 @@ target = "sglang.srt.grpc._core"
path = "../rust/sglang-grpc/Cargo.toml"
binding = "PyO3"
[[tool.setuptools-rust.ext-modules]]
target = "sglang.srt.multimodal._core"
path = "../rust/sglang-mm/Cargo.toml"
binding = "PyO3"
debug = false
[tool.kernels.dependencies]
"kernels-community/sgl-flash-attn3" = 1
+9 -1
View File
@@ -1,5 +1,5 @@
[build-system]
requires = ["setuptools>=61.0", "setuptools-scm>=8.0", "wheel"]
requires = ["setuptools>=61.0", "setuptools-rust>=1.10", "setuptools-scm>=8.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
@@ -30,6 +30,7 @@ runtime_common = [
"einops",
"fastapi",
"gguf",
"helion==0.2.6",
"interegular",
"IPython",
"llguidance>=1.7.6,<2.0.0",
@@ -191,6 +192,13 @@ dev_mps = ["sglang[all_mps]", "sglang[test]"]
[project.scripts]
sglang = "sglang.cli.main:main"
# Rust-accelerated multimodal preprocessing (sglang.srt.multimodal._core).
# grpc is intentionally omitted here (it needs proto/tonic); ROCm only builds mm.
[[tool.setuptools-rust.ext-modules]]
target = "sglang.srt.multimodal._core"
path = "../rust/sglang-mm/Cargo.toml"
binding = "PyO3"
[tool.setuptools.package-data]
"sglang" = [
"srt/**/*",
@@ -0,0 +1,217 @@
// Depthwise causal conv1d (extend/prefill) with the W-1 prefix taps gathered
// directly from sconv_cache.
//
// Semantics:
// For packed token t in sequence s (bos = cu_seqlens[s], slot = safe_idx[s]) and
// tap iw in 0..W-1, shifted = t - (W-1) + iw:
// shifted >= bos (in-seq history) -> tap = x[shifted, d]
// shifted < bos, pp=shifted-bos+(W-1)>=0 -> tap = cache[slot, pp, d]
// (* cache_mask[s] when !IS_DECODE)
// else -> tap = 0
// out[t,d] = act(sum_iw tap*weight[d,iw]) (+ x[t,d] if residual), fp32 accum.
// in_x / in_prefix are mutually exclusive, so the fp32 tap sum is bit-identical to
// the Triton bf16 add (one operand is always 0).
//
// Channel-independent control is shared by two channels packed as bf16x2.
// Each thread keeps a token strip and its prefix window in registers across taps.
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize, SymbolicDevice
#include <sgl_kernel/utils.h> // For RuntimeCheck, div_ceil
#include <sgl_kernel/type.cuh> // For fp32_t / bf16_t aliases
#include <sgl_kernel/utils.cuh> // For LaunchKernel, SGL_DEVICE
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
#include <cuda_bf16.h>
namespace {
struct CausalConv1dParams {
const void* __restrict__ x; // [T, D]
const void* __restrict__ cache; // [max_slots, W-1, D]
const void* __restrict__ safe_idx; // int64 [nseq] cache slot per sequence
const void* __restrict__ cache_mask; // bool [nseq,1,1] raw metadata
const void* __restrict__ weight; // [D, W]
const void* __restrict__ cu; // int64 [nseq+1] packed sequence starts
const void* __restrict__ seq_idx; // int32 [T] sequence id per token
void* __restrict__ y; // [T, D] contiguous output
int64_t x_stride_t;
int64_t cache_stride_slot;
int64_t cache_stride_w;
int64_t cache_mask_stride;
int64_t weight_stride_d;
int64_t y_stride_t;
uint32_t T;
uint32_t D;
};
constexpr int kConvBlockT = 4; // tokens per thread strip
constexpr uint32_t kConvThreads = 256; // threads per block (each owns 2 channels)
// blockIdx.x = token strip (BLOCK_T tokens); each thread owns channel pair (c0, c0+1).
// Requires bf16, D even, and unit channel/row-inner stride (host-checked).
template <int W, bool USE_SILU, bool USE_RESIDUAL, bool IS_DECODE, typename DType>
__global__ void causal_conv1d_kernel(const __grid_constant__ CausalConv1dParams p) {
constexpr int BT = kConvBlockT;
constexpr int WIN = BT + (W - 1);
__shared__ int s_bos[BT];
__shared__ int s_slot[BT];
__shared__ float s_m[BT];
const int T = static_cast<int>(p.T);
const int t0 = static_cast<int>(blockIdx.x) * BT;
if (threadIdx.x < static_cast<uint32_t>(BT)) {
const int j = static_cast<int>(threadIdx.x);
const int t = t0 + j;
if (t < T) {
const int seq = static_cast<const int32_t*>(p.seq_idx)[t];
s_bos[j] = static_cast<int>(static_cast<const int64_t*>(p.cu)[seq]);
s_slot[j] = static_cast<int>(static_cast<const int64_t*>(p.safe_idx)[seq]);
if constexpr (!IS_DECODE) {
s_m[j] = static_cast<const bool*>(p.cache_mask)[static_cast<int64_t>(seq) * p.cache_mask_stride] ? 1.0f : 0.0f;
}
}
}
__syncthreads();
const int c0 = (blockIdx.y * kConvThreads + threadIdx.x) * 2; // this thread's channel pair
if (c0 >= static_cast<int>(p.D)) return;
const int sxt = static_cast<int>(p.x_stride_t);
const int syt = static_cast<int>(p.y_stride_t);
const int swd = static_cast<int>(p.weight_stride_d);
const auto* xp = static_cast<const __nv_bfloat16*>(p.x);
const auto* cp = static_cast<const __nv_bfloat16*>(p.cache);
const auto* wp = static_cast<const __nv_bfloat16*>(p.weight);
auto* yp = static_cast<__nv_bfloat16*>(p.y);
// Window (bf16x2 per row), read once into registers.
__nv_bfloat162 xr[WIN];
#pragma unroll
for (int i = 0; i < WIN; ++i) {
const int row = t0 - (W - 1) + i;
xr[i] = (row >= 0 && row < T) ? *reinterpret_cast<const __nv_bfloat162*>(&xp[row * sxt + c0])
: __float2bfloat162_rn(0.0f);
}
// Weight taps for the two channels (weight[c0, iw], weight[c0+1, iw]).
float2 wv[W];
#pragma unroll
for (int iw = 0; iw < W; ++iw) {
wv[iw] = make_float2(__bfloat162float(wp[c0 * swd + iw]), __bfloat162float(wp[(c0 + 1) * swd + iw]));
}
#pragma unroll
for (int j = 0; j < BT; ++j) {
const int t = t0 + j;
if (t >= T) break;
const int bos = s_bos[j];
const float2 x_cur = __bfloat1622float2(xr[j + (W - 1)]); // tap iw == W-1
float acc0 = 0.0f, acc1 = 0.0f;
#pragma unroll
for (int iw = 0; iw < W; ++iw) {
float2 tap;
if (iw == W - 1) {
tap = x_cur;
} else {
const int shifted = t - (W - 1) + iw; // < T always
tap = (shifted >= bos) ? __bfloat1622float2(xr[j + iw]) : make_float2(0.0f, 0.0f);
const int prefix_pos = shifted - bos + (W - 1);
if (shifted < bos && prefix_pos >= 0 && prefix_pos < (W - 1)) { // rare: seq start
const int64_t coff = static_cast<int64_t>(s_slot[j]) * p.cache_stride_slot +
static_cast<int64_t>(prefix_pos) * p.cache_stride_w + static_cast<int64_t>(c0);
float2 pv = __bfloat1622float2(*reinterpret_cast<const __nv_bfloat162*>(&cp[coff]));
if constexpr (!IS_DECODE) {
pv.x *= s_m[j];
pv.y *= s_m[j];
}
tap.x += pv.x;
tap.y += pv.y;
}
}
acc0 += tap.x * wv[iw].x;
acc1 += tap.y * wv[iw].y;
}
if constexpr (USE_SILU) {
acc0 = __fdividef(acc0, 1.0f + __expf(-acc0)); // silu = x*sigmoid(x)
acc1 = __fdividef(acc1, 1.0f + __expf(-acc1));
}
if constexpr (USE_RESIDUAL) {
acc0 += x_cur.x;
acc1 += x_cur.y;
}
*reinterpret_cast<__nv_bfloat162*>(&yp[t * syt + c0]) = __floats2bfloat162_rn(acc0, acc1);
}
}
template <int W, bool USE_SILU, bool USE_RESIDUAL, bool IS_DECODE, typename DType>
struct CausalConv1dKernel {
static void
run(tvm::ffi::TensorView x,
tvm::ffi::TensorView cache,
tvm::ffi::TensorView safe_idx,
tvm::ffi::TensorView cache_mask,
tvm::ffi::TensorView weight,
tvm::ffi::TensorView cu,
tvm::ffi::TensorView seq_idx,
tvm::ffi::TensorView y) {
using namespace host;
auto T = SymbolicSize{"T"};
auto D = SymbolicSize{"D"};
auto Wd = SymbolicSize{"W"};
auto Km1 = SymbolicSize{"W_minus_1"};
auto NS = SymbolicSize{"nseq"};
auto dev = SymbolicDevice{};
dev.set_options<kDLCUDA>();
Wd.set_value(W);
Km1.set_value(W - 1);
// x may be a non-contiguous row view (stride_t arbitrary) but must be
// channel-contiguous. cache_mask is torch-bool (verify shape/device only).
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(x);
TensorMatcher({-1, Km1, D}).with_dtype<DType>().with_device(dev).verify(cache);
TensorMatcher({NS}).with_dtype<int64_t>().with_device(dev).verify(safe_idx);
TensorMatcher({NS, 1, 1}).with_device(dev).verify(cache_mask);
TensorMatcher({D, Wd}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(weight);
TensorMatcher({-1}).with_dtype<int64_t>().with_device(dev).verify(cu);
TensorMatcher({T}).with_dtype<int32_t>().with_device(dev).verify(seq_idx);
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(y);
RuntimeCheck(cu.size(0) == NS.unwrap() + 1, "cu must have length nseq+1");
RuntimeCheck(sizeof(DType) == 2, "causal_conv1d: bf16x2 kernel requires a 16-bit dtype");
RuntimeCheck(D.unwrap() % 2 == 0, "causal_conv1d: D must be even for the bf16x2 kernel");
RuntimeCheck(cache.stride(2) == 1, "causal_conv1d: sconv_cache must be channel-contiguous");
const auto params = CausalConv1dParams{
.x = x.data_ptr(),
.cache = cache.data_ptr(),
.safe_idx = safe_idx.data_ptr(),
.cache_mask = cache_mask.data_ptr(),
.weight = weight.data_ptr(),
.cu = cu.data_ptr(),
.seq_idx = seq_idx.data_ptr(),
.y = y.data_ptr(),
.x_stride_t = x.stride(0),
.cache_stride_slot = cache.stride(0),
.cache_stride_w = cache.stride(1),
.cache_mask_stride = cache_mask.stride(0),
.weight_stride_d = weight.stride(0),
.y_stride_t = y.stride(0),
.T = static_cast<uint32_t>(T.unwrap()),
.D = static_cast<uint32_t>(D.unwrap()),
};
const uint32_t d_pairs = params.D / 2;
const dim3 grid{div_ceil(params.T, static_cast<uint32_t>(kConvBlockT)), div_ceil(d_pairs, kConvThreads)};
const dim3 block{kConvThreads};
constexpr auto kernel = causal_conv1d_kernel<W, USE_SILU, USE_RESIDUAL, IS_DECODE, DType>;
LaunchKernel(grid, block, dev.unwrap())(kernel, params);
}
};
} // namespace
@@ -0,0 +1,147 @@
// Fused draft-extend convolution-cache update.
//
// Speculative draft-extend: for each sequence b (slot ci = cache_indices[b]) the new
// conv state is the length-W1 window of the "virtual padded" stream
// virtual = [ sconv_cache[ci] (W1 rows) ++ hidden[b, 0:T] (T rows) ]
// starting at num_accepted_tokens[b]: new[w] = virtual[n_acc + w], w in 0..W1-1
// n_acc + w < W1 -> sconv_cache[ci, n_acc + w] (initial state)
// n_acc + w >= W1 -> hidden[b*T + (n_acc + w - W1)] (a draft token)
// written back to sconv_cache[ci]. With tracking, the window at track_step[b] is also
// written to sconv_cache[mamba_track_indices[b]] wherever crossed[b].
// Pure copy/select (BIT-EXACT). Init state loaded to registers before writes (RAW-safe);
// 2 channels/thread as bf16x2. Requires bf16 + even D + channel-contiguous.
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize, SymbolicDevice
#include <sgl_kernel/utils.h> // For RuntimeCheck, div_ceil
#include <sgl_kernel/utils.cuh> // For LaunchKernel, SGL_DEVICE
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
#include <cuda_bf16.h>
namespace {
struct DraftExtendParams {
const void* __restrict__ hidden; // [B*T, D], channel-contiguous
void* __restrict__ cache; // [pool, W1, D], in-place
const void* __restrict__ cache_indices; // int32 [B]
const void* __restrict__ num_accepted; // int32 [B]
const void* __restrict__ crossed; // bool [B] (DO_TRACK only)
const void* __restrict__ track_step; // int32 [B] (DO_TRACK only)
const void* __restrict__ track_indices; // int64 [B] (DO_TRACK only)
int64_t hs_stride_t;
int64_t cache_stride_slot;
int64_t cache_stride_w;
uint32_t D;
uint32_t T; // draft_token_num
};
constexpr uint32_t kDEThreads = 256;
template <int W1, bool DO_TRACK, typename DType>
__global__ void draft_extend_kernel(const __grid_constant__ DraftExtendParams p) {
const int b = blockIdx.y;
const int c0 = (blockIdx.x * kDEThreads + threadIdx.x) * 2;
if (c0 >= static_cast<int>(p.D)) return;
const int ci = static_cast<const int32_t*>(p.cache_indices)[b];
const auto* hp = static_cast<const __nv_bfloat16*>(p.hidden);
auto* cp = static_cast<__nv_bfloat16*>(p.cache);
const int cw = static_cast<int>(p.cache_stride_w);
const int T = static_cast<int>(p.T);
const int b_off = b * T; // hidden row base for this sequence
const int64_t src_slot_base = static_cast<int64_t>(ci) * p.cache_stride_slot + c0;
// Initial state -> registers (RAW-safe against the cache[ci] writes below).
__nv_bfloat162 init_reg[W1];
#pragma unroll
for (int w = 0; w < W1; ++w) {
init_reg[w] = *reinterpret_cast<const __nv_bfloat162*>(&cp[src_slot_base + static_cast<int64_t>(w) * cw]);
}
// Select the window at `at` from the virtual stream and write it to cache[dst_base].
auto emit = [&](int at, int64_t dst_base) {
#pragma unroll
for (int w = 0; w < W1; ++w) {
const int pos = at + w;
__nv_bfloat162 v;
if (pos < W1) {
v = init_reg[0];
#pragma unroll
for (int src = 0; src < W1; ++src) {
if (src == pos) v = init_reg[src];
}
} else {
const int row = b_off + (pos - W1);
v = *reinterpret_cast<const __nv_bfloat162*>(&hp[static_cast<int64_t>(row) * p.hs_stride_t + c0]);
}
*reinterpret_cast<__nv_bfloat162*>(&cp[dst_base + static_cast<int64_t>(w) * cw]) = v;
}
};
const int n_acc = static_cast<const int32_t*>(p.num_accepted)[b];
if constexpr (DO_TRACK) {
// Track window first (reads init_reg, distinct dst slot) then the main window.
if (static_cast<const bool*>(p.crossed)[b]) {
const int tstep = static_cast<const int32_t*>(p.track_step)[b];
const int64_t tslot = static_cast<const int64_t*>(p.track_indices)[b];
emit(tstep, tslot * p.cache_stride_slot + c0);
}
}
emit(n_acc, src_slot_base);
}
template <int W1, bool DO_TRACK, typename DType>
struct DraftExtendSconvKernel {
static void
run(tvm::ffi::TensorView hidden,
tvm::ffi::TensorView cache,
tvm::ffi::TensorView cache_indices,
tvm::ffi::TensorView num_accepted,
int64_t draft_token_num,
tvm::ffi::TensorView crossed,
tvm::ffi::TensorView track_step,
tvm::ffi::TensorView track_indices) {
using namespace host;
auto BT = SymbolicSize{"B_times_T"};
auto D = SymbolicSize{"D"};
auto W1s = SymbolicSize{"W_minus_1"};
auto B = SymbolicSize{"B"};
auto dev = SymbolicDevice{};
dev.set_options<kDLCUDA>();
W1s.set_value(W1);
TensorMatcher({BT, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(hidden);
TensorMatcher({-1, W1s, D}).with_dtype<DType>().with_device(dev).verify(cache);
TensorMatcher({B}).with_dtype<int32_t>().with_device(dev).verify(cache_indices);
TensorMatcher({B}).with_dtype<int32_t>().with_device(dev).verify(num_accepted);
RuntimeCheck(sizeof(DType) == 2, "draft_extend: bf16x2 kernel requires 16-bit dtype");
RuntimeCheck(D.unwrap() % 2 == 0, "draft_extend: D must be even for the bf16x2 kernel");
RuntimeCheck(cache.stride(2) == 1, "draft_extend: cache must be channel-contiguous");
const auto params = DraftExtendParams{
.hidden = hidden.data_ptr(),
.cache = cache.data_ptr(),
.cache_indices = cache_indices.data_ptr(),
.num_accepted = num_accepted.data_ptr(),
.crossed = DO_TRACK ? crossed.data_ptr() : nullptr,
.track_step = DO_TRACK ? track_step.data_ptr() : nullptr,
.track_indices = DO_TRACK ? track_indices.data_ptr() : nullptr,
.hs_stride_t = hidden.stride(0),
.cache_stride_slot = cache.stride(0),
.cache_stride_w = cache.stride(1),
.D = static_cast<uint32_t>(D.unwrap()),
.T = static_cast<uint32_t>(draft_token_num),
};
const uint32_t d_pairs = params.D / 2;
const dim3 grid{div_ceil(d_pairs, kDEThreads), static_cast<uint32_t>(B.unwrap())};
const dim3 block{kDEThreads};
constexpr auto kernel = draft_extend_kernel<W1, DO_TRACK, DType>;
LaunchKernel(grid, block, dev.unwrap())(kernel, params);
}
};
} // namespace
@@ -0,0 +1,187 @@
// Fused decode causal_conv1d, cache shift-update, and optional track copy.
//
// Decode: each token t is its own sequence (bos=t). Per token:
// conv: acc = sum_{iw<W-1} cache[slot, iw, d]*cache_mask[t] * weight[d, iw]
// + x[t, d] * weight[d, W-1]
// y[t,d] = act(acc) (+ x[t,d] if residual).
// update (valid lanes, ci != PAD): shift the state left, append current token --
// new[iw] = cache[slot, iw+1]*cache_mask[t] (iw < W-2); new[W-2] = x[t].
// track (DO_TRACK): the same post-update window is also written to
// cache[track_indices[t]] wherever track_mask[t] (prefix-cache ping-pong slot).
// Working slots and ping-pong track slots are pairwise-distinct, so writes never race.
// Cache history is loaded to registers BEFORE any write (RAW-safe). 2 channels/thread
// as bf16x2; conv accumulates in fp32 (matches the fp32 reference), update is a
// bit-exact bf16 move. Requires bf16 + even D + channel-contiguous cache/x/y.
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize, SymbolicDevice
#include <sgl_kernel/utils.h> // For RuntimeCheck, div_ceil
#include <sgl_kernel/utils.cuh> // For LaunchKernel, SGL_DEVICE
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
#include <cuda_bf16.h>
namespace {
struct DecodeUpdateParams {
const void* __restrict__ x; // [T, D], channel-contiguous
void* __restrict__ cache; // [pool, W-1, D], in-place update
const void* __restrict__ cache_indices; // int32 [T] (PAD == -1)
const void* __restrict__ cache_mask; // bool [T]
const void* __restrict__ weight; // [D, W]
void* __restrict__ y; // [T, D] contiguous output
const void* __restrict__ track_mask; // bool [T] (DO_TRACK only)
const void* __restrict__ track_indices; // int64 [T] (DO_TRACK only)
int64_t x_stride_t;
int64_t cache_stride_slot;
int64_t cache_stride_w;
int64_t weight_stride_d;
int64_t y_stride_t;
int64_t track_idx_stride;
uint32_t D;
};
constexpr uint32_t kDecThreads = 256;
constexpr int kPadSlot = -1;
template <int W, bool USE_SILU, bool USE_RESIDUAL, bool DO_TRACK, typename DType>
__global__ void fused_decode_update_kernel(const __grid_constant__ DecodeUpdateParams p) {
constexpr int W1 = W - 1; // number of cached history taps / conv-state rows
const int t = blockIdx.y;
const int ci = static_cast<const int32_t*>(p.cache_indices)[t];
const bool valid = ci != kPadSlot;
const int slot = valid ? ci : 0; // clamp: PAD lanes still emit y (discarded), no cache write
const int c0 = (blockIdx.x * kDecThreads + threadIdx.x) * 2;
if (c0 >= static_cast<int>(p.D)) return;
const float cm = static_cast<const bool*>(p.cache_mask)[t] ? 1.0f : 0.0f;
const auto* xp = static_cast<const __nv_bfloat16*>(p.x);
const auto* wp = static_cast<const __nv_bfloat16*>(p.weight);
auto* cp = static_cast<__nv_bfloat16*>(p.cache);
auto* yp = static_cast<__nv_bfloat16*>(p.y);
const int cw = static_cast<int>(p.cache_stride_w);
const int swd = static_cast<int>(p.weight_stride_d);
const int64_t cache_base = static_cast<int64_t>(slot) * p.cache_stride_slot + c0;
// History taps -> registers (RAW-safe against the update writes below).
__nv_bfloat162 hist[W1];
#pragma unroll
for (int w = 0; w < W1; ++w) {
hist[w] = *reinterpret_cast<const __nv_bfloat162*>(&cp[cache_base + static_cast<int64_t>(w) * cw]);
}
const __nv_bfloat162 xv = *reinterpret_cast<const __nv_bfloat162*>(&xp[static_cast<int64_t>(t) * p.x_stride_t + c0]);
const float2 xf = __bfloat1622float2(xv);
float2 wv[W];
#pragma unroll
for (int iw = 0; iw < W; ++iw) {
wv[iw] = make_float2(__bfloat162float(wp[c0 * swd + iw]), __bfloat162float(wp[(c0 + 1) * swd + iw]));
}
// ---- conv (fp32 accum): W-1 cached taps (gated by cache_mask) + current token ----
float acc0 = 0.0f, acc1 = 0.0f;
#pragma unroll
for (int iw = 0; iw < W1; ++iw) {
const float2 h = __bfloat1622float2(hist[iw]);
acc0 += h.x * cm * wv[iw].x;
acc1 += h.y * cm * wv[iw].y;
}
acc0 += xf.x * wv[W1].x;
acc1 += xf.y * wv[W1].y;
if constexpr (USE_SILU) {
acc0 = __fdividef(acc0, 1.0f + __expf(-acc0));
acc1 = __fdividef(acc1, 1.0f + __expf(-acc1));
}
if constexpr (USE_RESIDUAL) {
acc0 += xf.x;
acc1 += xf.y;
}
*reinterpret_cast<__nv_bfloat162*>(&yp[static_cast<int64_t>(t) * p.y_stride_t + c0]) =
__floats2bfloat162_rn(acc0, acc1);
if (!valid) return;
// ---- update: shift state left (gated by cache_mask), append current token ----
const __nv_bfloat162 zero = __float2bfloat162_rn(0.0f);
int64_t track_base = 0;
bool do_tr = false;
if constexpr (DO_TRACK) {
do_tr = static_cast<const bool*>(p.track_mask)[t];
if (do_tr) {
const int64_t tslot = static_cast<const int64_t*>(p.track_indices)[static_cast<int64_t>(t) * p.track_idx_stride];
track_base = tslot * p.cache_stride_slot + c0;
}
}
#pragma unroll
for (int iw = 0; iw < W1; ++iw) {
const __nv_bfloat162 nv = (iw < W1 - 1) ? ((cm != 0.0f) ? hist[iw + 1] : zero) : xv;
*reinterpret_cast<__nv_bfloat162*>(&cp[cache_base + static_cast<int64_t>(iw) * cw]) = nv;
if constexpr (DO_TRACK) {
if (do_tr) {
*reinterpret_cast<__nv_bfloat162*>(&cp[track_base + static_cast<int64_t>(iw) * cw]) = nv;
}
}
}
}
template <int W, bool USE_SILU, bool USE_RESIDUAL, bool DO_TRACK, typename DType>
struct FusedDecodeUpdateKernel {
static void
run(tvm::ffi::TensorView x,
tvm::ffi::TensorView cache,
tvm::ffi::TensorView cache_indices,
tvm::ffi::TensorView cache_mask,
tvm::ffi::TensorView weight,
tvm::ffi::TensorView y,
tvm::ffi::TensorView track_mask,
tvm::ffi::TensorView track_indices) {
using namespace host;
auto T = SymbolicSize{"T"};
auto D = SymbolicSize{"D"};
auto Wd = SymbolicSize{"W"};
auto W1s = SymbolicSize{"W_minus_1"};
auto dev = SymbolicDevice{};
dev.set_options<kDLCUDA>();
Wd.set_value(W);
W1s.set_value(W - 1);
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(x);
TensorMatcher({-1, W1s, D}).with_dtype<DType>().with_device(dev).verify(cache);
TensorMatcher({T}).with_dtype<int32_t>().with_device(dev).verify(cache_indices);
TensorMatcher({T}).with_device(dev).verify(cache_mask);
TensorMatcher({D, Wd}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(weight);
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(y);
RuntimeCheck(sizeof(DType) == 2, "fused_decode: bf16x2 kernel requires 16-bit dtype");
RuntimeCheck(D.unwrap() % 2 == 0, "fused_decode: D must be even for the bf16x2 kernel");
RuntimeCheck(cache.stride(2) == 1, "fused_decode: cache must be channel-contiguous");
const auto params = DecodeUpdateParams{
.x = x.data_ptr(),
.cache = cache.data_ptr(),
.cache_indices = cache_indices.data_ptr(),
.cache_mask = cache_mask.data_ptr(),
.weight = weight.data_ptr(),
.y = y.data_ptr(),
.track_mask = DO_TRACK ? track_mask.data_ptr() : nullptr,
.track_indices = DO_TRACK ? track_indices.data_ptr() : nullptr,
.x_stride_t = x.stride(0),
.cache_stride_slot = cache.stride(0),
.cache_stride_w = cache.stride(1),
.weight_stride_d = weight.stride(0),
.y_stride_t = y.stride(0),
.track_idx_stride = DO_TRACK ? track_indices.stride(0) : 0,
.D = static_cast<uint32_t>(D.unwrap()),
};
const uint32_t d_pairs = params.D / 2;
const dim3 grid{div_ceil(d_pairs, kDecThreads), static_cast<uint32_t>(T.unwrap())};
const dim3 block{kDecThreads};
constexpr auto kernel = fused_decode_update_kernel<W, USE_SILU, USE_RESIDUAL, DO_TRACK, DType>;
LaunchKernel(grid, block, dev.unwrap())(kernel, params);
}
};
} // namespace
@@ -0,0 +1,109 @@
// Fused gather and scatter into sconv_cache.
//
// For each batch element b where mask[b] is true, copy the W1 = W-1 token rows
// hidden_states[track_idx[b, w]] -> sconv_cache[dst[b], w] (w = 0..W1-1).
// Masked-out lanes are left untouched. Pure copy (no arithmetic) => BIT-EXACT.
// 2 channels/thread packed as bf16x2. Requires bf16 + even D + channel-contiguous.
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize, SymbolicDevice
#include <sgl_kernel/utils.h> // For RuntimeCheck, div_ceil
#include <sgl_kernel/utils.cuh> // For LaunchKernel, SGL_DEVICE
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
#include <cuda_bf16.h>
namespace {
struct GatherScatterParams {
const void* __restrict__ hidden; // [T, D], channel-contiguous
void* __restrict__ cache; // [pool, W1, D], in-place scatter target
const void* __restrict__ track_idx; // int32 [B, W1]
const void* __restrict__ mask; // bool [B]
const void* __restrict__ dst; // int64 [B]
int64_t hs_stride_t;
int64_t cache_stride_slot;
int64_t cache_stride_w;
int64_t track_stride_b;
int64_t track_stride_w;
int64_t dst_stride_b;
uint32_t D;
};
constexpr uint32_t kGSThreads = 256;
template <int W1, typename DType>
__global__ void gather_scatter_kernel(const __grid_constant__ GatherScatterParams p) {
const int b = blockIdx.y;
if (!static_cast<const bool*>(p.mask)[b]) return; // masked-out lane: untouched
const int c0 = (blockIdx.x * kGSThreads + threadIdx.x) * 2;
if (c0 >= static_cast<int>(p.D)) return;
const auto* hp = static_cast<const __nv_bfloat16*>(p.hidden);
auto* cp = static_cast<__nv_bfloat16*>(p.cache);
const int64_t dst_slot = static_cast<const int64_t*>(p.dst)[static_cast<int64_t>(b) * p.dst_stride_b];
const int64_t cache_base = dst_slot * p.cache_stride_slot + c0;
const int64_t track_base = static_cast<int64_t>(b) * p.track_stride_b;
#pragma unroll
for (int w = 0; w < W1; ++w) {
const int64_t src_t =
static_cast<const int32_t*>(p.track_idx)[track_base + static_cast<int64_t>(w) * p.track_stride_w];
const __nv_bfloat162 v = *reinterpret_cast<const __nv_bfloat162*>(&hp[src_t * p.hs_stride_t + c0]);
*reinterpret_cast<__nv_bfloat162*>(&cp[cache_base + static_cast<int64_t>(w) * p.cache_stride_w]) = v;
}
}
template <int W1, typename DType>
struct GatherScatterSconvKernel {
static void
run(tvm::ffi::TensorView hidden,
tvm::ffi::TensorView cache,
tvm::ffi::TensorView track_idx,
tvm::ffi::TensorView mask,
tvm::ffi::TensorView dst) {
using namespace host;
auto T = SymbolicSize{"T"};
auto D = SymbolicSize{"D"};
auto W1s = SymbolicSize{"W_minus_1"};
auto B = SymbolicSize{"B"};
auto dev = SymbolicDevice{};
dev.set_options<kDLCUDA>();
W1s.set_value(W1);
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(hidden);
TensorMatcher({-1, W1s, D}).with_dtype<DType>().with_device(dev).verify(cache);
TensorMatcher({B, W1s}).with_dtype<int32_t>().with_device(dev).verify(track_idx);
TensorMatcher({B}).with_device(dev).verify(mask);
TensorMatcher({B}).with_dtype<int64_t>().with_device(dev).verify(dst);
RuntimeCheck(sizeof(DType) == 2, "gather_scatter: bf16x2 kernel requires 16-bit dtype");
RuntimeCheck(D.unwrap() % 2 == 0, "gather_scatter: D must be even for the bf16x2 kernel");
RuntimeCheck(cache.stride(2) == 1, "gather_scatter: cache must be channel-contiguous");
const auto params = GatherScatterParams{
.hidden = hidden.data_ptr(),
.cache = cache.data_ptr(),
.track_idx = track_idx.data_ptr(),
.mask = mask.data_ptr(),
.dst = dst.data_ptr(),
.hs_stride_t = hidden.stride(0),
.cache_stride_slot = cache.stride(0),
.cache_stride_w = cache.stride(1),
.track_stride_b = track_idx.stride(0),
.track_stride_w = track_idx.stride(1),
.dst_stride_b = dst.stride(0),
.D = static_cast<uint32_t>(D.unwrap()),
};
const uint32_t d_pairs = params.D / 2;
const dim3 grid{div_ceil(d_pairs, kGSThreads), static_cast<uint32_t>(B.unwrap())};
const dim3 block{kGSThreads};
constexpr auto kernel = gather_scatter_kernel<W1, DType>;
LaunchKernel(grid, block, dev.unwrap())(kernel, params);
}
};
} // namespace
@@ -0,0 +1,672 @@
// Two-shot (reduce-scatter + all-gather) all-reduce over a torch
// symmetric-memory buffer.
//
// It operates IN PLACE on the peer symm buffers: the producer (e.g. the wo_ud /
// MoE-combine GEMM) writes its local shard straight into THIS rank's symm buffer
// (via get_ar_buffer), so there is no stage-in copy; the reduced result is left
// in the buffer and handed back to Python as a view, so there is no copy-out.
//
// Correctness (two-shot is race-safe in place): rank r owns the disjoint vec
// slice [local_vec_start, local_vec_finish); it reads every peer's slice, sums,
// and broadcasts the sum back to every peer's slice. Only rank r ever writes
// slice S_r (in any buffer), so there is no write-write conflict, and each
// per-element load completes before its store (data dependency).
//
// Two variants:
// * ..._kernel (v1): no in-kernel sync; the caller fences with the symm-mem
// handle's barrier() on each side (3 launches total).
// * ..._fused_kernel (v2): an in-kernel per-block system barrier (entry:
// producers done + visible; exit: broadcasts done + visible), so the whole
// all-reduce is a single launch. The barrier uses a DEDICATED symmetric
// flags buffer (independent of torch's signal pad, so no interference with
// multimem) and a device-resident monotonic epoch counter per block, which
// keeps advancing across launches -- including CUDA-graph replays -- so
// flags never go stale (spin is `flag < epoch`, epoch strictly increasing).
//
// Fusion seam: the reduced `result` Storage below is where an epilogue (RMSNorm
// / short-conv / bias) plugs in -- applied in registers before the broadcast
// store, so the normed/conv'd result never makes an extra HBM round trip.
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/runtime.cuh>
#include <sgl_kernel/type.cuh>
#include <sgl_kernel/utils.cuh>
#include <sgl_kernel/vec.cuh>
#include <dlpack/dlpack.h>
#include "inkling_ar_barrier.cuh"
#include <bit>
#include <cstdint>
#include <mutex>
#include <type_traits>
#include <unordered_map>
namespace {
template <typename DType, uint32_t kNumGPU>
struct InklingAllReduceTrait {
static constexpr uint32_t kVecSize = 16 / (sizeof(DType) * 2);
static constexpr uint32_t kElemsPerVec = kVecSize * 2;
using DType2 = packed_t<DType>;
using Storage = device::AlignedVector<DType2, kVecSize>;
static_assert(sizeof(Storage) == 16 && alignof(Storage) == 16, "Storage must be 16B");
static_assert(std::has_single_bit(kNumGPU), "kNumGPU must be a power of two");
};
// Register-level fused add of two vecs (fp32 math, ONE round to DType) -- the
// exact numerics of torch.add on two bf16 tensors, so fusing the shared-expert
// partials stays bit-identical to the unfused {torch.add -> AR} chain.
template <typename DType>
__device__ __forceinline__ typename InklingAllReduceTrait<DType, 2>::Storage add_vec_rn(
const typename InklingAllReduceTrait<DType, 2>::Storage& a,
const typename InklingAllReduceTrait<DType, 2>::Storage& b) {
using namespace device;
using Trait = InklingAllReduceTrait<DType, 2>; // kNumGPU-independent
using DType2 = typename Trait::DType2;
typename Trait::Storage out;
#pragma unroll
for (uint32_t j = 0; j < Trait::kVecSize; ++j) {
const fp32x2_t x = cast<fp32x2_t>(a[j]);
const fp32x2_t y = cast<fp32x2_t>(b[j]);
fp32x2_t s;
s.x = x.x + y.x;
s.y = x.y + y.y;
out[j] = cast<DType2>(s);
}
return out;
}
// Fused-shared PROLOGUE for the pull-based kernels (v2/v3/v3b/v4): fold this
// rank's LOCAL shared-expert partials into its own symm input region before
// the ENTRY barrier, so every peer's ld_reduce / peer-read sums
// (routed_r + shared_r) across ranks. The entry barrier must then run in
// publish mode (grid_system_barrier, publish_writes=true): these are in-kernel
// stores by ALL CTAs, not prior-kernel stores, so each CTA has to
// system-publish them before the leader's release. (The per-block barrier
// cannot order this: block b's fold range is not the range peer block b
// reads.) The push-based kernels (v5 & the fused decode family) instead fold
// in registers at the push -- see the shared branch in the push loop.
template <typename DType, uint32_t kNumGPU>
__device__ __forceinline__ void
fold_shared_local(DType* __restrict__ buf, const DType* __restrict__ shared, uint32_t num_items) {
using Trait = InklingAllReduceTrait<DType, kNumGPU>;
using Storage = typename Trait::Storage;
const uint32_t total_vec = num_items / Trait::kElemsPerVec;
const uint32_t stride = gridDim.x * blockDim.x;
for (uint32_t v = blockIdx.x * blockDim.x + threadIdx.x; v < total_vec; v += stride) {
Storage a, b;
a.load(buf, v);
b.load(shared, v);
add_vec_rn<DType>(a, b).store(buf, v);
}
}
// Two-shot partition: contiguous, warp-aligned vec slice per rank. Returns
// {start, count} in vec units (empty for trailing ranks when the range is small).
template <typename DType, uint32_t kNumGPU>
__device__ __forceinline__ uint2 rank_vec_slice(uint32_t rank, uint32_t num_items) {
using namespace device;
using Trait = InklingAllReduceTrait<DType, kNumGPU>;
const uint32_t total_vec = num_items / Trait::kElemsPerVec;
const uint32_t vec_per_rank = div_ceil(div_ceil(total_vec, kNumGPU), kWarpThreads) * kWarpThreads;
const uint32_t start = min(rank * vec_per_rank, total_vec);
const uint32_t finish = min(start + vec_per_rank, total_vec);
return {start, finish - start};
}
// Offset each peer pointer to this rank's slice, and return the slice's local
// vec count.
template <typename DType, uint32_t kNumGPU>
__device__ __forceinline__ uint32_t
slice_setup(DType* (&input)[kNumGPU], void* const* peer_ptrs, uint32_t rank, uint32_t num_items) {
using Trait = InklingAllReduceTrait<DType, kNumGPU>;
const uint2 slice = rank_vec_slice<DType, kNumGPU>(rank, num_items);
const uint32_t base = slice.x * Trait::kElemsPerVec;
#pragma unroll
for (uint32_t i = 0; i < kNumGPU; ++i)
input[i] = static_cast<DType*>(peer_ptrs[i]) + base;
return slice.y; // local vec count
}
template <typename DType, uint32_t kNumGPU>
__device__ __forceinline__ void two_shot_reduce_local(DType* (&input)[kNumGPU], uint32_t local_vecs) {
using namespace device;
using Trait = InklingAllReduceTrait<DType, kNumGPU>;
using Storage = typename Trait::Storage;
using DType2 = typename Trait::DType2;
constexpr uint32_t kVecSize = Trait::kVecSize;
const uint32_t stride = gridDim.x * blockDim.x;
for (uint32_t v = blockIdx.x * blockDim.x + threadIdx.x; v < local_vecs; v += stride) {
Storage s[kNumGPU];
#pragma unroll
for (uint32_t i = 0; i < kNumGPU; ++i)
s[i].load(input[i], v);
Storage result;
#pragma unroll
for (uint32_t j = 0; j < kVecSize; ++j) {
fp32x2_t acc = cast<fp32x2_t>(s[0][j]);
#pragma unroll
for (uint32_t i = 1; i < kNumGPU; ++i) {
const fp32x2_t x = cast<fp32x2_t>(s[i][j]);
acc.x += x.x;
acc.y += x.y;
}
result[j] = cast<DType2>(acc); // <-- EPILOGUE SEAM
}
#pragma unroll
for (uint32_t i = 0; i < kNumGPU; ++i)
result.store(input[i], v);
}
}
// v1: no in-kernel barrier (caller fences via hdl.barrier()).
template <typename DType, uint32_t kNumGPU>
__global__ __launch_bounds__(1024, 1) void inkling_two_shot_all_reduce_kernel(
void* const* __restrict__ peer_ptrs, const uint32_t rank, const uint32_t num_items) {
DType* input[kNumGPU];
const uint32_t local_vecs = slice_setup<DType, kNumGPU>(input, peer_ptrs, rank, num_items);
two_shot_reduce_local<DType, kNumGPU>(input, local_vecs);
}
// v2: single-launch, fused entry + exit system barrier. `shared` (optional):
// this rank's LOCAL shared-expert partials, folded into its own buffer before
// the entry barrier (which then must publish -- see fold_shared_local).
template <typename DType, uint32_t kNumGPU>
__global__ __launch_bounds__(1024, 1) void inkling_two_shot_all_reduce_fused_kernel(
void* const* __restrict__ peer_ptrs,
void* const* __restrict__ flag_ptrs,
uint32_t* __restrict__ state,
const DType* __restrict__ shared,
const uint32_t rank,
const uint32_t num_items) {
DType* input[kNumGPU];
const uint32_t local_vecs = slice_setup<DType, kNumGPU>(input, peer_ptrs, rank, num_items);
if (shared != nullptr) {
fold_shared_local<DType, kNumGPU>(static_cast<DType*>(peer_ptrs[rank]), shared, num_items);
}
// ENTRY: producers done + visible (publish the fold's in-kernel stores too).
inkling_ar::grid_system_barrier<kNumGPU>(state, flag_ptrs, rank, 0, /*publish_writes=*/shared != nullptr);
two_shot_reduce_local<DType, kNumGPU>(input, local_vecs);
inkling_ar::grid_system_barrier<kNumGPU>(
state, flag_ptrs, rank, 1, /*publish_writes=*/true); // EXIT: broadcasts done + visible
}
// Multimem one-shot all-reduce: uses the NVLink multicast ld_reduce/st hardware
// instructions on the symm buffer's multicast pointer -- the same in-switch
// reduce torch's multimem_all_reduce_ uses -- so it matches multimem for the
// tiny, latency-bound decode messages where two-shot's N peer reads lose. Reduce
// is one transaction (hardware sums all GPUs); scatter partition keeps the store
// traffic minimal. bf16-only (multimem.add supports .bf16x2 on sm90/sm100).
// kPerBlockBarrier swaps both barriers for block_system_barrier (per-block
// peer handshake, no grid funnel). Correct for the two-shot too: any peer
// block's ENTRY signal proves that peer's producer kernel completed (kernel
// serialization on its stream), and kernel end is a grid-wide join, so my
// per-block EXIT waits compose into "every peer block's broadcasts done"
// before my consumer can run. The two calls share the per-block epoch slot
// (it just advances twice per launch).
template <typename DType, uint32_t kNumGPU, bool kPerBlockBarrier>
__global__ __launch_bounds__(1024, 1) void inkling_multimem_one_shot_fused_kernel(
DType* __restrict__ mc_ptr, // multicast base pointer (covers all peers)
DType* __restrict__ local_ptr, // this rank's LOCAL base of the same buffer
void* const* __restrict__ flag_ptrs,
uint32_t* __restrict__ state,
const DType* __restrict__ shared, // optional LOCAL shared-expert partials
const uint32_t rank,
const uint32_t num_items) {
using namespace device;
using Trait = InklingAllReduceTrait<DType, kNumGPU>;
static_assert(std::is_same_v<DType, bf16_t>, "multimem.add path is bf16-only");
constexpr uint32_t kElemsPerVec = Trait::kElemsPerVec; // 8 bf16 = 16 B
const uint2 slice = rank_vec_slice<DType, kNumGPU>(rank, num_items);
const uint32_t local_vecs = slice.y;
DType* mc = mc_ptr + slice.x * kElemsPerVec;
if (shared != nullptr) {
// Fold covers the FULL range while each peer ld_reduces only its slice, so
// the per-block handshake cannot order it -- use the publishing grid
// barrier for entry even in v3b (exit stays per-block).
fold_shared_local<DType, kNumGPU>(local_ptr, shared, num_items);
inkling_ar::grid_system_barrier<kNumGPU>(state, flag_ptrs, rank, 0, /*publish_writes=*/true);
} else if constexpr (kPerBlockBarrier) {
inkling_ar::block_system_barrier<kNumGPU>(state, flag_ptrs, rank); // ENTRY
} else {
inkling_ar::grid_system_barrier<kNumGPU>(
state, flag_ptrs, rank, 0, /*publish_writes=*/false); // ENTRY: producers done + visible
}
const uint32_t stride = gridDim.x * blockDim.x;
for (uint32_t v = blockIdx.x * blockDim.x + threadIdx.x; v < local_vecs; v += stride) {
DType* addr = mc + v * kElemsPerVec; // 16 B, 16-B aligned
uint32_t r0, r1, r2, r3;
// hardware reduce across all GPUs mapped to the multicast region.
asm volatile("multimem.ld_reduce.relaxed.sys.global.add.v4.bf16x2 {%0,%1,%2,%3}, [%4];"
: "=r"(r0), "=r"(r1), "=r"(r2), "=r"(r3)
: "l"(addr));
// <-- EPILOGUE SEAM (norm / sconv / bias on {r0..r3} before broadcast)
// broadcast the reduced slice to every GPU.
asm volatile(
"multimem.st.relaxed.sys.global.v4.bf16x2 [%0], {%1,%2,%3,%4};" ::"l"(addr), "r"(r0), "r"(r1), "r"(r2), "r"(r3)
: "memory");
}
if constexpr (kPerBlockBarrier) {
inkling_ar::block_system_barrier<kNumGPU>(state, flag_ptrs, rank); // EXIT (release signal publishes)
} else {
inkling_ar::grid_system_barrier<kNumGPU>(
state, flag_ptrs, rank, 1, /*publish_writes=*/true); // EXIT: broadcasts done + visible
}
}
// One-shot PUSH all-reduce (v5): each rank multicast-STORES its full input into
// its per-rank slot of a symmetric staging area (the NVSwitch replicates the
// slot to every GPU), ONE grid barrier waits for all pushes to land, then each
// rank reduces the N staged shards LOCALLY (fp32 accum) into a LOCAL output.
// Single barrier total: the push needs no entry barrier (it publishes only this
// rank's own producer data, stream-ordered locally) and the local output needs
// no exit barrier. Staging reuse is caller-managed (A/B rotation, like v4's
// input; the next AR's barrier proves peers consumed the old buffer).
//
// vs v3/mm (two-shot): drops one full cross-GPU barrier round trip -- wins the
// latency-bound band. vs v4 (full one-shot ld_reduce): switch REPLICATION is
// cheap where the switch's reduce engine serializes N redundant full-range
// reduces, so this scales past v4's 2-row ceiling. Fabric cost: n egress,
// (N-1)*n ingress per GPU; local HBM/L2: N*n read + n write. Like v4, each rank
// holds the FULL row at the epilogue seam (natural RMSNorm-fusion base).
// bf16-only (multimem.st .bf16x2).
//
// kPerBlockBarrier selects block_system_barrier (per-block peer handshake, no
// grid funnel -- the multi-block latency winner) over the single-leader grid
// barrier. Safe here because the reduce loop reads exactly the vec ranges the
// blockIdx-matched pushes wrote.
template <typename DType, uint32_t kNumGPU, bool kPerBlockBarrier>
__global__ __launch_bounds__(1024, 1) void inkling_multimem_push_oneshot_kernel(
const DType* __restrict__ in_ptr, // local input (producer's partial sums)
DType* __restrict__ mc_stage_ptr, // multicast staging base (slot r at r*num_items)
const DType* __restrict__ stage_ptr, // this GPU's LOCAL view of the staging base
DType* __restrict__ out_ptr, // local output
void* const* __restrict__ flag_ptrs,
uint32_t* __restrict__ state,
const DType* __restrict__ shared, // optional LOCAL shared-expert partials
const uint32_t rank,
const uint32_t num_items) {
using namespace device;
using Trait = InklingAllReduceTrait<DType, kNumGPU>;
static_assert(std::is_same_v<DType, bf16_t>, "multimem path is bf16-only");
constexpr uint32_t kElemsPerVec = Trait::kElemsPerVec; // 8 bf16 = 16 B
const uint32_t total_vec = num_items / kElemsPerVec;
const uint32_t stride = gridDim.x * blockDim.x;
// Phase 1: push. One multicast store per vec; the switch fans it out to every
// GPU's replica of slot `rank` (including our own). With `shared`, the
// shared-expert partials fold into the pushed value in registers (fp32 add,
// one bf16 round -- torch.add numerics) at ZERO extra fabric or HBM traffic;
// both barrier flavors stay valid because push/reduce mappings are unchanged.
DType* slot = mc_stage_ptr + rank * num_items;
if (shared != nullptr) {
using Storage = typename Trait::Storage;
for (uint32_t v = blockIdx.x * blockDim.x + threadIdx.x; v < total_vec; v += stride) {
Storage a, b;
a.load(in_ptr, v);
b.load(shared, v);
const Storage s = add_vec_rn<DType>(a, b);
const uint4 d = *reinterpret_cast<const uint4*>(&s);
asm volatile("multimem.st.relaxed.sys.global.v4.bf16x2 [%0], {%1,%2,%3,%4};" ::"l"(slot + v * kElemsPerVec),
"r"(d.x),
"r"(d.y),
"r"(d.z),
"r"(d.w)
: "memory");
}
} else {
for (uint32_t v = blockIdx.x * blockDim.x + threadIdx.x; v < total_vec; v += stride) {
const uint4 d = *reinterpret_cast<const uint4*>(in_ptr + v * kElemsPerVec);
asm volatile("multimem.st.relaxed.sys.global.v4.bf16x2 [%0], {%1,%2,%3,%4};" ::"l"(slot + v * kElemsPerVec),
"r"(d.x),
"r"(d.y),
"r"(d.z),
"r"(d.w)
: "memory");
}
}
// Single barrier: publish our pushes and wait until every rank's pushes for
// OUR ranges have landed in this GPU's local staging copy.
if constexpr (kPerBlockBarrier) {
inkling_ar::block_system_barrier<kNumGPU>(state, flag_ptrs, rank);
} else {
inkling_ar::grid_system_barrier<kNumGPU>(state, flag_ptrs, rank, 0, /*publish_writes=*/true);
}
// Phase 2: local reduce of the N staged shards -- all-local reads (the pushes
// just landed in L2), fp32 accumulation.
using Storage = typename Trait::Storage;
using DType2 = typename Trait::DType2;
constexpr uint32_t kVecSize = Trait::kVecSize;
for (uint32_t v = blockIdx.x * blockDim.x + threadIdx.x; v < total_vec; v += stride) {
Storage s[kNumGPU];
#pragma unroll
for (uint32_t i = 0; i < kNumGPU; ++i)
s[i].load(stage_ptr + i * num_items, v);
Storage result;
#pragma unroll
for (uint32_t j = 0; j < kVecSize; ++j) {
fp32x2_t acc = cast<fp32x2_t>(s[0][j]);
#pragma unroll
for (uint32_t i = 1; i < kNumGPU; ++i) {
const fp32x2_t x = cast<fp32x2_t>(s[i][j]);
acc.x += x.x;
acc.y += x.y;
}
result[j] = cast<DType2>(acc); // <-- EPILOGUE SEAM (full row on-rank)
}
result.store(out_ptr, v);
}
}
// Full one-shot: every rank ld_reduces the ENTIRE range (multicast hardware sum
// -> full result), writing it to a LOCAL output buffer. No broadcast and NO exit
// barrier -- the result is complete on this rank, and input-buffer reuse is the
// caller's responsibility (double-buffer the input). Halving the barrier count
// wins for tiny, latency-bound (decode) messages. bf16-only.
template <typename DType, uint32_t kNumGPU>
__global__ __launch_bounds__(1024, 1) void inkling_multimem_full_oneshot_kernel(
DType* __restrict__ mc_ptr, // multicast input base (covers all peers)
DType* __restrict__ local_in_ptr, // this rank's LOCAL base of the input
DType* __restrict__ out_ptr, // local output base
void* const* __restrict__ flag_ptrs,
uint32_t* __restrict__ state,
const DType* __restrict__ shared, // optional LOCAL shared-expert partials
const uint32_t rank,
const uint32_t num_items) {
using namespace device;
using Trait = InklingAllReduceTrait<DType, kNumGPU>;
static_assert(std::is_same_v<DType, bf16_t>, "multimem.add path is bf16-only");
constexpr uint32_t kElemsPerVec = Trait::kElemsPerVec; // 8 bf16 = 16 B
const uint32_t total_vec = num_items / kElemsPerVec;
if (shared != nullptr) {
// Fold into this rank's (double-buffered) input region; the publishing
// entry barrier then orders it for every peer's ld_reduce. v4 fires only
// for 1-2 rows, so the extra local pass is negligible next to the
// torch.add launch it replaces.
fold_shared_local<DType, kNumGPU>(local_in_ptr, shared, num_items);
}
inkling_ar::grid_system_barrier<kNumGPU>(
state, flag_ptrs, rank, 0, /*publish_writes=*/shared != nullptr); // ENTRY only (single barrier)
const uint32_t stride = gridDim.x * blockDim.x;
for (uint32_t v = blockIdx.x * blockDim.x + threadIdx.x; v < total_vec; v += stride) {
DType* in = mc_ptr + v * kElemsPerVec;
uint32_t r0, r1, r2, r3;
asm volatile("multimem.ld_reduce.relaxed.sys.global.add.v4.bf16x2 {%0,%1,%2,%3}, [%4];"
: "=r"(r0), "=r"(r1), "=r"(r2), "=r"(r3)
: "l"(in));
// <-- EPILOGUE SEAM (norm / sconv / bias on {r0..r3} before the local store)
*reinterpret_cast<uint4*>(out_ptr + v * kElemsPerVec) = make_uint4(r0, r1, r2, r3);
}
// NO exit barrier: result is local & complete; input reuse is caller-managed.
}
// Blocks needed to cover this rank's two-shot slice (v1/v2/v3 partition).
template <typename DType, uint32_t kNumGPU>
uint32_t work_num_blocks(uint32_t n, uint32_t block_size) {
using Trait = InklingAllReduceTrait<DType, kNumGPU>;
const uint32_t total_vec = n / Trait::kElemsPerVec;
const uint32_t vec_per_rank =
host::div_ceil(host::div_ceil(total_vec, kNumGPU), device::kWarpThreads) * device::kWarpThreads;
return max(1u, host::div_ceil(vec_per_rank, block_size));
}
// Blocks needed to cover the FULL vec range (the full one-shot kernel reads
// the entire range on every rank, not a per-rank slice).
template <typename DType>
uint32_t full_range_num_blocks(uint32_t n, uint32_t block_size) {
constexpr uint32_t kElemsPerVec = InklingAllReduceTrait<DType, 2>::kElemsPerVec; // kNumGPU-independent
return max(1u, host::div_ceil(n / kElemsPerVec, block_size));
}
// Max blocks that are simultaneously resident for `kernel` at `block_size`.
// The grid-level barrier REQUIRES all launched blocks to be co-resident (the
// leader waits for every block to arrive); launching more would deadlock, so
// the fused kernels cap their grid at this. Small messages need far fewer.
// Cached per (kernel, block_size, device): the occupancy query costs ~a few us
// on every eager launch of a latency-bound AR otherwise.
template <typename Kernel>
uint32_t max_resident_blocks(Kernel kernel, uint32_t block_size, DLDevice device) {
using namespace host;
static std::mutex mu;
static std::unordered_map<uint64_t, uint32_t> cache;
const uint64_t key = (std::bit_cast<uint64_t>(reinterpret_cast<void*>(kernel)) << 12) ^
(static_cast<uint64_t>(block_size) << 8) ^ static_cast<uint64_t>(device.device_id);
{
std::lock_guard<std::mutex> lk(mu);
if (auto it = cache.find(key); it != cache.end()) return it->second;
}
int sm_count = 0;
cudaDeviceGetAttribute(&sm_count, cudaDevAttrMultiProcessorCount, device.device_id);
RuntimeCheck(sm_count > 0, "failed to query multiProcessorCount");
const uint32_t bps = runtime::get_blocks_per_sm(kernel, block_size);
RuntimeCheck(bps > 0, "kernel has zero occupancy at block_size ", block_size);
const uint32_t result = static_cast<uint32_t>(sm_count) * bps;
std::lock_guard<std::mutex> lk(mu);
cache.emplace(key, result);
return result;
}
// Optional shared-expert partials: numel == 0 -> disabled (nullptr); else a
// LOCAL contiguous tensor covering num_items, folded in-kernel.
template <typename DType>
const DType* shared_ptr_or_null(tvm::ffi::TensorView shared, int64_t num_items) {
using namespace host;
if (shared.numel() == 0) return nullptr;
RuntimeCheck(shared.IsContiguous(), "shared must be contiguous");
RuntimeCheck(is_type<DType>(shared.dtype()), "shared dtype mismatch");
RuntimeCheck(shared.numel() >= num_items, "shared smaller than num_items");
RuntimeCheck(std::bit_cast<intptr_t>(shared.data_ptr()) % 16 == 0, "shared not 16B aligned");
return reinterpret_cast<const DType*>(shared.data_ptr());
}
template <typename DType, uint32_t kNumGPU>
void validate(tvm::ffi::TensorView buf, int64_t peer_ptrs_dev, int64_t rank, int64_t num_items, uint32_t& n) {
using namespace host;
using Trait = InklingAllReduceTrait<DType, kNumGPU>;
n = static_cast<uint32_t>(num_items);
RuntimeCheck(buf.IsContiguous(), "buffer must be contiguous");
RuntimeCheck(buf.device().device_type == kDLCUDA, "buffer must be on a CUDA device");
RuntimeCheck(is_type<DType>(buf.dtype()), "buffer dtype mismatch");
RuntimeCheck(static_cast<int64_t>(n) == num_items, "num_items exceeds 4G");
RuntimeCheck(buf.numel() >= num_items, "buffer smaller than num_items");
RuntimeCheck(n % Trait::kElemsPerVec == 0, "num_items must be a multiple of ", Trait::kElemsPerVec);
RuntimeCheck(std::bit_cast<intptr_t>(buf.data_ptr()) % 16 == 0, "buffer not 16B aligned");
RuntimeCheck(peer_ptrs_dev != 0, "peer_ptrs_dev is null");
RuntimeCheck(rank >= 0 && rank < kNumGPU, "rank out of range");
}
template <typename DType, uint32_t kNumGPU>
void inkling_two_shot_all_reduce(
tvm::ffi::TensorView local_buffer, int64_t peer_ptrs_dev, int64_t rank, int64_t num_items) {
using namespace host;
uint32_t n;
validate<DType, kNumGPU>(local_buffer, peer_ptrs_dev, rank, num_items, n);
const auto device = local_buffer.device();
const uint32_t num_blocks = work_num_blocks<DType, kNumGPU>(n, 1024u); // no in-kernel barrier -> uncapped
const auto stream = LaunchKernel::resolve_device(device);
LaunchKernel(num_blocks, 1024u, stream)(
inkling_two_shot_all_reduce_kernel<DType, kNumGPU>,
reinterpret_cast<void* const*>(peer_ptrs_dev),
static_cast<uint32_t>(rank),
n);
}
template <typename DType, uint32_t kNumGPU>
void inkling_two_shot_all_reduce_fused(
tvm::ffi::TensorView local_buffer,
int64_t data_ptrs_dev,
int64_t flag_ptrs_dev,
int64_t state_ptr,
int64_t rank,
int64_t num_items,
int64_t nb_override,
int64_t bs_override,
tvm::ffi::TensorView shared) {
using namespace host;
uint32_t n;
validate<DType, kNumGPU>(local_buffer, data_ptrs_dev, rank, num_items, n);
RuntimeCheck(flag_ptrs_dev != 0, "flag_ptrs_dev is null");
RuntimeCheck(state_ptr != 0, "state_ptr is null");
const DType* shared_ptr = shared_ptr_or_null<DType>(shared, num_items);
const auto device = local_buffer.device();
const auto kernel = inkling_two_shot_all_reduce_fused_kernel<DType, kNumGPU>;
const uint32_t block_size = bs_override > 0 ? static_cast<uint32_t>(bs_override) : 1024u;
const uint32_t cap = max_resident_blocks(kernel, block_size, device);
const uint32_t num_blocks = nb_override > 0 ? min(static_cast<uint32_t>(nb_override), cap)
: min(work_num_blocks<DType, kNumGPU>(n, block_size), cap);
const auto stream = LaunchKernel::resolve_device(device);
LaunchKernel(num_blocks, block_size, stream)(
kernel,
reinterpret_cast<void* const*>(data_ptrs_dev),
reinterpret_cast<void* const*>(flag_ptrs_dev),
reinterpret_cast<uint32_t*>(state_ptr),
shared_ptr,
static_cast<uint32_t>(rank),
n);
}
template <typename DType, uint32_t kNumGPU>
void inkling_multimem_one_shot_fused(
tvm::ffi::TensorView local_buffer,
int64_t multicast_ptr,
int64_t flag_ptrs_dev,
int64_t state_ptr,
int64_t rank,
int64_t num_items,
int64_t nb_override,
int64_t bs_override,
int64_t per_block_barrier,
tvm::ffi::TensorView shared) {
using namespace host;
uint32_t n;
// validate uses the local buffer view only for device/dtype/shape; the kernel
// operates on the multicast pointer (plus the local view for the shared fold).
validate<DType, kNumGPU>(local_buffer, multicast_ptr, rank, num_items, n);
RuntimeCheck(flag_ptrs_dev != 0, "flag_ptrs_dev is null");
RuntimeCheck(state_ptr != 0, "state_ptr is null");
RuntimeCheck(multicast_ptr % 16 == 0, "multicast_ptr not 16B aligned");
const DType* shared_ptr = shared_ptr_or_null<DType>(shared, num_items);
const auto device = local_buffer.device();
const auto kernel = per_block_barrier ? inkling_multimem_one_shot_fused_kernel<DType, kNumGPU, true>
: inkling_multimem_one_shot_fused_kernel<DType, kNumGPU, false>;
const uint32_t block_size = bs_override > 0 ? static_cast<uint32_t>(bs_override) : 1024u;
uint32_t cap = max_resident_blocks(kernel, block_size, device);
if (per_block_barrier) cap = min(cap, inkling_ar::kMaxBarrierBlocks);
const uint32_t num_blocks = nb_override > 0 ? min(static_cast<uint32_t>(nb_override), cap)
: min(work_num_blocks<DType, kNumGPU>(n, block_size), cap);
const auto stream = LaunchKernel::resolve_device(device);
LaunchKernel(num_blocks, block_size, stream)(
kernel,
reinterpret_cast<DType*>(multicast_ptr),
reinterpret_cast<DType*>(local_buffer.data_ptr()),
reinterpret_cast<void* const*>(flag_ptrs_dev),
reinterpret_cast<uint32_t*>(state_ptr),
shared_ptr,
static_cast<uint32_t>(rank),
n);
}
template <typename DType, uint32_t kNumGPU>
void inkling_multimem_push_oneshot(
tvm::ffi::TensorView in_buffer,
tvm::ffi::TensorView out_buffer,
int64_t mc_stage_ptr,
int64_t local_stage_ptr,
int64_t flag_ptrs_dev,
int64_t state_ptr,
int64_t rank,
int64_t num_items,
int64_t nb_override,
int64_t bs_override,
int64_t per_block_barrier,
tvm::ffi::TensorView shared) {
using namespace host;
uint32_t n;
// in_buffer is any LOCAL contiguous bf16 tensor (need not be a symm buffer);
// validate() covers contiguity/dtype/alignment; mc_stage stands in for the
// pointer null check.
validate<DType, kNumGPU>(in_buffer, mc_stage_ptr, rank, num_items, n);
const DType* shared_ptr = shared_ptr_or_null<DType>(shared, num_items);
RuntimeCheck(out_buffer.IsContiguous(), "out must be contiguous");
RuntimeCheck(is_type<DType>(out_buffer.dtype()), "out dtype mismatch");
RuntimeCheck(out_buffer.numel() >= num_items, "out smaller than num_items");
RuntimeCheck(std::bit_cast<intptr_t>(out_buffer.data_ptr()) % 16 == 0, "out not 16B aligned");
RuntimeCheck(flag_ptrs_dev != 0, "flag_ptrs_dev is null");
RuntimeCheck(state_ptr != 0, "state_ptr is null");
RuntimeCheck(local_stage_ptr != 0, "local_stage_ptr is null");
RuntimeCheck(mc_stage_ptr % 16 == 0, "mc_stage_ptr not 16B aligned");
RuntimeCheck(local_stage_ptr % 16 == 0, "local_stage_ptr not 16B aligned");
const auto device = in_buffer.device();
const auto kernel = per_block_barrier ? inkling_multimem_push_oneshot_kernel<DType, kNumGPU, true>
: inkling_multimem_push_oneshot_kernel<DType, kNumGPU, false>;
const uint32_t block_size = bs_override > 0 ? static_cast<uint32_t>(bs_override) : 1024u;
uint32_t cap = max_resident_blocks(kernel, block_size, device);
// The per-block barrier has kMaxBarrierBlocks flag/epoch slots per rank.
if (per_block_barrier) cap = min(cap, inkling_ar::kMaxBarrierBlocks);
const uint32_t num_blocks = nb_override > 0 ? min(static_cast<uint32_t>(nb_override), cap)
: min(full_range_num_blocks<DType>(n, block_size), cap);
const auto stream = LaunchKernel::resolve_device(device);
LaunchKernel(num_blocks, block_size, stream)(
kernel,
reinterpret_cast<const DType*>(in_buffer.data_ptr()),
reinterpret_cast<DType*>(mc_stage_ptr),
reinterpret_cast<const DType*>(local_stage_ptr),
reinterpret_cast<DType*>(out_buffer.data_ptr()),
reinterpret_cast<void* const*>(flag_ptrs_dev),
reinterpret_cast<uint32_t*>(state_ptr),
shared_ptr,
static_cast<uint32_t>(rank),
n);
}
template <typename DType, uint32_t kNumGPU>
void inkling_multimem_full_oneshot(
tvm::ffi::TensorView in_buffer,
tvm::ffi::TensorView out_buffer,
int64_t multicast_ptr,
int64_t flag_ptrs_dev,
int64_t state_ptr,
int64_t rank,
int64_t num_items,
int64_t nb_override,
int64_t bs_override,
tvm::ffi::TensorView shared) {
using namespace host;
uint32_t n;
validate<DType, kNumGPU>(in_buffer, multicast_ptr, rank, num_items, n);
RuntimeCheck(out_buffer.IsContiguous(), "out must be contiguous");
RuntimeCheck(is_type<DType>(out_buffer.dtype()), "out dtype mismatch");
RuntimeCheck(out_buffer.numel() >= num_items, "out smaller than num_items");
RuntimeCheck(std::bit_cast<intptr_t>(out_buffer.data_ptr()) % 16 == 0, "out not 16B aligned");
RuntimeCheck(flag_ptrs_dev != 0, "flag_ptrs_dev is null");
RuntimeCheck(state_ptr != 0, "state_ptr is null");
RuntimeCheck(multicast_ptr % 16 == 0, "multicast_ptr not 16B aligned");
const DType* shared_ptr = shared_ptr_or_null<DType>(shared, num_items);
const auto device = in_buffer.device();
const auto kernel = inkling_multimem_full_oneshot_kernel<DType, kNumGPU>;
const uint32_t block_size = bs_override > 0 ? static_cast<uint32_t>(bs_override) : 1024u;
const uint32_t cap = max_resident_blocks(kernel, block_size, device);
const uint32_t num_blocks = nb_override > 0 ? min(static_cast<uint32_t>(nb_override), cap)
: min(full_range_num_blocks<DType>(n, block_size), cap);
const auto stream = LaunchKernel::resolve_device(device);
LaunchKernel(num_blocks, block_size, stream)(
kernel,
reinterpret_cast<DType*>(multicast_ptr),
reinterpret_cast<DType*>(in_buffer.data_ptr()),
reinterpret_cast<DType*>(out_buffer.data_ptr()),
reinterpret_cast<void* const*>(flag_ptrs_dev),
reinterpret_cast<uint32_t*>(state_ptr),
shared_ptr,
static_cast<uint32_t>(rank),
n);
}
} // namespace
@@ -0,0 +1,193 @@
// Cross-GPU barrier primitives shared by the Inkling custom all-reduce kernels
// (inkling_all_reduce.cuh) and the fused AR+sconv+norm decode kernel
// (inkling_ar_fused_decode.cuh). Two designs are provided: a single-leader
// grid barrier and a per-block variant.
//
// Resources (see inkling_all_reduce.py):
// * flags: DEDICATED symmetric uint32 buffer, zero-initialized at setup:
// kNumGPU single-leader slots (one per peer), then
// kNumGPU * kMaxBarrierBlocks per-(writer, block) slots.
// * state: device-LOCAL uint32 buffer: [arrival0, arrival1, release0,
// release1, xepoch] padded to kLeaderStateWords, then kMaxBarrierBlocks
// per-block epochs. All epochs are monotonic (mod 2^32, wrap-safe compares)
// and advance under CUDA-graph replay, so flags never go stale.
#pragma once
#include <cstdint>
namespace inkling_ar {
constexpr uint32_t kLeaderStateWords = 8;
constexpr uint32_t kMaxBarrierBlocks = 256;
// Grid-level system barrier across all ranks. Two levels:
// 1. Grid: every block arrives at a self-resetting device counter
// (atomicInc wraps at gridDim.x-1); the last arriver is the leader.
// 2. Cross-GPU: ONLY the leader block does the peer release/acquire
// signal/wait, so that O(1) cost is independent of gridDim.x (the reason
// the old per-block barrier was slow for many-block launches).
// The leader then bumps a release counter; followers spin on it (device scope).
//
// `st` is a device-local uint32 state buffer: [arrival0, arrival1, release0,
// release1, xepoch]. idx 0/1 selects the entry/exit instances (distinct grid
// counters so the two barriers in one kernel don't collide). xepoch is a single
// monotonic cross-GPU epoch (entry uses e, exit uses e+1) -- consistent across
// ranks (SPMD) and advancing under CUDA-graph replay, so flags never go stale.
// s_prev is read BEFORE arriving, and the leader (last arriver) bumps release
// only after all blocks arrived, so no follower can miss the bump (no deadlock).
template <uint32_t kNumGPU>
__device__ __forceinline__ void grid_system_barrier(
uint32_t* __restrict__ st, void* const* __restrict__ flag_ptrs, uint32_t rank, uint32_t idx, bool publish_writes) {
// publish_writes=true (EXIT barriers): every CTA flushes its just-written
// reduced/broadcast slices to SYSTEM scope BEFORE it signals arrival, so the
// single leader's `st.release.sys` publishes ALL blocks' stores rather than
// only the leader thread's own. Without this, a multi-block launch (the tuned
// v2/v3 configs) lets a peer leave the exit barrier and read a slice a
// non-leader CTA wrote but never system-published. ONE fence per CTA suffices:
// the __syncthreads below orders every thread's stores before thread 0's
// fence (CTA-scope happens-before), and `fence.sys + relaxed arrival` is a
// release pattern, so the arrival publishes the whole CTA's stores. ENTRY
// barriers pass false: the data they gate on was written by a prior kernel
// and is already uniformly visible, which the leader's release then promotes
// for free. (The solo path needs no fence either way: its st.release.sys
// signals below are themselves release ops ordered after the __syncthreads.)
uint32_t* xepoch = st + 4;
__shared__ uint32_t s_e;
__shared__ uint32_t s_prev;
__shared__ int s_leader;
const bool solo = (gridDim.x == 1u); // token=1 etc.: the sole block IS the grid
__syncthreads();
if (threadIdx.x == 0) {
if (solo) {
s_leader = 1; // skip the grid arrival/release bookkeeping entirely
} else {
if (publish_writes) __threadfence_system(); // release pattern with the arrive below
s_prev = *static_cast<volatile uint32_t*>(st + 2 + idx); // pre-barrier release
// Self-resetting arrive (atomicInc semantics: wrap at gridDim.x-1).
// acq_rel: the release side pairs with the fence above (publishing this
// CTA's stores); the acquire side lets the last arriver (leader) inherit
// every earlier CTA's release pattern, so its st.release.sys to the peers
// covers the whole grid's writes.
uint32_t old;
asm volatile("atom.acq_rel.gpu.global.inc.u32 %0, [%1], %2;"
: "=r"(old)
: "l"(st + idx), "r"(gridDim.x - 1u)
: "memory");
s_leader = (old == gridDim.x - 1u) ? 1 : 0;
}
}
__syncthreads();
if (s_leader) {
if (threadIdx.x == 0) {
const uint32_t e = *xepoch + 1u;
*xepoch = e;
s_e = e;
}
__syncthreads();
const uint32_t e = s_e;
// Cross-GPU arrive+wait with release/acquire at system scope. The release
// store publishes THIS (leader) thread's system-visible writes and the
// acquire spin makes the peer's visible -- far cheaper than a full
// threadfence_system here. Data written by OTHER (non-leader) CTAs is made
// system-visible by the publish_writes=true fence they each ran before
// arriving (see top), so the leader's single release covers the whole grid.
if (threadIdx.x < kNumGPU) {
const uint32_t peer = threadIdx.x;
uint32_t* remote = static_cast<uint32_t*>(flag_ptrs[peer]) + rank;
asm volatile("st.release.sys.global.u32 [%0], %1;" ::"l"(remote), "r"(e) : "memory");
uint32_t* mine = static_cast<uint32_t*>(flag_ptrs[rank]) + peer;
uint32_t got;
do {
asm volatile("ld.acquire.sys.global.u32 %0, [%1];" : "=r"(got) : "l"(mine) : "memory");
} while (static_cast<int32_t>(got - e) < 0); // wrap-safe: epoch is mod-2^32
}
__syncthreads();
if (!solo && threadIdx.x == 0) {
// Release-ordered bump: pairs with the followers' ld.acquire.gpu so the
// leader's acquired peer state (and its xepoch store above) is visible to
// them -- a relaxed atomicAdd would leave that handoff formally unordered.
asm volatile("red.release.gpu.global.add.u32 [%0], %1;" ::"l"(st + 2 + idx), "r"(1u) : "memory");
}
} else {
if (threadIdx.x == 0) {
// `release` is this rank's LOCAL counter -> device-scope acquire suffices.
uint32_t* rel = st + 2 + idx;
uint32_t got;
do {
asm volatile("ld.acquire.gpu.global.u32 %0, [%1];" : "=r"(got) : "l"(rel) : "memory");
} while (static_cast<int32_t>(got - s_prev) <= 0); // wrap-safe
}
__syncthreads();
}
}
// Device-LOCAL grid sync (no cross-GPU traffic): all blocks arrive at a
// self-resetting counter (state word 5), the last arriver bumps a release
// counter (word 6), followers spin on it -- the grid level of
// grid_system_barrier without the peer handshake. Words 5/6 are spare in the
// kLeaderStateWords block. Requires all blocks co-resident (the launch cap the
// fused kernels already apply). Used by the two-phase {AR + scattered sconv}
// kernel to publish its local scratch between the reduce and conv phases.
__device__ __forceinline__ void grid_local_sync(uint32_t* __restrict__ st) {
__syncthreads();
if (gridDim.x > 1u) {
if (threadIdx.x == 0) {
uint32_t* arrive = st + 5;
uint32_t* release = st + 6;
const uint32_t prev = *static_cast<volatile uint32_t*>(release);
uint32_t old;
asm volatile("atom.acq_rel.gpu.global.inc.u32 %0, [%1], %2;"
: "=r"(old)
: "l"(arrive), "r"(gridDim.x - 1u)
: "memory");
if (old == gridDim.x - 1u) {
asm volatile("red.release.gpu.global.add.u32 [%0], %1;" ::"l"(release), "r"(1u) : "memory");
} else {
uint32_t got;
do {
asm volatile("ld.acquire.gpu.global.u32 %0, [%1];" : "=r"(got) : "l"(release) : "memory");
} while (static_cast<int32_t>(got - prev) <= 0); // wrap-safe
}
}
__syncthreads();
}
}
// Per-block cross-GPU barrier (no grid funnel): block b handshakes ONLY with
// block b on each peer -- one NVLink round trip per block, all blocks in
// parallel, no arrival/release atomics and no leader serialization. Valid
// whenever the consumer phase reads exactly the ranges its blockIdx-matched
// producers wrote (true for the push one-shot: its push and reduce loops use
// the same grid-stride mapping, and every rank launches the same grid). The
// signal is a release store, which covers the CTA's prior (multicast) stores
// via the preceding __syncthreads -- no explicit fence needed. Epochs live in
// per-block device-local slots (monotonic across launches and CUDA-graph
// replays, like xepoch).
template <uint32_t kNumGPU>
__device__ __forceinline__ void
block_system_barrier(uint32_t* __restrict__ st, void* const* __restrict__ flag_ptrs, uint32_t rank) {
__shared__ uint32_t s_e;
__syncthreads(); // CTA stores done before the release signals below
if (threadIdx.x == 0) {
uint32_t* epoch = st + kLeaderStateWords + blockIdx.x;
const uint32_t e = *epoch + 1u;
*epoch = e;
s_e = e;
}
__syncthreads();
const uint32_t e = s_e;
if (threadIdx.x < kNumGPU) {
const uint32_t peer = threadIdx.x;
uint32_t* remote = static_cast<uint32_t*>(flag_ptrs[peer]) + kNumGPU + rank * kMaxBarrierBlocks + blockIdx.x;
asm volatile("st.release.sys.global.u32 [%0], %1;" ::"l"(remote), "r"(e) : "memory");
uint32_t* mine = static_cast<uint32_t*>(flag_ptrs[rank]) + kNumGPU + peer * kMaxBarrierBlocks + blockIdx.x;
uint32_t got;
do {
asm volatile("ld.acquire.sys.global.u32 %0, [%1];" : "=r"(got) : "l"(mine) : "memory");
} while (static_cast<int32_t>(got - e) < 0); // wrap-safe
}
__syncthreads();
}
} // namespace inkling_ar
@@ -0,0 +1,830 @@
// Fused decode {all-reduce -> mlp/attn sconv -> residual-add + RMSNorm} for the
// Inkling (Moonrise) small-batch decode path -- the v5 push one-shot all-reduce
// (inkling_all_reduce.cuh) with the EPILOGUE SEAM filled in by the decode short-conv
// (fused_decode_update.cuh semantics) and the fused-add RMSNorm.
//
// Replaces THREE kernels (AR + fused_decode_update + fused_add_rmsnorm) and
// their intermediate HBM round trips with ONE launch per (AR, sconv, norm)
// chain. Layout: ONE BLOCK PER TOKEN (decode rows are few and the RMSNorm needs
// a per-row cross-hidden reduction), VPT 16B vecs (8 channels each) per thread
// -- a TUNED knob: fewer/fatter threads buy load ILP and a cheaper block
// reduction; more threads buy parallelism. Phases:
//
// 0. prefetch: sconv metadata, conv history and conv weights load FIRST --
// none depend on the producer kernel's output, and their HBM latency
// hides under the cross-GPU barrier below. (This -- not PDL -- is where
// the fused kernel's latency win comes from: the producer GEMMs never
// trigger programmatic launch early, so the PDL wait is a no-op in
// practice and the launch attribute only pipelines the launch tail.)
// 1. push: griddepcontrol.wait, then multicast-store this rank's partial
// row into staging slot (rank*T + t)*D; issue the residual load.
// 2. barrier: per-block peer handshake (block t <-> peers' block t).
// 3. reduce: fp32 sum of the kNumGPU staged shards; round to bf16 `xb`
// (bit-identical to what the unfused AR would have stored).
// 4. sconv: decode causal_conv1d on xb (W-1 cached taps gated by
// cache_mask + current token), optional SiLU, optional +xb
// residual; cache shift-update (+ optional track-copy) --
// identical semantics to fused_decode_update_kernel.
// 5. norm: r = residual_in + y (fp32); block-reduce sum(r^2); write
// residual_out = bf16(r) and hs_out = bf16(r * rsqrt(mean+eps)
// * gamma) (fused_add_rmsnorm semantics).
//
// Staging reuse is caller-managed (A/B rotation shared with v5 -- this kernel
// IS a v5 AR occupying one rotation slot). PAD rows (cache_indices == -1)
// still compute y/hs but never write the cache, matching the unfused kernel.
// bf16-only.
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
#include <sgl_kernel/runtime.cuh>
#include <sgl_kernel/utils.cuh>
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include "inkling_ar_barrier.cuh"
#include <bit>
#include <cstdint>
#include <cuda_bf16.h>
#include <type_traits>
namespace {
constexpr int kPadSlot = -1;
constexpr uint32_t kVecElems = 8; // bf16x8 = 16 B
// Register-level fused add of two bf16x8 vecs (fp32 math, ONE round to bf16)
// -- torch.add numerics, so folding the shared-expert partials into the push
// stays bit-identical to the unfused {torch.add -> AR} chain.
__device__ __forceinline__ uint4 add_bf16x8_rn(const uint4 a, const uint4 b) {
const auto* a2 = reinterpret_cast<const __nv_bfloat162*>(&a);
const auto* b2 = reinterpret_cast<const __nv_bfloat162*>(&b);
uint4 out;
auto* o2 = reinterpret_cast<__nv_bfloat162*>(&out);
#pragma unroll
for (int j = 0; j < 4; ++j) {
const float2 x = __bfloat1622float2(a2[j]);
const float2 y = __bfloat1622float2(b2[j]);
o2[j] = __floats2bfloat162_rn(x.x + y.x, x.y + y.y);
}
return out;
}
struct ArSconvNormParams {
// AR
const void* __restrict__ in; // [T, D] partial sums (LOCAL tensor)
const void* __restrict__ shared; // optional [T, D] shared-expert partials (LOCAL)
void* __restrict__ mc_stage; // multicast staging base (>= kNumGPU*T*D elems)
const void* __restrict__ stage; // this GPU's local view of the staging base
void* const* __restrict__ flag_ptrs;
uint32_t* __restrict__ state;
// sconv (fused_decode_update semantics)
void* __restrict__ cache; // [pool, W-1, D], in-place update
const void* __restrict__ cache_indices; // int32 [T] (PAD == -1)
const void* __restrict__ cache_mask; // bool [T]
const void* __restrict__ conv_weight; // [D, W]
const void* __restrict__ track_mask; // bool [T] (DO_TRACK only)
const void* __restrict__ track_indices; // int64 [T] (DO_TRACK only)
// norm
const void* __restrict__ residual_in; // [T, D]
void* __restrict__ residual_out; // [T, D]
void* __restrict__ hs_out; // [T, D]
const void* __restrict__ norm_weight; // [D]
float eps;
// strides (elements)
int64_t in_stride_t;
int64_t shared_stride_t;
int64_t res_in_stride_t;
int64_t res_out_stride_t;
int64_t hs_stride_t;
int64_t cache_stride_slot;
int64_t cache_stride_w;
int64_t conv_weight_stride_d;
int64_t track_idx_stride;
uint32_t rank;
uint32_t T;
uint32_t D;
};
// VPT = 16B vecs handled per thread (tuning knob; see the header comment).
// Vec i of a thread is at index threadIdx.x + i*blockDim.x (warp-coalesced).
template <typename DType, uint32_t kNumGPU, int W, bool USE_SILU, bool USE_RESIDUAL, bool DO_TRACK, int VPT>
__global__ __launch_bounds__(1024, 1) void inkling_ar_sconv_norm_kernel(const __grid_constant__ ArSconvNormParams p) {
static_assert(std::is_same_v<DType, __nv_bfloat16>, "multimem push path is bf16-only");
constexpr int W1 = W - 1;
const uint32_t t = blockIdx.x;
const uint32_t vecs = p.D / kVecElems;
uint32_t c0[VPT];
bool act[VPT];
#pragma unroll
for (int i = 0; i < VPT; ++i) {
const uint32_t v = threadIdx.x + i * blockDim.x;
act[i] = v < vecs;
c0[i] = (act[i] ? v : 0) * kVecElems; // clamp: inactive lanes never store
}
// ---- 0. prefetch (independent of the producer's output) ----
const int ci = static_cast<const int32_t*>(p.cache_indices)[t];
const bool valid = ci != kPadSlot;
const int slot_id = valid ? ci : 0; // PAD lanes still emit y, never write cache
const float cm = static_cast<const bool*>(p.cache_mask)[t] ? 1.0f : 0.0f;
auto* cp = static_cast<__nv_bfloat16*>(p.cache);
const auto* wp = static_cast<const __nv_bfloat16*>(p.conv_weight);
uint4 hist_raw[VPT][W1];
__nv_bfloat16 wtaps[VPT][kVecElems][W];
#pragma unroll
for (int i = 0; i < VPT; ++i) {
if (!act[i]) continue;
const int64_t cache_base = static_cast<int64_t>(slot_id) * p.cache_stride_slot + c0[i];
#pragma unroll
for (int w = 0; w < W1; ++w) {
hist_raw[i][w] = *reinterpret_cast<const uint4*>(&cp[cache_base + w * p.cache_stride_w]);
}
#pragma unroll
for (int j = 0; j < static_cast<int>(kVecElems); ++j) {
const int64_t wrow = static_cast<int64_t>(c0[i] + j) * p.conv_weight_stride_d;
if constexpr (W == 4) {
// One 8B load per channel row (bf16 x4, 8B-aligned for contiguous [D, W]).
if (p.conv_weight_stride_d == W) {
*reinterpret_cast<uint2*>(wtaps[i][j]) = *reinterpret_cast<const uint2*>(wp + wrow);
continue;
}
}
#pragma unroll
for (int w = 0; w < W; ++w)
wtaps[i][j][w] = wp[wrow + w];
}
}
// ---- 1. push: wait for the producer's output (PDL; no-op without a PDL
// launch or an early-triggering producer), multicast-store this rank's
// partial row, and issue the residual load (it lands under the barrier). ----
asm volatile("griddepcontrol.wait;" ::: "memory");
const auto* in_row = static_cast<const __nv_bfloat16*>(p.in) + t * p.in_stride_t;
const auto* sh_row =
p.shared == nullptr ? nullptr : static_cast<const __nv_bfloat16*>(p.shared) + t * p.shared_stride_t;
auto* slot = static_cast<__nv_bfloat16*>(p.mc_stage) + (static_cast<uint64_t>(p.rank) * p.T + t) * p.D;
uint4 res_raw[VPT];
#pragma unroll
for (int i = 0; i < VPT; ++i) {
if (!act[i]) continue;
uint4 d = *reinterpret_cast<const uint4*>(in_row + c0[i]);
if (sh_row != nullptr) {
d = add_bf16x8_rn(d, *reinterpret_cast<const uint4*>(sh_row + c0[i]));
}
asm volatile("multimem.st.relaxed.sys.global.v4.bf16x2 [%0], {%1,%2,%3,%4};" ::"l"(slot + c0[i]),
"r"(d.x),
"r"(d.y),
"r"(d.z),
"r"(d.w)
: "memory");
res_raw[i] = *reinterpret_cast<const uint4*>(
static_cast<const __nv_bfloat16*>(p.residual_in) + t * p.res_in_stride_t + c0[i]);
}
// ---- 2. per-block barrier: all ranks' row-t pushes have landed locally ----
inkling_ar::block_system_barrier<kNumGPU>(p.state, p.flag_ptrs, p.rank);
// Inactive lanes must NOT exit: they participate in the norm's __syncthreads
// and full-mask warp shuffles below (sumsq contribution 0).
float r[VPT][kVecElems];
float sumsq = 0.0f;
const auto* stage = static_cast<const __nv_bfloat16*>(p.stage);
#pragma unroll
for (int i = 0; i < VPT; ++i) {
if (!act[i]) continue;
// ---- 3. reduce: fp32 sum of the kNumGPU staged shards; round to bf16 ----
float xf[kVecElems];
#pragma unroll
for (int j = 0; j < static_cast<int>(kVecElems); ++j)
xf[j] = 0.0f;
#pragma unroll
for (uint32_t rr = 0; rr < kNumGPU; ++rr) {
const uint4 d = *reinterpret_cast<const uint4*>(stage + (static_cast<uint64_t>(rr) * p.T + t) * p.D + c0[i]);
const auto* h2 = reinterpret_cast<const __nv_bfloat162*>(&d);
#pragma unroll
for (int j = 0; j < 4; ++j) {
const float2 f = __bfloat1622float2(h2[j]);
xf[2 * j] += f.x;
xf[2 * j + 1] += f.y;
}
}
// Round to bf16 exactly as the unfused AR's store would (the sconv below
// and the cache append must see the same bits the unfused path sees).
__nv_bfloat162 xb2[4];
#pragma unroll
for (int j = 0; j < 4; ++j)
xb2[j] = __floats2bfloat162_rn(xf[2 * j], xf[2 * j + 1]);
// ---- 4. sconv: conv over W-1 cached taps (prefetched) + current token ----
float y[kVecElems];
#pragma unroll
for (int j = 0; j < static_cast<int>(kVecElems); ++j) {
const float xj = __bfloat162float(reinterpret_cast<const __nv_bfloat16*>(xb2)[j]);
float acc = 0.0f;
#pragma unroll
for (int w = 0; w < W1; ++w) {
const float h = __bfloat162float(reinterpret_cast<const __nv_bfloat16*>(&hist_raw[i][w])[j]);
acc += h * cm * __bfloat162float(wtaps[i][j][w]);
}
acc += xj * __bfloat162float(wtaps[i][j][W1]);
if constexpr (USE_SILU) acc = __fdividef(acc, 1.0f + __expf(-acc));
if constexpr (USE_RESIDUAL) acc += xj;
y[j] = acc;
}
if (valid) {
// Shift state left (gated by cache_mask), append current token (xb).
const int64_t cache_base = static_cast<int64_t>(slot_id) * p.cache_stride_slot + c0[i];
int64_t track_base = 0;
bool do_tr = false;
if constexpr (DO_TRACK) {
do_tr = static_cast<const bool*>(p.track_mask)[t];
if (do_tr) {
const int64_t tslot =
static_cast<const int64_t*>(p.track_indices)[static_cast<int64_t>(t) * p.track_idx_stride];
track_base = tslot * p.cache_stride_slot + c0[i];
}
}
const uint4 zero = make_uint4(0, 0, 0, 0);
#pragma unroll
for (int w = 0; w < W1; ++w) {
const uint4 nv =
(w < W1 - 1) ? ((cm != 0.0f) ? hist_raw[i][w + 1] : zero) : *reinterpret_cast<const uint4*>(xb2);
*reinterpret_cast<uint4*>(&cp[cache_base + w * p.cache_stride_w]) = nv;
if constexpr (DO_TRACK) {
if (do_tr) {
*reinterpret_cast<uint4*>(&cp[track_base + w * p.cache_stride_w]) = nv;
}
}
}
}
// ---- 5a. residual add (fused_add_rmsnorm semantics) ----
// yb: round the sconv output to bf16 first -- the unfused path writes y to
// HBM as bf16 before the norm kernel reads it back.
#pragma unroll
for (int j = 0; j < static_cast<int>(kVecElems); ++j) {
const float yb = __bfloat162float(__float2bfloat16_rn(y[j]));
r[i][j] = yb + __bfloat162float(reinterpret_cast<const __nv_bfloat16*>(&res_raw[i])[j]);
sumsq += r[i][j] * r[i][j];
}
}
// ---- 5b. block reduction of sumsq (warp shuffle + one smem slot/warp) ----
__shared__ float s_warp[32];
__shared__ float s_inv;
const uint32_t lane = threadIdx.x & 31u;
const uint32_t warp = threadIdx.x >> 5;
#pragma unroll
for (int off = 16; off > 0; off >>= 1)
sumsq += __shfl_down_sync(~0u, sumsq, off);
if (lane == 0) s_warp[warp] = sumsq;
__syncthreads();
if (warp == 0) {
const uint32_t nwarps = (blockDim.x + 31u) >> 5;
float total = (lane < nwarps && lane < 32u) ? s_warp[lane] : 0.0f;
#pragma unroll
for (int off = 16; off > 0; off >>= 1)
total += __shfl_down_sync(~0u, total, off);
if (lane == 0) s_inv = rsqrtf(total / static_cast<float>(p.D) + p.eps);
}
__syncthreads();
const float inv = s_inv;
const auto* gw = static_cast<const __nv_bfloat16*>(p.norm_weight);
auto* res_out = static_cast<__nv_bfloat16*>(p.residual_out) + t * p.res_out_stride_t;
auto* hs_out = static_cast<__nv_bfloat16*>(p.hs_out) + t * p.hs_stride_t;
#pragma unroll
for (int i = 0; i < VPT; ++i) {
if (!act[i]) continue;
__nv_bfloat162 ro[4], ho[4];
#pragma unroll
for (int j = 0; j < 4; ++j) {
const float g0 = __bfloat162float(gw[c0[i] + 2 * j]);
const float g1 = __bfloat162float(gw[c0[i] + 2 * j + 1]);
ro[j] = __floats2bfloat162_rn(r[i][2 * j], r[i][2 * j + 1]);
ho[j] = __floats2bfloat162_rn(r[i][2 * j] * inv * g0, r[i][2 * j + 1] * inv * g1);
}
*reinterpret_cast<uint4*>(res_out + c0[i]) = *reinterpret_cast<const uint4*>(ro);
*reinterpret_cast<uint4*>(hs_out + c0[i]) = *reinterpret_cast<const uint4*>(ho);
}
}
// ---------------------------------------------------------------------------
// Target-verify variant: {AR -> extend-style causal_conv1d ->
// save_intermediate_conv_windows -> add+RMSNorm} in one launch. Every sequence
// has exactly `q` (draft_token_num) consecutive tokens; token t belongs to
// seq = t/q with bos = seq*q. The conv's cross-token taps are RE-REDUCED from
// the v5 staging buffer (any block can rebuild any token's reduced row by
// summing the staged shards -- ~kNumGPU x 16B extra local L2 reads per tap, no
// cross-block dependency). The conv does NOT update the working cache at
// verify; instead the per-position windows are written to intermediate_out
// (consumed by update_conv_state_after_mtp_verify), whose values are exactly
// the cache prefix rows and the re-reduced x this kernel already holds.
struct ArSconvNormVerifyParams {
const void* __restrict__ in; // [T, D] partial sums (LOCAL tensor)
const void* __restrict__ shared; // optional [T, D] shared-expert partials (LOCAL)
void* __restrict__ mc_stage; // multicast staging base
const void* __restrict__ stage; // this GPU's local view of the staging base
void* const* __restrict__ flag_ptrs;
uint32_t* __restrict__ state;
const void* __restrict__ cache; // [pool, W-1, D] (read-only here)
const void* __restrict__ cache_indices; // int32 [B] per-SEQ slot (PAD == -1)
const void* __restrict__ cache_mask; // bool [B] per-SEQ prefix gate
const void* __restrict__ conv_weight; // [D, W]
void* __restrict__ inter_out; // [max_bs, q, W-1, D]
const void* __restrict__ residual_in; // [T, D]
void* __restrict__ residual_out; // [T, D]
void* __restrict__ hs_out; // [T, D]
const void* __restrict__ norm_weight; // [D]
float eps;
int64_t in_stride_t;
int64_t shared_stride_t;
int64_t res_in_stride_t;
int64_t res_out_stride_t;
int64_t hs_stride_t;
int64_t cache_stride_slot;
int64_t cache_stride_w;
int64_t conv_weight_stride_d;
int64_t inter_stride_b;
int64_t inter_stride_t;
int64_t inter_stride_w;
uint32_t rank;
uint32_t T;
uint32_t D;
uint32_t q; // draft_token_num
};
template <typename DType, uint32_t kNumGPU, int W, bool USE_SILU, bool USE_RESIDUAL>
__global__
__launch_bounds__(1024, 1) void inkling_ar_sconv_norm_verify_kernel(const __grid_constant__ ArSconvNormVerifyParams p) {
static_assert(std::is_same_v<DType, __nv_bfloat16>, "multimem push path is bf16-only");
constexpr int W1 = W - 1;
const uint32_t vecs = p.D / kVecElems;
const uint32_t v = threadIdx.x; // one 16B vec (8 channels) per thread
const bool active = v < vecs;
const uint32_t c0 = (active ? v : 0) * kVecElems;
const uint32_t stride_t = gridDim.x; // grid-stride over tokens
// Conv weights are token-independent (per channel) -- load once.
const auto* wp = static_cast<const __nv_bfloat16*>(p.conv_weight);
__nv_bfloat16 wtaps[kVecElems][W];
if (active) {
#pragma unroll
for (int j = 0; j < static_cast<int>(kVecElems); ++j) {
const int64_t wrow = static_cast<int64_t>(c0 + j) * p.conv_weight_stride_d;
if constexpr (W == 4) {
if (p.conv_weight_stride_d == W) {
*reinterpret_cast<uint2*>(wtaps[j]) = *reinterpret_cast<const uint2*>(wp + wrow);
continue;
}
}
#pragma unroll
for (int w = 0; w < W; ++w)
wtaps[j][w] = wp[wrow + w];
}
}
// ---- Phase 1: push every assigned row into staging (PDL-gated input). ----
// A single grid barrier (below) then makes ALL rows' pushes visible on this
// GPU, so Phase 2's cross-token (neighbor) staging reads are race-free -- the
// per-block barrier only synchronized the same blockIdx across ranks and did
// NOT order block t-j's push before block t's read.
asm volatile("griddepcontrol.wait;" ::: "memory");
auto* mc = static_cast<__nv_bfloat16*>(p.mc_stage);
const auto* in = static_cast<const __nv_bfloat16*>(p.in);
const auto* sh = static_cast<const __nv_bfloat16*>(p.shared);
if (active) {
for (uint32_t t = blockIdx.x; t < p.T; t += stride_t) {
uint4 d = *reinterpret_cast<const uint4*>(in + t * p.in_stride_t + c0);
if (sh != nullptr) {
// Fold the shared-expert partials in registers (torch.add numerics);
// the staged value then matches the unfused pre-added input, so the
// cross-token re-reduces below stay bit-identical too.
d = add_bf16x8_rn(d, *reinterpret_cast<const uint4*>(sh + t * p.shared_stride_t + c0));
}
auto* slot = mc + (static_cast<uint64_t>(p.rank) * p.T + t) * p.D + c0;
asm volatile("multimem.st.relaxed.sys.global.v4.bf16x2 [%0], {%1,%2,%3,%4};" ::"l"(slot),
"r"(d.x),
"r"(d.y),
"r"(d.z),
"r"(d.w)
: "memory");
}
}
// ---- Grid barrier: all pushes done + system-visible across all ranks. ----
inkling_ar::grid_system_barrier<kNumGPU>(
p.state,
p.flag_ptrs,
p.rank,
0,
/*publish_writes=*/true);
// ---- Phase 2: reduce + conv + save_windows + add-RMSNorm per row. ----
const auto* stage = static_cast<const __nv_bfloat16*>(p.stage);
const auto* cp = static_cast<const __nv_bfloat16*>(p.cache);
const auto* gw = static_cast<const __nv_bfloat16*>(p.norm_weight);
__shared__ float s_warp[32];
__shared__ float s_inv;
const uint32_t lane = threadIdx.x & 31u;
const uint32_t warp = threadIdx.x >> 5;
auto reduce_row = [&](uint32_t row, __nv_bfloat162* out2) {
float xf[kVecElems];
#pragma unroll
for (int j = 0; j < static_cast<int>(kVecElems); ++j)
xf[j] = 0.0f;
#pragma unroll
for (uint32_t rr = 0; rr < kNumGPU; ++rr) {
const uint4 d = *reinterpret_cast<const uint4*>(stage + (static_cast<uint64_t>(rr) * p.T + row) * p.D + c0);
const auto* h2 = reinterpret_cast<const __nv_bfloat162*>(&d);
#pragma unroll
for (int j = 0; j < 4; ++j) {
const float2 f = __bfloat1622float2(h2[j]);
xf[2 * j] += f.x;
xf[2 * j + 1] += f.y;
}
}
#pragma unroll
for (int j = 0; j < 4; ++j)
out2[j] = __floats2bfloat162_rn(xf[2 * j], xf[2 * j + 1]);
};
for (uint32_t t = blockIdx.x; t < p.T; t += stride_t) {
const uint32_t seq = t / p.q;
const uint32_t tq = t - seq * p.q;
const int bos = static_cast<int>(seq * p.q);
const int ci = static_cast<const int32_t*>(p.cache_indices)[seq];
const bool valid = ci != kPadSlot;
const int slot_id = valid ? ci : 0;
const float cm = (valid && static_cast<const bool*>(p.cache_mask)[seq]) ? 1.0f : 0.0f;
const int64_t cache_base = static_cast<int64_t>(slot_id) * p.cache_stride_slot + c0;
float r[kVecElems];
float sumsq = 0.0f;
if (active) {
uint4 pref_raw[W1];
#pragma unroll
for (int w = 0; w < W1; ++w) {
pref_raw[w] = *reinterpret_cast<const uint4*>(&cp[cache_base + w * p.cache_stride_w]);
}
const uint4 res_raw = *reinterpret_cast<const uint4*>(
static_cast<const __nv_bfloat16*>(p.residual_in) + t * p.res_in_stride_t + c0);
__nv_bfloat162 xb2[4]; // own row
__nv_bfloat162 xn2[W1][4]; // neighbors t-1 .. t-(W-1), where in-seq
reduce_row(t, xb2);
#pragma unroll
for (int j = 1; j <= W1; ++j) {
const int n = static_cast<int>(t) - j;
if (n >= bos) reduce_row(static_cast<uint32_t>(n), xn2[j - 1]);
}
// conv (jit causal_conv1d semantics, fp32 accum, ascending tap order).
float y[kVecElems];
#pragma unroll
for (int j = 0; j < static_cast<int>(kVecElems); ++j) {
const float xj = __bfloat162float(reinterpret_cast<const __nv_bfloat16*>(xb2)[j]);
float acc = 0.0f;
#pragma unroll
for (int iw = 0; iw < W1; ++iw) {
const int shifted = static_cast<int>(t) - W1 + iw;
float tap = 0.0f;
if (shifted >= bos) {
tap = __bfloat162float(reinterpret_cast<const __nv_bfloat16*>(xn2[W1 - 1 - iw])[j]);
} else {
const int prefix_pos = shifted - bos + W1;
if (prefix_pos >= 0) {
tap = cm * __bfloat162float(reinterpret_cast<const __nv_bfloat16*>(&pref_raw[prefix_pos])[j]);
}
}
acc += tap * __bfloat162float(wtaps[j][iw]);
}
acc += xj * __bfloat162float(wtaps[j][W1]);
if constexpr (USE_SILU) acc = __fdividef(acc, 1.0f + __expf(-acc));
if constexpr (USE_RESIDUAL) acc += xj;
y[j] = acc;
}
// save_intermediate_conv_windows: window after draft position tq is raw
// copies of {cache prefix rows | reduced x rows} (no cm gating).
if (valid) {
auto* op = static_cast<__nv_bfloat16*>(p.inter_out) + static_cast<int64_t>(seq) * p.inter_stride_b +
static_cast<int64_t>(tq) * p.inter_stride_t + c0;
#pragma unroll
for (int w = 0; w < W1; ++w) {
const int position = static_cast<int>(tq) + 1 + w;
uint4 val;
if (position < W1) {
val = pref_raw[position];
} else {
const int g = bos + position - W1;
val = (g == static_cast<int>(t)) ? *reinterpret_cast<const uint4*>(xb2)
: *reinterpret_cast<const uint4*>(xn2[t - g - 1]);
}
*reinterpret_cast<uint4*>(op + w * p.inter_stride_w) = val;
}
}
// residual add.
#pragma unroll
for (int j = 0; j < static_cast<int>(kVecElems); ++j) {
const float yb = __bfloat162float(__float2bfloat16_rn(y[j]));
r[j] = yb + __bfloat162float(reinterpret_cast<const __nv_bfloat16*>(&res_raw)[j]);
sumsq += r[j] * r[j];
}
}
// block reduction of sumsq (all threads participate; inactive contribute 0).
__syncthreads(); // protect s_warp/s_inv reuse across the token loop
float ss = sumsq;
#pragma unroll
for (int off = 16; off > 0; off >>= 1)
ss += __shfl_down_sync(~0u, ss, off);
if (lane == 0) s_warp[warp] = ss;
__syncthreads();
if (warp == 0) {
const uint32_t nwarps = (blockDim.x + 31u) >> 5;
float total = (lane < nwarps && lane < 32u) ? s_warp[lane] : 0.0f;
#pragma unroll
for (int off = 16; off > 0; off >>= 1)
total += __shfl_down_sync(~0u, total, off);
if (lane == 0) s_inv = rsqrtf(total / static_cast<float>(p.D) + p.eps);
}
__syncthreads();
const float inv = s_inv;
if (active) {
auto* res_out = static_cast<__nv_bfloat16*>(p.residual_out) + t * p.res_out_stride_t;
auto* hs_out = static_cast<__nv_bfloat16*>(p.hs_out) + t * p.hs_stride_t;
__nv_bfloat162 ro[4], ho[4];
#pragma unroll
for (int j = 0; j < 4; ++j) {
const float g0 = __bfloat162float(gw[c0 + 2 * j]);
const float g1 = __bfloat162float(gw[c0 + 2 * j + 1]);
ro[j] = __floats2bfloat162_rn(r[2 * j], r[2 * j + 1]);
ho[j] = __floats2bfloat162_rn(r[2 * j] * inv * g0, r[2 * j + 1] * inv * g1);
}
*reinterpret_cast<uint4*>(res_out + c0) = *reinterpret_cast<const uint4*>(ro);
*reinterpret_cast<uint4*>(hs_out + c0) = *reinterpret_cast<const uint4*>(ho);
}
}
}
template <typename DType, uint32_t kNumGPU, int W, bool USE_SILU, bool USE_RESIDUAL, bool DO_TRACK>
struct ArSconvNormKernel {
template <int VPT>
static void launch(const ArSconvNormParams& params, uint32_t t_num, uint32_t vecs, DLDevice dev, bool pdl) {
using namespace host;
const uint32_t block = min(1024u, div_ceil(div_ceil(vecs, VPT), 32u) * 32u);
constexpr auto kernel = inkling_ar_sconv_norm_kernel<DType, kNumGPU, W, USE_SILU, USE_RESIDUAL, DO_TRACK, VPT>;
LaunchKernel(dim3{t_num}, dim3{block}, dev).enable_pdl(pdl)(kernel, params);
}
static void
run(tvm::ffi::TensorView in,
tvm::ffi::TensorView residual_in,
tvm::ffi::TensorView residual_out,
tvm::ffi::TensorView hs_out,
tvm::ffi::TensorView norm_weight,
double eps,
tvm::ffi::TensorView cache,
tvm::ffi::TensorView cache_indices,
tvm::ffi::TensorView cache_mask,
tvm::ffi::TensorView conv_weight,
tvm::ffi::TensorView track_mask,
tvm::ffi::TensorView track_indices,
int64_t mc_stage_ptr,
int64_t local_stage_ptr,
int64_t flag_ptrs_dev,
int64_t state_ptr,
int64_t rank,
int64_t enable_pdl,
int64_t vecs_per_thread,
tvm::ffi::TensorView shared) {
using namespace host;
auto T = SymbolicSize{"T"};
auto D = SymbolicSize{"D"};
auto Wd = SymbolicSize{"W"};
auto W1s = SymbolicSize{"W_minus_1"};
auto dev = SymbolicDevice{};
dev.set_options<kDLCUDA>();
Wd.set_value(W);
W1s.set_value(W - 1);
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(in);
const bool do_shared = shared.numel() > 0;
if (do_shared) {
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(shared);
RuntimeCheck(shared.stride(0) % kVecElems == 0, "shared row stride must keep 16B alignment");
RuntimeCheck(std::bit_cast<intptr_t>(shared.data_ptr()) % 16 == 0, "shared not 16B aligned");
}
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(residual_in);
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(residual_out);
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(hs_out);
TensorMatcher({D}).with_dtype<DType>().with_device(dev).verify(norm_weight);
TensorMatcher({-1, W1s, D}).with_dtype<DType>().with_device(dev).verify(cache);
TensorMatcher({T}).with_dtype<int32_t>().with_device(dev).verify(cache_indices);
TensorMatcher({T}).with_device(dev).verify(cache_mask);
TensorMatcher({D, Wd}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(conv_weight);
const uint32_t t_num = static_cast<uint32_t>(T.unwrap());
const uint32_t d_num = static_cast<uint32_t>(D.unwrap());
const uint32_t vecs = d_num / kVecElems;
RuntimeCheck(
t_num >= 1 && t_num <= inkling_ar::kMaxBarrierBlocks,
"T must be in [1, kMaxBarrierBlocks] (one barrier slot per token)");
RuntimeCheck(d_num % kVecElems == 0, "D must be a multiple of 8");
RuntimeCheck(cache.stride(2) == 1, "cache must be channel-contiguous");
RuntimeCheck(mc_stage_ptr % 16 == 0, "mc_stage_ptr not 16B aligned");
RuntimeCheck(local_stage_ptr != 0 && local_stage_ptr % 16 == 0, "bad local_stage_ptr");
RuntimeCheck(flag_ptrs_dev != 0, "flag_ptrs_dev is null");
RuntimeCheck(state_ptr != 0, "state_ptr is null");
RuntimeCheck(rank >= 0 && rank < kNumGPU, "rank out of range");
RuntimeCheck(in.stride(0) % kVecElems == 0, "in row stride must keep 16B alignment");
RuntimeCheck(std::bit_cast<intptr_t>(in.data_ptr()) % 16 == 0, "in not 16B aligned");
const auto params = ArSconvNormParams{
.in = in.data_ptr(),
.shared = do_shared ? shared.data_ptr() : nullptr,
.mc_stage = reinterpret_cast<void*>(mc_stage_ptr),
.stage = reinterpret_cast<const void*>(local_stage_ptr),
.flag_ptrs = reinterpret_cast<void* const*>(flag_ptrs_dev),
.state = reinterpret_cast<uint32_t*>(state_ptr),
.cache = cache.data_ptr(),
.cache_indices = cache_indices.data_ptr(),
.cache_mask = cache_mask.data_ptr(),
.conv_weight = conv_weight.data_ptr(),
.track_mask = DO_TRACK ? track_mask.data_ptr() : nullptr,
.track_indices = DO_TRACK ? track_indices.data_ptr() : nullptr,
.residual_in = residual_in.data_ptr(),
.residual_out = residual_out.data_ptr(),
.hs_out = hs_out.data_ptr(),
.norm_weight = norm_weight.data_ptr(),
.eps = static_cast<float>(eps),
.in_stride_t = in.stride(0),
.shared_stride_t = do_shared ? shared.stride(0) : 0,
.res_in_stride_t = residual_in.stride(0),
.res_out_stride_t = residual_out.stride(0),
.hs_stride_t = hs_out.stride(0),
.cache_stride_slot = cache.stride(0),
.cache_stride_w = cache.stride(1),
.conv_weight_stride_d = conv_weight.stride(0),
.track_idx_stride = DO_TRACK ? track_indices.stride(0) : 0,
.rank = static_cast<uint32_t>(rank),
.T = t_num,
.D = d_num,
};
// vecs_per_thread (VPT) is the tuned knob; 0 -> 1. Each VPT must still fit
// one block (div_ceil(vecs, VPT) <= 1024).
const int vpt = vecs_per_thread > 0 ? static_cast<int>(vecs_per_thread) : 1;
const bool pdl = enable_pdl != 0;
switch (vpt) {
case 1:
RuntimeCheck(vecs <= 1024, "D/8 must fit one block at VPT=1");
launch<1>(params, t_num, vecs, dev.unwrap(), pdl);
break;
case 2:
launch<2>(params, t_num, vecs, dev.unwrap(), pdl);
break;
case 3:
launch<3>(params, t_num, vecs, dev.unwrap(), pdl);
break;
case 4:
launch<4>(params, t_num, vecs, dev.unwrap(), pdl);
break;
case 6:
launch<6>(params, t_num, vecs, dev.unwrap(), pdl);
break;
default:
RuntimeCheck(false, "unsupported vecs_per_thread (use 1/2/3/4/6)");
}
}
};
// Host wrapper for the target-verify variant. DO_TRACK is accepted (to share
// the module's template-arg string) but unused -- verify never tracks.
template <typename DType, uint32_t kNumGPU, int W, bool USE_SILU, bool USE_RESIDUAL, bool DO_TRACK>
struct ArSconvNormVerifyKernel {
static void
run(tvm::ffi::TensorView in,
tvm::ffi::TensorView residual_in,
tvm::ffi::TensorView residual_out,
tvm::ffi::TensorView hs_out,
tvm::ffi::TensorView norm_weight,
double eps,
tvm::ffi::TensorView cache,
tvm::ffi::TensorView cache_indices,
tvm::ffi::TensorView cache_mask,
tvm::ffi::TensorView conv_weight,
tvm::ffi::TensorView inter_out,
int64_t q,
int64_t mc_stage_ptr,
int64_t local_stage_ptr,
int64_t flag_ptrs_dev,
int64_t state_ptr,
int64_t rank,
int64_t enable_pdl,
tvm::ffi::TensorView shared) {
using namespace host;
auto T = SymbolicSize{"T"};
auto B = SymbolicSize{"B"};
auto D = SymbolicSize{"D"};
auto Wd = SymbolicSize{"W"};
auto W1s = SymbolicSize{"W_minus_1"};
auto dev = SymbolicDevice{};
dev.set_options<kDLCUDA>();
Wd.set_value(W);
W1s.set_value(W - 1);
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(in);
const bool do_shared = shared.numel() > 0;
if (do_shared) {
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(shared);
RuntimeCheck(shared.stride(0) % kVecElems == 0, "shared row stride must keep 16B alignment");
RuntimeCheck(std::bit_cast<intptr_t>(shared.data_ptr()) % 16 == 0, "shared not 16B aligned");
}
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(residual_in);
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(residual_out);
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(hs_out);
TensorMatcher({D}).with_dtype<DType>().with_device(dev).verify(norm_weight);
TensorMatcher({-1, W1s, D}).with_dtype<DType>().with_device(dev).verify(cache);
TensorMatcher({B}).with_dtype<int32_t>().with_device(dev).verify(cache_indices);
TensorMatcher({B}).with_device(dev).verify(cache_mask);
TensorMatcher({D, Wd}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(conv_weight);
const uint32_t t_num = static_cast<uint32_t>(T.unwrap());
const uint32_t b_num = static_cast<uint32_t>(B.unwrap());
const uint32_t d_num = static_cast<uint32_t>(D.unwrap());
RuntimeCheck(q > 0 && t_num == b_num * static_cast<uint32_t>(q), "T must equal B * draft_token_num");
RuntimeCheck(
t_num >= 1 && t_num <= inkling_ar::kMaxBarrierBlocks,
"T must be in [1, kMaxBarrierBlocks] (one barrier slot per token)");
RuntimeCheck(d_num % kVecElems == 0, "D must be a multiple of 8");
RuntimeCheck(d_num / kVecElems <= 1024, "D/8 must fit one block");
RuntimeCheck(cache.stride(2) == 1, "cache must be channel-contiguous");
// inter_out: [max_bs, q, W-1, D], channel-contiguous, batch B rows used.
auto MB = SymbolicSize{"max_bs"};
auto Qs = SymbolicSize{"q"};
Qs.set_value(q);
TensorMatcher({MB, Qs, W1s, D}).with_dtype<DType>().with_device(dev).verify(inter_out);
RuntimeCheck(MB.unwrap() >= b_num, "inter_out batch dim too small");
RuntimeCheck(inter_out.stride(3) == 1, "inter_out must be channel-contiguous");
RuntimeCheck(mc_stage_ptr % 16 == 0, "mc_stage_ptr not 16B aligned");
RuntimeCheck(local_stage_ptr != 0 && local_stage_ptr % 16 == 0, "bad local_stage_ptr");
RuntimeCheck(flag_ptrs_dev != 0 && state_ptr != 0, "null barrier resources");
RuntimeCheck(rank >= 0 && rank < kNumGPU, "rank out of range");
RuntimeCheck(in.stride(0) % kVecElems == 0, "in row stride must keep 16B alignment");
RuntimeCheck(std::bit_cast<intptr_t>(in.data_ptr()) % 16 == 0, "in not 16B aligned");
const auto params = ArSconvNormVerifyParams{
.in = in.data_ptr(),
.shared = do_shared ? shared.data_ptr() : nullptr,
.mc_stage = reinterpret_cast<void*>(mc_stage_ptr),
.stage = reinterpret_cast<const void*>(local_stage_ptr),
.flag_ptrs = reinterpret_cast<void* const*>(flag_ptrs_dev),
.state = reinterpret_cast<uint32_t*>(state_ptr),
.cache = cache.data_ptr(),
.cache_indices = cache_indices.data_ptr(),
.cache_mask = cache_mask.data_ptr(),
.conv_weight = conv_weight.data_ptr(),
.inter_out = inter_out.data_ptr(),
.residual_in = residual_in.data_ptr(),
.residual_out = residual_out.data_ptr(),
.hs_out = hs_out.data_ptr(),
.norm_weight = norm_weight.data_ptr(),
.eps = static_cast<float>(eps),
.in_stride_t = in.stride(0),
.shared_stride_t = do_shared ? shared.stride(0) : 0,
.res_in_stride_t = residual_in.stride(0),
.res_out_stride_t = residual_out.stride(0),
.hs_stride_t = hs_out.stride(0),
.cache_stride_slot = cache.stride(0),
.cache_stride_w = cache.stride(1),
.conv_weight_stride_d = conv_weight.stride(0),
.inter_stride_b = inter_out.stride(0),
.inter_stride_t = inter_out.stride(1),
.inter_stride_w = inter_out.stride(2),
.rank = static_cast<uint32_t>(rank),
.T = t_num,
.D = d_num,
.q = static_cast<uint32_t>(q),
};
const uint32_t block = min(1024u, div_ceil(d_num / kVecElems, 32u) * 32u);
constexpr auto kernel = inkling_ar_sconv_norm_verify_kernel<DType, kNumGPU, W, USE_SILU, USE_RESIDUAL>;
// The kernel grid-strides over tokens with ONE grid_system_barrier between
// the push and the neighbor-reading reduce, so all blocks must be
// co-resident (else the leader waits forever). Cap the grid at the
// occupancy limit; the token loop covers any remaining rows.
const uint32_t bps = host::runtime::get_blocks_per_sm(kernel, block);
const uint32_t cap = host::runtime::get_sm_count(dev.unwrap().device_id) * max(1u, bps);
const uint32_t grid = min(t_num, cap);
LaunchKernel(dim3{grid}, dim3{block}, dev.unwrap()).enable_pdl(enable_pdl != 0)(kernel, params);
}
};
} // namespace
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,145 @@
// Latency-lean rel_logits projection for SMALL token counts:
// out[t, h, :] = bf16(sum_d fp32(r[t, h, d]) * fp32(proj[d, :])) with an
// optional per-token tau prescale folded in registers (the shipped prescale
// semantics: r*tau rounds to bf16 BEFORE the dot, matching
// {row_scale -> einsum} exactly).
//
// At t=1 the cuBLAS GEMM ([16,16]@[16,1024]) is pure launch + entry overhead
// (~1.6 us for ~64 KB of traffic); this kernel is a no-smem no-sync grid of
// independent 8-wide dots reading proj straight from L2 (32 KB, hot across
// decode steps), so its floor is the launch itself. An earlier smem-staged
// bandwidth-oriented kernel lost to cuBLAS at EVERY size -- this one is only
// dispatched inside its measured small-t band; large t stays on cuBLAS.
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize, SymbolicDevice
#include <sgl_kernel/utils.h> // For RuntimeCheck, div_ceil
#include <sgl_kernel/type.cuh> // For bf16_t/fp32_t aliases
#include <sgl_kernel/utils.cuh> // For LaunchKernel, PDL helpers
#include <sgl_kernel/vec.cuh> // For AlignedVector (16B loads)
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
namespace {
constexpr uint32_t kRpVec = 8; // bf16x8 = 16 B
constexpr uint32_t kRpBlock = 256;
template <int kDRel, bool kUsePDL, bool kHasTau>
__global__ __launch_bounds__(kRpBlock, 1) void rel_proj_small_t_kernel(
const bf16_t* __restrict__ r, // [t, h, kDRel], token rows strided
const fp32_t* __restrict__ tau, // [t]; unread when !kHasTau
const bf16_t* __restrict__ proj, // [kDRel, e] contiguous
bf16_t* __restrict__ out, // [t, h, e] contiguous
const int64_t r_stride_t, // elems between token rows
const uint32_t h,
const uint32_t e,
const uint32_t t) {
using namespace device;
PDLWaitPrimary<kUsePDL>();
const uint32_t evecs = e / kRpVec;
const uint32_t total = t * h * evecs;
for (uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x; idx < total; idx += gridDim.x * blockDim.x) {
const uint32_t ev = idx % evecs;
const uint32_t th = idx / evecs;
const uint32_t ti = th / h;
const uint32_t hi = th % h;
// r[ti, hi, :] once into registers (2x 16B for kDRel=16), tau folded
// with the prescale rounding (bf16 round before the dot).
const bf16_t* rrow = r + static_cast<int64_t>(ti) * r_stride_t + static_cast<int64_t>(hi) * kDRel;
float rv[kDRel];
#pragma unroll
for (int d = 0; d < kDRel; d += static_cast<int>(kRpVec)) {
AlignedVector<bf16_t, kRpVec> a;
a.load(rrow, d / static_cast<int>(kRpVec));
#pragma unroll
for (int k = 0; k < static_cast<int>(kRpVec); ++k) {
if constexpr (kHasTau) {
rv[d + k] = static_cast<float>(static_cast<bf16_t>(static_cast<float>(a[k]) * tau[ti]));
} else {
rv[d + k] = static_cast<float>(a[k]);
}
}
}
float acc[kRpVec] = {};
#pragma unroll
for (int d = 0; d < kDRel; ++d) {
AlignedVector<bf16_t, kRpVec> p;
p.load(proj + static_cast<int64_t>(d) * e, ev);
#pragma unroll
for (int k = 0; k < static_cast<int>(kRpVec); ++k) {
acc[k] += rv[d] * static_cast<float>(p[k]);
}
}
AlignedVector<bf16_t, kRpVec> o;
#pragma unroll
for (int k = 0; k < static_cast<int>(kRpVec); ++k) {
o[k] = static_cast<bf16_t>(acc[k]);
}
o.store(out, idx);
}
PDLTriggerSecondary<kUsePDL>();
}
template <int kDRel, bool kUsePDL>
void rel_proj_small_t(
tvm::ffi::TensorView r,
tvm::ffi::TensorView tau, // numel-0 sentinel = no prescale
tvm::ffi::TensorView proj,
tvm::ffi::TensorView out) {
using namespace host;
auto T = SymbolicSize{"t"};
auto H = SymbolicSize{"h"};
auto D = SymbolicSize{"d_rel"};
auto E = SymbolicSize{"e"};
auto dev = SymbolicDevice{};
dev.set_options<kDLCUDA>();
TensorMatcher({T, H, D}).with_dtype<bf16_t>().with_device(dev).with_strides({-1, D, 1}).verify(r);
TensorMatcher({D, E}).with_dtype<bf16_t>().with_device(dev).verify(proj);
TensorMatcher({T, H, E}).with_dtype<bf16_t>().with_device(dev).verify(out);
const uint32_t t = static_cast<uint32_t>(T.unwrap());
const uint32_t h = static_cast<uint32_t>(H.unwrap());
const uint32_t e = static_cast<uint32_t>(E.unwrap());
RuntimeCheck(D.unwrap() == kDRel, "d_rel must be ", kDRel);
static_assert(kDRel % static_cast<int>(kRpVec) == 0, "d_rel must be a vector multiple (r loads are 16B)");
RuntimeCheck(e % kRpVec == 0, "e must be a multiple of ", kRpVec);
RuntimeCheck((r.stride(0) * 2) % 16 == 0, "r token stride must keep 16B alignment");
RuntimeCheck(std::bit_cast<intptr_t>(r.data_ptr()) % 16 == 0, "r not 16B aligned");
RuntimeCheck(std::bit_cast<intptr_t>(proj.data_ptr()) % 16 == 0, "proj not 16B aligned");
const bool has_tau = tau.numel() > 0;
if (has_tau) {
TensorMatcher({T}).with_dtype<fp32_t>().with_device(dev).verify(tau);
}
const uint32_t total = t * h * (e / kRpVec);
const uint32_t grid = div_ceil(total, kRpBlock);
auto launch = [&](auto kernel) {
LaunchKernel(grid, kRpBlock, dev.unwrap())
.enable_pdl(kUsePDL)(
kernel,
static_cast<const bf16_t*>(r.data_ptr()),
has_tau ? static_cast<const fp32_t*>(tau.data_ptr()) : nullptr,
static_cast<const bf16_t*>(proj.data_ptr()),
static_cast<bf16_t*>(out.data_ptr()),
r.stride(0),
h,
e,
t);
};
if (has_tau) {
launch(rel_proj_small_t_kernel<kDRel, kUsePDL, true>);
} else {
launch(rel_proj_small_t_kernel<kDRel, kUsePDL, false>);
}
}
} // namespace
@@ -0,0 +1,116 @@
// Vectorized per-row scale for the Inkling log-scaling tau paths:
// out[row, :] = bf16(fp32(x[row, :]) * tau[row]) -- the apply_log_scaling_tau
// contract (fp32 multiply, one bf16 round), replacing the scalar triton
// kernel (per-ELEMENT int64 div/mod + tau load; ~1.7 us at 512 B in-graph,
// ~2.5x off the copy floor at 16k rows) with 16 B vector loads/stores and one
// row divide per vector. x may be row-strided (a slice of the packed qkvr
// projection); out is contiguous.
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize, SymbolicDevice
#include <sgl_kernel/utils.h> // For RuntimeCheck, div_ceil
#include <sgl_kernel/runtime.cuh> // For get_blocks_per_sm / get_sm_count
#include <sgl_kernel/type.cuh> // For bf16_t/fp32_t aliases
#include <sgl_kernel/utils.cuh> // For LaunchKernel, PDL helpers
#include <sgl_kernel/vec.cuh> // For AlignedVector (16B loads)
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
namespace {
constexpr uint32_t kRsVec = 8; // bf16x8 = 16 B
constexpr uint32_t kRsBlock = 256;
// kHasTau=false is the pure row-compaction flavor (tau may be nullptr): same
// vectorized strided-rows -> contiguous copy, no multiply. It replaces the
// TensorIterator copy hidden inside einsum's reshape of the strided r operand
// (measured ~2.3 us slower per call at decode sizes).
template <bool kUsePDL, bool kHasTau>
__global__ __launch_bounds__(kRsBlock, 1) void row_scale_kernel(
const bf16_t* __restrict__ x, // [rows, inner], row-strided
const fp32_t* __restrict__ tau, // [rows]; unread when !kHasTau
bf16_t* __restrict__ out, // [rows, inner] contiguous
const int64_t x_stride_row, // elems
const uint32_t inner,
const uint32_t rows) {
using namespace device;
PDLWaitPrimary<kUsePDL>();
const uint32_t vrow = inner / kRsVec;
const uint32_t total = rows * vrow;
for (uint32_t idx = blockIdx.x * blockDim.x + threadIdx.x; idx < total; idx += gridDim.x * blockDim.x) {
const uint32_t row = idx / vrow;
const uint32_t v = idx % vrow;
AlignedVector<bf16_t, kRsVec> a;
a.load(x + static_cast<int64_t>(row) * x_stride_row, v);
if constexpr (kHasTau) {
const float tv = tau[row];
#pragma unroll
for (int k = 0; k < static_cast<int>(kRsVec); ++k) {
a[k] = static_cast<bf16_t>(static_cast<float>(a[k]) * tv);
}
}
a.store(out, idx);
}
PDLTriggerSecondary<kUsePDL>();
}
template <bool kUsePDL, bool kHasTau>
void row_scale_launch(
tvm::ffi::TensorView x,
const fp32_t* tau_ptr,
tvm::ffi::TensorView out,
host::SymbolicSize& R,
host::SymbolicSize& N,
host::SymbolicDevice& dev) {
using namespace host;
TensorMatcher({R, N}).with_dtype<bf16_t>().with_device(dev).with_strides({-1, 1}).verify(x);
TensorMatcher({R, N}).with_dtype<bf16_t>().with_device(dev).verify(out);
const uint32_t rows = static_cast<uint32_t>(R.unwrap());
const uint32_t inner = static_cast<uint32_t>(N.unwrap());
RuntimeCheck(inner % kRsVec == 0, "inner must be a multiple of ", kRsVec);
RuntimeCheck((x.stride(0) * 2) % 16 == 0, "x row stride must keep 16B alignment");
RuntimeCheck(std::bit_cast<intptr_t>(x.data_ptr()) % 16 == 0, "x not 16B aligned");
const auto kernel = row_scale_kernel<kUsePDL, kHasTau>;
const uint32_t sm = runtime::get_sm_count(dev.unwrap().device_id);
const uint32_t bps = runtime::get_blocks_per_sm(kernel, kRsBlock);
const uint32_t want = div_ceil(rows * (inner / kRsVec), kRsBlock);
const uint32_t grid = std::min(sm * std::max(1u, bps), std::max(1u, want));
LaunchKernel(grid, kRsBlock, dev.unwrap())
.enable_pdl(kUsePDL)(
kernel,
static_cast<const bf16_t*>(x.data_ptr()),
tau_ptr,
static_cast<bf16_t*>(out.data_ptr()),
x.stride(0),
inner,
rows);
}
template <bool kUsePDL>
void row_scale(tvm::ffi::TensorView x, tvm::ffi::TensorView tau, tvm::ffi::TensorView out) {
using namespace host;
auto R = SymbolicSize{"rows"};
auto N = SymbolicSize{"inner"};
auto dev = SymbolicDevice{};
dev.set_options<kDLCUDA>();
TensorMatcher({R}).with_dtype<fp32_t>().with_device(dev).verify(tau);
row_scale_launch<kUsePDL, true>(x, static_cast<const fp32_t*>(tau.data_ptr()), out, R, N, dev);
}
// Pure compaction: out = contiguous copy of the row-strided x (no tau).
template <bool kUsePDL>
void row_compact(tvm::ffi::TensorView x, tvm::ffi::TensorView out) {
using namespace host;
auto R = SymbolicSize{"rows"};
auto N = SymbolicSize{"inner"};
auto dev = SymbolicDevice{};
dev.set_options<kDLCUDA>();
row_scale_launch<kUsePDL, false>(x, nullptr, out, R, N, dev);
}
} // namespace
@@ -0,0 +1,138 @@
// Update the convolution cache from an extend/prefill token stream.
//
// For each sequence b with slot ci = cache_indices[b] and query range
// [start, end) (query_start_loc), the new conv state is the last W1 = W-1 entries of
// the virtual stream [ old_state (W1 rows, gated by has_initial_state) ++ x[start:end] ]:
// new_state[w] = virtual[qlen + w] for w in 0..W1-1 (qlen = end - start)
// qlen + w >= W1 -> x[end - W1 + w, d] (a "current" token)
// qlen + w < W1 -> old_cache[slot, w + qlen, d] * has_state (shifted state)
// PAD (ci == -1) or empty (qlen <= 0) lanes are left untouched. This is a pure
// select/copy (no arithmetic) => must be BIT-EXACT (bf16 values moved verbatim).
//
// RAW-safe: each thread loads all W1 old_cache rows into registers BEFORE writing any,
// so the in-place writes never clobber a not-yet-read shift source. 2 channels/thread
// are packed as bf16x2 (32-bit) to halve the moves. Requires bf16 + even D.
#include <sgl_kernel/tensor.h> // For TensorMatcher, SymbolicSize, SymbolicDevice
#include <sgl_kernel/utils.h> // For RuntimeCheck, div_ceil
#include <sgl_kernel/utils.cuh> // For LaunchKernel, SGL_DEVICE
#include <dlpack/dlpack.h>
#include <tvm/ffi/container/tensor.h>
#include <cstdint>
#include <cuda_bf16.h>
namespace {
struct UpdateSconvParams {
const void* __restrict__ x; // [T, D], channel-contiguous
void* __restrict__ cache; // [max_slots, W1, D], in-place update
const void* __restrict__ cache_indices; // int32 [B]
const void* __restrict__ has_state; // bool [B]
const void* __restrict__ qsl; // int32 [B+1] query_start_loc
int64_t x_stride_t;
int64_t cache_stride_slot;
int64_t cache_stride_w;
uint32_t D;
};
constexpr uint32_t kUpdThreads = 256; // threads/block, each owns a channel pair
constexpr int kPadSlot = -1;
template <int W1, typename DType>
__global__ void update_sconv_cache_kernel(const __grid_constant__ UpdateSconvParams p) {
const int b = blockIdx.y;
const int ci = static_cast<const int32_t*>(p.cache_indices)[b];
const int start = static_cast<const int32_t*>(p.qsl)[b];
const int end = static_cast<const int32_t*>(p.qsl)[b + 1];
const int qlen = end - start;
if (ci == kPadSlot || qlen <= 0) return; // PAD / empty lane: untouched
const int c0 = (blockIdx.x * kUpdThreads + threadIdx.x) * 2;
if (c0 >= static_cast<int>(p.D)) return;
const bool hs = static_cast<const bool*>(p.has_state)[b];
const auto* xp = static_cast<const __nv_bfloat16*>(p.x);
auto* cp = static_cast<__nv_bfloat16*>(p.cache);
const int cw = static_cast<int>(p.cache_stride_w);
const int64_t slot_base = static_cast<int64_t>(ci) * p.cache_stride_slot + c0;
// Load all old-state rows into registers first (RAW-safe against the writes below).
__nv_bfloat162 old_reg[W1];
#pragma unroll
for (int w = 0; w < W1; ++w) {
old_reg[w] = *reinterpret_cast<const __nv_bfloat162*>(&cp[slot_base + static_cast<int64_t>(w) * cw]);
}
const __nv_bfloat162 zero = __float2bfloat162_rn(0.0f);
#pragma unroll
for (int w = 0; w < W1; ++w) {
__nv_bfloat162 nv;
if (qlen >= (W1 - w)) {
// current token from x: index end - W1 + w >= start >= 0
const int x_idx = end - W1 + w;
nv = *reinterpret_cast<const __nv_bfloat162*>(&xp[static_cast<int64_t>(x_idx) * p.x_stride_t + c0]);
} else {
// shifted state old_cache[w + qlen] (w+qlen in [0, W1)), gated by has_state
__nv_bfloat162 shift = zero;
#pragma unroll
for (int src = 0; src < W1; ++src) {
if (src == w + qlen) shift = old_reg[src];
}
nv = hs ? shift : zero;
}
*reinterpret_cast<__nv_bfloat162*>(&cp[slot_base + static_cast<int64_t>(w) * cw]) = nv;
}
}
template <int W1, typename DType>
struct UpdateSconvCacheKernel {
static void
run(tvm::ffi::TensorView x,
tvm::ffi::TensorView cache,
tvm::ffi::TensorView cache_indices,
tvm::ffi::TensorView has_state,
tvm::ffi::TensorView qsl) {
using namespace host;
auto T = SymbolicSize{"T"};
auto D = SymbolicSize{"D"};
auto W1s = SymbolicSize{"W_minus_1"};
auto B = SymbolicSize{"B"};
auto dev = SymbolicDevice{};
dev.set_options<kDLCUDA>();
W1s.set_value(W1);
// x channel-contiguous (may be a non-contiguous row view); cache contiguous
// [slots, W1, D]. cache_indices/qsl int32, has_state torch-bool (shape/device only).
TensorMatcher({T, D}).with_strides({-1, 1}).with_dtype<DType>().with_device(dev).verify(x);
TensorMatcher({-1, W1s, D}).with_dtype<DType>().with_device(dev).verify(cache);
TensorMatcher({B}).with_dtype<int32_t>().with_device(dev).verify(cache_indices);
TensorMatcher({B}).with_device(dev).verify(has_state);
TensorMatcher({-1}).with_dtype<int32_t>().with_device(dev).verify(qsl);
RuntimeCheck(qsl.size(0) == B.unwrap() + 1, "qsl must have length B+1");
RuntimeCheck(sizeof(DType) == 2, "update_sconv_cache: bf16x2 kernel requires 16-bit dtype");
RuntimeCheck(D.unwrap() % 2 == 0, "update_sconv_cache: D must be even for the bf16x2 kernel");
RuntimeCheck(cache.stride(2) == 1, "update_sconv_cache: cache must be channel-contiguous");
const auto params = UpdateSconvParams{
.x = x.data_ptr(),
.cache = cache.data_ptr(),
.cache_indices = cache_indices.data_ptr(),
.has_state = has_state.data_ptr(),
.qsl = qsl.data_ptr(),
.x_stride_t = x.stride(0),
.cache_stride_slot = cache.stride(0),
.cache_stride_w = cache.stride(1),
.D = static_cast<uint32_t>(D.unwrap()),
};
const uint32_t d_pairs = params.D / 2;
const dim3 grid{div_ceil(d_pairs, kUpdThreads), static_cast<uint32_t>(B.unwrap())};
const dim3 block{kUpdThreads};
constexpr auto kernel = update_sconv_cache_kernel<W1, DType>;
LaunchKernel(grid, block, dev.unwrap())(kernel, params);
}
};
} // namespace
File diff suppressed because it is too large Load Diff
@@ -20,11 +20,9 @@ limitations under the License.
// and compute the merged virtual id inline (mirrors _fused_virtual_topk_ids),
// so virtual_topk_ids is never materialized to global memory.
//
// Commit 1 scope: pure fusion (inline virtual id), NO EP skip. Output is
// bucket-for-bucket equivalent to the old path (dropped/-1 tokens still land in
// the sentinel bucket 0), so it can be asserted equal to the old kernels.
// Only the `64 < num_buckets <= 1024` branch is implemented here; other expert
// counts keep the old path (handled by the Python dispatcher).
// Shared-outer and compact EP routing support up to 1024 effective buckets,
// using fused scatter for eligible shapes and two kernels otherwise. Larger
// domains keep the old path through the Python dispatcher.
#include <sgl_kernel/tensor.h>
#include <sgl_kernel/utils.h>
@@ -41,6 +41,11 @@ def flash_attn_with_kvcache(
sinks=None,
score_mod=None,
aux_tensors=None,
sfq=None,
sfk=None,
sfv=None,
rel_bias=None,
rel_bias_prep_cache=None,
ver=3,
out=None,
):
@@ -202,6 +207,11 @@ def flash_attn_with_kvcache(
sinks=sinks,
score_mod=score_mod,
aux_tensors=aux_tensors,
sfq=sfq,
sfk=sfk,
sfv=sfv,
rel_bias=rel_bias,
rel_bias_prep_cache=rel_bias_prep_cache,
return_softmax_lse=return_softmax_lse,
)
else:
@@ -236,6 +246,11 @@ def flash_attn_varlen_func(
sinks=None,
score_mod=None,
aux_tensors=None,
sfq=None,
sfk=None,
sfv=None,
rel_bias=None,
rel_bias_prep_cache=None,
ver=3,
out=None,
):
@@ -294,6 +309,14 @@ def flash_attn_varlen_func(
pack_gqa=pack_gqa,
score_mod=score_mod,
aux_tensors=aux_tensors,
q_descale=q_descale,
k_descale=k_descale,
v_descale=v_descale,
sfq=sfq,
sfk=sfk,
sfv=sfv,
rel_bias=rel_bias,
rel_bias_prep_cache=rel_bias_prep_cache,
return_softmax_lse=return_softmax_lse,
)
else:
+72 -7
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import os
from typing import Callable, Optional, Tuple, Union
import torch
@@ -7,7 +8,14 @@ import torch
from sglang.kernel_api_logging import debug_kernel_api
try:
from flash_attn.cute import flash_attn_varlen_func as _flash_attn_varlen_func
if os.environ.get("SGLANG_INKLING_FA4_USE_PIP") == "1":
# A/B debug escape hatch: route through the pip flash-attn-4 package
# (dev's stack). rel_bias is vendored-only, so SHEARED must be 0.
from flash_attn.cute import flash_attn_varlen_func as _flash_attn_varlen_func
else:
from sglang.jit_kernel.flash_attn.cute import (
flash_attn_varlen_func as _flash_attn_varlen_func,
)
except Exception as _e: # pragma: no cover
_flash_attn_varlen_func = None
_flash_attn_import_error = _e
@@ -41,12 +49,30 @@ def flash_attn_varlen_func(
pack_gqa: Optional[bool] = None,
score_mod: Optional[Callable] = None,
aux_tensors: Optional[list] = None,
q_descale: Optional[
torch.Tensor
] = None, # legacy per-tensor FP8 descale scalar (fp8_e4m3/e5m2 KV)
k_descale: Optional[torch.Tensor] = None, # legacy per-tensor FP8 descale scalar
v_descale: Optional[torch.Tensor] = None, # legacy per-tensor FP8 descale scalar
sfq: Optional[
torch.Tensor
] = None, # MXFP8 UE8M0 per-32-elem block scales (block-scaled QK^T)
sfk: Optional[
torch.Tensor
] = None, # MXFP8 UE8M0 per-32-elem block scales (block-scaled QK^T)
sfv: Optional[
torch.Tensor
] = None, # MXFP8 UE8M0 per-32-elem block scales (in-kernel V dequant)
rel_bias: Optional[torch.Tensor] = None,
rel_bias_prep_cache: Optional[dict] = None,
return_softmax_lse: bool = False,
**_: object,
):
if _flash_attn_varlen_func is None: # pragma: no cover
raise ImportError(
"Vendored FlashAttention CUTE is not available (cannot import "
"flash_attn.cute). Please check your source tree."
"FlashAttention-4 CUTE is not available. Install flash-attn-4 with "
"its CUDA/CUTE dependencies, or run from a source tree where the "
"vendored FA4 package is importable."
) from _flash_attn_import_error
q, k, v = [_maybe_contiguous(t) for t in (q, k, v)]
@@ -62,6 +88,32 @@ def flash_attn_varlen_func(
if window_size == (-1, -1):
window_size = (None, None)
# sf* = MXFP8 UE8M0 block scale factors (per-32-element), for the
# block-scaled QK^T / V-dequant path. *_descale = the legacy per-tensor
# FP8 descale scalars (kv_cache_dtype fp8_e4m3/fp8_e5m2). Only one group is
# ever populated for a given call. Non-None kwargs only, so bf16/other calls
# don't hand these to the kernel.
sf_kwargs = {}
if sfq is not None:
sf_kwargs["sfq"] = sfq
if sfk is not None:
sf_kwargs["sfk"] = sfk
if sfv is not None:
sf_kwargs["sfv"] = sfv
descale_kwargs = {}
if q_descale is not None:
descale_kwargs["q_descale"] = q_descale
if k_descale is not None:
descale_kwargs["k_descale"] = k_descale
if v_descale is not None:
descale_kwargs["v_descale"] = v_descale
rel_bias_kwargs = {}
if rel_bias is not None:
rel_bias_kwargs["rel_bias"] = rel_bias
if rel_bias_prep_cache is not None:
rel_bias_kwargs["rel_bias_prep_cache"] = rel_bias_prep_cache
result = _flash_attn_varlen_func(
q=q,
k=k,
@@ -83,6 +135,9 @@ def flash_attn_varlen_func(
score_mod=score_mod,
aux_tensors=aux_tensors,
return_lse=return_softmax_lse,
**sf_kwargs,
**descale_kwargs,
**rel_bias_kwargs,
)
if return_softmax_lse:
@@ -126,6 +181,11 @@ def flash_attn_with_kvcache(
sinks: Optional[torch.Tensor] = None,
score_mod: Optional[Callable] = None,
aux_tensors: Optional[list] = None,
sfq: Optional[torch.Tensor] = None,
sfk: Optional[torch.Tensor] = None,
sfv: Optional[torch.Tensor] = None,
rel_bias: Optional[torch.Tensor] = None,
rel_bias_prep_cache: Optional[dict] = None,
return_softmax_lse: bool = False,
**_: object,
):
@@ -137,9 +197,6 @@ def flash_attn_with_kvcache(
raise NotImplementedError(
"FA4 path does not support non-consecutive batch indices or left padding."
)
if q_descale is not None or k_descale is not None or v_descale is not None:
raise NotImplementedError("FA4 path does not support descale.")
if isinstance(cache_seqlens, int):
cache_seqlens = torch.full(
(k_cache.shape[0],), cache_seqlens, dtype=torch.int32, device=k_cache.device
@@ -157,11 +214,19 @@ def flash_attn_with_kvcache(
causal=causal,
softcap=softcap if softcap != 0.0 else None,
window_size=window_size,
num_splits=num_splits if num_splits != 0 else 1,
num_splits=num_splits,
pack_gqa=pack_gqa,
learnable_sink=sinks,
score_mod=score_mod,
aux_tensors=aux_tensors,
q_descale=q_descale,
k_descale=k_descale,
v_descale=v_descale,
sfq=sfq,
sfk=sfk,
sfv=sfv,
rel_bias=rel_bias,
rel_bias_prep_cache=rel_bias_prep_cache,
return_softmax_lse=True,
)
@@ -0,0 +1,4 @@
[flake8]
max-line-length = 100
# W503: line break before binary operator
ignore = E731, E741, F841, W503
@@ -0,0 +1,8 @@
Tri Dao
Jay Shah
Ted Zadouri
Markus Hoehnerbach
Vijay Thakkar
Timmy Liu
Driss Guessous
Reuben Stern
@@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2022, the respective contributors, as shown by the AUTHORS file.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,5 @@
global-exclude *.egg-info/*
prune flash_attn_4.egg-info
prune flash_attn.egg-info
prune build
prune dist
@@ -0,0 +1,33 @@
# FlashAttention-4 (CuTeDSL)
FlashAttention-4 is a CuTeDSL-based implementation of FlashAttention for Hopper and Blackwell GPUs.
## Installation
```sh
pip install flash-attn-4
```
If you're on CUDA 13, install with the `cu13` extra for best performance:
```sh
pip install "flash-attn-4[cu13]"
```
## Usage
```python
from flash_attn.cute import flash_attn_func, flash_attn_varlen_func
out = flash_attn_func(q, k, v, causal=True)
```
## Development
```sh
git clone https://github.com/Dao-AILab/flash-attention.git
cd flash-attention
pip install -e "flash_attn/cute[dev]" # CUDA 12.x
pip install -e "flash_attn/cute[dev,cu13]" # CUDA 13.x (e.g. B200)
pytest tests/cute/
```
@@ -0,0 +1,18 @@
"""Flash Attention CUTE (CUDA Template Engine) implementation."""
from importlib.metadata import PackageNotFoundError, version
try:
__version__ = version("fa4")
except PackageNotFoundError:
__version__ = "0.0.0"
from .interface import (
flash_attn_func,
flash_attn_varlen_func,
)
__all__ = [
"flash_attn_func",
"flash_attn_varlen_func",
]
@@ -0,0 +1,122 @@
# Copyright (c) 2025, Tri Dao.
from typing import Callable, Optional, Type
import cutlass
import cutlass.cute as cute
def get_smem_layout_atom(
dtype: Type[cutlass.Numeric], k_dim: int
) -> cute.ComposedLayout:
dtype_byte = cutlass.const_expr(dtype.width // 8)
bytes_per_row = cutlass.const_expr(k_dim * dtype_byte)
smem_k_block_size = (
cutlass.const_expr(
128
if bytes_per_row % 128 == 0
else (
64
if bytes_per_row % 64 == 0
else (32 if bytes_per_row % 32 == 0 else 16)
)
)
// dtype_byte
)
swizzle_bits = (
4
if smem_k_block_size == 128
else (3 if smem_k_block_size == 64 else (2 if smem_k_block_size == 32 else 1))
)
swizzle_base = 2 if dtype_byte == 4 else (3 if dtype_byte == 2 else 4)
return cute.make_composed_layout(
cute.make_swizzle(swizzle_bits, swizzle_base, swizzle_base),
0,
cute.make_ordered_layout(
(8 if cutlass.const_expr(k_dim % 32 == 0) else 16, smem_k_block_size),
order=(1, 0),
),
)
@cute.jit
def gemm(
tiled_mma: cute.TiledMma,
acc: cute.Tensor,
tCrA: cute.Tensor,
tCrB: cute.Tensor,
tCsA: cute.Tensor,
tCsB: cute.Tensor,
smem_thr_copy_A: cute.TiledCopy,
smem_thr_copy_B: cute.TiledCopy,
hook_fn: Optional[Callable] = None,
A_in_regs: cutlass.Constexpr[bool] = False,
B_in_regs: cutlass.Constexpr[bool] = False,
swap_AB: cutlass.Constexpr[bool] = False,
) -> None:
if cutlass.const_expr(swap_AB):
gemm(
tiled_mma,
acc,
tCrB,
tCrA,
tCsB,
tCsA,
smem_thr_copy_B,
smem_thr_copy_A,
hook_fn,
A_in_regs=B_in_regs,
B_in_regs=A_in_regs,
swap_AB=False,
)
else:
tCrA_copy_view = smem_thr_copy_A.retile(tCrA)
tCrB_copy_view = smem_thr_copy_B.retile(tCrB)
if cutlass.const_expr(not A_in_regs):
cute.copy(
smem_thr_copy_A, tCsA[None, None, 0], tCrA_copy_view[None, None, 0]
)
if cutlass.const_expr(not B_in_regs):
cute.copy(
smem_thr_copy_B, tCsB[None, None, 0], tCrB_copy_view[None, None, 0]
)
for k in cutlass.range_constexpr(cute.size(tCsA.shape[2])):
if k < cute.size(tCsA.shape[2]) - 1:
if cutlass.const_expr(not A_in_regs):
cute.copy(
smem_thr_copy_A,
tCsA[None, None, k + 1],
tCrA_copy_view[None, None, k + 1],
)
if cutlass.const_expr(not B_in_regs):
cute.copy(
smem_thr_copy_B,
tCsB[None, None, k + 1],
tCrB_copy_view[None, None, k + 1],
)
cute.gemm(tiled_mma, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
if cutlass.const_expr(k == 0 and hook_fn is not None):
hook_fn()
@cute.jit
def gemm_rs(
tiled_mma: cute.TiledMma,
acc: cute.Tensor,
tCrA: cute.Tensor,
tCrB: cute.Tensor,
tCsB: cute.Tensor,
smem_thr_copy_B: cute.TiledCopy,
hook_fn: Optional[Callable] = None,
) -> None:
tCrB_copy_view = smem_thr_copy_B.retile(tCrB)
cute.copy(smem_thr_copy_B, tCsB[None, None, 0], tCrB_copy_view[None, None, 0])
for k in cutlass.range_constexpr(cute.size(tCrA.shape[2])):
if cutlass.const_expr(k < cute.size(tCrA.shape[2]) - 1):
cute.copy(
smem_thr_copy_B,
tCsB[None, None, k + 1],
tCrB_copy_view[None, None, k + 1],
)
cute.gemm(tiled_mma, acc, tCrA[None, None, k], tCrB[None, None, k], acc)
if cutlass.const_expr(k == 0 and hook_fn is not None):
hook_fn()
@@ -0,0 +1,76 @@
import cutlass
import cutlass.cute as cute
from cutlass import Int32
from cutlass._mlir.dialects import llvm
from cutlass.cutlass_dsl import T, dsl_user_op
@dsl_user_op
def ld_acquire(lock_ptr: cute.Pointer, *, loc=None, ip=None) -> cutlass.Int32:
lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
state = llvm.inline_asm(
T.i32(),
[lock_ptr_i64],
"ld.global.acquire.gpu.b32 $0, [$1];",
"=r,l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
return cutlass.Int32(state)
@dsl_user_op
def red_relaxed(
lock_ptr: cute.Pointer, val: cutlass.Constexpr[Int32], *, loc=None, ip=None
) -> None:
lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
llvm.inline_asm(
None,
[lock_ptr_i64, Int32(val).ir_value(loc=loc, ip=ip)],
"red.relaxed.gpu.global.add.s32 [$0], $1;",
"l,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@dsl_user_op
def red_release(
lock_ptr: cute.Pointer, val: cutlass.Constexpr[Int32], *, loc=None, ip=None
) -> None:
lock_ptr_i64 = lock_ptr.toint(loc=loc, ip=ip).ir_value()
llvm.inline_asm(
None,
[lock_ptr_i64, Int32(val).ir_value(loc=loc, ip=ip)],
"red.release.gpu.global.add.s32 [$0], $1;",
"l,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@cute.jit
def wait_eq(
lock_ptr: cute.Pointer, thread_idx: int | Int32, flag_offset: int, val: Int32
) -> None:
flag_ptr = lock_ptr + flag_offset
if thread_idx == 0:
read_val = Int32(0)
while read_val != val:
read_val = ld_acquire(flag_ptr)
@cute.jit
def arrive_inc(
lock_ptr: cute.Pointer,
thread_idx: int | Int32,
flag_offset: int,
val: cutlass.Constexpr[Int32],
) -> None:
flag_ptr = lock_ptr + flag_offset
if thread_idx == 0:
red_release(flag_ptr, val)
# red_relaxed(flag_ptr, val)
@@ -0,0 +1,261 @@
"""Shared benchmark utilities: attention_ref, cuDNN helpers, flops calculation."""
import math
import torch
try:
import cudnn
except ImportError:
cudnn = None
# ── FLOPS calculation ────────────────────────────────────────────────────────
def flops(
batch,
nheads,
seqlen_q,
seqlen_k,
headdim,
headdim_v,
causal=False,
window_size=(None, None),
has_qv=False,
):
if causal:
avg_seqlen = (max(0, seqlen_k - seqlen_q) + seqlen_k) / 2
else:
if window_size == (None, None):
avg_seqlen = seqlen_k
else:
row_idx = torch.arange(seqlen_q, device="cuda")
col_left = (
torch.maximum(
row_idx + seqlen_k - seqlen_q - window_size[0], torch.tensor(0)
)
if window_size[0] is not None
else torch.zeros_like(row_idx)
)
col_right = (
torch.minimum(
row_idx + seqlen_k - seqlen_q + window_size[1],
torch.tensor(seqlen_k - 1),
)
if window_size[1] is not None
else torch.full_like(row_idx, seqlen_k - 1)
)
avg_seqlen = (col_right - col_left + 1).float().mean().item()
eff_headdim = headdim + headdim_v if has_qv else headdim
return batch * nheads * 2 * seqlen_q * avg_seqlen * (eff_headdim + headdim_v)
# ── Bandwidth calculation ────────────────────────────────────────────────────
def bandwidth_fwd_bytes(
batch,
nheads,
nheads_kv,
seqlen_q,
seqlen_k,
headdim,
headdim_v,
dtype_bytes=2,
has_qv=False,
):
"""HBM traffic for one attention pass: read Q,K,V + write O."""
q = batch * nheads * seqlen_q * headdim
qv = batch * nheads * seqlen_q * headdim_v if has_qv else 0
k = batch * nheads_kv * seqlen_k * headdim
v = batch * nheads_kv * seqlen_k * headdim_v
o = batch * nheads * seqlen_q * headdim_v
return (q + qv + k + v + o) * dtype_bytes
def bandwidth_bwd_bytes(
batch, nheads, nheads_kv, seqlen_q, seqlen_k, headdim, headdim_v, dtype_bytes=2
):
"""HBM traffic for one attention pass: read Q,K,V,dO + write dQ,dK,dV."""
q = batch * nheads * seqlen_q * headdim
k = batch * nheads_kv * seqlen_k * headdim
v = batch * nheads_kv * seqlen_k * headdim_v
do = batch * nheads * seqlen_q * headdim_v
dq = q
dk = k
dv = v
return (q + k + v + do + dq + dk + dv) * dtype_bytes
# ── Reference attention ─────────────────────────────────────────────────────
_attention_ref_mask_cache = {}
def attention_ref(q, k, v, causal=False):
"""Standard attention reference implementation.
Args:
q, k, v: (batch, seqlen, nheads, headdim) tensors.
causal: whether to apply causal mask.
"""
softmax_scale = 1.0 / math.sqrt(q.shape[-1])
scores = torch.einsum("bthd,bshd->bhts", q * softmax_scale, k)
if causal:
if scores.shape[-2] not in _attention_ref_mask_cache:
mask = torch.tril(
torch.ones(scores.shape[-2:], device=scores.device, dtype=torch.bool),
diagonal=0,
)
_attention_ref_mask_cache[scores.shape[-2]] = mask
else:
mask = _attention_ref_mask_cache[scores.shape[-2]]
scores = scores.masked_fill(mask, float("-inf"))
attn = torch.softmax(scores, dim=-1)
return torch.einsum("bhts,bshd->bthd", attn, v)
# ── cuDNN graph helpers ─────────────────────────────────────────────────────
_TORCH_TO_CUDNN_DTYPE = {
torch.float16: "HALF",
torch.bfloat16: "BFLOAT16",
torch.float32: "FLOAT",
torch.int32: "INT32",
torch.int64: "INT64",
}
def _build_cudnn_graph(io_dtype, tensors, build_fn):
"""Build a cuDNN graph. Returns (graph, variant_pack, workspace)."""
assert cudnn is not None, "cuDNN is not available"
cudnn_dtype = getattr(cudnn.data_type, _TORCH_TO_CUDNN_DTYPE[io_dtype])
graph = cudnn.pygraph(
io_data_type=cudnn_dtype,
intermediate_data_type=cudnn.data_type.FLOAT,
compute_data_type=cudnn.data_type.FLOAT,
)
graph_tensors = {name: graph.tensor_like(t.detach()) for name, t in tensors.items()}
variant_pack = build_fn(graph, graph_tensors)
graph.validate()
graph.build_operation_graph()
graph.create_execution_plans([cudnn.heur_mode.A, cudnn.heur_mode.FALLBACK])
graph.check_support()
graph.build_plans()
workspace = torch.empty(
graph.get_workspace_size(), device="cuda", dtype=torch.uint8
)
return graph, variant_pack, workspace
def cudnn_fwd_setup(q, k, v, causal=False, window_size_left=None):
"""Build a cuDNN forward SDPA graph.
Args:
q, k, v: (batch, nheads, seqlen, headdim) tensors (cuDNN layout).
causal: whether to apply causal mask.
window_size_left: sliding window size (None for no window).
Returns:
(fwd_fn, o_gpu, stats_gpu) where fwd_fn is a zero-arg callable.
"""
b, nheads, seqlen_q, headdim = q.shape
headdim_v = v.shape[-1]
o_gpu = torch.empty(b, nheads, seqlen_q, headdim_v, dtype=q.dtype, device=q.device)
stats_gpu = torch.empty(
b, nheads, seqlen_q, 1, dtype=torch.float32, device=q.device
)
def build(graph, gt):
o, stats = graph.sdpa(
name="sdpa",
q=gt["q"],
k=gt["k"],
v=gt["v"],
is_inference=False,
attn_scale=1.0 / math.sqrt(headdim),
use_causal_mask=causal or window_size_left is not None,
sliding_window_length=(
window_size_left
if window_size_left is not None and not causal
else None
),
)
o.set_output(True).set_dim(o_gpu.shape).set_stride(o_gpu.stride())
stats.set_output(True).set_data_type(cudnn.data_type.FLOAT)
return {gt["q"]: q, gt["k"]: k, gt["v"]: v, o: o_gpu, stats: stats_gpu}
graph, variant_pack, workspace = _build_cudnn_graph(
q.dtype, {"q": q, "k": k, "v": v}, build
)
def fwd_fn():
graph.execute(variant_pack, workspace)
return o_gpu
return fwd_fn, o_gpu, stats_gpu
def cudnn_bwd_setup(q, k, v, o, g, lse, causal=False, window_size_left=None):
"""Build a cuDNN backward SDPA graph.
Args:
q, k, v, o, g, lse: (batch, nheads, seqlen, dim) tensors (cuDNN layout).
causal: whether to apply causal mask.
window_size_left: sliding window size (None for no window).
Returns:
bwd_fn: zero-arg callable that returns (dq, dk, dv).
"""
headdim = q.shape[-1]
dq_gpu, dk_gpu, dv_gpu = (
torch.empty_like(q),
torch.empty_like(k),
torch.empty_like(v),
)
def build(graph, gt):
dq, dk, dv = graph.sdpa_backward(
name="sdpa_backward",
q=gt["q"],
k=gt["k"],
v=gt["v"],
o=gt["o"],
dO=gt["g"],
stats=gt["lse"],
attn_scale=1.0 / math.sqrt(headdim),
use_causal_mask=causal or window_size_left is not None,
sliding_window_length=(
window_size_left
if window_size_left is not None and not causal
else None
),
use_deterministic_algorithm=False,
)
dq.set_output(True).set_dim(dq_gpu.shape).set_stride(dq_gpu.stride())
dk.set_output(True).set_dim(dk_gpu.shape).set_stride(dk_gpu.stride())
dv.set_output(True).set_dim(dv_gpu.shape).set_stride(dv_gpu.stride())
return {
gt["q"]: q,
gt["k"]: k,
gt["v"]: v,
gt["o"]: o,
gt["g"]: g,
gt["lse"]: lse,
dq: dq_gpu,
dk: dk_gpu,
dv: dv_gpu,
}
graph, variant_pack, workspace = _build_cudnn_graph(
q.dtype,
{"q": q, "k": k, "v": v, "o": o, "g": g, "lse": lse},
build,
)
def bwd_fn():
graph.execute(variant_pack, workspace)
return dq_gpu, dk_gpu, dv_gpu
return bwd_fn
@@ -0,0 +1,281 @@
# Copyright (c) 2023, Tri Dao.
"""Useful functions for writing test code."""
import torch
import torch.utils.benchmark as benchmark
def benchmark_forward(
fn,
*inputs,
repeats=10,
desc="",
verbose=True,
amp=False,
amp_dtype=torch.float16,
**kwinputs,
):
"""Use Pytorch Benchmark on the forward pass of an arbitrary function."""
if verbose:
print(desc, "- Forward pass")
def amp_wrapper(*inputs, **kwinputs):
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
fn(*inputs, **kwinputs)
t = benchmark.Timer(
stmt="fn_amp(*inputs, **kwinputs)",
globals={"fn_amp": amp_wrapper, "inputs": inputs, "kwinputs": kwinputs},
num_threads=torch.get_num_threads(),
)
m = t.timeit(repeats)
if verbose:
print(m)
return t, m
def benchmark_backward(
fn,
*inputs,
grad=None,
repeats=10,
desc="",
verbose=True,
amp=False,
amp_dtype=torch.float16,
**kwinputs,
):
"""Use Pytorch Benchmark on the backward pass of an arbitrary function."""
if verbose:
print(desc, "- Backward pass")
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
y = fn(*inputs, **kwinputs)
if type(y) is tuple:
y = y[0]
if grad is None:
grad = torch.randn_like(y)
else:
if grad.shape != y.shape:
raise RuntimeError("Grad shape does not match output shape")
def f(*inputs, y, grad):
# Set .grad to None to avoid extra operation of gradient accumulation
for x in inputs:
if isinstance(x, torch.Tensor):
x.grad = None
y.backward(grad, retain_graph=True)
t = benchmark.Timer(
stmt="f(*inputs, y=y, grad=grad)",
globals={"f": f, "inputs": inputs, "y": y, "grad": grad},
num_threads=torch.get_num_threads(),
)
m = t.timeit(repeats)
if verbose:
print(m)
return t, m
def benchmark_combined(
fn,
*inputs,
grad=None,
repeats=10,
desc="",
verbose=True,
amp=False,
amp_dtype=torch.float16,
**kwinputs,
):
"""Use Pytorch Benchmark on the forward+backward pass of an arbitrary function."""
if verbose:
print(desc, "- Forward + Backward pass")
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
y = fn(*inputs, **kwinputs)
if type(y) is tuple:
y = y[0]
if grad is None:
grad = torch.randn_like(y)
else:
if grad.shape != y.shape:
raise RuntimeError("Grad shape does not match output shape")
def f(grad, *inputs, **kwinputs):
for x in inputs:
if isinstance(x, torch.Tensor):
x.grad = None
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
y = fn(*inputs, **kwinputs)
if type(y) is tuple:
y = y[0]
y.backward(grad, retain_graph=True)
t = benchmark.Timer(
stmt="f(grad, *inputs, **kwinputs)",
globals={
"f": f,
"fn": fn,
"inputs": inputs,
"grad": grad,
"kwinputs": kwinputs,
},
num_threads=torch.get_num_threads(),
)
m = t.timeit(repeats)
if verbose:
print(m)
return t, m
def benchmark_fwd_bwd(
fn,
*inputs,
grad=None,
repeats=10,
desc="",
verbose=True,
amp=False,
amp_dtype=torch.float16,
**kwinputs,
):
"""Use Pytorch Benchmark on the forward+backward pass of an arbitrary function."""
return (
benchmark_forward(
fn,
*inputs,
repeats=repeats,
desc=desc,
verbose=verbose,
amp=amp,
amp_dtype=amp_dtype,
**kwinputs,
),
benchmark_backward(
fn,
*inputs,
grad=grad,
repeats=repeats,
desc=desc,
verbose=verbose,
amp=amp,
amp_dtype=amp_dtype,
**kwinputs,
),
)
def benchmark_all(
fn,
*inputs,
grad=None,
repeats=10,
desc="",
verbose=True,
amp=False,
amp_dtype=torch.float16,
**kwinputs,
):
"""Use Pytorch Benchmark on the forward+backward pass of an arbitrary function."""
return (
benchmark_forward(
fn,
*inputs,
repeats=repeats,
desc=desc,
verbose=verbose,
amp=amp,
amp_dtype=amp_dtype,
**kwinputs,
),
benchmark_backward(
fn,
*inputs,
grad=grad,
repeats=repeats,
desc=desc,
verbose=verbose,
amp=amp,
amp_dtype=amp_dtype,
**kwinputs,
),
benchmark_combined(
fn,
*inputs,
grad=grad,
repeats=repeats,
desc=desc,
verbose=verbose,
amp=amp,
amp_dtype=amp_dtype,
**kwinputs,
),
)
def pytorch_profiler(
fn,
*inputs,
trace_filename=None,
backward=False,
amp=False,
amp_dtype=torch.float16,
cpu=False,
verbose=True,
**kwinputs,
):
"""Wrap benchmark functions in Pytorch profiler to see CUDA information."""
if backward:
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
out = fn(*inputs, **kwinputs)
if type(out) is tuple:
out = out[0]
g = torch.randn_like(out)
for _ in range(30): # Warm up
if backward:
for x in inputs:
if isinstance(x, torch.Tensor):
x.grad = None
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
out = fn(*inputs, **kwinputs)
if type(out) is tuple:
out = out[0]
# Backward should be done outside autocast
if backward:
out.backward(g, retain_graph=True)
activities = ([torch.profiler.ProfilerActivity.CPU] if cpu else []) + [
torch.profiler.ProfilerActivity.CUDA
]
with torch.profiler.profile(
activities=activities,
record_shapes=True,
# profile_memory=True,
with_stack=True,
) as prof:
if backward:
for x in inputs:
if isinstance(x, torch.Tensor):
x.grad = None
with torch.autocast(device_type="cuda", dtype=amp_dtype, enabled=amp):
out = fn(*inputs, **kwinputs)
if type(out) is tuple:
out = out[0]
if backward:
out.backward(g, retain_graph=True)
if verbose:
# print(prof.key_averages().table(sort_by="self_cuda_time_total", row_limit=50))
print(prof.key_averages().table(row_limit=50))
if trace_filename is not None:
prof.export_chrome_trace(trace_filename)
def benchmark_memory(fn, *inputs, desc="", verbose=True, **kwinputs):
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats()
torch.cuda.synchronize()
fn(*inputs, **kwinputs)
torch.cuda.synchronize()
mem = torch.cuda.max_memory_allocated() / ((2**20) * 1000)
if verbose:
print(f"{desc} max memory: {mem}GB")
torch.cuda.empty_cache()
return mem
@@ -0,0 +1,487 @@
# Benchmark FP8 attention for FA4 (CuTe-DSL) on SM100.
#
# Run (recommended):
# python -m flash_attn.cute.benchmark_flash_attention_fp8
#
# Notes:
# - This is intended to be used while bringing up FP8 support for SM100.
# - FP8 correctness depends on descales + max-offset scaling being implemented in the SM100 kernel.
# This script optionally checks output vs a BF16 PyTorch baseline on dequantized FP8 inputs.
#
# Adapted from: `hopper/benchmark_flash_attention_fp8.py`
from __future__ import annotations
import argparse
import inspect
import math
import time
from typing import Iterable
import torch
from einops import rearrange
from sglang.jit_kernel.flash_attn.cute.benchmark import benchmark_forward
from sglang.jit_kernel.flash_attn.cute.interface import (
_flash_attn_fwd as flash_attn_cute_fwd,
)
try:
import cudnn
except ImportError:
cudnn = None
def _torch_float8_dtype(name: str) -> torch.dtype:
if name in ("fp8", "fp8_e4m3", "fp8_e4m3fn"):
return torch.float8_e4m3fn
if name in ("fp8_e5m2", "fp8_e5m2fn"):
return torch.float8_e5m2
raise ValueError(f"Unsupported fp8 dtype name: {name}")
def _parse_int_list(csv: str) -> list[int]:
out: list[int] = []
for part in csv.split(","):
part = part.strip()
if not part:
continue
out.append(int(part))
return out
def attention_pytorch(qkv: torch.Tensor, causal: bool) -> torch.Tensor:
"""
qkv: (batch, seqlen, 3, nheads, headdim)
out: (batch, seqlen, nheads, headdim)
"""
batch_size, seqlen, _, nheads, d = qkv.shape
q, k, v = qkv.unbind(dim=2)
q = rearrange(q, "b t h d -> (b h) t d")
k = rearrange(k, "b s h d -> (b h) d s")
softmax_scale = 1.0 / math.sqrt(d)
scores = torch.empty(
batch_size * nheads, seqlen, seqlen, dtype=qkv.dtype, device=qkv.device
)
scores = rearrange(
torch.baddbmm(scores, q, k, beta=0, alpha=softmax_scale),
"(b h) t s -> b h t s",
h=nheads,
)
if causal:
causal_mask = torch.triu(
torch.full((seqlen, seqlen), -10000.0, device=scores.device), 1
)
scores = scores + causal_mask.to(dtype=scores.dtype)
attention = torch.softmax(scores, dim=-1)
output = torch.einsum("bhts,bshd->bthd", attention, v)
return output.to(dtype=qkv.dtype)
def flops(batch: int, seqlen: int, headdim: int, nheads: int, causal: bool) -> int:
# Matches the hopper benchmarks convention.
return 4 * batch * seqlen**2 * nheads * headdim // (2 if causal else 1)
def efficiency(flop: int, seconds: float) -> float:
return (flop / seconds / 1e12) if not math.isnan(seconds) else 0.0
def time_fwd(fn, *args, repeats: int, **kwargs) -> float:
time.sleep(1) # reduce residual throttling effects between benchmarks
_, m = benchmark_forward(fn, *args, repeats=repeats, verbose=False, **kwargs)
return float(m.mean)
def convert_to_cudnn_type(torch_type):
if torch_type == torch.float16:
return cudnn.data_type.HALF
if torch_type == torch.bfloat16:
return cudnn.data_type.BFLOAT16
if torch_type == torch.float32:
return cudnn.data_type.FLOAT
if torch_type == torch.int32:
return cudnn.data_type.INT32
if torch_type == torch.int64:
return cudnn.data_type.INT64
if torch_type == torch.float8_e4m3fn:
return cudnn.data_type.FP8_E4M3
if torch_type == torch.float8_e5m2:
return cudnn.data_type.FP8_E5M2
raise ValueError("Unsupported tensor data type.")
def cudnn_sdpa_fp8_setup(qkv: torch.Tensor, seqlen_q: int, seqlen_k: int, causal: bool):
"""Minimal cudnn.fp8 sdpa runner (optional)."""
assert cudnn is not None, "cudnn python bindings not available"
b, _, _, nheads, headdim = qkv.shape
o_gpu = torch.zeros(
b, seqlen_q, nheads, headdim, dtype=qkv.dtype, device=qkv.device
)
o_gpu_transposed = torch.as_strided(
o_gpu,
[b, nheads, seqlen_q, headdim],
[nheads * seqlen_q * headdim, headdim, nheads * headdim, 1],
)
amax_s_gpu = torch.empty(1, 1, 1, 1, dtype=torch.float32, device=qkv.device)
amax_o_gpu = torch.empty(1, 1, 1, 1, dtype=torch.float32, device=qkv.device)
graph = cudnn.pygraph(
io_data_type=convert_to_cudnn_type(qkv.dtype),
intermediate_data_type=cudnn.data_type.FLOAT,
compute_data_type=cudnn.data_type.FLOAT,
)
new_q = torch.as_strided(
qkv,
[b, nheads, seqlen_q, headdim],
[seqlen_q * nheads * headdim * 3, headdim, headdim * nheads * 3, 1],
storage_offset=0,
)
q = graph.tensor(
name="Q",
dim=list(new_q.shape),
stride=list(new_q.stride()),
data_type=convert_to_cudnn_type(qkv.dtype),
)
new_k = torch.as_strided(
qkv,
[b, nheads, seqlen_k, headdim],
[seqlen_k * nheads * headdim * 3, headdim, headdim * nheads * 3, 1],
storage_offset=nheads * headdim,
)
k = graph.tensor(
name="K",
dim=list(new_k.shape),
stride=list(new_k.stride()),
data_type=convert_to_cudnn_type(qkv.dtype),
)
new_v = torch.as_strided(
qkv,
[b, nheads, seqlen_k, headdim],
[seqlen_k * nheads * headdim * 3, headdim, headdim * nheads * 3, 1],
storage_offset=nheads * headdim * 2,
)
v = graph.tensor(
name="V",
dim=list(new_v.shape),
stride=list(new_v.stride()),
data_type=convert_to_cudnn_type(qkv.dtype),
)
def _scale_tensor():
return graph.tensor(
dim=[1, 1, 1, 1], stride=[1, 1, 1, 1], data_type=cudnn.data_type.FLOAT
)
default_scale_gpu = torch.ones(1, 1, 1, 1, dtype=torch.float32, device="cuda")
descale_q = _scale_tensor()
descale_k = _scale_tensor()
descale_v = _scale_tensor()
descale_s = _scale_tensor()
scale_s = _scale_tensor()
scale_o = _scale_tensor()
o, _, amax_s, amax_o = graph.sdpa_fp8(
q=q,
k=k,
v=v,
descale_q=descale_q,
descale_k=descale_k,
descale_v=descale_v,
descale_s=descale_s,
scale_s=scale_s,
scale_o=scale_o,
is_inference=True,
attn_scale=1.0 / math.sqrt(headdim),
use_causal_mask=causal,
name="sdpa",
)
o.set_output(True).set_dim(o_gpu_transposed.shape).set_stride(
o_gpu_transposed.stride()
)
amax_s.set_output(False).set_dim(amax_s_gpu.shape).set_stride(amax_s_gpu.stride())
amax_o.set_output(False).set_dim(amax_o_gpu.shape).set_stride(amax_o_gpu.stride())
graph.validate()
graph.build_operation_graph()
graph.create_execution_plans([cudnn.heur_mode.A, cudnn.heur_mode.FALLBACK])
graph.check_support()
graph.build_plans()
variant_pack = {
q: new_q,
k: new_k,
v: new_v,
descale_q: default_scale_gpu,
descale_k: default_scale_gpu,
descale_v: default_scale_gpu,
descale_s: default_scale_gpu,
scale_s: default_scale_gpu,
scale_o: default_scale_gpu,
o: o_gpu_transposed,
amax_s: amax_s_gpu,
amax_o: amax_o_gpu,
}
workspace = torch.empty(
graph.get_workspace_size(), device="cuda", dtype=torch.uint8
)
def run():
graph.execute(variant_pack, workspace)
return o_gpu
return run
def _maybe_pass_descales(callable_, **kwargs):
sig = inspect.signature(callable_)
return {k: v for k, v in kwargs.items() if k in sig.parameters}
def main(argv: Iterable[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--repeats", type=int, default=30)
parser.add_argument("--dim", type=int, default=2048)
parser.add_argument("--headdims", default="64,128")
parser.add_argument("--dtype", default="fp8_e4m3fn")
parser.add_argument("--seed", type=int, default=0)
parser.add_argument(
"--check",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable correctness checks vs BF16 PyTorch baseline.",
)
parser.add_argument(
"--check-quantization-only",
action="store_true",
help="Check FP8 kernel vs dequantized-FP8 baseline (quantization error only).",
)
parser.add_argument("--atol-bf16", type=float, default=0.10)
parser.add_argument("--rtol-bf16", type=float, default=0.10)
parser.add_argument("--atol-fp8", type=float, default=0.50)
parser.add_argument("--rtol-fp8", type=float, default=0.50)
parser.add_argument("--run-cudnn", action="store_true")
args = parser.parse_args(list(argv) if argv is not None else None)
if not torch.cuda.is_available():
raise RuntimeError("CUDA is required")
major, minor = torch.cuda.get_device_capability()
if major != 10:
raise RuntimeError(
f"This benchmark is for SM100 (compute capability 10.x). Got {major}.{minor}."
)
torch.manual_seed(args.seed)
device = "cuda"
fp8_dtype = _torch_float8_dtype(args.dtype)
headdim_vals = _parse_int_list(args.headdims)
bs_seqlen_vals = [
(32, 512),
(16, 1024),
(8, 2048),
(4, 4096),
(2, 8192),
(1, 16384),
]
methods = ["Pytorch", "FA4-CuTe-BF16", "FA4-CuTe-FP8"] + (
["cuDNN-FP8"] if args.run_cudnn and cudnn is not None else []
)
fp8_failures = []
for headdim in headdim_vals:
for causal in (False, True):
for batch, seqlen in bs_seqlen_vals:
torch.cuda.empty_cache()
nheads = args.dim // headdim
if args.dim % headdim != 0:
raise ValueError(
f"--dim must be divisible by headdim ({args.dim=} {headdim=})"
)
q_bf16 = torch.randn(
batch, seqlen, nheads, headdim, device=device, dtype=torch.bfloat16
)
k_bf16 = torch.randn(
batch, seqlen, nheads, headdim, device=device, dtype=torch.bfloat16
)
v_bf16 = torch.randn(
batch, seqlen, nheads, headdim, device=device, dtype=torch.bfloat16
)
qkv_bf16 = torch.stack([q_bf16, k_bf16, v_bf16], dim=2)
times = {}
speeds = {}
out_ref_bf16 = None
try:
out_ref_bf16 = attention_pytorch(
qkv_bf16, causal=causal
) # warmup / reference
t = time_fwd(
attention_pytorch, qkv_bf16, causal=causal, repeats=args.repeats
)
times["Pytorch"] = t
except RuntimeError as e:
if "out of memory" in str(e).lower():
times["Pytorch"] = float("nan")
out_ref_bf16 = None
else:
raise
# FA4 / CuTe BF16 baseline
try:
softmax_scale = headdim**-0.5
out_fa4_bf16, _ = flash_attn_cute_fwd(
q_bf16,
k_bf16,
v_bf16,
softmax_scale=softmax_scale,
causal=causal,
) # warmup / compile
t = time_fwd(
flash_attn_cute_fwd,
q_bf16,
k_bf16,
v_bf16,
softmax_scale=softmax_scale,
causal=causal,
repeats=args.repeats,
)
times["FA4-CuTe-BF16"] = t
if args.check and out_ref_bf16 is not None:
torch.testing.assert_close(
out_fa4_bf16,
out_ref_bf16,
atol=args.atol_bf16,
rtol=args.rtol_bf16,
)
except Exception as e:
# Treat as fatal: BF16 kernel should be usable for basic sanity checking.
raise RuntimeError("FA4-CuTe BF16 baseline failed") from e
# FA4 / CuTe FP8
q_fp8 = q_bf16.to(fp8_dtype)
k_fp8 = k_bf16.to(fp8_dtype)
v_fp8 = v_bf16.to(fp8_dtype)
# Placeholder descales (FA3-style: per-(batch, kv_head)).
q_descale = torch.ones(
batch, nheads, device=device, dtype=torch.float32
)
k_descale = torch.ones(
batch, nheads, device=device, dtype=torch.float32
)
v_descale = torch.ones(
batch, nheads, device=device, dtype=torch.float32
)
# Optional: FP8 reference baseline (dequantized FP8 -> PyTorch) for quantization-error-only checks
out_ref_fp8 = None
if args.check and args.check_quantization_only:
try:
# Dequantize FP8 inputs back to BF16 (applying descales)
q_ref_fp8 = (
q_fp8.to(torch.bfloat16) * q_descale[:, None, :, None]
).to(torch.bfloat16)
k_ref_fp8 = (
k_fp8.to(torch.bfloat16) * k_descale[:, None, :, None]
).to(torch.bfloat16)
v_ref_fp8 = (
v_fp8.to(torch.bfloat16) * v_descale[:, None, :, None]
).to(torch.bfloat16)
qkv_ref_fp8 = torch.stack(
[q_ref_fp8, k_ref_fp8, v_ref_fp8], dim=2
)
out_ref_fp8 = attention_pytorch(qkv_ref_fp8, causal=causal)
except RuntimeError as e:
if "out of memory" in str(e).lower():
out_ref_fp8 = None
else:
raise
fa4_kwargs = dict(softmax_scale=softmax_scale, causal=causal)
fa4_kwargs.update(
_maybe_pass_descales(
flash_attn_cute_fwd,
q_descale=q_descale,
k_descale=k_descale,
v_descale=v_descale,
)
)
try:
# Warmup/compile (will raise until FP8 is implemented)
out_fa4_fp8, _ = flash_attn_cute_fwd(
q_fp8, k_fp8, v_fp8, **fa4_kwargs
)
t = time_fwd(
flash_attn_cute_fwd,
q_fp8,
k_fp8,
v_fp8,
repeats=args.repeats,
**fa4_kwargs,
)
times["FA4-CuTe-FP8"] = t
if args.check:
# Choose baseline: quantization-only (dequantized FP8) or full (BF16)
if args.check_quantization_only:
ref_baseline = out_ref_fp8
else:
ref_baseline = out_ref_bf16
if ref_baseline is not None:
torch.testing.assert_close(
out_fa4_fp8,
ref_baseline,
atol=args.atol_fp8,
rtol=args.rtol_fp8,
)
except Exception as e:
fp8_failures.append((causal, headdim, batch, seqlen, repr(e)))
times["FA4-CuTe-FP8"] = float("nan")
if args.run_cudnn and cudnn is not None:
qkv_fp8 = qkv_bf16.to(fp8_dtype)
runner = cudnn_sdpa_fp8_setup(
qkv_fp8, seqlen, seqlen, causal=causal
)
_ = runner() # warmup
t = time_fwd(lambda: runner(), repeats=args.repeats)
times["cuDNN-FP8"] = t
print(
f"### causal={causal}, headdim={headdim}, batch={batch}, seqlen={seqlen} ###"
)
for method in methods:
t = times.get(method, float("nan"))
speeds[method] = efficiency(
flops(batch, seqlen, headdim, nheads, causal), t
)
if math.isnan(t):
print(f"{method} fwd: (skipped)")
else:
print(
f"{method} fwd: {speeds[method]:.2f} TFLOPs/s, {t * 1e3:.3f} ms"
)
if math.isnan(times.get("FA4-CuTe-FP8", float("nan"))):
print("FA4-CuTe-FP8 status: FAILED")
if fp8_failures:
print(f"\nFP8 failures: {len(fp8_failures)} (showing first 5)")
for causal, headdim, batch, seqlen, err in fp8_failures[:5]:
print(
f"- causal={causal} headdim={headdim} batch={batch} seqlen={seqlen}: {err}"
)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,203 @@
# Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
from dataclasses import dataclass
from typing import Optional, Tuple
import cutlass
import cutlass.cute as cute
from cutlass import Int32, const_expr
from sglang.jit_kernel.flash_attn.cute.seqlen_info import SeqlenInfoQK, SeqlenInfoQKNewK
@dataclass(frozen=True)
class BlockInfo:
tile_m: cutlass.Constexpr[int]
tile_n: cutlass.Constexpr[int]
is_causal: cutlass.Constexpr[bool]
is_local: cutlass.Constexpr[bool] = False
is_split_kv: cutlass.Constexpr[bool] = False
window_size_left: Optional[Int32] = None
window_size_right: Optional[Int32] = None
qhead_per_kvhead_packgqa: cutlass.Constexpr[int] = 1
@cute.jit
def get_n_idx_left_right(
self,
seqlen_info: SeqlenInfoQK,
m_idx: Int32,
) -> Tuple[Int32, Int32]:
m_idx_actual = m_idx // self.qhead_per_kvhead_packgqa
if const_expr(
self.is_causal or (self.is_local and self.window_size_right is not None)
):
n_idx_right = m_idx_actual + 1 + seqlen_info.seqlen_k - seqlen_info.seqlen_q
if const_expr(self.window_size_right is not None):
n_idx_right += self.window_size_right
else:
n_idx_right = seqlen_info.seqlen_k
if const_expr(self.is_local and self.window_size_left is not None):
n_idx_left = (
m_idx_actual
+ seqlen_info.seqlen_k
- seqlen_info.seqlen_q
- self.window_size_left
)
n_idx_left = cutlass.max(n_idx_left, 0)
else:
n_idx_left = 0
# inclusive n_idx_left, exclusive n_idx_right
# e.g. for causal, return (0, m_idx + 1)
return n_idx_left, n_idx_right
@cute.jit
def get_n_block_min_max(
self,
seqlen_info: SeqlenInfoQK,
m_block: Int32,
split_idx: Int32 = 0,
num_splits: Int32 = 1,
half_tile_m: bool = False,
absolute: bool = False,
half_tile_n: bool = False,
) -> Tuple[Int32, Int32]:
tile_m = self.tile_m // 2 if const_expr(half_tile_m) else self.tile_m
tile_n = self.tile_n // 2 if const_expr(half_tile_n) else self.tile_n
n_block_max = cute.ceil_div(seqlen_info.seqlen_k, tile_n)
if const_expr(
self.is_causal or (self.is_local and self.window_size_right is not None)
):
m_idx_max = (m_block + 1) * tile_m
if const_expr(self.qhead_per_kvhead_packgqa > 1):
m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa)
n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
n_idx_right = (
n_idx if const_expr(self.is_causal) else n_idx + self.window_size_right
)
n_block_max = min(n_block_max, cute.ceil_div(n_idx_right, tile_n))
n_block_min = 0
if const_expr(self.is_local and self.window_size_left is not None):
m_idx_min = m_block * tile_m
if const_expr(self.qhead_per_kvhead_packgqa > 1):
m_idx_min = m_idx_min // self.qhead_per_kvhead_packgqa
n_idx = m_idx_min + seqlen_info.seqlen_k - seqlen_info.seqlen_q
n_idx_left = n_idx - self.window_size_left
n_block_min = cutlass.max(n_idx_left // tile_n, 0)
if cutlass.const_expr(self.is_split_kv and not absolute):
num_n_blocks_per_split = (
Int32(0)
if n_block_max <= n_block_min
else (n_block_max - n_block_min + num_splits - 1) // num_splits
)
n_block_min = n_block_min + split_idx * num_n_blocks_per_split
n_block_max = cutlass.min(n_block_min + num_n_blocks_per_split, n_block_max)
return n_block_min, n_block_max
@cute.jit
def get_m_block_min_max(
self, seqlen_info: SeqlenInfoQK, n_block: Int32
) -> Tuple[Int32, Int32]:
m_block_max = cute.ceil_div(seqlen_info.seqlen_q, self.tile_m)
m_block_min = 0
if const_expr(
self.is_causal or (self.is_local and self.window_size_right is not None)
):
n_idx_min = n_block * self.tile_n
m_idx = n_idx_min + seqlen_info.seqlen_q - seqlen_info.seqlen_k
m_idx_right = (
m_idx if const_expr(self.is_causal) else m_idx - self.window_size_right
)
m_block_min = max(m_block_min, m_idx_right // self.tile_m)
if const_expr(self.is_local and self.window_size_left is not None):
n_idx_max = (n_block + 1) * self.tile_n
m_idx = n_idx_max + seqlen_info.seqlen_q - seqlen_info.seqlen_k
m_idx_left = m_idx + self.window_size_left
m_block_max = min(m_block_max, cute.ceil_div(m_idx_left, self.tile_m))
return m_block_min, m_block_max
@cute.jit
def get_n_block_k_new_min_max(
self,
seqlen_info: SeqlenInfoQKNewK,
m_block: Int32,
split_idx: Int32 = 0,
num_splits: Int32 = 1,
) -> Tuple[Int32, Int32]:
"""Get the block range for new K tokens (append KV).
First computes the full n_block range via get_n_block_min_max, then maps
those blocks into the new-K index space by subtracting seqlen_k_og.
"""
n_block_min, n_block_max = self.get_n_block_min_max(
seqlen_info,
m_block,
split_idx,
num_splits,
)
idx_k_new_min = cutlass.max(
n_block_min * self.tile_n - seqlen_info.seqlen_k_og, 0
)
idx_k_new_max = cutlass.min(
n_block_max * self.tile_n - seqlen_info.seqlen_k_og,
seqlen_info.seqlen_k_new,
)
n_block_new_min = idx_k_new_min // self.tile_n
n_block_new_max = (
cute.ceil_div(idx_k_new_max, self.tile_n)
if idx_k_new_max > idx_k_new_min
else n_block_new_min
)
return n_block_new_min, n_block_new_max
@cute.jit
def get_n_block_min_causal_local_mask(
self,
seqlen_info: SeqlenInfoQK,
m_block: Int32,
n_block_min: Int32,
) -> Int32:
"""If we have separate iterations with causal or local masking at the start, where do we stop"""
m_idx_min = m_block * self.tile_m
if const_expr(self.qhead_per_kvhead_packgqa > 1):
m_idx_min = m_idx_min // self.qhead_per_kvhead_packgqa
n_idx = m_idx_min + seqlen_info.seqlen_k - seqlen_info.seqlen_q
n_idx_right = (
n_idx
if const_expr(not self.is_local or self.window_size_right is None)
else n_idx + self.window_size_right
)
return cutlass.max(n_block_min, n_idx_right // self.tile_n)
@cute.jit
def get_n_block_min_before_local_mask(
self,
seqlen_info: SeqlenInfoQK,
m_block: Int32,
n_block_min: Int32,
) -> Int32:
"""If we have separate iterations with local masking at the end, where do we stop the non-masked iterations"""
if const_expr(not self.is_local or self.window_size_left is None):
return n_block_min
else:
m_idx_max = (m_block + 1) * self.tile_m
if const_expr(self.qhead_per_kvhead_packgqa > 1):
m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa)
n_idx = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
n_idx_left = n_idx - self.window_size_left
return cutlass.max(n_block_min, cute.ceil_div(n_idx_left, self.tile_n))
@cute.jit
def get_n_block_max_for_m_block(
self,
seqlen_info: SeqlenInfoQK,
m_block: Int32,
) -> Int32:
n_block_max = cute.ceil_div(seqlen_info.seqlen_k, self.tile_n)
if const_expr(self.is_causal or self.window_size_right is not None):
m_idx_max = (m_block + 1) * self.tile_m
if const_expr(self.qhead_per_kvhead_packgqa > 1):
m_idx_max = cute.ceil_div(m_idx_max, self.qhead_per_kvhead_packgqa)
n_idx_right = m_idx_max + seqlen_info.seqlen_k - seqlen_info.seqlen_q
if const_expr(self.window_size_right is not None):
n_idx_right += self.window_size_right
n_block_max = min(n_block_max, cute.ceil_div(n_idx_right, self.tile_n))
return n_block_max
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,722 @@
"""
Block-sparsity utilities for FlexAttention
"""
from typing import Callable, NamedTuple, Tuple
import cutlass.cute as cute
import torch
from sglang.jit_kernel.flash_attn.cute.cute_dsl_utils import (
get_broadcast_dims,
to_cute_tensor,
)
def ceildiv(a: int, b: int) -> int:
return (a + b - 1) // b
class BlockSparseTensors(NamedTuple):
mask_block_cnt: cute.Tensor
mask_block_idx: cute.Tensor
full_block_cnt: cute.Tensor | None = None
full_block_idx: cute.Tensor | None = None
cu_total_m_blocks: cute.Tensor | None = None
cu_block_idx_offsets: cute.Tensor | None = None
dq_write_order: cute.Tensor | None = None
dq_write_order_full: cute.Tensor | None = None
def __new_from_mlir_values__(self, values):
new_fields = []
idx = 0
for original in self:
if original is None:
new_fields.append(None)
else:
new_fields.append(values[idx])
idx += 1
return BlockSparseTensors(*new_fields)
class BlockSparseTensorsTorch(NamedTuple):
mask_block_cnt: torch.Tensor
mask_block_idx: torch.Tensor
full_block_cnt: torch.Tensor | None = None
full_block_idx: torch.Tensor | None = None
cu_total_m_blocks: torch.Tensor | None = None
cu_block_idx_offsets: torch.Tensor | None = None
block_size: tuple[int, int] | None = None
dq_write_order: torch.Tensor | None = None
dq_write_order_full: torch.Tensor | None = None
spt: bool | None = None
def _ordered_to_dense_simple(
num_blocks: torch.Tensor,
indices: torch.Tensor,
num_cols: int,
) -> torch.Tensor:
"""Convert ordered sparse representation to dense binary matrix.
Args:
num_blocks: [B, H, num_rows] count of valid entries per row
indices: [B, H, num_rows, max_entries] column indices (valid entries packed left)
num_cols: total number of columns
Returns:
dense: [B, H, num_rows, num_cols] binary int32 matrix
"""
B, H, num_rows, max_entries = indices.shape
device = indices.device
dense = torch.zeros(B, H, num_rows, num_cols + 1, dtype=torch.int32, device=device)
col_range = torch.arange(max_entries, device=device)
valid = col_range[None, None, None, :] < num_blocks[:, :, :, None]
safe_indices = torch.where(valid, indices.long(), num_cols)
row_idx = torch.arange(num_rows, device=device)[None, None, :, None].expand_as(
indices
)
b_idx = torch.arange(B, device=device)[:, None, None, None].expand_as(indices)
h_idx = torch.arange(H, device=device)[None, :, None, None].expand_as(indices)
dense[b_idx, h_idx, row_idx, safe_indices] = 1
return dense[:, :, :, :num_cols]
def compute_dq_write_order(
fwd_mask_cnt: torch.Tensor,
fwd_mask_idx: torch.Tensor,
fwd_full_cnt: torch.Tensor | None,
fwd_full_idx: torch.Tensor | None,
bwd_mask_cnt: torch.Tensor,
bwd_mask_idx: torch.Tensor,
bwd_full_cnt: torch.Tensor | None,
bwd_full_idx: torch.Tensor | None,
spt: bool = False,
) -> tuple[torch.Tensor, torch.Tensor | None]:
"""Compute dQ write-order metadata for deterministic block-sparse backward.
For each (n_block, i) in the backward iteration, computes the semaphore
lock value: the rank of n_block in the combined (partial + full) sorted
contributor list for the target m_block.
Lock values are assigned in ascending n_block order (or descending if spt=True)
to guarantee deadlock-freedom with the CTA scheduling order.
Args:
fwd_mask_cnt: [B, H, num_m_blocks] partial contributor counts per m_block
fwd_mask_idx: [B, H, num_m_blocks, max_kv] partial contributor n_block indices (ascending)
fwd_full_cnt: [B, H, num_m_blocks] full contributor counts per m_block (optional)
fwd_full_idx: [B, H, num_m_blocks, max_kv] full contributor n_block indices (optional)
bwd_mask_cnt: [B, H, num_n_blocks] partial iteration counts per n_block
bwd_mask_idx: [B, H, num_n_blocks, max_q] partial iteration m_block indices
bwd_full_cnt: [B, H, num_n_blocks] full iteration counts per n_block (optional)
bwd_full_idx: [B, H, num_n_blocks, max_q] full iteration m_block indices (optional)
spt: if True, reverse ordering (highest n_block gets lock_value=0)
Returns:
(dq_write_order, dq_write_order_full): tensors parallel to bwd_mask_idx
and bwd_full_idx respectively, containing lock values.
"""
device = fwd_mask_idx.device
B, H, num_m, max_kv_partial = fwd_mask_idx.shape
_, _, num_n, max_q_partial = bwd_mask_idx.shape
has_full = fwd_full_cnt is not None and fwd_full_idx is not None
dense_partial = _ordered_to_dense_simple(fwd_mask_cnt, fwd_mask_idx, num_n)
if has_full:
dense_full = _ordered_to_dense_simple(fwd_full_cnt, fwd_full_idx, num_n)
dense = (dense_partial + dense_full).clamp(max=1)
else:
dense = dense_partial
cumsum = dense.cumsum(dim=-1)
rank_table = (cumsum - dense).to(torch.int32)
if spt:
total_per_m = cumsum[:, :, :, -1:]
rank_table = (total_per_m - 1 - rank_table).to(torch.int32)
def _gather_write_order(bwd_idx, bwd_cnt):
b_i = torch.arange(B, device=device)[:, None, None, None].expand_as(bwd_idx)
h_i = torch.arange(H, device=device)[None, :, None, None].expand_as(bwd_idx)
n_i = torch.arange(bwd_idx.shape[2], device=device)[
None, None, :, None
].expand_as(bwd_idx)
m_vals = bwd_idx.long().clamp(0, num_m - 1)
return rank_table[b_i, h_i, m_vals, n_i].to(torch.int32)
dq_write_order = _gather_write_order(bwd_mask_idx, bwd_mask_cnt)
dq_write_order_full = None
if has_full and bwd_full_cnt is not None and bwd_full_idx is not None:
dq_write_order_full = _gather_write_order(bwd_full_idx, bwd_full_cnt)
return dq_write_order, dq_write_order_full
def compute_dq_write_order_from_block_mask(
block_mask,
spt: bool = False,
) -> tuple[torch.Tensor, torch.Tensor | None]:
(
_seq_q,
_seq_k,
kv_mask_cnt,
kv_mask_idx,
full_kv_cnt,
full_kv_idx,
q_mask_cnt,
q_mask_idx,
full_q_cnt,
full_q_idx,
*_,
) = block_mask.as_tuple()
return compute_dq_write_order(
kv_mask_cnt,
kv_mask_idx,
full_kv_cnt,
full_kv_idx,
q_mask_cnt,
q_mask_idx,
full_q_cnt,
full_q_idx,
spt=spt,
)
def get_sparse_q_block_size(
tensors: BlockSparseTensorsTorch | None,
seqlen_q: int,
) -> int | None:
"""Return the Q sparse block size, or None when sparsity is unset or ambiguous."""
if tensors is None:
return None
if tensors.block_size is not None:
return tensors.block_size[0]
num_m_blocks = tensors.mask_block_idx.shape[2]
min_block_size = ceildiv(seqlen_q, num_m_blocks)
max_block_size = (
seqlen_q if num_m_blocks == 1 else (seqlen_q - 1) // (num_m_blocks - 1)
)
if min_block_size != max_block_size:
return None
return min_block_size
def _expand_sparsity_tensor(
tensor: torch.Tensor,
expected_shape: Tuple[int, ...],
tensor_name: str,
context: str | None,
hint: str | Callable[[], str] | None,
) -> torch.Tensor:
"""Check if we need to expand the tensor to expected shape, and do so if possible."""
needs_expand = tensor.shape != expected_shape
if not needs_expand:
return tensor
can_expand = all(
map(lambda cur, tgt: cur == tgt or cur == 1, tensor.shape, expected_shape)
)
if not can_expand:
context_clause = f" ({context})" if context else ""
resolved_hint = hint() if callable(hint) else hint
hint_clause = f" Hint: {resolved_hint}" if resolved_hint else ""
raise ValueError(
f"{tensor_name}{context_clause} with shape {tensor.shape} cannot be expanded to expected shape {expected_shape}."
f"{hint_clause}"
)
return tensor.expand(*expected_shape)
def _check_and_expand_block(
name: str,
cnt: torch.Tensor | None,
idx: torch.Tensor | None,
expected_count_shape: Tuple[int, ...],
expected_index_shape: Tuple[int, ...],
context: str | None,
hint: str | Callable[[], str] | None,
) -> Tuple[torch.Tensor | None, torch.Tensor | None]:
if (cnt is None) != (idx is None):
raise ValueError(
f"{name}_block_cnt and {name}_block_idx must both be provided or both be None"
)
if cnt is None or idx is None:
return None, None
if cnt.dtype != torch.int32 or idx.dtype != torch.int32:
raise ValueError(f"{name}_block tensors must have dtype torch.int32")
if cnt.device != idx.device:
raise ValueError(
f"{name}_block_cnt and {name}_block_idx must be on the same device"
)
if not cnt.is_cuda or not idx.is_cuda:
raise ValueError(f"{name}_block tensors must live on CUDA")
expanded_cnt = _expand_sparsity_tensor(
cnt, expected_count_shape, f"{name}_block_cnt", context, hint
)
# [Note] Allow Compact block sparse indices
# Allow the last dimension (n_blocks) of idx to be <= expected, since
# FA4 only accesses indices 0..cnt-1 per query tile. This enables compact
# index tensors that avoid O(N^2) memory at long sequence lengths.
if idx.ndim == 4 and idx.shape[3] <= expected_index_shape[3]:
expected_index_shape = (*expected_index_shape[:3], idx.shape[3])
expanded_idx = _expand_sparsity_tensor(
idx, expected_index_shape, f"{name}_block_idx", context, hint
)
return expanded_cnt, expanded_idx
def _check_and_expand_metadata_tensor(
name: str,
tensor: torch.Tensor | None,
expected_shape: Tuple[int, ...],
context: str | None,
hint: str | Callable[[], str] | None,
device: torch.device,
) -> torch.Tensor | None:
if tensor is None:
return None
if tensor.dtype != torch.int32:
raise ValueError(f"{name} must have dtype torch.int32")
if tensor.device != device:
raise ValueError(f"{name} must be on the same device as block sparse tensors")
if not tensor.is_cuda:
raise ValueError(f"{name} must live on CUDA")
return _expand_sparsity_tensor(tensor, expected_shape, name, context, hint)
def get_block_sparse_expected_shapes(
batch_size: int,
num_head: int,
seqlen_q: int,
seqlen_k: int,
m_block_size: int,
n_block_size: int,
q_stage: int,
) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
"""Return (expected_count_shape, expected_index_shape) for block sparse normalization."""
m_block_size_effective = q_stage * m_block_size
expected_m_blocks = ceildiv(seqlen_q, m_block_size_effective)
expected_n_blocks = ceildiv(seqlen_k, n_block_size)
expected_count_shape = (batch_size, num_head, expected_m_blocks)
expected_index_shape = (batch_size, num_head, expected_m_blocks, expected_n_blocks)
return expected_count_shape, expected_index_shape
def infer_block_sparse_expected_shapes(
tensors: BlockSparseTensorsTorch,
*,
batch_size: int,
num_head: int,
seqlen_q: int,
seqlen_k: int,
m_block_size: int,
n_block_size: int,
q_stage: int,
context: str,
sparse_block_size_q: int | None = None,
sparse_block_size_kv: int | None = None,
) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int], int]:
"""Infer shapes and scaling for block-sparse tensors.
Expectations:
- mask_block_cnt is (B, H, M) and mask_block_idx is (B, H, M, N).
- Batch/head dims may be 1 for broadcast, or match the requested sizes.
- sparse_block_size_kv must match tile_n.
- sparse_block_size_q must be a multiple of q_stage * tile_m.
- If sparse_block_size_q is omitted and seqlen_q/num_m_blocks is ambiguous,
the caller must provide block_size to disambiguate. TODO will make this required in a future PR.
"""
base_m_block = q_stage * m_block_size
base_n_block = n_block_size
if sparse_block_size_kv is None:
sparse_block_size_kv = base_n_block
if sparse_block_size_kv != base_n_block:
raise ValueError(
f"Block sparse tensors{context} require BLOCK_SIZE_KV={base_n_block}."
)
if tensors.mask_block_idx is None:
raise ValueError(
"mask_block_cnt and mask_block_idx must be provided for block sparsity."
)
num_m_blocks = tensors.mask_block_idx.shape[2]
if sparse_block_size_q is None:
sparse_block_size_q = get_sparse_q_block_size(tensors, seqlen_q)
if sparse_block_size_q is None and base_m_block != 1:
raise ValueError(
f"Block sparse tensors{context} require explicit sparse_block_size[0] "
f"to disambiguate block size for seqlen_q={seqlen_q} and num_m_blocks={num_m_blocks}."
)
if sparse_block_size_q is None:
sparse_block_size_q = ceildiv(seqlen_q, num_m_blocks)
if sparse_block_size_q % base_m_block != 0:
raise ValueError(
f"Block sparse tensors{context} have block size {sparse_block_size_q}, "
f"which must be a multiple of {base_m_block}."
)
expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
expected_n_blocks = ceildiv(seqlen_k, sparse_block_size_kv)
q_subtile_factor = sparse_block_size_q // base_m_block
expected_count_shape = (batch_size, num_head, expected_m_blocks)
expected_index_shape = (batch_size, num_head, expected_m_blocks, expected_n_blocks)
mask_block_cnt = tensors.mask_block_cnt
mask_block_idx = tensors.mask_block_idx
if mask_block_cnt is None or mask_block_idx is None:
raise ValueError(
"mask_block_cnt and mask_block_idx must be provided for block sparsity."
)
if mask_block_cnt.ndim != 3 or mask_block_idx.ndim != 4:
raise ValueError(
f"Block sparse tensors{context} must have shapes (B, H, M) and (B, H, M, N)."
)
for dim_name, cur, tgt in (
("batch", mask_block_cnt.shape[0], expected_count_shape[0]),
("head", mask_block_cnt.shape[1], expected_count_shape[1]),
):
if cur != tgt and cur != 1:
raise ValueError(
f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1."
)
for dim_name, cur, tgt in (
("batch", mask_block_idx.shape[0], expected_index_shape[0]),
("head", mask_block_idx.shape[1], expected_index_shape[1]),
):
if cur != tgt and cur != 1:
raise ValueError(
f"Block sparse tensors{context} {dim_name} dim must be {tgt} or 1."
)
if mask_block_cnt.shape[2] != mask_block_idx.shape[2]:
raise ValueError(
f"Block sparse tensors{context} must share the same m-block dimension."
)
# [Note] Allow Compact block sparse indices: FA4 only accesses indices 0..cnt-1
# per query tile, so idx.shape[3] can be <= expected_n_blocks.
if mask_block_idx.shape[3] > expected_n_blocks:
raise ValueError(
f"Block sparse tensors{context} n-block dimension must be <= {expected_n_blocks}."
)
if expected_m_blocks != num_m_blocks:
raise ValueError(
f"Block sparse tensors{context} m-block dimension {num_m_blocks} does not match "
f"sparse_block_size_q={sparse_block_size_q}. "
f"Set BlockSparseTensorsTorch.block_size to match the BlockMask BLOCK_SIZE."
)
return expected_count_shape, expected_index_shape, q_subtile_factor
def get_block_sparse_expected_shapes_bwd(
batch_size: int,
num_head: int,
seqlen_q: int,
seqlen_k: int,
m_block_size: int,
n_block_size: int,
subtile_factor: int,
) -> Tuple[Tuple[int, int, int], Tuple[int, int, int, int]]:
"""Return (expected_count_shape, expected_index_shape) for backward block sparse normalization.
Backward uses Q-direction indexing (transposed from forward), where shapes are
indexed by N-blocks first, then M-blocks. The sparse_block_size_q is determined
by subtile_factor * m_block_size.
"""
sparse_block_size_q = subtile_factor * m_block_size
expected_m_blocks = ceildiv(seqlen_q, sparse_block_size_q)
expected_n_blocks = ceildiv(seqlen_k, n_block_size)
expected_count_shape = (batch_size, num_head, expected_n_blocks)
expected_index_shape = (batch_size, num_head, expected_n_blocks, expected_m_blocks)
return expected_count_shape, expected_index_shape
def normalize_block_sparse_tensors(
tensors: BlockSparseTensorsTorch,
*,
expected_count_shape: Tuple[int, ...],
expected_index_shape: Tuple[int, ...],
context: str | None = None,
hint: str | Callable[[], str] | None = None,
) -> BlockSparseTensorsTorch:
if tensors.mask_block_cnt is None or tensors.mask_block_idx is None:
raise ValueError(
"mask_block_cnt and mask_block_idx must be provided for block sparsity."
)
mask_cnt, mask_idx = _check_and_expand_block(
"mask",
tensors.mask_block_cnt,
tensors.mask_block_idx,
expected_count_shape,
expected_index_shape,
context,
hint,
)
if mask_cnt is None or mask_idx is None:
raise ValueError(
"mask_block_cnt and mask_block_idx must be provided for block sparsity."
)
full_cnt, full_idx = _check_and_expand_block(
"full",
tensors.full_block_cnt,
tensors.full_block_idx,
expected_count_shape,
expected_index_shape,
context,
hint,
)
if full_cnt is not None and mask_cnt.device != full_cnt.device:
raise ValueError("All block sparse tensors must be on the same device")
dq_write_order = _check_and_expand_metadata_tensor(
"dq_write_order",
tensors.dq_write_order,
tuple(mask_idx.shape),
context,
hint,
mask_cnt.device,
)
dq_write_order_full = _check_and_expand_metadata_tensor(
"dq_write_order_full",
tensors.dq_write_order_full,
tuple(full_idx.shape) if full_idx is not None else expected_index_shape,
context,
hint,
mask_cnt.device,
)
spt = tensors.spt
if spt is not None and not isinstance(spt, bool):
raise ValueError("spt must be a bool when provided")
if spt is not None and dq_write_order is None:
raise ValueError("spt requires dq_write_order to be provided")
return BlockSparseTensorsTorch(
mask_block_cnt=mask_cnt,
mask_block_idx=mask_idx,
full_block_cnt=full_cnt,
full_block_idx=full_idx,
cu_total_m_blocks=tensors.cu_total_m_blocks,
cu_block_idx_offsets=tensors.cu_block_idx_offsets,
block_size=tensors.block_size,
dq_write_order=dq_write_order,
dq_write_order_full=dq_write_order_full,
spt=spt,
)
def is_block_sparsity_enabled(tensors: BlockSparseTensorsTorch) -> bool:
return any(t is not None for t in (tensors.full_block_cnt, tensors.mask_block_cnt))
def get_block_sparse_broadcast_pattern(
tensors: BlockSparseTensorsTorch,
) -> Tuple[Tuple[bool, ...], ...] | None:
"""Return broadcast pattern for block sparse tensors by checking actual strides.
Returns a tuple of broadcast patterns (one per tensor) where each pattern
is a tuple of bools indicating which dims have stride=0.
This is used in compile keys to ensure kernels are recompiled when
broadcast patterns change, since CuTe's mark_layout_dynamic() keeps
stride=0 as static.
The tensors should already be expanded/normalized before calling this function.
Returns None if block sparsity is not enabled.
"""
if not is_block_sparsity_enabled(tensors):
return None
patterns = []
for tensor in (
tensors.mask_block_cnt,
tensors.mask_block_idx,
tensors.full_block_cnt,
tensors.full_block_idx,
tensors.dq_write_order,
tensors.dq_write_order_full,
):
if tensor is not None:
patterns.append(get_broadcast_dims(tensor))
else:
patterns.append(None)
return tuple(patterns)
def normalize_block_sparse_config(
tensors: BlockSparseTensorsTorch,
*,
batch_size: int,
num_head: int,
seqlen_q: int,
seqlen_k: int,
block_size: tuple[int, int],
q_stage: int,
) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None, int]:
"""Validate the block-sparse config, infer expected shapes, and normalize.
Handles both fixed-length (3D `[B, H, M]` / 4D `[B, H, M, N]`) and varlen
(2D `[H, total_m_blocks]` / `[H, total_n_blocks]`) layouts. Varlen is
detected by `tensors.cu_total_m_blocks is not None` and forces
`q_subtile_factor == 1` (TODO: potentially remove this restriction).
"""
m_block_size, n_block_size = block_size
if tensors.block_size is None:
sparse_block_size_q, sparse_block_size_kv = None, n_block_size
else:
sparse_block_size_q, sparse_block_size_kv = tensors.block_size
if sparse_block_size_kv != n_block_size:
raise ValueError(
f"Block sparsity requires sparse_block_size[1]={n_block_size} to match tile_n."
)
if tensors.cu_total_m_blocks is not None:
base_m_block = q_stage * m_block_size
if sparse_block_size_q is not None and sparse_block_size_q != base_m_block:
raise ValueError(
f"Varlen block sparsity requires sparse_block_size[0]={base_m_block} "
f"(= q_stage * tile_m); got {sparse_block_size_q}."
)
total_m_blocks = tensors.mask_block_cnt.shape[-1]
total_n_blocks = tensors.mask_block_idx.shape[-1]
expected_count_shape = (num_head, total_m_blocks)
expected_index_shape = (num_head, total_n_blocks)
q_subtile_factor = 1
else:
expected_count_shape, expected_index_shape, q_subtile_factor = (
infer_block_sparse_expected_shapes(
tensors,
batch_size=batch_size,
num_head=num_head,
seqlen_q=seqlen_q,
seqlen_k=seqlen_k,
m_block_size=m_block_size,
n_block_size=n_block_size,
q_stage=q_stage,
context="forward",
sparse_block_size_q=sparse_block_size_q,
sparse_block_size_kv=sparse_block_size_kv,
)
)
normalized_tensors = normalize_block_sparse_tensors(
tensors,
expected_count_shape=expected_count_shape,
expected_index_shape=expected_index_shape,
)
return (
normalized_tensors,
get_block_sparse_broadcast_pattern(normalized_tensors),
q_subtile_factor,
)
def normalize_block_sparse_config_bwd(
tensors: BlockSparseTensorsTorch,
*,
batch_size: int,
num_head: int,
seqlen_q: int,
seqlen_k: int,
block_size: tuple[int, int],
subtile_factor: int,
) -> tuple[BlockSparseTensorsTorch, Tuple[Tuple[bool, ...], ...] | None]:
m_block_size, n_block_size = block_size
if tensors.block_size is None:
sparse_block_size_q, sparse_block_size_kv = (
subtile_factor * m_block_size,
n_block_size,
)
else:
sparse_block_size_q, sparse_block_size_kv = tensors.block_size
if sparse_block_size_q != subtile_factor * m_block_size:
raise ValueError(
f"Block sparsity expects sparse_block_size_q={subtile_factor * m_block_size} "
f"for subtile_factor={subtile_factor}."
)
if sparse_block_size_kv != n_block_size:
raise ValueError(
f"Block sparsity expects sparse_block_size[1]={n_block_size} to match tile_n."
)
expected_count_shape, expected_index_shape = get_block_sparse_expected_shapes_bwd(
batch_size,
num_head,
seqlen_q,
seqlen_k,
m_block_size,
n_block_size,
subtile_factor,
)
normalized_tensors = normalize_block_sparse_tensors(
tensors,
expected_count_shape=expected_count_shape,
expected_index_shape=expected_index_shape,
context="_flash_attn_bwd",
hint=lambda: (
f"Backward expects Q-direction block-sparse tensors (q_mask_cnt/q_mask_idx, "
f"and optionally full_q_cnt/full_q_idx). Regenerate the backward BlockMask with "
f"BLOCK_SIZE=({subtile_factor * m_block_size}, {n_block_size})."
),
)
return normalized_tensors, get_block_sparse_broadcast_pattern(normalized_tensors)
def to_cute_block_sparse_tensors(
tensors: BlockSparseTensorsTorch, enable_tvm_ffi: bool = True
) -> BlockSparseTensors | None:
"""Convert torch block sparsity tensors to CuTe tensors, optionally for tvm ffi"""
if not is_block_sparsity_enabled(tensors):
return None
mask_block_cnt_tensor, mask_block_idx_tensor = [
to_cute_tensor(
t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi
)
for t in (tensors.mask_block_cnt, tensors.mask_block_idx)
]
full_block_cnt_tensor, full_block_idx_tensor = [
(
to_cute_tensor(
t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi
)
if t is not None
else None
)
for t in (tensors.full_block_cnt, tensors.full_block_idx)
]
cu_total_m_blocks_tensor, cu_block_idx_offsets_tensor = [
(
to_cute_tensor(
t, assumed_align=4, leading_dim=0, enable_tvm_ffi=enable_tvm_ffi
)
if t is not None
else None
)
for t in (tensors.cu_total_m_blocks, tensors.cu_block_idx_offsets)
]
dq_write_order_tensor, dq_write_order_full_tensor = [
(
to_cute_tensor(
t, assumed_align=4, leading_dim=-1, enable_tvm_ffi=enable_tvm_ffi
)
if t is not None
else None
)
for t in (tensors.dq_write_order, tensors.dq_write_order_full)
]
return BlockSparseTensors(
mask_block_cnt_tensor,
mask_block_idx_tensor,
full_block_cnt_tensor,
full_block_idx_tensor,
cu_total_m_blocks_tensor,
cu_block_idx_offsets_tensor,
dq_write_order_tensor,
dq_write_order_full_tensor,
)
def fast_sampling(mask_mod):
"""Convenience decorator to mark mask_mod as safe for 5-point fast sampling"""
mask_mod.use_fast_sampling = True
return mask_mod
@@ -0,0 +1,289 @@
# Manage Ahead-of-Time (AOT) compiled kernels
import ctypes
import fcntl
import hashlib
import os
import pickle
import sys
import tempfile
import time
from functools import lru_cache
from getpass import getuser
from pathlib import Path
from typing import Hashable, TypeAlias
import cutlass
import cutlass.cute as cute
import tvm_ffi
from cutlass.cutlass_dsl import JitCompiledFunction
from sglang.jit_kernel.flash_attn.cute.fa_logging import fa_log
# Pre-load cute DSL runtime libraries with RTLD_GLOBAL so that their symbols
# (e.g. _cudaLibraryLoadData) are visible to .so modules loaded later via dlopen.
# Upstream cute.runtime.load_module loads these without RTLD_GLOBAL, which causes
# "undefined symbol" errors when loading cached kernels from disk.
for _lib_path in cute.runtime.find_runtime_libraries(enable_tvm_ffi=False):
if Path(_lib_path).exists():
ctypes.CDLL(_lib_path, mode=ctypes.RTLD_GLOBAL)
CompileKeyType: TypeAlias = tuple[Hashable, ...]
CallableFunction: TypeAlias = JitCompiledFunction | tvm_ffi.Function
# Enable cache via `FLASH_ATTENTION_CUTE_DSL_CACHE_ENABLED=1`
CUTE_DSL_CACHE_ENABLED: bool = (
os.getenv("FLASH_ATTENTION_CUTE_DSL_CACHE_ENABLED", "0") == "1"
)
# Customize cache dir via `FLASH_ATTENTION_CUTE_DSL_CACHE_DIR`, default is
# `/tmp/${USER}/flash_attention_cute_dsl_cache``
CUTE_DSL_CACHE_DIR: str | None = os.getenv("FLASH_ATTENTION_CUTE_DSL_CACHE_DIR", None)
def get_cache_path() -> Path:
if CUTE_DSL_CACHE_DIR is not None:
cache_dir = Path(CUTE_DSL_CACHE_DIR)
else:
cache_dir = (
Path(tempfile.gettempdir()) / getuser() / "flash_attention_cute_dsl_cache"
)
cache_dir.mkdir(parents=True, exist_ok=True)
return cache_dir
@lru_cache(maxsize=1)
def _compute_source_fingerprint() -> str:
"""
Hash all CuTe Python sources plus runtime ABI stamps into a short fingerprint.
The fingerprint changes whenever:
- Any .py file under flash_attn/cute is added, removed, renamed, or modified.
- The Python minor version changes (e.g. 3.13 -> 3.14).
- The cutlass or tvm_ffi package version changes.
Computed once per process and cached.
"""
cute_root = Path(__file__).resolve().parent
h = hashlib.sha256()
h.update(f"py{sys.version_info.major}.{sys.version_info.minor}".encode())
h.update(f"cutlass={cutlass.__version__}".encode())
h.update(f"tvm_ffi={tvm_ffi.__version__}".encode())
for src in sorted(cute_root.rglob("*.py")):
if not src.is_file():
continue
h.update(src.relative_to(cute_root).as_posix().encode())
content = src.read_bytes()
h.update(len(content).to_bytes(8, "little"))
h.update(content)
return h.hexdigest()
class FileLock:
"""Context manager for advisory file locks using fcntl.flock.
Supports exclusive (write) and shared (read) locks.
Always blocks with polling until the lock is acquired or timeout is reached.
Usage:
with FileLock(lock_path, exclusive=True, timeout=15, label="abc"):
# do work under lock
"""
def __init__(
self,
lock_path: Path,
exclusive: bool,
timeout: float = 15,
label: str = "",
):
"""
Args:
lock_path: Path to the lock file on disk.
exclusive: True for exclusive (write) lock, False for shared (read) lock.
timeout: Max seconds to wait for lock acquisition before raising RuntimeError.
label: Optional human-readable label for error messages.
"""
self.lock_path: Path = lock_path
self.exclusive: bool = exclusive
self.timeout: float = timeout
self.label: str = label
self._fd: int = -1
@property
def _lock_label(self) -> str:
kind = "exclusive" if self.exclusive else "shared"
return f"{kind} {self.label}" if self.label else kind
def __enter__(self) -> "FileLock":
open_flags = (
os.O_WRONLY | os.O_CREAT if self.exclusive else os.O_RDONLY | os.O_CREAT
)
lock_type = fcntl.LOCK_EX if self.exclusive else fcntl.LOCK_SH
self._fd = os.open(str(self.lock_path), open_flags)
deadline = time.monotonic() + self.timeout
acquired = False
while time.monotonic() < deadline:
try:
fcntl.flock(self._fd, lock_type | fcntl.LOCK_NB)
acquired = True
break
except OSError:
time.sleep(0.1)
if not acquired:
os.close(self._fd)
self._fd = None
raise RuntimeError(
f"Timed out after {self.timeout}s waiting for "
f"{self._lock_label} lock: {self.lock_path}"
)
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
if self._fd is not None:
fcntl.flock(self._fd, fcntl.LOCK_UN)
os.close(self._fd)
self._fd = None
class JITCache:
"""
In-memory cache for compiled functions.
"""
def __init__(self):
self.cache: dict[CompileKeyType, CallableFunction] = {}
def __setitem__(self, key: CompileKeyType, fn: JitCompiledFunction) -> None:
self.cache[key] = fn
def __getitem__(self, key: CompileKeyType) -> CallableFunction:
return self.cache[key]
def __contains__(self, key: CompileKeyType) -> bool:
return key in self.cache
def clear(self) -> None:
"""
Clear in-memory cache of compiled functions
"""
self.cache.clear()
class JITPersistentCache(JITCache):
"""
In-memory cache for compiled functions, which is also backed by persistent storage.
Use cutedsl ahead-of-time (AOT) compilation, only supporting enable_tvm_ffi=True
"""
EXPORT_FUNCTION_PREFIX = "func"
LOCK_TIMEOUT_SECONDS = 15
def __init__(self, cache_path: Path):
super().__init__()
cache_path.mkdir(parents=True, exist_ok=True)
self.cache_path: Path = cache_path
def __setitem__(self, key: CompileKeyType, fn: JitCompiledFunction) -> None:
JITCache.__setitem__(self, key, fn)
self._try_export_to_storage(key, fn)
def __getitem__(self, key: CompileKeyType) -> CallableFunction:
# Use __contains__ to try populating in-memory cache with persistent storage
self.__contains__(key)
return JITCache.__getitem__(self, key)
def __contains__(self, key: CompileKeyType) -> bool:
# Checks in-memory cache first, then tries loading from storage.
# When returning True, guarantees the in-memory cache is populated.
if JITCache.__contains__(self, key):
return True
return self._try_load_from_storage(key)
def _try_load_from_storage(self, key: CompileKeyType) -> bool:
"""
Try to load a function from persistent storage into in-memory cache.
Returns True if loaded successfully, False if not found on disk.
Holds a shared lock during loading to prevent concurrent writes.
"""
sha256_hex = self._key_to_hash(key)
obj_path = self.cache_path / f"{sha256_hex}.o"
with FileLock(
self._lock_path(sha256_hex),
exclusive=False,
timeout=self.LOCK_TIMEOUT_SECONDS,
label=sha256_hex,
):
if obj_path.exists():
fa_log(1, f"Loading compiled function from disk: {obj_path}")
m = cute.runtime.load_module(str(obj_path), enable_tvm_ffi=True)
fn = getattr(m, self.EXPORT_FUNCTION_PREFIX)
JITCache.__setitem__(self, key, fn)
return True
else:
fa_log(1, f"Cache miss on disk for key hash {sha256_hex}")
return False
def _try_export_to_storage(
self, key: CompileKeyType, fn: JitCompiledFunction
) -> None:
"""Export a compiled function to persistent storage under exclusive lock."""
sha256_hex = self._key_to_hash(key)
with FileLock(
self._lock_path(sha256_hex),
exclusive=True,
timeout=self.LOCK_TIMEOUT_SECONDS,
label=sha256_hex,
):
obj_path = self.cache_path / f"{sha256_hex}.o"
if obj_path.exists():
# Another process already exported.
fa_log(1, f"Skipping export, already on disk: {obj_path}")
return
fa_log(1, f"Exporting compiled function to disk: {obj_path}")
fn.export_to_c(
object_file_path=str(obj_path),
function_name=self.EXPORT_FUNCTION_PREFIX,
)
fa_log(1, f"Successfully exported compiled function to disk: {obj_path}")
def _key_to_hash(self, key: CompileKeyType) -> str:
return hashlib.sha256(pickle.dumps(key)).hexdigest()
def _lock_path(self, sha256_hex: str) -> Path:
return self.cache_path / f"{sha256_hex}.lock"
def clear(self) -> None:
"""
Not only clear the in-memory cache. Also purge persistent compilation cache.
"""
fa_log(1, f"Clearing persistent cache at {self.cache_path}")
super().clear()
for child in self.cache_path.iterdir():
child.unlink()
def get_jit_cache(name: str | None = None) -> JITCache:
"""
JIT cache factory.
`name` is an optional identifier to create subdirectories to manage cache.
When persistent caching is enabled, artifacts are namespaced under a
source fingerprint directory so that code or dependency changes
automatically invalidate stale entries.
"""
if CUTE_DSL_CACHE_ENABLED:
path = get_cache_path() / _compute_source_fingerprint()
if name:
path = path / name
fa_log(1, f"Creating persistent JIT cache at {path}")
return JITPersistentCache(path)
else:
fa_log(1, "Persistent cache disabled, using in-memory JIT cache")
return JITCache()
@@ -0,0 +1,591 @@
from functools import partial
from typing import Callable, Optional, Tuple
import cutlass
import cutlass.cute as cute
import torch
from cutlass import Boolean, Int8, Int32, const_expr
from sglang.jit_kernel.flash_attn.cute.block_sparse_utils import (
get_curr_blocksparse_tensors,
)
from sglang.jit_kernel.flash_attn.cute.block_sparsity import (
BlockSparseTensors,
BlockSparseTensorsTorch,
to_cute_block_sparse_tensors,
)
from sglang.jit_kernel.flash_attn.cute.cute_dsl_utils import (
get_aux_tensor_metadata,
to_cute_aux_tensor,
to_cute_tensor,
)
from sglang.jit_kernel.flash_attn.cute.mask import call_mask_mod
from sglang.jit_kernel.flash_attn.cute.seqlen_info import SeqlenInfoQK
from sglang.jit_kernel.flash_attn.cute.testing import is_fake_mode
from sglang.jit_kernel.flash_attn.cute.utils import (
AuxData,
get_batch_from_cu_tensor,
hash_callable,
scalar_to_ssa,
ssa_to_scalar,
)
class BlockSparsityKernel:
"""Block sparsity kernel for FlexAttention.
This kernel computes `mask_mod` for every token of each block
to determine if an n block is full, masked, or neither.
Writes block counts and indices to a BlockSparseTensors object.
When use_fast_sampling=True, uses 5-point sampling (4 corners + center)
which is much faster but only suitable for masks where this is sufficient.
TODO:
- optimize mask_mod evaluation
- transposed tensors for bwd pass
"""
def __init__(
self,
mask_mod: Callable,
tile_mn: Tuple[int, int],
compute_full_blocks: bool = True,
use_aux_tensors: bool = False,
use_fast_sampling: bool = False,
):
self.mask_mod = mask_mod
self.tile_mn = tile_mn
self.compute_full_blocks = compute_full_blocks
self.use_aux_tensors = use_aux_tensors
self.use_fast_sampling = use_fast_sampling
@cute.jit
def __call__(
self,
blocksparse_tensors: BlockSparseTensors,
seqlen_q: Int32,
seqlen_k: Int32,
mCuSeqlensQ: Optional[cute.Tensor] = None,
mCuSeqlensK: Optional[cute.Tensor] = None,
mSeqUsedQ: Optional[cute.Tensor] = None,
mSeqUsedK: Optional[cute.Tensor] = None,
aux_data: AuxData = AuxData(),
):
(
mask_cnt,
mask_idx,
full_cnt,
full_idx,
mCuTotalMBlocks,
mCuBlockIdxOffsets,
*_,
) = blocksparse_tensors
self.is_varlen_q = const_expr(mCuSeqlensQ is not None)
if const_expr(self.compute_full_blocks):
assert (
full_cnt is not None and full_idx is not None
), "full block tensors must be provided when computing full blocks"
if const_expr(not self.is_varlen_q):
batch_size, num_heads, num_m_blocks, _ = mask_idx.shape
total_m_blocks = batch_size * num_m_blocks
else:
assert const_expr(
mCuTotalMBlocks is not None
), "mCuTotalMBlocks must be provided when varlen q"
num_heads, total_m_blocks = mask_cnt.shape # num_m_blocks is total_m_blocks
batch_size = mCuSeqlensQ.shape[0] - 1
if const_expr(self.use_fast_sampling):
num_threads = 5
self.num_warps = 1
else:
num_threads = self.tile_mn[0]
self.num_warps = (num_threads + 32 - 1) // 32
if const_expr(not self.is_varlen_q):
grid = [num_m_blocks, num_heads, batch_size]
else:
grid = [total_m_blocks, num_heads, 1]
self.kernel(
blocksparse_tensors,
seqlen_q,
seqlen_k,
batch_size,
mCuSeqlensQ,
mCuSeqlensK,
mSeqUsedQ,
mSeqUsedK,
mCuTotalMBlocks,
mCuBlockIdxOffsets,
aux_data,
).launch(grid=grid, block=[num_threads, 1, 1])
@cute.kernel
def kernel(
self,
blocksparse_tensors: BlockSparseTensors,
seqlen_q: Int32,
seqlen_k: Int32,
batch_size: Int32,
mCuSeqlensQ: Optional[cute.Tensor] = None,
mCuSeqlensK: Optional[cute.Tensor] = None,
mSeqUsedQ: Optional[cute.Tensor] = None,
mSeqUsedK: Optional[cute.Tensor] = None,
mCuTotalMBlocks: Optional[cute.Tensor] = None,
mCuBlockIdxOffsets: Optional[cute.Tensor] = None,
aux_data: AuxData = AuxData(),
):
tidx, _, _ = cute.arch.thread_idx()
warp_idx = cute.arch.warp_idx()
lane_id = cute.arch.lane_idx()
ssa = partial(scalar_to_ssa, dtype=Int32)
@cute.struct
class SharedStorage:
reduction_buffer_smem: cute.struct.Align[
cute.struct.MemRange[cutlass.Int8, 2 * self.num_warps], 1024
]
smem = cutlass.utils.SmemAllocator()
storage = smem.allocate(SharedStorage, 16)
reduction_buffer = storage.reduction_buffer_smem.get_tensor(
cute.make_layout((self.num_warps, 2))
)
SeqlenInfoCls = partial(
SeqlenInfoQK.create,
seqlen_q_static=seqlen_q,
seqlen_k_static=seqlen_k,
mCuSeqlensQ=mCuSeqlensQ,
mCuSeqlensK=mCuSeqlensK,
mSeqUsedQ=mSeqUsedQ,
mSeqUsedK=mSeqUsedK,
mCuTotalMBlocks=mCuTotalMBlocks,
mCuBlockIdxOffsets=mCuBlockIdxOffsets,
tile_m=self.tile_mn[0],
tile_n=self.tile_mn[1],
)
if const_expr(not self.is_varlen_q):
m_block, head_idx, batch_idx = cute.arch.block_idx()
else:
global_m_block, head_idx, _ = cute.arch.block_idx()
batch_idx = get_batch_from_cu_tensor(global_m_block, mCuTotalMBlocks)
m_block = global_m_block - mCuTotalMBlocks[batch_idx]
seqlen = SeqlenInfoCls(batch_idx)
seqlen_q = seqlen.seqlen_q
seqlen_k = seqlen.seqlen_k
global_m_block = seqlen.m_block_offset + m_block
num_n_blocks = (seqlen_k + self.tile_mn[1] - 1) // self.tile_mn[1]
_, curr_mask_idx, _, curr_full_idx = get_curr_blocksparse_tensors(
batch_idx, head_idx, m_block, blocksparse_tensors, seqlen
)
num_mask_blocks = Int32(0)
num_full_blocks = Int32(0)
m_base = m_block * self.tile_mn[0]
if const_expr(self.use_fast_sampling):
# Loop-invariant per-thread q_idx for the 5 sample points
# (tidx 0, 1: top corners; 2, 3: bottom corners; 4: center).
q_idx_sample = m_base
if tidx == 2 or tidx == 3:
q_idx_sample = cutlass.min(m_base + self.tile_mn[0] - 1, seqlen_q - 1)
elif tidx == 4:
q_idx_sample = (
m_base + cutlass.min(seqlen_q - m_base, self.tile_mn[0]) // 2
)
else:
q_idx_thread = m_base + tidx
thread_in_bounds = Boolean(
tidx < self.tile_mn[0] and q_idx_thread < seqlen_q
)
for n_block in cutlass.range(num_n_blocks):
n_base = n_block * self.tile_mn[1]
if const_expr(self.use_fast_sampling):
# 5-point sampling (4 corners + center). Interior n_blocks
# (n_base + tile_n <= seqlen_k) skip the OOB clamp on the right /
# center samples.
is_interior = (n_base + self.tile_mn[1]) <= seqlen_k
n_right = Int32(0)
n_mid = Int32(0)
if is_interior:
n_right = n_base + self.tile_mn[1] - 1
n_mid = n_base + self.tile_mn[1] // 2
else:
n_right = cutlass.min(n_base + self.tile_mn[1] - 1, seqlen_k - 1)
n_mid = (
n_base + cutlass.min(seqlen_k - n_base, self.tile_mn[1]) // 2
)
kv_idx = n_base
if tidx == 1 or tidx == 3:
kv_idx = n_right
elif tidx == 4:
kv_idx = n_mid
thread_result = Boolean(False)
thread_is_valid = Boolean(False)
if tidx < 5:
thread_is_valid = Boolean(True)
thread_result = ssa_to_scalar(
call_mask_mod(
self.mask_mod,
ssa(batch_idx),
ssa(head_idx),
ssa(q_idx_sample),
ssa(kv_idx),
seqlen,
aux_data,
)
)
has_unmasked = cute.arch.vote_any_sync(thread_result & thread_is_valid)
has_masked = cute.arch.vote_any_sync(
Boolean(not thread_result) & thread_is_valid
)
else:
# Full path. Interior blocks (n_base + tile_n <= seqlen_k) drop the
# per-element bound check; the boundary block (at most one) keeps it.
thread_has_unmasked = Boolean(False)
thread_has_masked = Boolean(False)
kv_idx = Int32(0)
is_interior = (n_base + self.tile_mn[1]) <= seqlen_k
if is_interior:
if thread_in_bounds:
for c in cutlass.range(self.tile_mn[1], unroll_full=True):
mask_val = ssa_to_scalar(
call_mask_mod(
self.mask_mod,
ssa(batch_idx),
ssa(head_idx),
ssa(q_idx_thread),
ssa(n_base + c),
seqlen,
aux_data,
)
)
thread_has_unmasked |= Boolean(mask_val)
thread_has_masked |= Boolean(not mask_val)
else:
if thread_in_bounds:
for c in cutlass.range(self.tile_mn[1], unroll_full=True):
kv_idx = n_base + c
if kv_idx < seqlen_k:
mask_val = ssa_to_scalar(
call_mask_mod(
self.mask_mod,
ssa(batch_idx),
ssa(head_idx),
ssa(q_idx_thread),
ssa(kv_idx),
seqlen,
aux_data,
)
)
thread_has_unmasked |= Boolean(mask_val)
thread_has_masked |= Boolean(not mask_val)
warp_unmasked = cute.arch.vote_any_sync(
thread_has_unmasked & thread_in_bounds
)
warp_masked = cute.arch.vote_any_sync(
thread_has_masked & thread_in_bounds
)
if lane_id == 0:
reduction_buffer[warp_idx, 0] = (
Int8(1) if warp_unmasked else Int8(0)
)
reduction_buffer[warp_idx, 1] = Int8(1) if warp_masked else Int8(0)
cute.arch.sync_threads()
# Cross-warp OR via warp 0; thread 0 (lane 0 of warp 0) holds the result.
has_unmasked = Boolean(False)
has_masked = Boolean(False)
if warp_idx == 0:
lane_unmasked = Boolean(False)
lane_masked = Boolean(False)
if lane_id < self.num_warps:
lane_unmasked = reduction_buffer[lane_id, 0] != Int8(0)
lane_masked = reduction_buffer[lane_id, 1] != Int8(0)
has_unmasked = cute.arch.vote_any_sync(lane_unmasked)
has_masked = cute.arch.vote_any_sync(lane_masked)
# Only thread 0 updates the output arrays (common to both paths)
if tidx == 0:
# Block classification based on what we found:
# - If has_masked and has_unmasked: partial block (needs masking)
# - If only has_unmasked: full block (no masking needed)
# - If only has_masked: skip this block entirely
is_partial = Boolean(has_masked and has_unmasked)
is_full = Boolean(has_unmasked and (not has_masked))
if is_partial:
curr_mask_idx[num_mask_blocks] = n_block
num_mask_blocks += 1
elif is_full and const_expr(self.compute_full_blocks):
curr_full_idx[num_full_blocks] = n_block
num_full_blocks += 1
# Only thread 0 writes back the counts
if tidx == 0:
mask_cnt, _, full_cnt, *_ = blocksparse_tensors
if const_expr(self.is_varlen_q):
mask_cnt[head_idx, global_m_block] = num_mask_blocks
if const_expr(self.compute_full_blocks):
full_cnt[head_idx, global_m_block] = num_full_blocks
else:
mask_cnt[batch_idx, head_idx, m_block] = num_mask_blocks
if const_expr(self.compute_full_blocks):
full_cnt[batch_idx, head_idx, m_block] = num_full_blocks
def compute_block_sparsity(
tile_m,
tile_n,
batch_size,
num_heads,
seqlen_q,
seqlen_k,
mask_mod: Callable,
aux_tensors: Optional[list],
device,
aux_scalars: Optional[tuple] = None,
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
cu_total_m_blocks: Optional[torch.Tensor] = None,
cu_block_idx_offsets: Optional[torch.Tensor] = None,
compute_full_blocks: bool = True,
use_fast_sampling: bool = False,
) -> BlockSparseTensorsTorch:
"""
Computes block sparsity for a given `mask_mod`.
Args:
tile_m: The tile size for the m dimension.
tile_n: The tile size for the n dimension.
batch_size: The batch size.
num_heads: The number of heads.
seqlen_q: The sequence length for the query.
seqlen_k: The sequence length for the key.
mask_mod: The `mask_mod` callable to use.
aux_tensors: A list of auxiliary tensors.
device: The device to use.
cu_seqlens_q: Cumulative q sequence lengths for varlen
cu_seqlens_k: Cumulative k sequence lengths for varlen
seqused_q: Per-batch effective q sequence lengths
seqused_k: Per-batch effective k sequence lengths
cu_total_m_blocks: Cumulative total m blocks tensor for varlen q
cu_block_idx_offsets: Cumulative offsets into the packed mask_block_idx /
full_block_idx tensors per batch (== cumsum of M_b * N_b).
compute_full_blocks: Whether to compute full blocks. If False, only partially-masked blocks are computed.
use_fast_sampling: Whether to use 5-point sampling (4 corners + center). This is much faster, but only suitable for masks where this check is sufficient.
Returns:
BlockSparseTensorsTorch
"""
aux_scalars = tuple(aux_scalars) if aux_scalars else None
# Check if mask_mod is marked as suitable for 5-point sampling
use_fast_sampling = getattr(mask_mod, "use_fast_sampling", use_fast_sampling)
num_m_blocks = (seqlen_q + tile_m - 1) // tile_m
num_n_blocks = (seqlen_k + tile_n - 1) // tile_n
if cu_seqlens_q is not None:
assert (
cu_total_m_blocks is not None
), "total m blocks must be provided when varlen q"
total_m_blocks = cu_total_m_blocks[-1].item()
if cu_block_idx_offsets is None and (
cu_seqlens_k is not None or seqused_k is not None
):
# Derive cu_block_idx_offsets from per-batch K seqlens.
cu_block_idx_offsets_list = [0]
for batch_idx in range(batch_size):
batch_seqlen_q = (
cu_seqlens_q[batch_idx + 1].item() - cu_seqlens_q[batch_idx].item()
)
if cu_seqlens_k is not None:
batch_seqlen_k = (
cu_seqlens_k[batch_idx + 1].item()
- cu_seqlens_k[batch_idx].item()
)
else:
batch_seqlen_k = seqused_k[batch_idx].item()
num_m_blocks_batch = (batch_seqlen_q + tile_m - 1) // tile_m
num_n_blocks_batch = (batch_seqlen_k + tile_n - 1) // tile_n
cu_block_idx_offsets_list.append(
cu_block_idx_offsets_list[-1]
+ num_m_blocks_batch * num_n_blocks_batch
)
cu_block_idx_offsets = torch.tensor(
cu_block_idx_offsets_list, dtype=torch.int32, device=device
)
if cu_block_idx_offsets is not None:
total_n_blocks = cu_block_idx_offsets[-1].item()
else:
# Uniform-K varlen-Q: every batch has the same K seqlen.
total_n_blocks = total_m_blocks * num_n_blocks
mask_block_cnt = torch.zeros(
(num_heads, total_m_blocks), device=device, dtype=torch.int32
)
mask_block_idx = torch.zeros(
(num_heads, total_n_blocks), device=device, dtype=torch.int32
)
full_block_cnt = (
torch.zeros((num_heads, total_m_blocks), device=device, dtype=torch.int32)
if compute_full_blocks
else None
)
full_block_idx = (
torch.zeros((num_heads, total_n_blocks), device=device, dtype=torch.int32)
if compute_full_blocks
else None
)
else:
total_m_blocks = batch_size * num_m_blocks
total_n_blocks = batch_size * num_m_blocks * num_n_blocks
mask_block_cnt = torch.zeros(
(batch_size, num_heads, num_m_blocks), device=device, dtype=torch.int32
)
mask_block_idx = torch.zeros(
(batch_size, num_heads, num_m_blocks, num_n_blocks),
device=device,
dtype=torch.int32,
)
full_block_cnt = (
torch.zeros(
(batch_size, num_heads, num_m_blocks), device=device, dtype=torch.int32
)
if compute_full_blocks
else None
)
full_block_idx = (
torch.zeros(
(batch_size, num_heads, num_m_blocks, num_n_blocks),
device=device,
dtype=torch.int32,
)
if compute_full_blocks
else None
)
blocksparse_tensors_torch = BlockSparseTensorsTorch(
mask_block_cnt=mask_block_cnt,
mask_block_idx=mask_block_idx,
full_block_cnt=full_block_cnt,
full_block_idx=full_block_idx,
cu_total_m_blocks=cu_total_m_blocks,
cu_block_idx_offsets=cu_block_idx_offsets,
block_size=(tile_m, tile_n),
)
mask_mod_hash = hash_callable(mask_mod)
if aux_tensors is not None:
aux_tensor_metadata = get_aux_tensor_metadata(aux_tensors)
else:
aux_tensor_metadata = None
aux_scalar_metadata = (
tuple(type(s) for s in aux_scalars) if aux_scalars is not None else None
)
compile_key = (
tile_m,
tile_n,
mask_mod_hash,
aux_tensor_metadata,
aux_scalar_metadata,
compute_full_blocks,
cu_seqlens_q is None,
cu_seqlens_k is None,
seqused_q is None,
seqused_k is None,
aux_tensors is not None,
use_fast_sampling,
)
if compile_key not in compute_block_sparsity.compile_cache:
(
cu_seqlens_q_tensor,
cu_seqlens_k_tensor,
seqused_q_tensor,
seqused_k_tensor,
) = [
to_cute_tensor(t, assumed_align=4, leading_dim=0) if t is not None else None
for t in (
cu_seqlens_q,
cu_seqlens_k,
seqused_q,
seqused_k,
)
]
blocksparse_tensors = to_cute_block_sparse_tensors(
blocksparse_tensors_torch, enable_tvm_ffi=True
)
if aux_tensors is not None:
cute_aux_tensors = [to_cute_aux_tensor(buf) for buf in aux_tensors]
else:
cute_aux_tensors = None
kernel = BlockSparsityKernel(
mask_mod,
tile_mn=(tile_m, tile_n),
compute_full_blocks=compute_full_blocks,
use_aux_tensors=aux_tensors is not None,
use_fast_sampling=use_fast_sampling,
)
compute_block_sparsity.compile_cache[compile_key] = cute.compile(
kernel,
blocksparse_tensors,
seqlen_q,
seqlen_k,
cu_seqlens_q_tensor,
cu_seqlens_k_tensor,
seqused_q_tensor,
seqused_k_tensor,
AuxData(cute_aux_tensors, aux_scalars),
options="--enable-tvm-ffi",
)
if not is_fake_mode():
compute_block_sparsity.compile_cache[compile_key](
(
blocksparse_tensors_torch.mask_block_cnt,
blocksparse_tensors_torch.mask_block_idx,
blocksparse_tensors_torch.full_block_cnt,
blocksparse_tensors_torch.full_block_idx,
blocksparse_tensors_torch.cu_total_m_blocks,
blocksparse_tensors_torch.cu_block_idx_offsets,
blocksparse_tensors_torch.dq_write_order,
blocksparse_tensors_torch.dq_write_order_full,
),
seqlen_q,
seqlen_k,
cu_seqlens_q,
cu_seqlens_k,
seqused_q,
seqused_k,
AuxData(aux_tensors, aux_scalars),
)
return blocksparse_tensors_torch
compute_block_sparsity.compile_cache = {}
@@ -0,0 +1,402 @@
# Copyright (c) 2025, Wentao Guo, Ted Zadouri, Tri Dao.
import math
from typing import Callable, Optional, Type
import cutlass
import cutlass.cute as cute
import cutlass.pipeline
import cutlass.utils.blackwell_helpers as sm100_utils
from cutlass import Float32, Int32, const_expr
from cutlass._mlir.dialects import llvm
from cutlass.cute.nvgpu import cpasync
from cutlass.cutlass_dsl import T, dsl_user_op
@dsl_user_op
def cvt_copy(
atom: cute.CopyAtom,
src: cute.Tensor,
dst: cute.Tensor,
*,
pred: Optional[cute.Tensor] = None,
loc=None,
ip=None,
**kwargs,
) -> None:
assert (
isinstance(src.iterator, cute.Pointer)
and src.memspace == cute.AddressSpace.rmem
)
if const_expr(src.element_type != dst.element_type):
src_cvt = cute.make_fragment_like(src, dst.element_type, loc=loc, ip=ip)
src_cvt.store(src.load().to(dst.element_type))
src = src_cvt
cute.copy(atom, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
@dsl_user_op
def load_s2r(src: cute.Tensor, *, loc=None, ip=None) -> cute.Tensor:
dst = cute.make_fragment_like(src, src.element_type, loc=loc, ip=ip)
cute.autovec_copy(src, dst, loc=loc, ip=ip)
return dst
@dsl_user_op
def get_copy_atom(
dtype: Type[cutlass.Numeric],
num_copy_elems: int,
is_async: bool = False,
*,
loc=None,
ip=None,
) -> cute.CopyAtom:
num_copy_bits = const_expr(min(128, num_copy_elems * dtype.width))
copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
return cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
@dsl_user_op
def make_tmem_copy(
tmem_copy_atom: cute.CopyAtom, num_wg: int = 1, *, loc=None, ip=None
) -> cute.CopyAtom:
num_dp, num_bits, num_rep, _ = sm100_utils.get_tmem_copy_properties(tmem_copy_atom)
assert num_dp == 32
assert num_bits == 32
tiler_mn = (cute.make_layout((128 * num_rep * num_wg // 32, 32), stride=(32, 1)),)
layout_tv = cute.make_layout(
((32, 4, num_wg), (num_rep, 32)),
stride=((0, 1, 4 * num_rep), (4, 4 * num_rep * num_wg)),
)
return cute.make_tiled_copy(tmem_copy_atom, layout_tv, tiler_mn)
@dsl_user_op
def copy(
src: cute.Tensor,
dst: cute.Tensor,
*,
pred: Optional[cute.Tensor] = None,
num_copy_elems: int = 1,
is_async: bool = False,
loc=None,
ip=None,
**kwargs,
) -> None:
copy_atom = get_copy_atom(src.element_type, num_copy_elems, is_async)
cute.copy(copy_atom, src, dst, pred=pred, loc=loc, ip=ip, **kwargs)
def tiled_copy_1d(
dtype: Type[cutlass.Numeric],
num_threads: int,
num_copy_elems: int = 1,
is_async: bool = False,
) -> cute.TiledCopy:
num_copy_bits = num_copy_elems * dtype.width
copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
thr_layout = cute.make_layout(num_threads)
val_layout = cute.make_layout(num_copy_elems)
return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
def tiled_copy_2d(
dtype: Type[cutlass.Numeric],
major_mode_size: int,
num_threads: int,
is_async: bool = False,
) -> cute.TiledCopy:
num_copy_bits = math.gcd(major_mode_size, 128 // dtype.width) * dtype.width
copy_elems = num_copy_bits // dtype.width
copy_op = cpasync.CopyG2SOp() if is_async else cute.nvgpu.CopyUniversalOp()
copy_atom = cute.make_copy_atom(copy_op, dtype, num_bits_per_copy=num_copy_bits)
gmem_threads_per_row = major_mode_size // copy_elems
assert num_threads % gmem_threads_per_row == 0
thr_layout = cute.make_ordered_layout(
(num_threads // gmem_threads_per_row, gmem_threads_per_row),
order=(1, 0),
)
val_layout = cute.make_layout((1, copy_elems))
return cute.make_tiled_copy_tv(copy_atom, thr_layout, val_layout)
@dsl_user_op
def atomic_add_fp32x4(
a: Float32,
b: Float32,
c: Float32,
d: Float32,
gmem_ptr: cute.Pointer,
*,
loc=None,
ip=None,
) -> None:
gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
# cache_hint = cutlass.Int64(0x12F0000000000000)
llvm.inline_asm(
None,
[
gmem_ptr_i64,
Float32(a).ir_value(loc=loc, ip=ip),
Float32(b).ir_value(loc=loc, ip=ip),
Float32(c).ir_value(loc=loc, ip=ip),
Float32(d).ir_value(loc=loc, ip=ip),
],
# [gmem_ptr_i64, Float32(a).ir_value(loc=loc, ip=ip), cache_hint.ir_value()],
"{\n\t"
# ".reg .b128 abcd;\n\t"
# "mov.b128 abcd, {$1, $2, $3, $4};\n\t"
".reg .v4 .f32 abcd;\n\t"
# "mov.b128 abcd, {$1, $2, $3, $4};\n\t"
"mov.f32 abcd.x, $1;\n\t"
"mov.f32 abcd.y, $2;\n\t"
"mov.f32 abcd.z, $3;\n\t"
"mov.f32 abcd.w, $4;\n\t"
"red.global.add.v4.f32 [$0], abcd;\n\t"
# "red.global.add.L2::cache_hint.v4.f32 [$0], abcd, 0x14F0000000000000;\n\t"
"}\n",
# "red.global.add.L2::cache_hint.f32 [$0], $1, 0x12F0000000000000;",
# "red.global.add.L2::cache_hint.f32 [$0], $1, $2;",
"l,f,f,f,f",
# "l,f,l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@dsl_user_op
def set_block_rank(
smem_ptr: cute.Pointer, peer_cta_rank_in_cluster: Int32, *, loc=None, ip=None
) -> Int32:
"""Map the given smem pointer to the address at another CTA rank in the cluster."""
smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
return Int32(
llvm.inline_asm(
T.i32(),
[smem_ptr_i32, peer_cta_rank_in_cluster.ir_value()],
"mapa.shared::cluster.u32 $0, $1, $2;",
"=r,r,r",
has_side_effects=False,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
)
@dsl_user_op
def store_shared_remote_fp32x4(
a: Float32,
b: Float32,
c: Float32,
d: Float32,
smem_ptr: cute.Pointer,
mbar_ptr: cute.Pointer,
peer_cta_rank_in_cluster: Int32,
*,
loc=None,
ip=None,
) -> None:
remote_smem_ptr_i32 = set_block_rank(
smem_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
).ir_value()
remote_mbar_ptr_i32 = set_block_rank(
mbar_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
).ir_value()
llvm.inline_asm(
None,
[
remote_smem_ptr_i32,
remote_mbar_ptr_i32,
Float32(a).ir_value(loc=loc, ip=ip),
Float32(b).ir_value(loc=loc, ip=ip),
Float32(c).ir_value(loc=loc, ip=ip),
Float32(d).ir_value(loc=loc, ip=ip),
],
"{\n\t"
".reg .v4 .f32 abcd;\n\t"
"mov.f32 abcd.x, $2;\n\t"
"mov.f32 abcd.y, $3;\n\t"
"mov.f32 abcd.z, $4;\n\t"
"mov.f32 abcd.w, $5;\n\t"
"st.async.shared::cluster.mbarrier::complete_tx::bytes.v4.f32 [$0], abcd, [$1];\n\t"
"}\n",
"r,r,f,f,f,f",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@dsl_user_op
def cpasync_bulk_s2cluster(
smem_src_ptr: cute.Pointer,
smem_dst_ptr: cute.Pointer,
mbar_ptr: cute.Pointer,
size: int | Int32,
peer_cta_rank_in_cluster: Int32,
*,
loc=None,
ip=None,
):
smem_src_ptr_i32 = smem_src_ptr.toint(loc=loc, ip=ip).ir_value()
smem_dst_ptr_i32 = set_block_rank(
smem_dst_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
).ir_value()
mbar_ptr_i32 = set_block_rank(
mbar_ptr, peer_cta_rank_in_cluster, loc=loc, ip=ip
).ir_value()
llvm.inline_asm(
None,
[
smem_dst_ptr_i32,
smem_src_ptr_i32,
mbar_ptr_i32,
Int32(size).ir_value(loc=loc, ip=ip),
],
"cp.async.bulk.shared::cluster.shared::cta.mbarrier::complete_tx::bytes [$0], [$1], $3, [$2];",
"r,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@dsl_user_op
def cpasync_bulk_g2s(
gmem_ptr: cute.Pointer,
smem_ptr: cute.Pointer,
tma_bar_ptr: cute.Pointer,
size: int | Int32,
*,
loc=None,
ip=None,
):
gmem_ptr_i64 = gmem_ptr.toint(loc=loc, ip=ip).ir_value()
smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
mbar_ptr_i32 = tma_bar_ptr.toint(loc=loc, ip=ip).ir_value()
llvm.inline_asm(
None,
[gmem_ptr_i64, smem_ptr_i32, mbar_ptr_i32, Int32(size).ir_value()],
"cp.async.bulk.shared::cta.global.mbarrier::complete_tx::bytes [$1], [$0], $3, [$2];",
"l,r,r,r",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
@dsl_user_op
def cpasync_reduce_bulk_add_f32(
smem_ptr: cute.Pointer,
gmem_ptr: cute.Pointer,
store_bytes: int | Int32,
*,
loc=None,
ip=None,
):
smem_ptr_i32 = smem_ptr.toint(loc=loc, ip=ip).ir_value()
# cache_hint = cutlass.Int64(0x14F0000000000000) # EVICT_LAST
llvm.inline_asm(
None,
[gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value()],
"cp.reduce.async.bulk.global.shared::cta.bulk_group.add.f32 [$0], [$1], $2;",
"l,r,r",
# [gmem_ptr.llvm_ptr, smem_ptr_i32, Int32(store_bytes).ir_value(), cache_hint.ir_value()],
# "cp.reduce.async.bulk.global.shared::cta.bulk_group.L2::cache_hint.add.f32 [$0], [$1], $2, $3;",
# "l,r,r,l",
has_side_effects=True,
is_align_stack=False,
asm_dialect=llvm.AsmDialect.AD_ATT,
)
def cpasync_bulk_get_copy_fn(
src_tensor: cute.Tensor,
dst_tensor: cute.Tensor,
single_stage: bool = False,
**kwargs,
) -> Callable:
# src_is_smem = const_expr(
# isinstance(src_tensor.iterator, cute.Pointer)
# and src_tensor.memspace == cute.AddressSpace.smem
# )
group_rank_src = const_expr(cute.rank(src_tensor) - (1 if not single_stage else 0))
group_rank_dst = const_expr(cute.rank(dst_tensor) - (1 if not single_stage else 0))
# ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
src = cute.group_modes(src_tensor, 0, group_rank_src)
dst = cute.group_modes(dst_tensor, 0, group_rank_dst)
def copy_bulk(src_idx, dst_idx, **new_kwargs):
size = const_expr(cute.size(src.shape[:-1]) * src.element_type.width // 8)
cpasync_bulk_g2s(
src[None, src_idx].iterator,
dst[None, dst_idx].iterator,
size=size,
**new_kwargs,
**kwargs,
)
def copy_bulk_single_stage(**new_kwargs):
size = const_expr(cute.size(src.shape) * src.element_type.width // 8)
cpasync_bulk_g2s(src.iterator, dst.iterator, size=size, **new_kwargs, **kwargs)
return copy_bulk if const_expr(not single_stage) else copy_bulk_single_stage
def tma_get_copy_fn(
atom: cute.CopyAtom,
cta_coord: cute.Coord,
cta_layout: cute.Layout,
src_tensor: cute.Tensor,
dst_tensor: cute.Tensor,
filter_zeros: bool = False,
single_stage: bool = False,
**kwargs,
) -> Callable:
src_is_smem = const_expr(
isinstance(src_tensor.iterator, cute.Pointer)
and src_tensor.memspace == cute.AddressSpace.smem
)
smem_tensor, gmem_tensor = (
(src_tensor, dst_tensor) if src_is_smem else (dst_tensor, src_tensor)
)
group_rank_smem = const_expr(
cute.rank(smem_tensor) - (1 if not single_stage else 0)
)
group_rank_gmem = const_expr(
cute.rank(gmem_tensor) - (1 if not single_stage else 0)
)
# ((atom_v, rest_v), STAGE), ((atom_v, rest_v), RestK)
s, g = cpasync.tma_partition(
atom,
cta_coord,
cta_layout,
cute.group_modes(smem_tensor, 0, group_rank_smem),
cute.group_modes(gmem_tensor, 0, group_rank_gmem),
)
if const_expr(filter_zeros):
s = cute.filter_zeros(s)
g = cute.filter_zeros(g)
src, dst = (s, g) if src_is_smem else (g, s)
def copy_tma(src_idx, dst_idx, **new_kwargs):
cute.copy(atom, src[None, src_idx], dst[None, dst_idx], **new_kwargs, **kwargs)
def copy_tma_single_stage(**new_kwargs):
cute.copy(atom, src, dst, **new_kwargs, **kwargs)
return (copy_tma if const_expr(not single_stage) else copy_tma_single_stage), s, g
def tma_producer_copy_fn(copy: Callable, pipeline: cutlass.pipeline.PipelineAsync):
def copy_fn(src_idx, producer_state: cutlass.pipeline.PipelineState, **new_kwargs):
copy(
src_idx=src_idx,
dst_idx=producer_state.index,
tma_bar_ptr=pipeline.producer_get_barrier(producer_state),
**new_kwargs,
)
return copy_fn
@@ -0,0 +1,123 @@
from typing import Callable
import cuda.bindings.driver as cuda
import cutlass
import cutlass.cute as cute
from cutlass import Int32, const_expr
class CuSeqlensToBlocksKernel:
"""Single-CTA prep for block-packed shear scheduling: computes the cumulative
per-batch group-block counts and the block -> batch index map in one launch."""
def __init__(
self,
tile: int = 128,
num_threads: int = 1024,
seqlen_multiple: int = 1,
use_pdl: bool = False,
):
self.tile = tile
self.num_threads = num_threads
assert num_threads % 32 == 0
self.num_warps = num_threads // cute.arch.WARP_SIZE
self.seqlen_multiple = seqlen_multiple
self.use_pdl = use_pdl
@cute.jit
def __call__(
self,
mCuBlocks: cute.Tensor,
mCuSeqlens: cute.Tensor,
mBlocksToBatchIdx: cute.Tensor,
# Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
stream: cuda.CUstream = None,
):
@cute.struct
class SharedStorage:
warp_block_count: cute.struct.MemRange[Int32, self.num_warps]
cu_blocks: cute.struct.MemRange[Int32, self.num_threads + 1]
self.kernel(
mCuBlocks,
mCuSeqlens,
mBlocksToBatchIdx,
SharedStorage,
).launch(
grid=[1, 1, 1],
block=[self.num_threads, 1, 1],
stream=stream,
use_pdl=self.use_pdl,
)
@cute.kernel
def kernel(
self,
mCuBlocks: cute.Tensor,
mCuSeqlens: cute.Tensor,
mBlocksToBatchIdx: cute.Tensor,
SharedStorage: cutlass.Constexpr[Callable],
):
if const_expr(self.use_pdl):
cute.arch.griddepcontrol_wait()
cute.arch.griddepcontrol_launch_dependents()
batch_size = mCuBlocks.shape[0] - 1
batch_idx = cute.arch.thread_idx()[0]
lane_idx = cute.arch.lane_idx()
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
smem = cutlass.utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
warp_block_count = storage.warp_block_count.get_tensor(
cute.make_layout(self.num_warps)
)
sCuBlocks = storage.cu_blocks.get_tensor(cute.make_layout(self.num_threads + 1))
if batch_idx == 0:
mCuBlocks[0] = 0
sCuBlocks[0] = 0
seqlen = Int32(0)
if batch_idx < batch_size:
seqlen = mCuSeqlens[batch_idx + 1] - mCuSeqlens[batch_idx]
seqlen *= self.seqlen_multiple
num_blocks = (seqlen + self.tile - 1) // self.tile
total_blocks_for_batch = num_blocks
for delta in (1, 2, 4, 8, 16):
other = cute.arch.shuffle_sync_up(
total_blocks_for_batch, delta, mask_and_clamp=0
)
if lane_idx >= delta:
total_blocks_for_batch += other
if lane_idx == 31:
warp_block_count[warp_idx] = total_blocks_for_batch
cute.arch.sync_threads()
if warp_idx * 32 < batch_size:
for idx in cutlass.range(warp_idx):
total_blocks_for_batch += warp_block_count[idx]
if batch_idx < batch_size:
mCuBlocks[batch_idx + 1] = total_blocks_for_batch
sCuBlocks[batch_idx + 1] = total_blocks_for_batch
cute.arch.sync_threads()
total_blocks = sCuBlocks[batch_size]
num_iters = (total_blocks + self.num_threads - 1) // self.num_threads
for it in cutlass.range(num_iters, unroll=1):
block = it * self.num_threads + batch_idx
if block < total_blocks:
lo = Int32(0)
hi = Int32(batch_size)
while lo < hi:
mid = (lo + hi) // 2
if sCuBlocks[mid + 1] <= block:
lo = mid + 1
else:
hi = mid
mBlocksToBatchIdx[block] = lo
@@ -0,0 +1,159 @@
"""
System ptxas replacement for CUTLASS DSL.
Environment variables:
CUTE_DSL_PTXAS_PATH - Path to ptxas (e.g., /usr/local/cuda/bin/ptxas)
CUTE_DSL_PTXAS_VERBOSE - Set to 1 for verbose output
"""
import ctypes
import os
import re
import subprocess
import sys
from pathlib import Path
import cutlass
CUTE_DSL_PTXAS_PATH = os.environ.get("CUTE_DSL_PTXAS_PATH", None)
VERBOSE = os.environ.get("CUTE_DSL_PTXAS_VERBOSE", "0") == "1"
_original_load_cuda_library = None
_user_wanted_ptx = False # True if user originally set CUTE_DSL_KEEP_PTX=1
def _log(msg):
if VERBOSE:
print(f"[ptxas] {msg}", file=sys.stderr)
def _get_ptx(compiled_func) -> tuple[str, Path] | None:
"""Find and read PTX file, stripping null bytes."""
func_name = getattr(compiled_func, "function_name", None)
if not func_name:
return None
dump_dir = os.environ.get("CUTE_DSL_DUMP_DIR", Path.cwd())
for ptx_path in Path(dump_dir).glob(f"*{func_name}*.ptx"):
content = ptx_path.read_text().rstrip("\x00")
if ".entry " in content and content.rstrip().endswith("}"):
_log(f"Found PTX: {ptx_path}")
return content, ptx_path
return None
def _compile_ptx(ptx_path: Path, ptx_content: str) -> bytes:
"""Compile PTX to cubin using system ptxas."""
# Extract arch from PTX
match = re.search(r"\.target\s+(sm_\d+[a-z]?)", ptx_content)
arch = match.group(1) if match else "sm_90a"
# Write stripped content back if needed
if ptx_path.read_text() != ptx_content:
ptx_path.write_text(ptx_content)
# Compile
cubin_tmp = ptx_path.with_suffix(".cubin.tmp")
try:
assert CUTE_DSL_PTXAS_PATH is not None
result = subprocess.run(
[
CUTE_DSL_PTXAS_PATH,
f"-arch={arch}",
"-O3",
"-o",
str(cubin_tmp),
str(ptx_path),
],
capture_output=True,
text=True,
)
if result.returncode != 0:
raise RuntimeError(f"ptxas failed: {result.stderr}")
cubin_data = cubin_tmp.read_bytes()
_log(f"Compiled {ptx_path.name} -> {len(cubin_data)} bytes ({arch})")
# Save cubin if CUTE_DSL_KEEP_CUBIN is set
if os.environ.get("CUTE_DSL_KEEP_CUBIN", "0") == "1":
cubin_out = ptx_path.with_suffix(".cubin")
cubin_out.write_bytes(cubin_data)
_log(f"Saved: {cubin_out}")
return cubin_data
finally:
cubin_tmp.unlink(missing_ok=True)
def _patched_load_cuda_library(self):
"""Replacement for _load_cuda_library that uses system ptxas."""
result = _get_ptx(self)
if not result:
_log("PTX not found, falling back to embedded ptxas")
return _original_load_cuda_library(self)
ptx_content, ptx_path = result
try:
cubin = _compile_ptx(ptx_path, ptx_content)
except Exception as e:
_log(f"Compilation failed ({e}), falling back to embedded ptxas")
return _original_load_cuda_library(self)
# Load cubin
import cuda.bindings.runtime as cuda_runtime
err, library = cuda_runtime.cudaLibraryLoadData(cubin, None, None, 0, None, None, 0)
if err != cuda_runtime.cudaError_t.cudaSuccess:
_log(f"cudaLibraryLoadData failed ({err}), falling back to embedded ptxas")
return _original_load_cuda_library(self)
# Register kernels on all devices
_, cuda_load_to_device = self._get_cuda_init_and_load()
lib_ptr = ctypes.c_void_p(int(library))
dev_id = ctypes.c_int32(0)
err_val = ctypes.c_int32(0)
args = (ctypes.c_void_p * 3)(
ctypes.cast(ctypes.pointer(lib_ptr), ctypes.c_void_p),
ctypes.cast(ctypes.pointer(dev_id), ctypes.c_void_p),
ctypes.cast(ctypes.pointer(err_val), ctypes.c_void_p),
)
for dev in range(self.num_devices):
dev_id.value = dev
cuda_load_to_device(args)
if err_val.value != 0:
_log("cuda_load_to_device failed, falling back to embedded ptxas")
return _original_load_cuda_library(self)
_log(f"Loaded kernel from {ptx_path.name}")
# Delete PTX if user didn't originally want it kept
if not _user_wanted_ptx:
ptx_path.unlink(missing_ok=True)
return [cuda_runtime.cudaLibrary_t(lib_ptr.value)]
def patch():
"""Install system ptxas hook. Call before importing cutlass."""
global _original_load_cuda_library, _user_wanted_ptx
assert CUTE_DSL_PTXAS_PATH is not None
if not os.path.isfile(CUTE_DSL_PTXAS_PATH) or not os.access(
CUTE_DSL_PTXAS_PATH, os.X_OK
):
raise RuntimeError(f"ptxas not found: {CUTE_DSL_PTXAS_PATH}")
# Track if user originally wanted PTX kept
_user_wanted_ptx = os.environ.get("CUTE_DSL_KEEP_PTX", "0") == "1"
# os.environ['CUTE_DSL_KEEP_PTX'] = '1'
assert (
os.environ.get("CUTE_DSL_KEEP_PTX", "0") == "1"
), "Require CUTE_DSL_KEEP_PTX=1 to use system's ptxas"
cls = cutlass.cutlass_dsl.cuda_jit_executor.CudaDialectJitCompiledFunction
_original_load_cuda_library = cls._load_cuda_library
cls._load_cuda_library = _patched_load_cuda_library
_log("Patch applied")
return
@@ -0,0 +1,176 @@
# Copyright (c) 2025, Tri Dao.
from functools import lru_cache
from typing import Tuple
import torch
try:
from triton.tools.disasm import extract
except ImportError:
extract = None
import cutlass
import cutlass.cute as cute
from cutlass.cute.runtime import from_dlpack
from cutlass.cutlass_dsl import NumericMeta
StaticTypes = (cutlass.Constexpr, NumericMeta, int, bool, str, float, type(None))
load_cubin_module_data_og = cutlass.base_dsl.runtime.cuda.load_cubin_module_data
cute_compile_og = cute.compile
torch2cute_dtype_map = {
torch.float16: cutlass.Float16,
torch.bfloat16: cutlass.BFloat16,
torch.float32: cutlass.Float32,
torch.float8_e4m3fn: cutlass.Float8E4M3FN,
torch.float8_e5m2: cutlass.Float8E5M2,
}
@lru_cache
def get_max_active_clusters(cluster_size):
return cutlass.utils.HardwareInfo().get_max_active_clusters(
cluster_size=cluster_size
)
@lru_cache
def get_device_capacity(device: torch.device = None) -> Tuple[int, int]:
return torch.cuda.get_device_capability(device)
def assume_strides_aligned(t, align=16):
"""Assume all strides except the last are divisible by `align` bytes (128
bits by default; 4 bytes for the packed UE8M0 scale-factor tensors).
Python int strides (e.g., stride=0 from GQA expand) are kept as-is
since they're static and don't need alignment assumptions.
"""
divby = (align * 8) // t.element_type.width
strides = tuple(
s if isinstance(s, int) else cute.assume(s, divby=divby) for s in t.stride[:-1]
)
return (*strides, t.stride[-1])
def assume_tensor_aligned(t, align=16):
"""Rebuild a tensor with aligned stride assumptions. Passes through None."""
if t is None:
return None
return cute.make_tensor(
t.iterator,
cute.make_layout(t.shape, stride=assume_strides_aligned(t, align=align)),
)
def to_cute_tensor(
t, assumed_align=16, leading_dim=-1, fully_dynamic=False, enable_tvm_ffi=True
):
"""Convert torch tensor to cute tensor for TVM FFI. leading_dim=-1 defaults to t.ndim-1."""
if t is None:
return None
# NOTE: torch 2.9.1 doesn't support fp8 via DLPack but 2.11.0 nightly does
# currently export raw bytes as uint8 and tell cutlass correct type
# can directly export as fp8 when torch supports it
if t.dtype in (torch.float8_e4m3fn, torch.float8_e5m2):
tensor = from_dlpack(
t.view(torch.uint8).detach(),
assumed_align=assumed_align,
enable_tvm_ffi=enable_tvm_ffi,
)
tensor.element_type = (
cutlass.Float8E4M3FN
if t.dtype == torch.float8_e4m3fn
else cutlass.Float8E5M2
)
else:
tensor = from_dlpack(
t.detach(), assumed_align=assumed_align, enable_tvm_ffi=enable_tvm_ffi
)
if fully_dynamic:
return tensor.mark_layout_dynamic()
if leading_dim == -1:
leading_dim = t.ndim - 1
return tensor.mark_layout_dynamic(leading_dim=leading_dim)
def to_cute_aux_tensor(t, enable_tvm_ffi=True):
"""Convert torch tensor to cute tensor for TVM FFI, tailored to FlexAttention aux tensors.
This allows the user to specify alignment and leading dimension for aux tensors used in
custom score_mod callables.
"""
assumed_align: int = getattr(t, "__assumed_align__", None)
leading_dim: int = getattr(t, "__leading_dim__", None)
fully_dynamic: bool = leading_dim is None
return to_cute_tensor(
t,
assumed_align=assumed_align,
leading_dim=leading_dim,
fully_dynamic=fully_dynamic,
enable_tvm_ffi=enable_tvm_ffi,
)
def get_aux_tensor_metadata(aux_tensors):
return tuple(
(
getattr(t, "__assumed_align__", 0),
getattr(t, "__leading_dim__", -1),
hasattr(t, "__leading_dim__"),
)
for t in aux_tensors
)
def get_broadcast_dims(tensor: torch.Tensor) -> Tuple[bool, ...]:
"""Return tuple of bools indicating which dims have stride=0 (broadcast).
This is useful for compile keys since CuTe's mark_layout_dynamic() keeps
stride=0 as static, meaning kernels compiled with different broadcast
patterns are not interchangeable.
"""
return tuple(s == 0 for s in tensor.stride())
# credit: monellz (https://github.com/NVIDIA/cutlass/issues/2658#issuecomment-3630564264)
def dump_kernel_attributes(compiled_kernel):
import torch
from cuda.bindings import driver
from cutlass.utils import HardwareInfo
device_id = torch.cuda.current_device()
hardware_info = HardwareInfo(device_id=device_id)
cubin_data = compiled_kernel.artifacts.CUBIN
assert (
cubin_data is not None
), "cubin_data is None, need '--keep-cubin' option when compiling"
cuda_library = hardware_info._checkCudaErrors(
driver.cuLibraryLoadData(cubin_data, None, None, 0, None, None, 0)
)
kernels = hardware_info._checkCudaErrors(
driver.cuLibraryEnumerateKernels(1, cuda_library)
)
kernel = hardware_info._checkCudaErrors(driver.cuKernelGetFunction(kernels[0]))
# more metrics: https://docs.nvidia.com/cuda/cuda-driver-api/group__CUDA__EXEC.html#group__CUDA__EXEC_1g5e92a1b0d8d1b82cb00dcfb2de15961b
local_size_bytes = hardware_info._checkCudaErrors(
driver.cuFuncGetAttribute(
driver.CUfunction_attribute.CU_FUNC_ATTRIBUTE_LOCAL_SIZE_BYTES,
kernel,
)
)
num_regs = hardware_info._checkCudaErrors(
driver.cuFuncGetAttribute(
driver.CUfunction_attribute.CU_FUNC_ATTRIBUTE_NUM_REGS,
kernel,
)
)
print("--- Kernel Info ---")
print(f"local_size_bytes: {local_size_bytes}")
print(f"num_regs: {num_regs}")
print("--- End Kernel Info ---")
@@ -0,0 +1,97 @@
# Copyright (c) 2025, Tri Dao.
"""Unified FlashAttention logging controlled by a single ``FA_LOG_LEVEL`` env var.
Host-side messages go through Python ``logging`` (logger name ``flash_attn``).
A default ``StreamHandler`` is attached automatically when ``FA_LOG_LEVEL >= 1``
so that standalone scripts get output without extra setup; applications that
configure their own logging can remove or replace it via the standard API.
FA_LOG_LEVEL mapping::
0 off nothing logged
1 host host-side summaries only (no kernel printf)
2 kernel host + curated kernel traces
3 max host + all kernel traces (noisy, perf hit)
Set via environment variable::
FA_LOG_LEVEL=1 python train.py
Device-side ``cute.printf`` calls are compile-time eliminated via
``cutlass.const_expr`` when the log level is below the callsite threshold,
so there is zero performance cost when device logging is off.
Changing the log level after kernel compilation requires a recompile
(the level participates in the forward compile key).
"""
import logging
import os
import sys
import cutlass.cute as cute
from cutlass import const_expr
_LOG_LEVEL_NAMES = {"off": 0, "host": 1, "kernel": 2, "max": 3}
def _parse_log_level(raw: str) -> int:
if raw in _LOG_LEVEL_NAMES:
return _LOG_LEVEL_NAMES[raw]
try:
level = int(raw)
except ValueError:
return 0
return max(0, min(level, 3))
_fa_log_level: int = _parse_log_level(os.environ.get("FA_LOG_LEVEL", "0"))
_logger = logging.getLogger("flash_attn")
_logger.addHandler(logging.NullHandler())
_default_handler: logging.Handler | None = None
def _configure_default_handler() -> None:
global _default_handler
if _fa_log_level >= 1:
if _default_handler is None:
_default_handler = logging.StreamHandler(sys.stdout)
_default_handler.setFormatter(logging.Formatter("[FA] %(message)s"))
_logger.addHandler(_default_handler)
_logger.setLevel(logging.DEBUG)
else:
if _default_handler is not None:
_logger.removeHandler(_default_handler)
_default_handler = None
_logger.setLevel(logging.WARNING)
_configure_default_handler()
def get_fa_log_level() -> int:
return _fa_log_level
def set_fa_log_level(level: int | str) -> None:
"""Set the FA log level programmatically.
Host logging takes effect immediately. Device logging changes only
affect kernels compiled after this call (new compile-key selection).
"""
global _fa_log_level
if isinstance(level, str):
level = _parse_log_level(level)
_fa_log_level = max(0, min(int(level), 3))
_configure_default_handler()
def fa_log(level: int, msg: str):
if _fa_log_level >= level:
_logger.info(msg)
def fa_printf(level: int, fmt, *args):
if const_expr(_fa_log_level >= level):
cute.printf(fmt, *args)
@@ -0,0 +1,21 @@
# Copyright (c) 2025, Tri Dao.
import cutlass
import cutlass.cute as cute
from cutlass import Int32
@cute.jit
def clz(x: Int32) -> Int32:
# for i in cutlass.range_constexpr(32):
# if (1 << (31 - i)) & x:
# return Int32(i)
# return Int32(32)
# Early exit is not supported yet
res = Int32(32)
done = False
for i in cutlass.range(32):
if ((1 << (31 - i)) & x) and not done:
res = Int32(i)
done = True
return res
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,767 @@
# Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
# A reimplementation of https://github.com/Dao-AILab/flash-attention/blob/main/hopper/flash_fwd_combine_kernel.h
# from Cutlass C++ to Cute-DSL.
import math
from functools import partial
from typing import Optional, Type
import cuda.bindings.driver as cuda
import cutlass
import cutlass.cute as cute
from cutlass import Boolean, Float32, Int32, const_expr
from cutlass.cute import FastDivmodDivisor
from cutlass.cute.nvgpu import cpasync
from sglang.jit_kernel.flash_attn.cute import utils
from sglang.jit_kernel.flash_attn.cute.cute_dsl_utils import assume_tensor_aligned
from sglang.jit_kernel.flash_attn.cute.seqlen_info import SeqlenInfo
class FlashAttentionForwardCombine:
def __init__(
self,
dtype: Type[cutlass.Numeric],
dtype_partial: Type[cutlass.Numeric],
head_dim: int,
tile_m: int = 8,
k_block_size: int = 64,
log_max_splits: int = 4,
num_threads: int = 256,
stages: int = 4,
use_pdl: bool = False,
):
"""
Forward combine kernel for split attention computation.
:param dtype: output data type
:param dtype_partial: partial accumulation data type
:param head_dim: head dimension
:param tile_m: m block size
:param k_block_size: k block size
:param log_max_splits: log2 of maximum splits
:param num_threads: number of threads
:param varlen: whether using variable length sequences
:param stages: number of pipeline stages
"""
self.dtype = dtype
self.dtype_partial = dtype_partial
self.head_dim = head_dim
self.tile_m = tile_m
self.k_block_size = k_block_size
self.max_splits = 1 << log_max_splits
self.num_threads = num_threads
self.is_even_k = head_dim % k_block_size == 0
self.stages = stages
self.use_pdl = use_pdl
@staticmethod
def can_implement(
dtype,
dtype_partial,
head_dim,
tile_m,
k_block_size,
log_max_splits,
num_threads,
) -> bool:
"""Check if the kernel can be implemented with the given parameters."""
if dtype not in [cutlass.Float16, cutlass.BFloat16, cutlass.Float32]:
return False
if dtype_partial not in [cutlass.Float16, cutlass.BFloat16, Float32]:
return False
if head_dim % 8 != 0:
return False
if num_threads % 32 != 0:
return False
if tile_m % 8 != 0:
return False
max_splits = 1 << log_max_splits
if max_splits > 256:
return False
if (tile_m * max_splits) % num_threads != 0:
return False
return True
def _setup_attributes(self):
# GMEM copy setup for O partial
universal_copy_bits = 128
async_copy_elems = universal_copy_bits // self.dtype_partial.width
assert self.k_block_size % async_copy_elems == 0
k_block_gmem = (
128
if self.k_block_size % 128 == 0
else (64 if self.k_block_size % 64 == 0 else 32)
)
gmem_threads_per_row = k_block_gmem // async_copy_elems
assert self.num_threads % gmem_threads_per_row == 0
# Async copy atom for O partial load
atom_async_copy_partial = cute.make_copy_atom(
cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
self.dtype_partial,
num_bits_per_copy=universal_copy_bits,
)
tOpartial_layout = cute.make_ordered_layout(
(self.num_threads // gmem_threads_per_row, gmem_threads_per_row),
order=(1, 0),
)
vOpartial_layout = cute.make_layout((1, async_copy_elems)) # 4 vals per load
self.gmem_tiled_copy_O_partial = cute.make_tiled_copy_tv(
atom_async_copy_partial, tOpartial_layout, vOpartial_layout
)
# GMEM copy setup for final O (use universal copy for store)
atom_universal_copy = cute.make_copy_atom(
cute.nvgpu.CopyUniversalOp(),
self.dtype,
num_bits_per_copy=async_copy_elems * self.dtype.width,
)
self.gmem_tiled_copy_O = cute.make_tiled_copy_tv(
atom_universal_copy,
tOpartial_layout,
vOpartial_layout, # 4 vals per store
)
# LSE copy setup with async copy (alignment = 1)
lse_copy_bits = Float32.width # 1 element per copy, width is in bits
m_block_smem = (
128
if self.tile_m % 128 == 0
else (
64
if self.tile_m % 64 == 0
else (
32
if self.tile_m % 32 == 0
else (16 if self.tile_m % 16 == 0 else 8)
)
)
)
gmem_threads_per_row_lse = m_block_smem
assert self.num_threads % gmem_threads_per_row_lse == 0
# Async copy atom for LSE load
atom_async_copy_lse = cute.make_copy_atom(
cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.ALWAYS),
Float32,
num_bits_per_copy=lse_copy_bits,
)
tLSE_layout = cute.make_ordered_layout(
(self.num_threads // gmem_threads_per_row_lse, gmem_threads_per_row_lse),
order=(1, 0),
)
vLSE_layout = cute.make_layout(1)
self.gmem_tiled_copy_LSE = cute.make_tiled_copy_tv(
atom_async_copy_lse, tLSE_layout, vLSE_layout
)
# ///////////////////////////////////////////////////////////////////////////////
# Shared memory
# ///////////////////////////////////////////////////////////////////////////////
# Shared memory to register copy for LSE
self.smem_threads_per_col_lse = self.num_threads // m_block_smem
assert 32 % self.smem_threads_per_col_lse == 0 # Must divide warp size
s2r_layout_atom_lse = cute.make_ordered_layout(
(
self.smem_threads_per_col_lse,
self.num_threads // self.smem_threads_per_col_lse,
),
order=(0, 1),
)
self.s2r_tiled_copy_LSE = cute.make_tiled_copy_tv(
cute.make_copy_atom(cute.nvgpu.CopyUniversalOp(), Float32),
s2r_layout_atom_lse,
cute.make_layout(1),
)
# LSE shared memory layout with swizzling to avoid bank conflicts
# This works for kBlockMSmem = 8, 16, 32, 64, 128, no bank conflicts
if const_expr(m_block_smem == 8):
smem_lse_swizzle = cute.make_swizzle(5, 0, 5)
elif const_expr(m_block_smem == 16):
smem_lse_swizzle = cute.make_swizzle(4, 0, 4)
else:
smem_lse_swizzle = cute.make_swizzle(3, 2, 3)
smem_layout_atom_lse = cute.make_composed_layout(
smem_lse_swizzle,
0,
cute.make_ordered_layout((8, m_block_smem), order=(1, 0)),
)
self.smem_layout_lse = cute.tile_to_shape(
smem_layout_atom_lse, (self.max_splits, self.tile_m), (0, 1)
)
# O partial shared memory layout (simple layout for pipeline stages)
self.smem_layout_o = cute.make_ordered_layout(
(self.tile_m, self.k_block_size, self.stages), order=(1, 0, 2)
)
@cute.jit
def __call__(
self,
mO_partial: cute.Tensor,
mLSE_partial: cute.Tensor,
mO: cute.Tensor,
mLSE: Optional[cute.Tensor] = None,
cu_seqlens: Optional[cute.Tensor] = None,
seqused: Optional[cute.Tensor] = None,
num_splits_dynamic_ptr: Optional[cute.Tensor] = None,
varlen_batch_idx: Optional[cute.Tensor] = None,
semaphore_to_reset: Optional[cute.Tensor] = None,
# Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
stream: cuda.CUstream = None,
):
# Type checking
if const_expr(not (mO_partial.element_type == self.dtype_partial)):
raise TypeError("O partial tensor must match dtype_partial")
if const_expr(not (mO.element_type == self.dtype)):
raise TypeError("O tensor must match dtype")
if const_expr(mLSE_partial.element_type not in [Float32]):
raise TypeError("LSE partial tensor must be Float32")
if const_expr(mLSE is not None and mLSE.element_type not in [Float32]):
raise TypeError("LSE tensor must be Float32")
# Shape validation - input tensors are in user format, need to be converted to kernel format
if const_expr(len(mO_partial.shape) not in [4, 5]):
raise ValueError(
"O partial tensor must have 4 or 5 dimensions: (num_splits, batch, seqlen, nheads, headdim) or (num_splits, total_q, nheads, headdim)"
)
if const_expr(len(mLSE_partial.shape) not in [3, 4]):
raise ValueError(
"LSE partial tensor must have 3 or 4 dimensions: (num_splits, batch, seqlen, nheads) or (num_splits, total_q, nheads)"
)
if const_expr(len(mO.shape) not in [3, 4]):
raise ValueError(
"O tensor must have 3 or 4 dimensions: (batch, seqlen, nheads, headdim) or (total_q, nheads, headdim)"
)
if const_expr(mLSE is not None and len(mLSE.shape) not in [2, 3]):
raise ValueError(
"LSE tensor must have 2 or 3 dimensions: (batch, seqlen, nheads) or (total_q, nheads)"
)
mO_partial, mO = [assume_tensor_aligned(t) for t in (mO_partial, mO)]
# (num_splits, b, seqlen, h, d) -> (seqlen, d, num_splits, h, b)
# or (num_splits, total_q, h, d) -> (total_q, d, num_splits, h)
O_partial_layout_transpose = (
[2, 4, 0, 3, 1] if const_expr(cu_seqlens is None) else [1, 3, 0, 2]
)
# (b, seqlen, h, d) -> (seqlen, d, h, b) or (total_q, h, d) -> (total_q, d, h)
mO_partial = cute.make_tensor(
mO_partial.iterator,
cute.select(mO_partial.layout, mode=O_partial_layout_transpose),
)
O_layout_transpose = (
[1, 3, 2, 0] if const_expr(cu_seqlens is None) else [0, 2, 1]
)
mO = cute.make_tensor(
mO.iterator, cute.select(mO.layout, mode=O_layout_transpose)
)
# (num_splits, b, seqlen, h) -> (seqlen, num_splits, h, b)
# or (num_splits, total_q, h) -> (total_q, num_splits, h)
LSE_partial_layout_transpose = (
[2, 0, 3, 1] if const_expr(cu_seqlens is None) else [1, 0, 2]
)
mLSE_partial = cute.make_tensor(
mLSE_partial.iterator,
cute.select(mLSE_partial.layout, mode=LSE_partial_layout_transpose),
)
# (b, seqlen, h) -> (seqlen, h, b) or (total_q, h) -> (total_q, h)
LSE_layout_transpose = [1, 2, 0] if const_expr(cu_seqlens is None) else [0, 1]
mLSE = (
cute.make_tensor(
mLSE.iterator, cute.select(mLSE.layout, mode=LSE_layout_transpose)
)
if mLSE is not None
else None
)
# Determine if we have variable length sequences
varlen = const_expr(cu_seqlens is not None or seqused is not None)
self._setup_attributes()
@cute.struct
class SharedStorage:
sLSE: cute.struct.Align[
cute.struct.MemRange[Float32, cute.cosize(self.smem_layout_lse)], 128
]
sMaxValidSplit: cute.struct.Align[
cute.struct.MemRange[Int32, self.tile_m], 128
]
sO: cute.struct.Align[
cute.struct.MemRange[
self.dtype_partial, cute.cosize(self.smem_layout_o)
],
128,
]
smem_size = SharedStorage.size_in_bytes()
# Grid dimensions: (ceil_div(seqlen, m_block), ceil_div(head_dim, k_block), num_head * batch)
seqlen = mO_partial.shape[0]
num_head = mO_partial.shape[3]
batch_size = (
mO_partial.shape[4]
if const_expr(cu_seqlens is None)
else Int32(cu_seqlens.shape[0] - 1)
)
# Create FastDivmodDivisor objects for efficient division
seqlen_divmod = FastDivmodDivisor(seqlen)
head_divmod = FastDivmodDivisor(num_head)
grid_dim = (
cute.ceil_div(seqlen * num_head, self.tile_m),
cute.ceil_div(self.head_dim, self.k_block_size),
batch_size,
)
self.kernel(
mO_partial,
mLSE_partial,
mO,
mLSE,
cu_seqlens,
seqused,
num_splits_dynamic_ptr,
varlen_batch_idx,
semaphore_to_reset,
SharedStorage,
self.smem_layout_lse,
self.smem_layout_o,
self.gmem_tiled_copy_O_partial,
self.gmem_tiled_copy_O,
self.gmem_tiled_copy_LSE,
self.s2r_tiled_copy_LSE,
seqlen_divmod,
head_divmod,
varlen,
).launch(
grid=grid_dim,
block=[self.num_threads, 1, 1],
smem=smem_size,
stream=stream,
use_pdl=self.use_pdl,
)
@cute.kernel
def kernel(
self,
mO_partial: cute.Tensor,
mLSE_partial: cute.Tensor,
mO: cute.Tensor,
mLSE: Optional[cute.Tensor],
cu_seqlens: Optional[cute.Tensor],
seqused: Optional[cute.Tensor],
num_splits_dynamic_ptr: Optional[cute.Tensor],
varlen_batch_idx: Optional[cute.Tensor],
semaphore_to_reset: Optional[cute.Tensor],
SharedStorage: cutlass.Constexpr,
smem_layout_lse: cute.Layout | cute.ComposedLayout,
smem_layout_o: cute.Layout,
gmem_tiled_copy_O_partial: cute.TiledCopy,
gmem_tiled_copy_O: cute.TiledCopy,
gmem_tiled_copy_LSE: cute.TiledCopy,
s2r_tiled_copy_LSE: cute.TiledCopy,
seqlen_divmod: FastDivmodDivisor,
head_divmod: FastDivmodDivisor,
varlen: cutlass.Constexpr[bool],
):
# Thread and block indices
tidx, _, _ = cute.arch.thread_idx()
m_block, k_block, maybe_virtual_batch = cute.arch.block_idx()
# Map virtual batch index to real batch index (for persistent tile schedulers)
batch_idx = (
varlen_batch_idx[maybe_virtual_batch]
if const_expr(varlen_batch_idx is not None)
else maybe_virtual_batch
)
# ///////////////////////////////////////////////////////////////////////////////
# Get shared memory buffer
# ///////////////////////////////////////////////////////////////////////////////
smem = cutlass.utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
sLSE = storage.sLSE.get_tensor(smem_layout_lse)
sMaxValidSplit = storage.sMaxValidSplit.get_tensor((self.tile_m,))
sO = storage.sO.get_tensor(smem_layout_o)
# Handle semaphore reset — wait for dependent grids first
if const_expr(semaphore_to_reset is not None):
if (
tidx == 0
and m_block == cute.arch.grid_dim()[0] - 1
and k_block == cute.arch.grid_dim()[1] - 1
and maybe_virtual_batch == cute.arch.grid_dim()[2] - 1
):
if const_expr(self.use_pdl):
cute.arch.griddepcontrol_wait()
semaphore_to_reset[0] = 0
# Get number of splits (use maybe_virtual_batch for per-batch-slot splits)
num_splits = (
num_splits_dynamic_ptr[maybe_virtual_batch]
if const_expr(num_splits_dynamic_ptr is not None)
else mLSE_partial.shape[1]
)
# Handle variable length sequences using SeqlenInfo
seqlen_info = SeqlenInfo.create(
batch_idx=batch_idx,
seqlen_static=mO_partial.shape[0],
cu_seqlens=cu_seqlens,
seqused=seqused,
# Don't need to pass in tile size since we won't use offset_padded
)
seqlen, offset = seqlen_info.seqlen, seqlen_info.offset
# Extract number of heads (head index will be determined dynamically)
num_head = mO_partial.shape[3]
max_idx = seqlen * num_head
# Early exit for single split if dynamic
if (const_expr(num_splits_dynamic_ptr is None) or num_splits > 1) and (
const_expr(not varlen) or m_block * self.tile_m < max_idx
):
# Wait for dependent grids (e.g., the main attention kernel that produces O_partial/LSE_partial)
if const_expr(self.use_pdl):
cute.arch.griddepcontrol_wait()
# ===============================
# Step 1: Load LSE_partial from gmem to shared memory
# ===============================
mLSE_partial_cur = seqlen_info.offset_batch(mLSE_partial, batch_idx, dim=3)
mLSE_partial_copy = cute.tiled_divide(mLSE_partial_cur, (1,))
gmem_thr_copy_LSE = gmem_tiled_copy_LSE.get_slice(tidx)
tLSEsLSE = gmem_thr_copy_LSE.partition_D(sLSE)
# Create identity tensor for coordinate tracking
cLSE = cute.make_identity_tensor((self.max_splits, self.tile_m))
tLSEcLSE = gmem_thr_copy_LSE.partition_S(cLSE)
# Load LSE partial values
for m in cutlass.range(cute.size(tLSEcLSE, mode=[2]), unroll_full=True):
mi = tLSEcLSE[0, 0, m][1] # Get m coordinate
idx = m_block * self.tile_m + mi
if idx < max_idx:
# Calculate actual sequence position and head using FastDivmodDivisor
if const_expr(not varlen):
head_idx, m_idx = divmod(idx, seqlen_divmod)
else:
head_idx = idx // seqlen
m_idx = idx - head_idx * seqlen
mLSE_partial_cur_copy = mLSE_partial_copy[
None, m_idx, None, head_idx
]
for s in cutlass.range(
cute.size(tLSEcLSE, mode=[1]), unroll_full=True
):
si = tLSEcLSE[0, s, 0][0] # Get split coordinate
if si < num_splits:
cute.copy(
gmem_thr_copy_LSE,
mLSE_partial_cur_copy[None, si],
tLSEsLSE[None, s, m],
)
else:
tLSEsLSE[None, s, m].fill(-Float32.inf)
# Don't need to zero out the rest of the LSEs, as we will not write the output to gmem
cute.arch.cp_async_commit_group()
# ===============================
# Step 2: Load O_partial for pipeline stages
# ===============================
gmem_thr_copy_O_partial = gmem_tiled_copy_O_partial.get_slice(tidx)
cO = cute.make_identity_tensor((self.tile_m, self.k_block_size))
tOcO = gmem_thr_copy_O_partial.partition_D(cO)
tOsO_partial = gmem_thr_copy_O_partial.partition_D(sO)
mO_partial_cur = seqlen_info.offset_batch(mO_partial, batch_idx, dim=4)
# Precompute these values to avoid recomputing them in the loop
num_rows = const_expr(cute.size(tOcO, mode=[1]))
tOmidx = cute.make_rmem_tensor(num_rows, cutlass.Int32)
tOhidx = cute.make_rmem_tensor(num_rows, cutlass.Int32)
tOrOptr = cute.make_rmem_tensor(num_rows, cutlass.Int64)
for m in cutlass.range(num_rows, unroll_full=True):
mi = tOcO[0, m, 0][0] # m coordinate
idx = m_block * self.tile_m + mi
if const_expr(not varlen):
tOhidx[m], tOmidx[m] = divmod(idx, seqlen_divmod)
else:
tOhidx[m] = idx // seqlen
tOmidx[m] = idx - tOhidx[m] * seqlen
tOrOptr[m] = utils.elem_pointer(
mO_partial_cur,
(tOmidx[m], k_block * self.k_block_size, 0, tOhidx[m]),
).toint()
if idx >= max_idx:
tOhidx[m] = -1
tOpO = None
if const_expr(not self.is_even_k):
tOpO = cute.make_rmem_tensor(cute.size(tOcO, mode=[2]), Boolean)
for k in cutlass.range(cute.size(tOpO), unroll_full=True):
tOpO[k] = (
tOcO[0, 0, k][1]
< mO_partial.shape[1] - k_block * self.k_block_size
)
# if cute.arch.thread_idx()[0] == 0 and k_block == 1: cute.print_tensor(tOpO)
load_O_partial = partial(
self.load_O_partial,
gmem_tiled_copy_O_partial,
tOrOptr,
tOsO_partial,
tOhidx,
tOpO,
tOcO,
mO_partial_cur.layout,
)
# Load first few stages of O_partial
for stage in cutlass.range(self.stages - 1, unroll_full=True):
if stage < num_splits:
load_O_partial(stage, stage)
cute.arch.cp_async_commit_group()
# ===============================
# Step 3: Load and transpose LSE from smem to registers
# ===============================
# Wait for LSE and initial O partial stages to complete
cute.arch.cp_async_wait_group(self.stages - 1)
cute.arch.sync_threads()
# if cute.arch.thread_idx()[0] == 0:
# # cute.print_tensor(sLSE)
# for i in range(64):
# cute.printf("sLSE[%d, 0] = %f", i, sLSE[i, 0])
# cute.arch.sync_threads()
s2r_thr_copy_LSE = s2r_tiled_copy_LSE.get_slice(tidx)
ts2rsLSE = s2r_thr_copy_LSE.partition_S(sLSE)
ts2rrLSE = cute.make_rmem_tensor_like(ts2rsLSE)
cute.copy(s2r_tiled_copy_LSE, ts2rsLSE, ts2rrLSE)
# ===============================
# Step 4: Compute final LSE along split dimension
# ===============================
lse_sum = cute.make_rmem_tensor(cute.size(ts2rrLSE, mode=[2]), Float32)
ts2rcLSE = s2r_thr_copy_LSE.partition_D(cLSE)
# We compute the max valid split for each row to short-circuit the computation later
max_valid_split = cute.make_rmem_tensor(
cute.size(ts2rrLSE, mode=[2]), Int32
)
assert cute.size(ts2rrLSE, mode=[0]) == 1
# Compute max, scales, and final LSE for each row
for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
# Find max LSE value across splits
threads_per_col = const_expr(self.smem_threads_per_col_lse)
lse_max = cute.arch.warp_reduction_max(
ts2rrLSE[None, None, m]
.load()
.reduce(
cute.ReductionOp.MAX, init_val=-Float32.inf, reduction_profile=0
),
threads_in_group=threads_per_col,
)
# if cute.arch.thread_idx()[0] == 0: cute.printf(lse_max)
# Find max valid split index
max_valid_idx = -1
for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
if ts2rrLSE[0, s, m] != -Float32.inf:
max_valid_idx = ts2rcLSE[0, s, 0][0] # Get split coordinate
# if cute.arch.thread_idx()[0] < 32: cute.printf(max_valid_idx)
max_valid_split[m] = cute.arch.warp_reduction_max(
max_valid_idx, threads_in_group=threads_per_col
)
# Compute exp scales and sum
lse_max_cur = (
0.0 if lse_max == -Float32.inf else lse_max
) # In case all local LSEs are -inf
LOG2_E = math.log2(math.e)
lse_sum_cur = 0.0
for s in cutlass.range(cute.size(ts2rrLSE, mode=[1]), unroll_full=True):
scale = cute.math.exp2(
ts2rrLSE[0, s, m] * LOG2_E - (lse_max_cur * LOG2_E),
fastmath=True,
)
lse_sum_cur += scale
ts2rrLSE[0, s, m] = scale # Store scale for later use
lse_sum_cur = cute.arch.warp_reduction_sum(
lse_sum_cur, threads_in_group=threads_per_col
)
lse_sum[m] = cute.math.log(lse_sum_cur, fastmath=True) + lse_max
# Normalize scales
inv_sum = (
0.0
if (lse_sum_cur == 0.0 or lse_sum_cur != lse_sum_cur)
else 1.0 / lse_sum_cur
)
ts2rrLSE[None, None, m].store(ts2rrLSE[None, None, m].load() * inv_sum)
# Store the scales exp(lse - lse_logsum) back to smem
cute.copy(s2r_tiled_copy_LSE, ts2rrLSE, ts2rsLSE)
# Store max valid split to smem
for m in cutlass.range(cute.size(ts2rrLSE, mode=[2]), unroll_full=True):
if ts2rcLSE[0, 0, m][0] == 0: # Only thread responsible for s=0 writes
mi = ts2rcLSE[0, 0, m][1]
if mi < self.tile_m:
sMaxValidSplit[mi] = max_valid_split[m]
# ===============================
# Step 5: Store final LSE to gmem
# ===============================
if const_expr(mLSE is not None):
if const_expr(cu_seqlens is None):
mLSE_cur = mLSE[None, None, batch_idx]
else:
mLSE_cur = cute.domain_offset((offset, 0), mLSE)
if k_block == 0: # Only first k_block writes LSE when mLSE is provided
for m in cutlass.range(
cute.size(ts2rrLSE, mode=[2]), unroll_full=True
):
if (
ts2rcLSE[0, 0, m][0] == 0
): # Only thread responsible for s=0 writes
mi = ts2rcLSE[0, 0, m][1]
idx = m_block * self.tile_m + mi
if idx < max_idx:
if const_expr(not varlen):
head_idx, m_idx = divmod(idx, seqlen_divmod)
else:
head_idx = idx // seqlen
m_idx = idx - head_idx * seqlen
mLSE_cur[m_idx, head_idx] = lse_sum[m]
# ===============================
# Step 6: Read O_partial and accumulate final O
# ===============================
cute.arch.sync_threads()
# Get max valid split for this thread
thr_max_valid_split = sMaxValidSplit[tOcO[0, 0, 0][0]]
for m in cutlass.range(1, cute.size(tOcO, mode=[1]), unroll_full=True):
thr_max_valid_split = max(
thr_max_valid_split, sMaxValidSplit[tOcO[0, m, 0][0]]
)
tOrO_partial = cute.make_rmem_tensor_like(tOsO_partial[None, None, None, 0])
tOrO = cute.make_rmem_tensor_like(tOrO_partial, Float32)
tOrO.fill(0.0)
stage_load = self.stages - 1
stage_compute = 0
# Main accumulation loop
for s in cutlass.range(thr_max_valid_split + 1, unroll=4):
# Get scales for this split
scale = cute.make_rmem_tensor(num_rows, Float32)
for m in cutlass.range(num_rows, unroll_full=True):
scale[m] = sLSE[s, tOcO[0, m, 0][0]] # Get scale from smem
# Load next stage if needed
split_to_load = s + self.stages - 1
if split_to_load <= thr_max_valid_split:
load_O_partial(split_to_load, stage_load)
cute.arch.cp_async_commit_group()
stage_load = 0 if stage_load == self.stages - 1 else stage_load + 1
# Wait for the current stage to be ready
cute.arch.cp_async_wait_group(self.stages - 1)
# We don't need __syncthreads() because each thread is just reading its own data from smem
# Copy from smem to registers
cute.autovec_copy(
tOsO_partial[None, None, None, stage_compute], tOrO_partial
)
stage_compute = (
0 if stage_compute == self.stages - 1 else stage_compute + 1
)
# Accumulate scaled partial results
for m in cutlass.range(num_rows, unroll_full=True):
if tOhidx[m] >= 0 and scale[m] > 0.0:
tOrO[None, m, None].store(
tOrO[None, m, None].load()
+ scale[m] * tOrO_partial[None, m, None].load().to(Float32)
)
# ===============================
# Step 7: Write final O to gmem
# ===============================
rO = cute.make_rmem_tensor_like(tOrO, self.dtype)
rO.store(tOrO.load().to(self.dtype))
mO_cur = seqlen_info.offset_batch(mO, batch_idx, dim=3)
if const_expr(cu_seqlens is None):
mO_cur = mO[None, None, None, batch_idx]
else:
mO_cur = cute.domain_offset((offset, 0, 0), mO)
mO_cur = utils.domain_offset_aligned(
(0, k_block * self.k_block_size, 0), mO_cur
)
elems_per_store = const_expr(
cute.size(gmem_tiled_copy_O.layout_tv_tiled[1])
)
# mO_cur_copy = cute.tiled_divide(mO_cur, (1, elems_per_store,))
gmem_thr_copy_O = gmem_tiled_copy_O.get_slice(tidx)
# Write final results
for m in cutlass.range(num_rows, unroll_full=True):
if tOhidx[m] >= 0:
mO_cur_copy = cute.tiled_divide(
mO_cur[tOmidx[m], None, tOhidx[m]], (elems_per_store,)
)
for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
k_idx = tOcO[0, 0, k][1] // elems_per_store
if const_expr(self.is_even_k) or tOpO[k]:
cute.copy(
gmem_thr_copy_O,
rO[None, m, k],
mO_cur_copy[None, k_idx],
)
@cute.jit
def load_O_partial(
self,
gmem_tiled_copy_O_partial: cute.TiledCopy,
tOrOptr: cute.Tensor,
tOsO_partial: cute.Tensor,
tOhidx: cute.Tensor,
tOpO: Optional[cute.Tensor],
tOcO: cute.Tensor,
mO_cur_partial_layout: cute.Layout,
split: Int32,
stage: Int32,
) -> None:
elems_per_load = const_expr(
cute.size(gmem_tiled_copy_O_partial.layout_tv_tiled[1])
)
tOsO_partial_cur = tOsO_partial[None, None, None, stage]
for m in cutlass.range(cute.size(tOcO, [1]), unroll_full=True):
if tOhidx[m] >= 0:
o_gmem_ptr = cute.make_ptr(
tOsO_partial.element_type,
tOrOptr[m],
cute.AddressSpace.gmem,
assumed_align=16,
)
mO_partial_cur = cute.make_tensor(
o_gmem_ptr, cute.slice_(mO_cur_partial_layout, (0, None, None, 0))
)
mO_partial_cur_copy = cute.tiled_divide(
mO_partial_cur, (elems_per_load,)
)
for k in cutlass.range(cute.size(tOcO, mode=[2]), unroll_full=True):
k_idx = tOcO[0, 0, k][1] // elems_per_load
if const_expr(tOpO is None) or tOpO[k]:
cute.copy(
gmem_tiled_copy_O_partial,
mO_partial_cur_copy[None, k_idx, split],
tOsO_partial_cur[None, m, k],
)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,61 @@
# Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
# SM120 (Blackwell GeForce / DGX Spark) forward pass.
#
# SM120 uses the same SM80-era MMA instructions (mma.sync.aligned.m16n8k16) but has
# a smaller shared memory capacity (99 KB vs 163 KB on SM80). This module subclasses
# FlashAttentionForwardSm80 and overrides the SMEM capacity check accordingly.
import cutlass
import cutlass.utils as utils_basic
from sglang.jit_kernel.flash_attn.cute.flash_fwd import FlashAttentionForwardSm80
class FlashAttentionForwardSm120(FlashAttentionForwardSm80):
# Keep arch = 80 to use CpAsync code paths (no TMA for output).
# The compilation target is determined by the GPU at compile time, not this field.
arch = 80
@staticmethod
def can_implement(
dtype,
head_dim,
head_dim_v,
tile_m,
tile_n,
num_stages,
num_threads,
is_causal,
Q_in_regs=False,
) -> bool:
"""Check if the kernel can be implemented on SM120.
Same logic as SM80 but uses SM120's shared memory capacity (99 KB).
"""
if dtype not in [cutlass.Float16, cutlass.BFloat16]:
return False
if head_dim % 8 != 0:
return False
if head_dim_v % 8 != 0:
return False
if tile_n % 16 != 0:
return False
if num_threads % 32 != 0:
return False
# Shared memory usage: Q tile + (K tile + V tile)
smem_usage_Q = tile_m * head_dim * 2
smem_usage_K = tile_n * head_dim * num_stages * 2
smem_usage_V = tile_n * head_dim_v * num_stages * 2
smem_usage_QV = (
(smem_usage_Q + smem_usage_V)
if not Q_in_regs
else max(smem_usage_Q, smem_usage_V)
)
smem_usage = smem_usage_QV + smem_usage_K
# SM120 has 99 KB shared memory (vs 163 KB on SM80)
smem_capacity = utils_basic.get_smem_capacity_in_bytes("sm_120")
if smem_usage > smem_capacity:
return False
if (tile_m * 2) % num_threads != 0:
return False
return True
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,319 @@
# Copyright (c) 2025, Tri Dao.
# Ported Cutlass code from C++ to Python:
# https://github.com/NVIDIA/cutlass/blob/main/include/cute/arch/mma_sm100_desc.hpp
# https://github.com/NVIDIA/cutlass/blob/main/include/cute/atom/mma_traits_sm100.hpp
from enum import IntEnum
import cutlass
import cutlass.cute as cute
# ---------------------------------------------------------------------------
# Enumerations that match the HW encodings (values MUST stay identical)
# ---------------------------------------------------------------------------
class Major(IntEnum): # matrix “layout” in the ISA docs
K = 0
MN = 1
class ScaleIn(IntEnum): # negate flags
One = 0
Neg = 1
class Saturate(IntEnum):
False_ = 0
True_ = 1
class CFormat(IntEnum): # 2-bit field (bits 4-5)
F16 = 0
F32 = 1
S32 = 2
class F16F32Format(IntEnum): # 3-bit field (A/B element type)
F16 = 0
BF16 = 1
TF32 = 2
class S8Format(IntEnum):
UINT8 = 0
INT8 = 1
class MXF8F6F4Format(IntEnum):
E4M3 = 0
E5M2 = 1
E2M3 = 3
E3M2 = 4
E2M1 = 5
class MaxShift(IntEnum):
NoShift = 0
MaxShift8 = 1
MaxShift16 = 2
MaxShift32 = 3
# ---------------------------------------------------------------------------
# CUTLASS-type → encoding helpers
# ---------------------------------------------------------------------------
def to_UMMA_format(cutlass_type) -> int:
"""
Map a CUTLASS scalar class to the 3-bit encoding for Matrix A/B.
"""
if cutlass_type is cutlass.Int8:
return S8Format.INT8
# Unsigned 8-bit (if available in your CUTLASS build)
if cutlass_type is cutlass.Uint8:
return S8Format.UINT8
# FP-16 / BF-16
if cutlass_type is cutlass.Float16:
return F16F32Format.F16
if cutlass_type is cutlass.BFloat16:
return F16F32Format.BF16
# TensorFloat-32 (8-bit exponent, 10-bit mantissa packed in 19 bits)
if cutlass_type is cutlass.TFloat32:
return F16F32Format.TF32
# Float-8 / Float-6 / Float-4 add whenever CUTLASS exposes them
if cutlass_type is cutlass.Float8E4M3FN:
return MXF8F6F4Format.E4M3
if cutlass_type is cutlass.Float8E5M2:
return MXF8F6F4Format.E5M2
raise TypeError(f"Unsupported CUTLASS scalar type for A/B: {cutlass_type!r}")
def to_C_format(cutlass_type) -> int:
"""
Map a CUTLASS scalar class to the 2-bit accumulator encoding.
"""
if cutlass_type is cutlass.Float16:
return CFormat.F16
if cutlass_type is cutlass.Float32:
return CFormat.F32
if cutlass_type is cutlass.Int32:
return CFormat.S32
raise TypeError(
f"Unsupported CUTLASS scalar type for accumulator: {cutlass_type!r}"
)
# ---------------------------------------------------------------------------
# The constructor accepts only CUTLASS scalar classes
# ---------------------------------------------------------------------------
def make_instr_desc(
a_type, # CUTLASS scalar class, e.g. cutlass.Int8
b_type,
c_type,
M: int, # 64, 128 or 256
N: int, # 8 … 256 (multiple of 8)
a_major: Major,
b_major: Major,
a_neg: ScaleIn = ScaleIn.One,
b_neg: ScaleIn = ScaleIn.One,
c_sat: Saturate = Saturate.False_,
is_sparse: bool = False,
max_shift: MaxShift = MaxShift.NoShift,
) -> int:
"""
Build the 32-bit instruction descriptor for Blackwell MMA.
All matrix/accumulator **types must be CUTLASS scalar classes**
passing integers is forbidden.
"""
# --- encode element formats -------------------------------------------------
a_fmt = int(to_UMMA_format(a_type))
b_fmt = int(to_UMMA_format(b_type))
c_fmt = int(to_C_format(c_type))
# --- range checks on M/N -----------------------------------------------------
if M not in (64, 128, 256):
raise ValueError("M must be 64, 128 or 256")
if N < 8 or N > 256 or (N & 7):
raise ValueError("N must be a multiple of 8 in the range 8…256")
m_dim = M >> 4 # 5-bit field
n_dim = N >> 3 # 6-bit field
# fmt: off
# --- pack the bit-fields -----------------------------------------------------
desc = 0
desc |= (0 & 0x3) << 0 # sparse_id2 (always 0 here)
desc |= (int(is_sparse) & 0x1) << 2 # sparse_flag
desc |= (int(c_sat) & 0x1) << 3 # saturate
desc |= (c_fmt & 0x3) << 4 # c_format
desc |= (a_fmt & 0x7) << 7 # a_format
desc |= (b_fmt & 0x7) << 10 # b_format
desc |= (int(a_neg) & 0x1) << 13 # a_negate
desc |= (int(b_neg) & 0x1) << 14 # b_negate
desc |= (int(a_major) & 0x1) << 15 # a_major
desc |= (int(b_major) & 0x1) << 16 # b_major
desc |= (n_dim & 0x3F) << 17 # n_dim (6 bits)
desc |= (m_dim & 0x1F) << 24 # m_dim (5 bits)
desc |= (int(max_shift) & 0x3) << 30 # max_shift (2 bits)
# fmt: on
return desc & 0xFFFF_FFFF # ensure 32-bit result
def mma_op_to_idesc(op: cute.nvgpu.tcgen05.mma.MmaOp):
return make_instr_desc(
op.a_dtype,
op.b_dtype,
op.acc_dtype,
op.shape_mnk[0],
op.shape_mnk[1],
(
Major.K
if op.a_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K
else Major.MN
),
(
Major.K
if op.b_major_mode == cute.nvgpu.tcgen05.mma.OperandMajorMode.K
else Major.MN
),
)
class LayoutType(IntEnum): # occupies the top-3 bits [61:64)
SWIZZLE_NONE = 0 # (a.k.a. “INTERLEAVE” in older docs)
SWIZZLE_128B_BASE32B = 1
SWIZZLE_128B = 2
SWIZZLE_64B = 4
SWIZZLE_32B = 6
# values 3,5,7 are reserved / illegal for UMMA
# ---------------------------------------------------------------------------
# Helpers figure out the SWIZZLE_* family from the tensor layout
# ---------------------------------------------------------------------------
def _layout_type(swizzle: cute.Swizzle) -> LayoutType:
B, M, S = swizzle.num_bits, swizzle.num_base, swizzle.num_shift
if M == 4: # Swizzle<*,4,3>
if S != 3:
raise ValueError("Unexpected swizzle shift want S==3 for M==4")
return {
0: LayoutType.SWIZZLE_NONE,
1: LayoutType.SWIZZLE_32B,
2: LayoutType.SWIZZLE_64B,
3: LayoutType.SWIZZLE_128B,
}[
B
] # KeyError ⇒ invalid B→ raise
if M == 5: # Swizzle<2,5,2> (the only legal triple for M==5)
if (B, S) != (2, 2):
raise ValueError("Only Swizzle<2,5,2> supported for 128B_BASE32B")
return LayoutType.SWIZZLE_128B_BASE32B
# Any other (M,B,S) triple is not a UMMA-legal shared-memory layout
raise ValueError("Unsupported swizzle triple for UMMA smem descriptor")
def make_smem_desc_base(
layout: cute.Layout, swizzle: cute.Swizzle, major: Major
) -> int:
"""
Convert a 2-D *shared-memory* Cute layout into the Blackwell 64-bit
smem-descriptor, without the smem start address.
layout must correspond to layout of an uint128 tensor.
"""
# ------------------------------------------------------------------ meta
layout_type = _layout_type(swizzle) # resolve SWIZZLE_* family
VERSION = 1 # bits 4647
LBO_MODE = 0 # bit 52
BASE_OFFSET = 0 # bits 4951 (CUTLASS always 0)
# ---------------------------------------------------------- strides (units: uint128_t = 16 B)
swizzle_atom_mn_size = {
LayoutType.SWIZZLE_NONE: 1,
LayoutType.SWIZZLE_32B: 2,
LayoutType.SWIZZLE_64B: 4,
LayoutType.SWIZZLE_128B: 8,
LayoutType.SWIZZLE_128B_BASE32B: 8,
}[layout_type]
if major is Major.MN:
swizzle_atom_k_size = 4 if layout_type is LayoutType.SWIZZLE_128B_BASE32B else 8
canonical_layout = cute.logical_divide(
layout, (swizzle_atom_mn_size, swizzle_atom_k_size)
)
if not cute.is_congruent(canonical_layout, ((1, 1), (1, 1))):
raise ValueError(
"Not a canonical UMMA_MN Layout: Expected profile failure."
)
stride_00 = canonical_layout.stride[0][0]
if layout_type is not LayoutType.SWIZZLE_NONE and stride_00 != 1:
raise ValueError("Not a canonical UMMA_MN Layout: Expected stride failure.")
stride_10 = canonical_layout.stride[1][0]
if stride_10 != swizzle_atom_mn_size:
raise ValueError("Not a canonical UMMA_MN Layout: Expected stride failure.")
stride_01, stride_11 = (
canonical_layout.stride[0][1],
canonical_layout.stride[1][1],
)
if layout_type is LayoutType.SWIZZLE_NONE:
stride_byte_offset, leading_byte_offset = stride_01, stride_11
else:
stride_byte_offset, leading_byte_offset = stride_11, stride_01
else:
if layout_type == LayoutType.SWIZZLE_128B_BASE32B:
raise ValueError("SWIZZLE_128B_BASE32B is invalid for Major-K")
if not cute.size(layout.shape[0]) % 8 == 0:
raise ValueError(
"Not a canonical UMMA_K Layout: Expected MN-size multiple of 8."
)
canonical_layout = cute.logical_divide(layout, (8, 2))
if not cute.is_congruent(canonical_layout, ((1, 1), (1, 1))):
raise ValueError("Not a canonical UMMA_K Layout: Expected profile failure.")
stride_00 = canonical_layout.stride[0][0]
if stride_00 != swizzle_atom_mn_size:
raise ValueError("Not a canonical UMMA_K Layout: Expected stride failure.")
stride_10 = canonical_layout.stride[1][0]
if layout_type is not LayoutType.SWIZZLE_NONE and stride_10 != 1:
raise ValueError("Not a canonical UMMA_K Layout: Expected stride failure.")
stride_01 = canonical_layout.stride[0][1]
stride_byte_offset, leading_byte_offset = stride_01, stride_10
# ------------------------------------------------------------------ pack
desc = 0
# leading_byte_offset_ [16:30)
desc |= (leading_byte_offset & 0x3FFF) << 16
# stride_byte_offset_ [32:46)
desc |= (stride_byte_offset & 0x3FFF) << 32
# version_ [46:48)
desc |= (VERSION & 0x3) << 46
# base_offset_ [49:52)
desc |= (BASE_OFFSET & 0x7) << 49
# lbo_mode_ [52:53)
desc |= (LBO_MODE & 0x1) << 52
# layout_type_ [61:64)
desc |= (int(layout_type) & 0x7) << 61
return desc & 0xFFFF_FFFF_FFFF_FFFF # force 64-bit width
def make_smem_desc_start_addr(start_addr: cute.Pointer) -> cutlass.Int32:
# 14 bits, remove 4 LSB (bits 0-13 in desc)
return (start_addr.toint() & 0x3FFFF) >> 4
def smem_desc_base_from_tensor(sA: cute.Tensor, major: Major) -> int:
sA_swizzle = sA.iterator.type.swizzle_type
return make_smem_desc_base(
cute.recast_layout(128, sA.element_type.width, sA.layout[0]),
sA_swizzle,
major,
)
@@ -0,0 +1,58 @@
# Copyright (c) 2025, Jay Shah, Ganesh Bikshandi, Ying Zhang, Vijay Thakkar, Pradeep Ramani, Tri Dao.
import enum
class NamedBarrierFwd(enum.IntEnum):
Epilogue = enum.auto() # starts from 1 as barrier 0 is reserved for sync_threads()
WarpSchedulerWG1 = enum.auto()
WarpSchedulerWG2 = enum.auto()
WarpSchedulerWG3 = enum.auto()
PFull = enum.auto()
PEmpty = enum.auto()
class NamedBarrierFwdSm100(enum.IntEnum):
Epilogue = enum.auto() # starts from 1 as barrier 0 is reserved for sync_threads()
TmemPtr = enum.auto()
SoftmaxStatsW0 = enum.auto()
SoftmaxStatsW1 = enum.auto()
SoftmaxStatsW2 = enum.auto()
SoftmaxStatsW3 = enum.auto()
SoftmaxStatsW4 = enum.auto()
SoftmaxStatsW5 = enum.auto()
SoftmaxStatsW6 = enum.auto()
SoftmaxStatsW7 = enum.auto()
Softmax = enum.auto()
Correction = enum.auto()
class NamedBarrierBwd(enum.IntEnum):
Epilogue = enum.auto()
WarpSchedulerWG1 = enum.auto()
WarpSchedulerWG2 = enum.auto()
WarpSchedulerWG3 = enum.auto()
PdS = enum.auto()
dQFullWG0 = enum.auto()
dQFullWG1 = enum.auto()
dQFullWG2 = enum.auto()
dQEmptyWG0 = enum.auto()
dQEmptyWG1 = enum.auto()
dQEmptyWG2 = enum.auto()
class NamedBarrierBwdSm100(enum.IntEnum):
EpilogueWG1 = enum.auto()
EpilogueWG2 = enum.auto()
Compute = enum.auto()
dQaccReduce = enum.auto()
TmemPtr = enum.auto()
class NamedBarrierFwdSm100_MLA2CTA(enum.IntEnum):
Epilogue = enum.auto()
TmemPtr = enum.auto()
Cpasync = enum.auto()
Softmax = enum.auto()
SoftmaxStatsFull = enum.auto()
SoftmaxStatsEmpty = enum.auto()
@@ -0,0 +1,300 @@
# Copyright (c) 2025, Tri Dao.
from dataclasses import dataclass
from typing import Tuple, Union
import cutlass
import cutlass.cute as cute
from cutlass.cute.nvgpu import cpasync
from quack import layout_utils
import sglang.jit_kernel.flash_attn.cute.utils as utils
def pack_gqa_layout(T, qhead_per_kvhead, nheads_kv, head_idx):
"""Reshape a tensor to fold qhead_per_kvhead into the seqlen dimension (mode 0).
The head dimension is at mode ``head_idx``. Modes before it (1..head_idx-1)
are kept as-is (e.g. headdim for Q/O tensors), and modes after it are kept
as-is (e.g. batch).
For Q/O tensors (head_idx=2):
(seqlen_q, headdim, nheads, batch, ...) -> ((qhead_per_kvhead, seqlen_q), headdim, nheads_kv, batch, ...)
For LSE tensors (head_idx=1):
(seqlen_q, nheads, batch, ...) -> ((qhead_per_kvhead, seqlen_q), nheads_kv, batch, ...)
"""
head_stride = T.stride[head_idx]
shape_packed = (
(qhead_per_kvhead, T.shape[0]),
*[T.shape[i] for i in range(1, head_idx)],
nheads_kv,
*[T.shape[i] for i in range(head_idx + 1, len(T.shape))],
)
stride_packed = (
(head_stride, T.stride[0]),
*[T.stride[i] for i in range(1, head_idx)],
head_stride * qhead_per_kvhead,
*[T.stride[i] for i in range(head_idx + 1, len(T.shape))],
)
return cute.make_tensor(
T.iterator, cute.make_layout(shape_packed, stride=stride_packed)
)
def make_packgqa_tiled_tma_atom(
op: cute.atom.CopyOp,
gmem_tensor: cute.Tensor,
smem_layout: Union[cute.Layout, cute.ComposedLayout],
cta_tiler: Tuple[int, int],
qhead_per_kvhead: int,
head_idx: int,
):
# This packing and unpacking of the layout is so that we keep the same TMA dimension as usual.
# e.g. for (seqlen, d, nheads, b) layout, we still have 4D TMA after packing to
# ((nheads, seqlen), d, b).
# If we instead pack directly to ((qhead_per_kvhead, seqlen), d, nheads_kv, b) we'd have 5D TMA.
# Pack headdim and seqlen dim into 1: (seqlen, d, nheads, b) -> ((nheads, seqlen), d, b)
gmem_tensor = layout_utils.select(
gmem_tensor,
[head_idx, *range(head_idx), *range(head_idx + 1, cute.rank(gmem_tensor))],
)
gmem_tensor = cute.group_modes(gmem_tensor, 0, 2)
assert (
cta_tiler[0] % qhead_per_kvhead == 0
), "CTA tile size in the seqlen dimension must be divisible by qhead_per_kvhead"
tma_atom, tma_tensor = cpasync.make_tiled_tma_atom(
op,
gmem_tensor,
smem_layout,
(
(qhead_per_kvhead, cta_tiler[0] // qhead_per_kvhead),
cta_tiler[1],
), # No mcast
)
# Unpack from ((nheads, seqlen), d, b) -> ((qhead_per_kvhead, seqlen), d, nheads_kv, b)
T = tma_tensor
shape_packed = (
(qhead_per_kvhead, T.shape[0][1]),
*[T.shape[i] for i in range(1, head_idx)],
T.shape[0][0] // qhead_per_kvhead,
*[T.shape[i] for i in range(head_idx, len(T.shape))],
)
stride_packed = (
*[T.stride[i] for i in range(head_idx)],
T.stride[0][0] * qhead_per_kvhead,
*[T.stride[i] for i in range(head_idx, len(T.shape))],
)
tma_tensor = cute.make_tensor(
T.iterator, cute.make_layout(shape_packed, stride=stride_packed)
)
return tma_atom, tma_tensor
def unpack_gqa_layout(T, qhead_per_kvhead, head_idx):
"""Reverse of pack_gqa_layout: unfold qhead_per_kvhead from the seqlen dimension (mode 0).
The head dimension is at mode ``head_idx``. Modes before it (1..head_idx-1)
are kept as-is (e.g. headdim for Q/O tensors), and modes after it are kept
as-is (e.g. batch).
For Q/O tensors (head_idx=2):
((qhead_per_kvhead, seqlen_q), headdim, nheads_kv, batch, ...) -> (seqlen_q, headdim, nheads, batch, ...)
For LSE tensors (head_idx=1):
((qhead_per_kvhead, seqlen_q), nheads_kv, batch, ...) -> (seqlen_q, nheads, batch, ...)
"""
seqlen_stride = T.stride[0][1]
head_stride = T.stride[0][0]
shape_unpacked = (
T.shape[0][1],
*[T.shape[i] for i in range(1, head_idx)],
T.shape[head_idx] * qhead_per_kvhead,
*[T.shape[i] for i in range(head_idx + 1, len(T.shape))],
)
stride_unpacked = (
seqlen_stride,
*[T.stride[i] for i in range(1, head_idx)],
head_stride,
*[T.stride[i] for i in range(head_idx + 1, len(T.shape))],
)
return cute.make_tensor(
T.iterator, cute.make_layout(shape_unpacked, stride=stride_unpacked)
)
@dataclass
class PackGQA:
m_block_size: cutlass.Constexpr[int]
head_dim_padded: cutlass.Constexpr[int]
check_hdim_oob: cutlass.Constexpr[bool]
qhead_per_kvhead: cutlass.Constexpr[bool]
@cute.jit
def compute_ptr(
self,
tensor: cute.Tensor,
cRows: cute.Tensor,
tidx: cutlass.Int32,
block: cutlass.Int32,
threads_per_row: cutlass.Constexpr[int],
num_threads: cutlass.Constexpr[int],
):
num_ptr_per_thread = cute.ceil_div(cute.size(cRows), threads_per_row)
tPrPtr = cute.make_rmem_tensor(num_ptr_per_thread, cutlass.Int64)
for i in cutlass.range_constexpr(num_ptr_per_thread):
row = i * num_threads + cRows[tidx % threads_per_row][0]
idx = block * self.m_block_size + row
m_idx = idx // self.qhead_per_kvhead
h_idx = idx - m_idx * self.qhead_per_kvhead
tPrPtr[i] = utils.elem_pointer(tensor, ((h_idx, m_idx),)).toint()
return tPrPtr
@cute.jit
def load_Q(
self,
mQ: cute.Tensor, # ((qhead_per_kvhead, seqlen_q), headdim)
sQ: cute.Tensor, # (m_block_size, head_dim_padded)
gmem_tiled_copy: cute.TiledCopy,
tidx: cutlass.Int32,
block: cutlass.Int32,
seqlen: cutlass.Int32,
):
gmem_thr_copy = gmem_tiled_copy.get_slice(tidx)
cQ = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
tQsQ = gmem_thr_copy.partition_D(sQ)
tQcQ = gmem_thr_copy.partition_S(cQ)
t0QcQ = gmem_thr_copy.get_slice(0).partition_S(cQ)
tQpQ = utils.predicate_k(tQcQ, limit=mQ.shape[1])
tQcQ_row = tQcQ[0, None, 0]
threads_per_row = gmem_tiled_copy.layout_tv_tiled.shape[0][0]
assert (
cute.arch.WARP_SIZE % threads_per_row == 0
), "threads_per_row must divide WARP_SIZE"
num_threads = gmem_tiled_copy.size
tPrQPtr = self.compute_ptr(
mQ[None, 0], tQcQ_row, tidx, block, threads_per_row, num_threads
)
for m in cutlass.range_constexpr(cute.size(tQsQ.shape[1])):
q_ptr_i64 = utils.shuffle_sync(
tPrQPtr[m // threads_per_row],
m % threads_per_row,
width=threads_per_row,
)
q_gmem_ptr = cute.make_ptr(
mQ.element_type, q_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
)
if (
t0QcQ[0, m, 0][0]
< seqlen * self.qhead_per_kvhead
- block * self.m_block_size
- tQcQ_row[0][0]
):
mQ_cur = cute.make_tensor(q_gmem_ptr, (self.head_dim_padded,))
elems_per_load = cute.size(tQsQ.shape[0][0])
mQ_cur_copy = cute.tiled_divide(mQ_cur, (elems_per_load,))
for k in cutlass.range_constexpr(cute.size(tQsQ.shape[2])):
ki = tQcQ[0, 0, k][1] // elems_per_load
cute.copy(
gmem_thr_copy,
mQ_cur_copy[None, ki],
tQsQ[None, m, k],
pred=(
tQpQ[None, m, k]
if cutlass.const_expr(self.check_hdim_oob)
else None
),
)
# We don't need to clear the sQ smem tiles since we'll only write out the valid outputs
@cute.jit
def store_LSE(
self,
mLSE: cute.Tensor, # (qhead_per_kvhead, seqlen_q)
tLSErLSE: cute.Tensor, # (m_block_size, head_dim_padded)
tiled_mma: cute.TiledMma,
tidx: cutlass.Int32,
block: cutlass.Int32,
seqlen: cutlass.Int32,
):
thr_mma = tiled_mma.get_slice(tidx)
caccO = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
taccOcO = thr_mma.partition_C(caccO)
taccOcO_row = layout_utils.reshape_acc_to_mn(taccOcO)[None, 0]
assert cute.size(tLSErLSE) == cute.size(taccOcO_row)
threads_per_row = tiled_mma.tv_layout_C.shape[0][0]
assert (
cute.arch.WARP_SIZE % threads_per_row == 0
), "threads_per_row must divide WARP_SIZE"
assert cute.size(tLSErLSE) <= threads_per_row
num_threads = tiled_mma.size
tPrLSEPtr = self.compute_ptr(
mLSE, taccOcO_row, tidx, block, threads_per_row, num_threads
)
for m in cutlass.range_constexpr(cute.size(tLSErLSE)):
lse_ptr_i64 = utils.shuffle_sync(
tPrLSEPtr[m // threads_per_row],
m % threads_per_row,
width=threads_per_row,
)
lse_gmem_ptr = cute.make_ptr(
mLSE.element_type, lse_ptr_i64, cute.AddressSpace.gmem, assumed_align=4
)
row = block * self.m_block_size + taccOcO_row[m][0]
# Only the thread corresponding to column 0 writes out the lse to gmem
if taccOcO[0][1] == 0 and row < seqlen * self.qhead_per_kvhead:
mLSE_copy = cute.make_tensor(lse_gmem_ptr, (1,))
mLSE_copy[0] = tLSErLSE[m]
@cute.jit
def store_O(
self,
mO: cute.Tensor, # ((qhead_per_kvhead, seqlen_q), headdim)
tOrO: cute.Tensor, # (m_block_size, head_dim_padded) split across threads according to gmem_tiled_copy
gmem_tiled_copy: cute.TiledCopy,
tidx: cutlass.Int32,
block: cutlass.Int32,
seqlen: cutlass.Int32,
):
gmem_thr_copy = gmem_tiled_copy.get_slice(tidx)
cO = cute.make_identity_tensor((self.m_block_size, self.head_dim_padded))
tOcO = gmem_thr_copy.partition_S(cO)
t0OcO = gmem_thr_copy.get_slice(0).partition_S(cO)
tOpO = utils.predicate_k(tOcO, limit=mO.shape[1])
tOcO_row = tOcO[0, None, 0]
threads_per_row = gmem_tiled_copy.layout_tv_tiled.shape[0][0]
assert (
cute.arch.WARP_SIZE % threads_per_row == 0
), "threads_per_row must divide WARP_SIZE"
num_threads = gmem_tiled_copy.size
tPrOPtr = self.compute_ptr(
mO[None, 0], tOcO_row, tidx, block, threads_per_row, num_threads
)
for m in cutlass.range_constexpr(cute.size(tOrO.shape[1])):
o_ptr_i64 = utils.shuffle_sync(
tPrOPtr[m // threads_per_row],
m % threads_per_row,
width=threads_per_row,
)
o_gmem_ptr = cute.make_ptr(
mO.element_type, o_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
)
if (
t0OcO[0, m, 0][0]
< seqlen * self.qhead_per_kvhead
- block * self.m_block_size
- tOcO_row[0][0]
):
mO_cur = cute.make_tensor(o_gmem_ptr, (self.head_dim_padded,))
elems_per_load = cute.size(tOrO.shape[0][0])
mO_cur_copy = cute.tiled_divide(mO_cur, (elems_per_load,))
for k in cutlass.range_constexpr(cute.size(tOrO.shape[2])):
ki = tOcO[0, 0, k][1] // elems_per_load
cute.copy(
gmem_thr_copy,
tOrO[None, m, k],
mO_cur_copy[None, ki],
pred=(
tOpO[None, m, k]
if cutlass.const_expr(self.check_hdim_oob)
else None
),
)
@@ -0,0 +1,393 @@
import math
from dataclasses import dataclass
from typing import Optional, Type
import cutlass
import cutlass.cute as cute
from cutlass import Int32, const_expr
from cutlass.cute import FastDivmodDivisor
from cutlass.cute.nvgpu import cpasync
from quack.cute_dsl_utils import ParamsBase
from sglang.jit_kernel.flash_attn.cute import utils
@dataclass
class PagedKVManager(ParamsBase):
mPageTable: cute.Tensor
mK_paged: cute.Tensor
mV_paged: cute.Tensor
mSFK_paged: Optional[cute.Tensor]
mSFV_paged: Optional[cute.Tensor]
thread_idx: Int32
page_size_divmod: FastDivmodDivisor
seqlen_k: Int32
leftpad_k: Int32
n_block_size: Int32
num_threads: cutlass.Constexpr[Int32]
head_dim_padded: cutlass.Constexpr[Int32]
head_dim_v_padded: cutlass.Constexpr[Int32]
arch: cutlass.Constexpr[Int32]
v_gmem_transposed: cutlass.Constexpr[bool]
gmem_threads_per_row: cutlass.Constexpr[Int32]
page_entry_per_thread: Int32
async_copy_elems: Int32
gmem_tiled_copy_KV: cute.TiledCopy
gmem_thr_copy_KV: cute.TiledCopy
gmem_tiled_copy_sf_KV: Optional[cute.TiledCopy]
gmem_thr_copy_sf_KV: Optional[cute.TiledCopy]
tPrPage: cute.Tensor
tPrPageOffset: cute.Tensor
tKpK: cute.Tensor
tVpV: cute.Tensor
@staticmethod
def create(
mPageTable: cute.Tensor,
mK_paged: cute.Tensor,
mV_paged: cute.Tensor,
page_size_divmod: FastDivmodDivisor,
bidb: Int32,
bidh: Int32,
thread_idx: Int32,
seqlen_k: Int32,
leftpad_k: Int32,
n_block_size: cutlass.Constexpr[Int32],
head_dim_padded: cutlass.Constexpr[Int32],
head_dim_v_padded: cutlass.Constexpr[Int32],
num_threads: cutlass.Constexpr[Int32],
dtype: Type[cutlass.Numeric],
mSFK_paged: Optional[cute.Tensor] = None,
mSFV_paged: Optional[cute.Tensor] = None,
arch: cutlass.Constexpr[int] = 100,
):
# SM100 transposes V in gmem to (dv, page_size, num_pages);
# SM90 keeps V as (page_size, dv, num_pages), same layout as K.
v_gmem_transposed = arch != 90
universal_copy_bits = 128
async_copy_elems = universal_copy_bits // dtype.width
dtype_bytes = dtype.width // 8
gmem_k_block_size = math.gcd(
head_dim_padded,
head_dim_v_padded,
128 // dtype_bytes,
)
assert gmem_k_block_size % async_copy_elems == 0
gmem_threads_per_row = gmem_k_block_size // async_copy_elems
assert cute.arch.WARP_SIZE % gmem_threads_per_row == 0
atom_async_copy = cute.make_copy_atom(
cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
dtype,
num_bits_per_copy=universal_copy_bits,
)
thr_layout = cute.make_ordered_layout(
(num_threads // gmem_threads_per_row, gmem_threads_per_row),
order=(1, 0),
)
val_layout = cute.make_layout((1, async_copy_elems))
gmem_tiled_copy_KV = cute.make_tiled_copy_tv(
atom_async_copy, thr_layout, val_layout
)
gmem_thr_copy_KV = gmem_tiled_copy_KV.get_slice(thread_idx)
page_entry_per_thread = n_block_size // num_threads
if const_expr(mSFK_paged is not None or mSFV_paged is not None):
atom_async_copy_sf = cute.make_copy_atom(
cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.ALWAYS),
dtype,
num_bits_per_copy=32,
)
thr_layout_sf = cute.make_ordered_layout(
((num_threads // gmem_threads_per_row, gmem_threads_per_row), 1),
order=((1, 0), 2),
)
val_layout_sf = cute.make_layout((1, 4))
gmem_tiled_copy_sf_KV = cute.make_tiled_copy_tv(
atom_async_copy_sf,
thr_layout_sf,
val_layout_sf,
)
gmem_thr_copy_sf_KV = gmem_tiled_copy_sf_KV.get_slice(thread_idx)
else:
gmem_tiled_copy_sf_KV = None
gmem_thr_copy_sf_KV = None
tPrPage = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
tPrPageOffset = cute.make_rmem_tensor((page_entry_per_thread,), Int32)
mPageTable = mPageTable[bidb, None]
mK_paged = mK_paged[None, None, bidh, None]
mV_paged = mV_paged[None, None, bidh, None]
if const_expr(mSFK_paged is not None):
mSFK_paged = mSFK_paged[None, None, bidh, None]
if const_expr(mSFV_paged is not None):
mSFV_paged = mSFV_paged[None, None, bidh, None]
cK = cute.make_identity_tensor((n_block_size, head_dim_padded))
tKcK = gmem_thr_copy_KV.partition_S(cK)
tKpK = utils.predicate_k(tKcK, limit=mK_paged.shape[1])
if const_expr(head_dim_padded == head_dim_v_padded):
tVpV = tKpK
else:
cV = cute.make_identity_tensor((n_block_size, head_dim_v_padded))
tVcV = gmem_thr_copy_KV.partition_S(cV)
# When V is transposed in gmem, dv is shape[0]; otherwise dv is shape[1] (same as K)
V_limit = cute.size(mV_paged.shape[0 if v_gmem_transposed else 1])
tVpV = utils.predicate_k(tVcV, limit=V_limit)
return PagedKVManager(
mPageTable,
mK_paged,
mV_paged,
mSFK_paged,
mSFV_paged,
thread_idx,
page_size_divmod,
seqlen_k,
leftpad_k,
n_block_size,
num_threads,
head_dim_padded,
head_dim_v_padded,
arch,
v_gmem_transposed,
gmem_threads_per_row,
page_entry_per_thread,
async_copy_elems,
gmem_tiled_copy_KV,
gmem_thr_copy_KV,
gmem_tiled_copy_sf_KV,
gmem_thr_copy_sf_KV,
tPrPage,
tPrPageOffset,
tKpK,
tVpV,
)
@cute.jit
def load_page_table(self, n_block: Int32):
for i in cutlass.range(self.page_entry_per_thread, unroll=1):
row = (
i * self.num_threads
+ (self.thread_idx % self.gmem_threads_per_row)
* (self.num_threads // self.gmem_threads_per_row)
+ (self.thread_idx // self.gmem_threads_per_row)
)
row_idx = n_block * self.n_block_size + row
page_idx, page_offset = divmod(
row_idx + self.leftpad_k, self.page_size_divmod
)
is_valid = (
(i + 1) * self.num_threads <= self.n_block_size
or row < self.n_block_size
) and row_idx < self.seqlen_k
page = self.mPageTable[page_idx] if is_valid else 0
self.tPrPage[i] = page
self.tPrPageOffset[i] = page_offset
@cute.jit
def compute_X_ptr(self, K_or_V: str, d_offset: int = 0):
tPrXPtr = cute.make_rmem_tensor((self.page_entry_per_thread,), cutlass.Int64)
mX = self.mK_paged if const_expr(K_or_V == "K") else self.mV_paged
# K is always (page_size, d, num_pages). V matches K when not transposed,
# but is (dv, page_size, num_pages) when transposed (SM100).
transposed = const_expr(K_or_V == "V" and self.v_gmem_transposed)
for i in cutlass.range(self.page_entry_per_thread, unroll=1):
page = self.tPrPage[i]
page_offset = self.tPrPageOffset[i]
if const_expr(transposed):
tPrXPtr[i] = utils.elem_pointer(
mX, (d_offset, page_offset, page)
).toint()
else:
tPrXPtr[i] = utils.elem_pointer(
mX, (page_offset, d_offset, page)
).toint()
return tPrXPtr
@cute.jit
def _flatten_smem_sm100(self, sX: cute.Tensor, K_or_V: str):
"""Flatten SM100 smem ((a,b), cta_split, k) to (a,(b,k)); transpose V to (d,page_size)."""
sX_pi = cute.make_tensor(
sX.iterator,
cute.make_layout(
(sX.shape[0][0], (sX.shape[0][1], sX.shape[2])),
stride=(sX.stride[0][0], (sX.stride[0][1], sX.stride[2])),
),
)
if const_expr(K_or_V == "V"):
sX_pi = cute.make_tensor(
sX_pi.iterator, cute.select(sX_pi.layout, mode=[1, 0])
)
return sX_pi
@cute.jit
def _copy_row_async(
self,
tXsX: cute.Tensor,
tXcX: cute.Tensor,
mX_paged_cur_copy: cute.Tensor,
m: Int32,
should_load: cute.Tensor,
):
"""Issue cp.async copies for one row across all k-tiles."""
for k in cutlass.range_constexpr(cute.size(tXsX, mode=[2])):
ki = tXcX[0, 0, k][1] // self.async_copy_elems
mX_paged_cur_copy_ki = mX_paged_cur_copy[None, ki]
tXsX_k = tXsX[None, m, k]
mX_paged_cur_copy_ki = cute.make_tensor(
mX_paged_cur_copy_ki.iterator, tXsX_k.layout
)
cute.copy(
self.gmem_tiled_copy_KV,
mX_paged_cur_copy_ki,
tXsX_k,
pred=should_load,
)
@cute.jit
def compute_sf_X_ptr(self, K_or_V: str):
tPrXPtr = cute.make_rmem_tensor((self.page_entry_per_thread,), cutlass.Int64)
for i in cutlass.range(self.page_entry_per_thread, unroll=1):
page = self.tPrPage[i]
page_offset = self.tPrPageOffset[i]
if const_expr(K_or_V == "K"):
tPrXPtr[i] = utils.elem_pointer(
self.mSFK_paged, (page_offset, 0, page)
).toint()
else:
tPrXPtr[i] = utils.elem_pointer(
self.mSFV_paged, (0, page_offset, page)
).toint()
return tPrXPtr
@cute.jit
def load_KV(self, n_block: Int32, sX: cute.Tensor, K_or_V: str):
assert K_or_V in ("K", "V")
tPrXPtr = self.compute_X_ptr(K_or_V)
if const_expr(self.arch == 90):
# SM90: sX is already stage-sliced by caller (sK[None, None, stage]).
# Flatten hierarchical modes to get (n_block_size, head_dim).
sX_pi = cute.group_modes(sX, 0, 1)
# SM90 does NOT transpose V here (it's transposed via utils.transpose_view before MMA)
else:
sX_pi = self._flatten_smem_sm100(sX, K_or_V)
head_dim = (
self.head_dim_v_padded
if const_expr(K_or_V == "V")
else self.head_dim_padded
)
cX = cute.make_identity_tensor((self.n_block_size, head_dim))
tXsX = self.gmem_thr_copy_KV.partition_D(sX_pi)
tXcX = self.gmem_thr_copy_KV.partition_S(cX)
tXc0X = self.gmem_thr_copy_KV.get_slice(0).partition_S(cX)
seqlenk_row_limit = (
self.seqlen_k - n_block * self.n_block_size - tXcX[0][0]
if n_block >= 0
else 0
)
for m in cutlass.range_constexpr(cute.size(tXsX, mode=[1])):
row_valid = tXc0X[0, m, 0][0] < seqlenk_row_limit
should_load = cute.make_fragment_like(tXsX[(0, None), m, 0], cute.Boolean)
should_load.fill(row_valid)
x_ptr_i64 = utils.shuffle_sync(
tPrXPtr[m // self.gmem_threads_per_row],
m % self.gmem_threads_per_row,
width=self.gmem_threads_per_row,
)
x_gmem_ptr = cute.make_ptr(
self.mK_paged.element_type,
x_ptr_i64,
cute.AddressSpace.gmem,
assumed_align=16,
)
mX_paged_cur = cute.make_tensor(x_gmem_ptr, cute.make_layout((head_dim,)))
mX_paged_cur_copy = cute.tiled_divide(
mX_paged_cur, (self.async_copy_elems,)
)
self._copy_row_async(tXsX, tXcX, mX_paged_cur_copy, m, should_load)
@cute.jit
def load_sf_KV(self, n_block: Int32, sSFX: cute.Tensor, K_or_V: str):
# sSFX expected as SFK or SFV
assert (
cute.rank(sSFX) == 3
), f"mismatched rank for sSFX, expected 3 but got {cute.rank(sSFX)}"
assert self.gmem_thr_copy_sf_KV is not None
# sSFK: tensor<ptr<f8E8M0FNU, smem, align<1024>> o ((((32,4),1),(32,1)),1,4,2):((((16,4),0),(0,0)),0,1,512)>
# sSFV: tensor<ptr<f8E8M0FNU, smem, align<1024>> o ((((32,4),1),(32,1)),1,4,2):((((16,4),0),(0,0)),0,1,512)>
head_dim = (
self.head_dim_v_padded
if const_expr(K_or_V == "V")
else self.head_dim_padded
)
sSFX_cpt = cute.filter_zeros(sSFX)
sSFX_cpt_shape_nd = (self.n_block_size, head_dim // 32)
sSFX_cpt_layout_nd = cute.make_ordered_layout(
sSFX_cpt_shape_nd,
order=(0, 1),
)
# (tile_n, 4)
sSFX_cpt_nd = cute.composition(sSFX_cpt, sSFX_cpt_layout_nd)
cX = cute.make_identity_tensor(sSFX_cpt_shape_nd)
# ((V, 1), M, 1)
tXsX = self.gmem_thr_copy_sf_KV.partition_D(sSFX_cpt_nd)
tXcX = self.gmem_thr_copy_sf_KV.partition_S(cX)
tXc0X = self.gmem_thr_copy_sf_KV.get_slice(0).partition_S(cX)
seqlenk_row_limit = (
self.seqlen_k - n_block * self.n_block_size - tXcX[0][0]
if n_block >= 0
else 0
)
tPrSFXPtr = self.compute_sf_X_ptr(K_or_V)
assert cute.size(tPrSFXPtr) == cute.size(
tXsX, mode=[1]
), "SFX pointer size mismatch"
# loop over rows
for m in cutlass.range_constexpr(cute.size(tXsX, mode=[1])):
row_valid = tXc0X[0, m, 0][0] < seqlenk_row_limit
should_load = cute.make_fragment_like(
tXsX[(0, None), m, None], cute.Boolean
)
should_load.fill(row_valid)
# Make gmem tensor of size (4,) using tPrSFXPtr
# Simplified version of load_KV, no shuffle, 4 elements to copy (hdim = 128)
sfx_ptr_i64 = tPrSFXPtr[m]
sfx_gmem_ptr = cute.make_ptr(
self.mSFK_paged.element_type,
sfx_ptr_i64,
cute.AddressSpace.gmem,
assumed_align=4,
)
sf_frg_layout = cute.make_layout(((head_dim // 32, 1), 1))
mSFX_paged_cur = cute.make_tensor(sfx_gmem_ptr, sf_frg_layout)
assert cute.size(mSFX_paged_cur) == cute.size(
tXsX[None, 0, None]
), "SFX gmem-smem tensor size mismatch"
cute.copy(
self.gmem_tiled_copy_sf_KV,
mSFX_paged_cur,
tXsX[None, m, None],
pred=should_load,
)
@@ -0,0 +1,412 @@
# Copyright (c) 2025, Tri Dao.
from dataclasses import dataclass
from typing import Optional
import cutlass.cute as cute
from cutlass import Boolean, Int32, const_expr
from cutlass.cutlass_dsl import dsl_user_op, if_generate
from cutlass.pipeline import NamedBarrier as NamedBarrierOg
from cutlass.pipeline import PipelineAsync as PipelineAsyncOg
from cutlass.pipeline import PipelineAsyncUmma as PipelineAsyncUmmaOg
from cutlass.pipeline import PipelineCpAsync as PipelineCpAsyncOg
from cutlass.pipeline import PipelineState
from cutlass.pipeline import PipelineTmaAsync as PipelineTmaAsyncOg
from cutlass.pipeline import PipelineTmaUmma as PipelineTmaUmmaOg
from cutlass.pipeline import PipelineUmmaAsync as PipelineUmmaAsyncOg
from cutlass.pipeline import PipelineUserType
def _override_create(parent_cls, child_cls):
"""Create a static factory that constructs parent_cls then re-classes to child_cls."""
@staticmethod
def create(*args, **kwargs):
obj = parent_cls.create(*args, **kwargs)
# Can't assign to __class__ directly since the dataclass is frozen
object.__setattr__(obj, "__class__", child_cls)
return obj
return create
def _make_state(index: Int32, phase: Int32) -> PipelineState:
"""Construct a PipelineState from index and phase (count/stages unused by callers)."""
return PipelineState(stages=0, count=Int32(0), index=index, phase=phase)
class PipelineStateSimple:
"""
Pipeline state contains an index and phase bit corresponding to the current position in the circular buffer.
Use a single Int32 to store both the index and phase bit, then we use divmod to get the
index and phase. If stages is a power of 2, divmod turns into bit twiddling.
"""
def __init__(self, stages: int, phase_index: Int32):
self._stages = stages
self._phase_index = phase_index
def clone(self) -> "PipelineStateSimple":
return PipelineStateSimple(self.stages, self._phase_index)
@property
def stages(self) -> int:
return self._stages
@property
def index(self) -> Int32:
if const_expr(self._stages == 1):
return Int32(0)
else:
return self._phase_index % self._stages
@property
def phase(self) -> Int32:
# PTX docs say that the phase parity needs to be 0 or 1, so by right we need to
# take modulo 2. But in practice just passing the phase in without modulo works fine.
if const_expr(self._stages == 1):
return self._phase_index
else:
return self._phase_index // self._stages
def advance(self):
if const_expr(self._stages == 1):
self._phase_index ^= 1
else:
self._phase_index += 1
def __extract_mlir_values__(self):
phase_index = self._phase_index
return [phase_index.ir_value()]
def __new_from_mlir_values__(self, values):
return PipelineStateSimple(self.stages, Int32(values[0]))
def make_pipeline_state(type: PipelineUserType, stages: int):
"""
Creates a pipeline state. Producers are assumed to start with an empty buffer and have a flipped phase bit of 1.
"""
if type is PipelineUserType.Producer:
return PipelineStateSimple(stages, Int32(stages))
elif type is PipelineUserType.Consumer:
return PipelineStateSimple(stages, Int32(0))
else:
assert (
False
), "Error: invalid PipelineUserType specified for make_pipeline_state."
# ── Shared helpers ───────────────────────────────────────────────────────────
def _call_with_elect_one(parent_method, self, state, elect_one, syncwarp, loc, ip):
"""Optionally wrap a parent pipeline method call in sync_warp + elect_one."""
if const_expr(elect_one):
if const_expr(syncwarp):
cute.arch.sync_warp()
with cute.arch.elect_one():
parent_method(self, state, loc=loc, ip=ip)
else:
parent_method(self, state, loc=loc, ip=ip)
# ── Mixin: _w_index / _w_index_phase variants that delegate to parent ───────
# Each parent class has PipelineState-based methods (producer_acquire, producer_commit,
# consumer_wait, consumer_release). The _w_index_phase variants just construct a
# PipelineState from (index, phase) and delegate.
class _PipelineIndexPhaseMixin:
"""Mixin providing _w_index_phase / _w_index methods that delegate to PipelineState-based parents."""
@dsl_user_op
def producer_acquire_w_index_phase(
self,
index: Int32,
phase: Int32,
try_acquire_token: Optional[Boolean] = None,
*,
loc=None,
ip=None,
):
state = _make_state(index, phase)
# Call the parent's producer_acquire (which takes PipelineState)
self.producer_acquire(state, try_acquire_token, loc=loc, ip=ip)
@dsl_user_op
def producer_commit_w_index(self, index: Int32, *, loc=None, ip=None):
state = _make_state(index, Int32(0))
self.producer_commit(state, loc=loc, ip=ip)
@dsl_user_op
def consumer_wait_w_index_phase(
self,
index: Int32,
phase: Int32,
try_wait_token: Optional[Boolean] = None,
*,
loc=None,
ip=None,
):
state = _make_state(index, phase)
self.consumer_wait(state, try_wait_token, loc=loc, ip=ip)
@dsl_user_op
def consumer_release_w_index(self, index: Int32, *, loc=None, ip=None):
state = _make_state(index, Int32(0))
self.consumer_release(state, loc=loc, ip=ip)
# ── NamedBarrier ─────────────────────────────────────────────────────────────
@dataclass(frozen=True)
class NamedBarrier(NamedBarrierOg):
create = _override_create(NamedBarrierOg, None) # patched below
@dsl_user_op
def arrive_w_index(self, index: Int32, *, loc=None, ip=None) -> None:
"""
The aligned flavor of arrive is used when all threads in the CTA will execute the
same instruction. See PTX documentation.
"""
cute.arch.barrier_arrive(
barrier_id=self.barrier_id + index,
number_of_threads=self.num_threads,
loc=loc,
ip=ip,
)
@dsl_user_op
def arrive_and_wait_w_index(self, index: Int32, *, loc=None, ip=None) -> None:
cute.arch.barrier(
barrier_id=self.barrier_id + index,
number_of_threads=self.num_threads,
loc=loc,
ip=ip,
)
NamedBarrier.create = _override_create(NamedBarrierOg, NamedBarrier)
# ── PipelineAsync ────────────────────────────────────────────────────────────
@dataclass(frozen=True)
class PipelineAsync(_PipelineIndexPhaseMixin, PipelineAsyncOg):
"""
PipelineAsync with optional elect_one for producer_commit and consumer_release.
When elect_one_*=True (set at create time), only one elected thread per warp
signals the barrier arrive. This is useful when the mask count is set to 1 per warp.
Args (to create):
elect_one_commit: If True, only elected thread signals producer_commit.
syncwarp_before_commit: If True (default), issue syncwarp before elect_one.
elect_one_release: If True, only elected thread signals consumer_release.
syncwarp_before_release: If True (default), issue syncwarp before elect_one.
Set syncwarp to False when threads are already converged (e.g. after wgmma wait_group).
"""
_elect_one_commit: bool = False
_syncwarp_before_commit: bool = True
_elect_one_release: bool = False
_syncwarp_before_release: bool = True
@staticmethod
def create(
*args,
elect_one_commit: bool = False,
syncwarp_before_commit: bool = True,
elect_one_release: bool = False,
syncwarp_before_release: bool = True,
**kwargs,
):
obj = PipelineAsyncOg.create(*args, **kwargs)
object.__setattr__(obj, "__class__", PipelineAsync)
object.__setattr__(obj, "_elect_one_commit", elect_one_commit)
object.__setattr__(obj, "_syncwarp_before_commit", syncwarp_before_commit)
object.__setattr__(obj, "_elect_one_release", elect_one_release)
object.__setattr__(obj, "_syncwarp_before_release", syncwarp_before_release)
return obj
@dsl_user_op
def producer_commit(self, state: PipelineState, *, loc=None, ip=None):
_call_with_elect_one(
PipelineAsyncOg.producer_commit,
self,
state,
self._elect_one_commit,
self._syncwarp_before_commit,
loc,
ip,
)
@dsl_user_op
def consumer_release(self, state: PipelineState, *, loc=None, ip=None):
_call_with_elect_one(
PipelineAsyncOg.consumer_release,
self,
state,
self._elect_one_release,
self._syncwarp_before_release,
loc,
ip,
)
# _w_index variants inherited from _PipelineIndexPhaseMixin, which delegate
# to producer_commit / consumer_release above.
# ── PipelineCpAsync ──────────────────────────────────────────────────────────
@dataclass(frozen=True)
class PipelineCpAsync(_PipelineIndexPhaseMixin, PipelineCpAsyncOg):
_elect_one_release: bool = False
_syncwarp_before_release: bool = True
@staticmethod
def create(
*args,
elect_one_release: bool = False,
syncwarp_before_release: bool = True,
**kwargs,
):
obj = PipelineCpAsyncOg.create(*args, **kwargs)
object.__setattr__(obj, "__class__", PipelineCpAsync)
object.__setattr__(obj, "_elect_one_release", elect_one_release)
object.__setattr__(obj, "_syncwarp_before_release", syncwarp_before_release)
return obj
@dsl_user_op
def consumer_release(self, state: PipelineState, *, loc=None, ip=None):
_call_with_elect_one(
PipelineCpAsyncOg.consumer_release,
self,
state,
self._elect_one_release,
self._syncwarp_before_release,
loc,
ip,
)
# _w_index variants inherited from _PipelineIndexPhaseMixin.
# ── PipelineTmaAsync ────────────────────────────────────────────────────────
@dataclass(frozen=True)
class PipelineTmaAsync(_PipelineIndexPhaseMixin, PipelineTmaAsyncOg):
"""Override producer_acquire to take in extra_tx_count parameter."""
@dsl_user_op
def producer_acquire(
self,
state: PipelineState,
try_acquire_token: Optional[Boolean] = None,
extra_tx_count: int = 0,
*,
loc=None,
ip=None,
):
"""
TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
"""
if_generate(
try_acquire_token is None or try_acquire_token == 0,
lambda: self.sync_object_empty.wait(
state.index, state.phase, loc=loc, ip=ip
),
loc=loc,
ip=ip,
)
if const_expr(extra_tx_count == 0):
self.sync_object_full.arrive(
state.index, self.producer_mask, loc=loc, ip=ip
)
else:
tx_count = self.sync_object_full.tx_count + extra_tx_count
self.sync_object_full.arrive_and_expect_tx(
state.index, tx_count, loc=loc, ip=ip
)
PipelineTmaAsync.create = _override_create(PipelineTmaAsyncOg, PipelineTmaAsync)
# ── PipelineTmaUmma ─────────────────────────────────────────────────────────
@dataclass(frozen=True)
class PipelineTmaUmma(_PipelineIndexPhaseMixin, PipelineTmaUmmaOg):
"""Override producer_acquire to take in extra_tx_count parameter."""
@dsl_user_op
def producer_acquire(
self,
state: PipelineState,
try_acquire_token: Optional[Boolean] = None,
extra_tx_count: int = 0,
*,
loc=None,
ip=None,
):
"""
TMA producer commit conditionally waits on buffer empty and sets the transaction barrier for leader threadblocks.
"""
if_generate(
try_acquire_token is None or try_acquire_token == 0,
lambda: self.sync_object_empty.wait(
state.index, state.phase, loc=loc, ip=ip
),
loc=loc,
ip=ip,
)
if const_expr(extra_tx_count == 0):
if_generate(
self.is_leader_cta,
lambda: self.sync_object_full.arrive(
state.index, self.producer_mask, loc=loc, ip=ip
),
loc=loc,
ip=ip,
)
else:
tx_count = self.sync_object_full.tx_count + extra_tx_count
if_generate(
self.is_leader_cta,
lambda: self.sync_object_full.arrive_and_expect_tx(
state.index, tx_count, loc=loc, ip=ip
),
loc=loc,
ip=ip,
)
PipelineTmaUmma.create = _override_create(PipelineTmaUmmaOg, PipelineTmaUmma)
# ── PipelineUmmaAsync ───────────────────────────────────────────────────────
@dataclass(frozen=True)
class PipelineUmmaAsync(_PipelineIndexPhaseMixin, PipelineUmmaAsyncOg):
pass
PipelineUmmaAsync.create = _override_create(PipelineUmmaAsyncOg, PipelineUmmaAsync)
# ── PipelineAsyncUmma ───────────────────────────────────────────────────────
@dataclass(frozen=True)
class PipelineAsyncUmma(_PipelineIndexPhaseMixin, PipelineAsyncUmmaOg):
pass
PipelineAsyncUmma.create = _override_create(PipelineAsyncUmmaOg, PipelineAsyncUmma)
@@ -0,0 +1,75 @@
[build-system]
requires = ["setuptools>=75", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[project]
name = "flash-attn-4"
dynamic = ["version"]
description = "Flash Attention CUTE (CUDA Template Engine) implementation"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "BSD 3-Clause License"}
authors = [
{name = "Tri Dao"},
]
classifiers = [
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"nvidia-cutlass-dsl>=4.5.2",
"torch",
"einops",
"typing_extensions",
"apache-tvm-ffi>=0.1.5,<0.2",
"torch-c-dlpack-ext",
"quack-kernels>=0.5.0",
]
[project.optional-dependencies]
cu13 = ["nvidia-cutlass-dsl[cu13]>=4.5.2"]
dev = [
"pytest",
"pytest-xdist",
"ruff",
]
[project.urls]
Homepage = "https://github.com/Dao-AILab/flash-attention"
Repository = "https://github.com/Dao-AILab/flash-attention"
[tool.setuptools]
packages = ["flash_attn.cute"]
package-dir = {"flash_attn.cute" = "."}
[tool.setuptools_scm]
root = "../.."
tag_regex = "^fa4-v(?P<version>.+)$"
git_describe_command = "git describe --dirty --tags --long --match 'fa4-v*'"
fallback_version = "0.0.0"
[[tool.uv.index]]
name = "pytorch-cu130"
url = "https://download.pytorch.org/whl/cu130"
explicit = true
[tool.uv.sources]
torch = [
{ index = "pytorch-cu130", marker = "extra == 'cu13'" },
]
[tool.ruff]
line-length = 100
[tool.ruff.lint]
ignore = [
"E731", # do not assign a lambda expression, use a def
"E741", # Do not use variables named 'I', 'O', or 'l'
"F841", # local variable is assigned to but never used
"D102", # Missing docstring in public methods
]
@@ -0,0 +1,331 @@
from dataclasses import dataclass
from typing import Optional
import cutlass
import cutlass.cute as cute
from cutlass import Int32, const_expr
from quack import copy_utils
"""
This consolidates all the info related to sequence length. This is so that we can do all
the gmem reads once at the beginning of each tile, rather than having to repeat these reads
to compute various things like n_block_min, n_block_max, etc.
"""
@dataclass(frozen=True)
class SeqlenInfo:
offset: Int32
offset_padded: Int32
seqlen: Int32
has_cu_seqlens: cutlass.Constexpr[bool] = False
@staticmethod
def create(
batch_idx: Int32,
seqlen_static: Int32,
cu_seqlens: Optional[cute.Tensor] = None,
seqused: Optional[cute.Tensor] = None,
tile: cutlass.Constexpr[int] = 128,
):
offset = 0 if const_expr(cu_seqlens is None) else cu_seqlens[batch_idx]
offset_padded = (
0
if const_expr(cu_seqlens is None)
# Add divby so that the compiler knows the alignment when moving by offset_padded
else cute.assume((offset + batch_idx * tile) // tile * tile, divby=tile)
)
if const_expr(seqused is not None):
seqlen = seqused[batch_idx]
elif const_expr(cu_seqlens is not None):
seqlen = cu_seqlens[batch_idx + 1] - cu_seqlens[batch_idx]
else:
seqlen = seqlen_static
return SeqlenInfo(
offset, offset_padded, seqlen, has_cu_seqlens=cu_seqlens is not None
)
def offset_batch(
self,
mT: cute.Tensor,
batch_idx: Int32,
dim: int,
padded: cutlass.Constexpr[bool] = False,
multiple: int = 1,
) -> cute.Tensor:
"""Offset a tensor by batch index. batch dim is at position `dim`, seqlen is at dim=0."""
if const_expr(not self.has_cu_seqlens):
idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mT) - 1 - dim)
return mT[idx]
else:
off = multiple * (
self.offset if const_expr(not padded) else self.offset_padded
)
offset = off if const_expr(cute.rank(mT.shape[0]) == 1) else (0, off)
idx = (offset,) + (None,) * (cute.rank(mT) - 1)
return cute.domain_offset(idx, mT)
@dataclass(frozen=True)
class SeqlenInfoQK:
offset_q: Int32
offset_k: Int32
padded_offset_q: Int32
padded_offset_k: Int32
seqlen_q: Int32
seqlen_k: Int32
m_block_offset: Int32
block_idx_offset: Int32
num_n_blocks: Int32
has_cu_seqlens_q: cutlass.Constexpr[bool]
has_cu_seqlens_k: cutlass.Constexpr[bool]
has_seqused_q: cutlass.Constexpr[bool]
has_seqused_k: cutlass.Constexpr[bool]
@staticmethod
def create(
batch_idx: Int32,
seqlen_q_static: Int32,
seqlen_k_static: Int32,
mCuSeqlensQ: Optional[cute.Tensor] = None,
mCuSeqlensK: Optional[cute.Tensor] = None,
mSeqUsedQ: Optional[cute.Tensor] = None,
mSeqUsedK: Optional[cute.Tensor] = None,
mCuTotalMBlocks: Optional[cute.Tensor] = None,
mCuBlockIdxOffsets: Optional[cute.Tensor] = None,
tile_m: cutlass.Constexpr[Int32] = 128,
tile_n: cutlass.Constexpr[Int32] = 128,
):
offset_q = 0 if const_expr(mCuSeqlensQ is None) else mCuSeqlensQ[batch_idx]
offset_k = 0 if const_expr(mCuSeqlensK is None) else mCuSeqlensK[batch_idx]
padded_offset_q = (
0
if const_expr(mCuSeqlensQ is None)
else cute.assume(
(offset_q + batch_idx * tile_m) // tile_m * tile_m, divby=tile_m
)
)
padded_offset_k = (
0
if const_expr(mCuSeqlensK is None)
else cute.assume(
(offset_k + batch_idx * tile_n) // tile_n * tile_n, divby=tile_n
)
)
if const_expr(mSeqUsedQ is not None):
seqlen_q = mSeqUsedQ[batch_idx]
else:
seqlen_q = (
seqlen_q_static
if const_expr(mCuSeqlensQ is None)
else mCuSeqlensQ[batch_idx + 1] - offset_q
)
if const_expr(mSeqUsedK is not None):
seqlen_k = mSeqUsedK[batch_idx]
else:
seqlen_k = (
seqlen_k_static
if const_expr(mCuSeqlensK is None)
else mCuSeqlensK[batch_idx + 1] - offset_k
)
m_block_offset = (
0 if const_expr(mCuTotalMBlocks is None) else mCuTotalMBlocks[batch_idx]
)
num_n_blocks = (seqlen_k + tile_n - 1) // tile_n
block_idx_offset = (
mCuBlockIdxOffsets[batch_idx]
if const_expr(mCuBlockIdxOffsets is not None)
else m_block_offset * num_n_blocks
)
return SeqlenInfoQK(
offset_q,
offset_k,
padded_offset_q,
padded_offset_k,
seqlen_q,
seqlen_k,
m_block_offset,
block_idx_offset,
num_n_blocks,
has_cu_seqlens_q=mCuSeqlensQ is not None,
has_cu_seqlens_k=mCuSeqlensK is not None,
has_seqused_q=mSeqUsedQ is not None,
has_seqused_k=mSeqUsedK is not None,
)
def offset_batch_Q(
self,
mQ: cute.Tensor,
batch_idx: Int32,
dim: int,
padded: cutlass.Constexpr[bool] = False,
ragged: cutlass.Constexpr[bool] = False,
) -> cute.Tensor:
"""Seqlen must be the first dimension of mQ"""
if const_expr(not ragged):
if const_expr(not self.has_cu_seqlens_q):
idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mQ) - 1 - dim)
return mQ[idx]
else:
offset_q = (
self.offset_q if const_expr(not padded) else self.padded_offset_q
)
offset_q = (
offset_q
if const_expr(cute.rank(mQ.shape[0]) == 1)
else (None, offset_q)
)
idx = (offset_q,) + (None,) * (cute.rank(mQ) - 1)
return cute.domain_offset(idx, mQ)
else:
if const_expr(not self.has_cu_seqlens_q):
offset_q = 0
idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mQ) - 1 - dim)
mQ = mQ[idx]
else:
offset_q = (
self.offset_q if const_expr(not padded) else self.padded_offset_q
)
if const_expr(cute.rank(mQ.shape[0]) == 1):
return copy_utils.offset_ragged_tensor(
mQ, offset_q, self.seqlen_q, ragged_dim=0, ptr_shift=True
)
else: # PackGQA
assert cute.rank(mQ.shape[0]) == 2
# Unpack before calling offset_ragged_tensor, then pack
idx = ((None, None),) + (None,) * (cute.rank(mQ) - 1)
mQ = mQ[idx]
mQ = copy_utils.offset_ragged_tensor(
mQ, offset_q, self.seqlen_q, ragged_dim=1, ptr_shift=True
)
return cute.group_modes(mQ, 0, 2)
def offset_batch_K(
self,
mK: cute.Tensor,
batch_idx: Int32,
dim: int,
padded: cutlass.Constexpr[bool] = False,
ragged: cutlass.Constexpr[bool] = False,
multiple: int = 1,
) -> cute.Tensor:
"""Seqlen must be the first dimension of mK"""
if const_expr(not ragged):
if const_expr(not self.has_cu_seqlens_k):
idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mK) - 1 - dim)
return mK[idx]
else:
offset_k = (
self.offset_k if const_expr(not padded) else self.padded_offset_k
)
offset_k *= multiple
idx = (offset_k,) + (None,) * (cute.rank(mK) - 1)
return cute.domain_offset(idx, mK)
else:
if const_expr(not self.has_cu_seqlens_k):
offset_k = 0
idx = (None,) * dim + (batch_idx,) + (None,) * (cute.rank(mK) - 1 - dim)
mK = mK[idx]
else:
offset_k = (
self.offset_k if const_expr(not padded) else self.padded_offset_k
)
offset_k *= multiple
return copy_utils.offset_ragged_tensor(
mK, offset_k, self.seqlen_k, ragged_dim=0, ptr_shift=True
)
@dataclass(frozen=True)
class SeqlenInfoQKNewK:
"""Sequence length info for append-KV with left-padding and new K support.
Extends SeqlenInfoQK with:
- leftpad_k: left padding for K (tokens to skip at the start of the KV cache)
- offset_k_new: offset into the new K tensor
- seqlen_k_og: original K length (before appending new K), excluding leftpad
- seqlen_k_new: length of new K to append
- seqlen_k: total K length (seqlen_k_og + seqlen_k_new)
- seqlen_rotary: position for rotary embedding computation
"""
leftpad_k: Int32
offset_q: Int32
offset_k: Int32
offset_k_new: Int32
seqlen_q: Int32
seqlen_k_og: Int32
seqlen_k_new: Int32
seqlen_k: Int32
seqlen_rotary: Int32
@staticmethod
def create(
batch_idx: Int32,
seqlen_q_static: Int32,
seqlen_k_static: Int32,
shape_K_new_0: Int32,
mCuSeqlensQ: Optional[cute.Tensor] = None,
mCuSeqlensK: Optional[cute.Tensor] = None,
mCuSeqlensKNew: Optional[cute.Tensor] = None,
mSeqUsedQ: Optional[cute.Tensor] = None,
mSeqUsedK: Optional[cute.Tensor] = None,
mLeftpadK: Optional[cute.Tensor] = None,
mSeqlensRotary: Optional[cute.Tensor] = None,
):
leftpad_k = 0 if const_expr(mLeftpadK is None) else mLeftpadK[batch_idx]
offset_q = 0 if const_expr(mCuSeqlensQ is None) else mCuSeqlensQ[batch_idx]
if const_expr(mCuSeqlensK is not None):
offset_k = mCuSeqlensK[batch_idx] + leftpad_k
else:
offset_k = leftpad_k if const_expr(mCuSeqlensQ is not None) else 0
offset_k_new = (
0 if const_expr(mCuSeqlensKNew is None) else mCuSeqlensKNew[batch_idx]
)
# seqlen_q
if const_expr(mSeqUsedQ is not None):
seqlen_q = mSeqUsedQ[batch_idx]
elif const_expr(mCuSeqlensQ is not None):
seqlen_q = mCuSeqlensQ[batch_idx + 1] - mCuSeqlensQ[batch_idx]
else:
seqlen_q = seqlen_q_static
# seqlen_k_og: original K length (excluding leftpad)
if const_expr(mSeqUsedK is not None):
seqlen_k_og = mSeqUsedK[batch_idx] - leftpad_k
elif const_expr(mCuSeqlensK is not None):
seqlen_k_og = (
mCuSeqlensK[batch_idx + 1] - mCuSeqlensK[batch_idx] - leftpad_k
)
else:
seqlen_k_og = (
seqlen_k_static - leftpad_k
if const_expr(mCuSeqlensQ is not None)
else seqlen_k_static
)
# seqlen_k_new
if const_expr(mCuSeqlensKNew is None):
seqlen_k_new = 0 if const_expr(mCuSeqlensQ is None) else shape_K_new_0
else:
seqlen_k_new = mCuSeqlensKNew[batch_idx + 1] - mCuSeqlensKNew[batch_idx]
seqlen_k = (
seqlen_k_og
if const_expr(mCuSeqlensQ is None)
else seqlen_k_og + seqlen_k_new
)
# seqlen_rotary: defaults to seqlen_k_og + leftpad_k unless explicitly provided
if const_expr(mSeqlensRotary is not None):
seqlen_rotary = mSeqlensRotary[batch_idx]
else:
seqlen_rotary = seqlen_k_og + leftpad_k
return SeqlenInfoQKNewK(
leftpad_k,
offset_q,
offset_k,
offset_k_new,
seqlen_q,
seqlen_k_og,
seqlen_k_new,
seqlen_k,
seqlen_rotary,
)
@@ -0,0 +1,573 @@
# Copyright (c) 2026, Colfax International.
import math
from functools import partial
from typing import Callable, Optional
import cuda.bindings.driver as cuda
import cutlass
import cutlass.cute as cute
from cutlass import Float32, Int32, const_expr
from sglang.jit_kernel.flash_attn.cute.block_info import BlockInfo
from sglang.jit_kernel.flash_attn.cute.copy_utils import tiled_copy_2d
from sglang.jit_kernel.flash_attn.cute.cute_dsl_utils import assume_tensor_aligned
from sglang.jit_kernel.flash_attn.cute.pack_gqa import pack_gqa_layout
from sglang.jit_kernel.flash_attn.cute.seqlen_info import SeqlenInfoQK
from sglang.jit_kernel.flash_attn.cute.tile_scheduler import (
ParamsBase,
SingleTileScheduler,
SingleTileVarlenScheduler,
TileSchedulerArguments,
)
from sglang.jit_kernel.flash_attn.cute.utils import get_batch_from_cu_tensor
class ShearingBias:
def __init__(
self,
rel_extent: int = 512,
is_causal: bool = True,
is_local: bool = False,
pack_gqa: bool = False,
qhead_per_kvhead: cutlass.Constexpr[int] = 1,
rows_per_cta: int = 4,
tile_m: int = 128,
max_m_blocks_leq_one: bool = False,
use_pdl: bool = False,
clamp_subtiles: bool = True,
):
self.is_causal = is_causal
self.is_local = is_local
assert is_causal or is_local, "Doesn't make sense otherwise"
self.pack_gqa = pack_gqa
self.qhead_per_kvhead = qhead_per_kvhead
if self.pack_gqa:
assert (
128 % self.qhead_per_kvhead == 0
), "pack_gqa only supported when qhead_per_kvhead divides 128"
self.qhead_per_kvhead_packgqa = qhead_per_kvhead if self.pack_gqa else 1
self.rel_extent = rel_extent
assert rel_extent % 128 == 0
self.rel_extent_padded = rel_extent + 256
self.num_bias_blocks_padded = (self.rel_extent_padded) // 128
# tuneable parameters
assert rows_per_cta % 4 == 0
self.rows_per_cta = rows_per_cta
self.num_threads = self.rows_per_cta * 32
self.cta_tiler = (self.rows_per_cta, self.rel_extent)
self.cta_out_tiler = (self.rows_per_cta, self.rel_extent_padded)
self.buffer_align_bytes = 1024
self.max_m_blocks_leq_one = max_m_blocks_leq_one
self.use_pdl = use_pdl
# only used with block packed scheduling
self.tile_m = tile_m
# Shrink the subtile grid dim to the rows a block can actually hold
# (decode blocks hold qhead_per_kvhead*seqlen_q rows, not tile_m).
self.clamp_subtiles = clamp_subtiles
@cute.jit
def __call__(
self,
mPreBias: cute.Tensor, # (b, s_q, h, rel_extent) or (total_q, h, rel_extent)
mBias: cute.Tensor, # (b, s_q, h, rel_extent_padded) or (total_q, h, rel_extent_padded)
max_seqlen_q: Int32 | int,
max_seqlen_k: Int32 | int,
mCuSeqlensQ: Optional[cute.Tensor] = None,
mCuSeqlensK: Optional[cute.Tensor] = None,
mSeqUsedQ: Optional[cute.Tensor] = None,
mSeqUsedK: Optional[cute.Tensor] = None,
mCuTotalMBlocks: Optional[cute.Tensor] = None,
mBlocksToBatchIdx: Optional[cute.Tensor] = None,
window_size_left: Int32 | int | None = None,
window_size_right: Int32 | int | None = None,
# Always keep stream as the last parameter (EnvStream: obtained implicitly via TVM FFI).
stream: cuda.CUstream = None,
):
assert mPreBias.element_type == mBias.element_type
self.bias_dtype = mBias.element_type
right_pad_value = -Float32.inf
left_pad_value = (
-Float32.inf if const_expr(window_size_left is not None) else 0.0
)
self.vec_size = 32 // self.bias_dtype.width
self.cols_per_iter = 32 * self.vec_size
assert self.vec_size <= 2
assert 128 % self.cols_per_iter == 0
max_seqlen_k = Int32(max_seqlen_k)
if const_expr(window_size_left is not None):
window_size_left = Int32(window_size_left)
if const_expr(window_size_right is not None):
window_size_right = Int32(window_size_right)
mPreBias, mBias = [assume_tensor_aligned(t) for t in (mPreBias, mBias)]
# (s_q, rel_extent, h, b) or (total_q, rel_extent, h)
Q_layout_transpose = (
[1, 3, 2, 0] if const_expr(mCuSeqlensQ is None) else [0, 2, 1]
)
mPreBias, mBias = [
cute.make_tensor(t.iterator, cute.select(t.layout, mode=Q_layout_transpose))
for t in (mPreBias, mBias)
]
if const_expr(self.pack_gqa):
nheads_kv = mBias.shape[2] // self.qhead_per_kvhead
mPreBias, mBias = [
pack_gqa_layout(t, self.qhead_per_kvhead, nheads_kv, head_idx=2)
for t in (mPreBias, mBias)
]
# SMEM layouts
prebias_tile_shape = (self.rows_per_cta, self.rel_extent)
bias_tile_shape = (self.rows_per_cta, self.rel_extent_padded)
sPreBias_layout = cute.make_ordered_layout(prebias_tile_shape, order=(1, 0))
sBias_layout = cute.make_ordered_layout(bias_tile_shape, order=(1, 0))
sPreBias_size = cute.cosize(sPreBias_layout)
sBias_size = cute.cosize(sBias_layout)
in_major_size = math.gcd(256, self.rel_extent)
assert in_major_size % 128 == 0
self.num_g2s_threads = self.num_threads if in_major_size == 256 else 128
g2s_tiled_copy = tiled_copy_2d(
self.bias_dtype,
math.gcd(256, self.rel_extent),
self.num_g2s_threads,
is_async=True,
)
out_major_size = math.gcd(256, self.rel_extent_padded)
assert out_major_size % 128 == 0
self.num_s2g_threads = self.num_threads if out_major_size == 256 else 128
s2g_tiled_copy = tiled_copy_2d(
self.bias_dtype,
math.gcd(256, self.rel_extent_padded),
self.num_s2g_threads,
)
@cute.struct
class SharedStorage:
sPreBias: cute.struct.Align[
cute.struct.MemRange[self.bias_dtype, sPreBias_size],
self.buffer_align_bytes,
]
sBias: cute.struct.Align[
cute.struct.MemRange[self.bias_dtype, sBias_size],
self.buffer_align_bytes,
]
self.shared_storage = SharedStorage
varlen_q = mCuSeqlensQ is not None or mSeqUsedQ is not None
self.use_block_packed_scheduling = (
mCuTotalMBlocks is not None
and mCuSeqlensQ is not None
and not self.max_m_blocks_leq_one
# and False
)
if const_expr(varlen_q and not self.max_m_blocks_leq_one):
if const_expr(self.use_block_packed_scheduling):
TileScheduler = SingleTileScheduler
else:
TileScheduler = SingleTileVarlenScheduler
else:
TileScheduler = SingleTileScheduler
batch_size = (
cute.size(mPreBias.shape[3])
if const_expr(mCuSeqlensQ is None)
else cute.size(mCuSeqlensQ.shape[0] - 1)
)
eff_seqlen_q = (
max_seqlen_q
if const_expr(not self.pack_gqa)
else max_seqlen_q * self.qhead_per_kvhead
)
total_q = (
cute.size(mPreBias.shape[0])
if const_expr(mCuSeqlensQ is not None)
else cute.size(mPreBias.shape[0]) * cute.size(mPreBias.shape[3])
)
# same formula as in varlen scheduler -- only used with block packed scheduling
total_blocks_max = (total_q + batch_size * (self.tile_m - 1)) // self.tile_m
num_blocks_for_sched = (
cute.ceil_div(eff_seqlen_q, self.rows_per_cta)
if const_expr(not self.use_block_packed_scheduling)
else total_blocks_max
)
if const_expr(not self.use_block_packed_scheduling):
batch_size_for_sched = batch_size
elif const_expr(self.clamp_subtiles):
# A block covers at most min(tile_m, eff_seqlen_q) valid rows; subtiles
# past that would fail the per-row seqlen guards and exit immediately.
batch_size_for_sched = cute.ceil_div(
min(self.tile_m, eff_seqlen_q), self.rows_per_cta
)
else:
batch_size_for_sched = self.tile_m // self.rows_per_cta
tile_sched_args = TileSchedulerArguments(
num_blocks_for_sched,
cute.size(mPreBias.shape[2]),
batch_size_for_sched,
1,
1,
1,
1,
total_q=total_q,
tile_shape_mn=self.cta_tiler,
mCuSeqlensQ=mCuSeqlensQ,
mSeqUsedQ=mSeqUsedQ,
qhead_per_kvhead_packgqa=self.qhead_per_kvhead_packgqa,
element_size=self.bias_dtype.width // 8,
)
tile_sched_params = TileScheduler.to_underlying_arguments(tile_sched_args)
self.tile_scheduler_cls = TileScheduler
grid_dim = TileScheduler.get_grid_shape(tile_sched_params)
self.kernel(
mPreBias,
mBias,
left_pad_value,
right_pad_value,
max_seqlen_k,
mCuSeqlensQ,
mCuSeqlensK,
mSeqUsedQ,
mSeqUsedK,
mCuTotalMBlocks,
mBlocksToBatchIdx,
sPreBias_layout,
sBias_layout,
window_size_left,
window_size_right,
g2s_tiled_copy,
s2g_tiled_copy,
SharedStorage,
tile_sched_params,
).launch(
grid=grid_dim,
block=(self.num_threads, 1, 1),
stream=stream,
use_pdl=self.use_pdl,
)
@cute.kernel
def kernel(
self,
mPreBias: cute.Tensor,
mBias: cute.Tensor,
left_pad_value: cutlass.Float32,
right_pad_value: cutlass.Float32,
max_seqlen_k: Int32,
mCuSeqlensQ: Optional[cute.Tensor],
mCuSeqlensK: Optional[cute.Tensor],
mSeqUsedQ: Optional[cute.Tensor],
mSeqUsedK: Optional[cute.Tensor],
mCuTotalMBlocks: Optional[cute.Tensor],
mBlocksToBatchIdx: Optional[cute.Tensor],
sPreBias_layout: cute.ComposedLayout | cute.Layout,
sBias_layout: cute.ComposedLayout | cute.Layout,
window_size_left: Optional[Int32],
window_size_right: Optional[Int32],
g2s_tiled_copy: cute.TiledCopy,
s2g_tiled_copy: cute.TiledCopy,
SharedStorage: cutlass.Constexpr[Callable],
tile_sched_params: ParamsBase,
):
tidx, _, _ = cute.arch.thread_idx()
warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx())
lane_idx = cute.arch.lane_idx()
smem = cutlass.utils.SmemAllocator()
storage = smem.allocate(SharedStorage)
sPreBias = storage.sPreBias.get_tensor(sPreBias_layout)
sBias = storage.sBias.get_tensor(sBias_layout)
TileSchedulerCls = partial(self.tile_scheduler_cls.create, tile_sched_params)
tile_scheduler = TileSchedulerCls()
work_tile = tile_scheduler.initial_work_tile_info()
# if pack_gqa, head_idx means head_idx_kv
m_block, head_idx, batch_idx, _ = work_tile.tile_idx
subtile_idx = batch_idx if const_expr(self.use_block_packed_scheduling) else 0
if const_expr(self.use_pdl):
cute.arch.griddepcontrol_wait()
cute.arch.griddepcontrol_launch_dependents()
is_valid_tile = work_tile.is_valid_tile
if const_expr(self.use_block_packed_scheduling):
batch_size = mCuTotalMBlocks.shape[0] - 1
is_valid_tile = m_block < mCuTotalMBlocks[batch_size]
if is_valid_tile:
if const_expr(self.use_block_packed_scheduling):
if const_expr(mBlocksToBatchIdx is not None):
batch_idx = mBlocksToBatchIdx[m_block]
else:
batch_idx = get_batch_from_cu_tensor(m_block, mCuTotalMBlocks)
# get local m_block for batch
m_block -= mCuTotalMBlocks[batch_idx]
m_block = m_block * (self.tile_m // self.rows_per_cta) + subtile_idx
seqlen_info = SeqlenInfoQK.create(
batch_idx=batch_idx,
seqlen_q_static=(
mPreBias.shape[0]
if const_expr(not self.pack_gqa)
else mPreBias.shape[0][1]
),
seqlen_k_static=max_seqlen_k,
mCuSeqlensQ=mCuSeqlensQ,
mCuSeqlensK=mCuSeqlensK,
mSeqUsedQ=mSeqUsedQ,
mSeqUsedK=mSeqUsedK,
)
block_info = BlockInfo(
128,
128,
self.is_causal,
self.is_local,
window_size_left=window_size_left,
window_size_right=window_size_right,
qhead_per_kvhead_packgqa=self.qhead_per_kvhead_packgqa,
)
# (seqlen, rel_extent) or ((seqlen, qhead_per_kvhead), rel_extent)
mPreBias_cur = seqlen_info.offset_batch_Q(mPreBias, batch_idx, dim=3)[
None, None, head_idx
]
# (rows_per_cta, rel_extent)
gPreBias = cute.local_tile(mPreBias_cur, self.cta_tiler, (m_block, 0))
cPreBias = cute.make_identity_tensor(self.cta_tiler)
g2s_thr_copy = g2s_tiled_copy.get_slice(tidx)
# (V, M, N)
tBgPreBias = g2s_thr_copy.partition_S(gPreBias)
tBsPreBias = g2s_thr_copy.partition_D(sPreBias)
tBcPreBias = g2s_thr_copy.partition_S(cPreBias)
if (
const_expr(self.num_g2s_threads == self.num_threads)
or warp_idx < self.num_g2s_threads // 32
):
num_rows_per_load = tBgPreBias.shape[1]
for m in cutlass.range_constexpr(num_rows_per_load):
local_m_idx = tBcPreBias[0, m, 0][0]
load_m_idx = local_m_idx + m_block * self.rows_per_cta
local_m_idx_in_bounds = (
const_expr(self.rows_per_cta % 8 == 0)
or local_m_idx < self.rows_per_cta
)
load_m_idx_in_bounds = (
load_m_idx // self.qhead_per_kvhead_packgqa
< seqlen_info.seqlen_q
)
if local_m_idx_in_bounds and load_m_idx_in_bounds:
cute.copy(
g2s_tiled_copy,
tBgPreBias[None, m, None],
tBsPreBias[None, m, None],
)
cute.arch.cp_async_commit_group()
# Convention: inclusive min, exclusive max
m_idx = m_block * self.rows_per_cta + warp_idx
attn_m_block = m_idx // 128
_, attn_n_block_max = block_info.get_n_block_min_max(
seqlen_info,
attn_m_block,
)
n_idx_left, n_idx_right = block_info.get_n_idx_left_right(
seqlen_info, m_idx
)
num_bias_vals = n_idx_right - max(n_idx_left, n_idx_right - self.rel_extent)
is_even = n_idx_right % 2 == 0
# get bias block and idx bounds for row
n_block_for_rel0 = (n_idx_right - 1) // 128 # inclusive
bias_block_idx_right = 1 + max(
self.rel_extent_padded // 128 - (attn_n_block_max - n_block_for_rel0), 0
)
bias_idx_right = (
(bias_block_idx_right - 1) * 128 + ((n_idx_right - 1) % 128) + 1
)
bias_idx_left = max(0, bias_idx_right - num_bias_vals)
bias_block_idx_left = bias_idx_left // 128
# num_bias_blocks = self.num_bias_blocks_padded - bias_block_idx_left
# num_right_padding_blocks = 0
num_bias_blocks = (
bias_block_idx_right - bias_block_idx_left if num_bias_vals > 0 else 0
)
num_right_padding_blocks = (
self.num_bias_blocks_padded - bias_block_idx_right
if num_bias_vals > 0
else self.num_bias_blocks_padded
)
# might help compiler unroll loops
num_bias_blocks = min(num_bias_blocks, self.num_bias_blocks_padded)
num_right_padding_blocks = min(
num_right_padding_blocks, self.num_bias_blocks_padded
)
sPreBias_row = cute.flat_divide(
sPreBias[(warp_idx, None)], (self.vec_size,)
)
sBias_row = cute.flat_divide(sBias[(warp_idx, None)], (self.vec_size,))
sBias_row_vec4 = cute.flat_divide(sBias[(warp_idx, None)], (4,))
bias_idx = (
self.rel_extent_padded + lane_idx * self.vec_size - self.cols_per_iter
)
cute.arch.cp_async_wait_group(0)
cute.arch.sync_threads()
if m_idx // self.qhead_per_kvhead_packgqa < seqlen_info.seqlen_q:
# We can try handling right padding separately
for i in cutlass.range(num_right_padding_blocks, unroll_full=True):
bias_frg = cute.make_rmem_tensor((4,), dtype=self.bias_dtype)
bias_frg.fill(self.bias_dtype(right_pad_value))
bias_right_pad_idx = (
self.num_bias_blocks_padded - 1 - i
) * 32 + lane_idx
cute.autovec_copy(
bias_frg, sBias_row_vec4[None, bias_right_pad_idx]
)
bias_idx -= 128
for _ in cutlass.range(num_bias_blocks, unroll_full=True):
# 2 subblocks for half bias dtype
for _ in cutlass.range_constexpr(128 // self.cols_per_iter):
prebias_idx = bias_idx_right - 1 - bias_idx
# (vec_size, lower/upper)
prebias_frg = cute.make_rmem_tensor(
(self.vec_size, self.vec_size), dtype=self.bias_dtype
)
in_bounds = (
prebias_idx >= 0
and prebias_idx - self.vec_size + 1 < num_bias_vals
)
prebias_idx_lower = (
prebias_idx - 1 if is_even else max(prebias_idx - 2, 0)
)
prebias_idx_upper = (
prebias_idx - 1
if is_even
else min(prebias_idx, self.rel_extent - 2)
)
if in_bounds:
cute.autovec_copy(
sPreBias_row[None, prebias_idx_lower // 2],
prebias_frg[None, 0],
)
if const_expr(self.vec_size == 2) and not is_even:
cute.autovec_copy(
sPreBias_row[None, prebias_idx_upper // 2],
prebias_frg[None, 1],
)
bias_frg = cute.make_rmem_tensor(
(self.vec_size,), dtype=self.bias_dtype
)
bias_frg.fill(self.bias_dtype(left_pad_value))
if const_expr(self.vec_size == 1):
if in_bounds:
bias_frg[0] = prebias_frg[0, 0]
elif prebias_idx < 0:
bias_frg.fill(self.bias_dtype(right_pad_value))
else:
if in_bounds:
if is_even:
# reverse: [prebias_idx, prebias_idx-1] = bias frg
bias_frg[0] = prebias_frg[1, 0]
bias_frg[1] = prebias_frg[0, 0]
else:
# lower = [2x-2, 2x-1], upper = [2x, 2x+1], 2x = prebias_idx
# want bias = [2x, 2x-1]
bias_frg[0] = prebias_frg[0, 1]
bias_frg[1] = prebias_frg[1, 0]
elif prebias_idx < 0:
bias_frg.fill(self.bias_dtype(right_pad_value))
cute.autovec_copy(bias_frg, sBias_row[None, bias_idx // 2])
bias_idx -= self.cols_per_iter
cute.arch.sync_warp()
# Handle edge cases. For N = rel_extent:
# [0, -1], -1 at bias_idx_right and [N, N-1], N-1 at bias_idx_left
if not is_even and num_bias_vals > 0:
sBias[(warp_idx, bias_idx_right)] = self.bias_dtype(right_pad_value)
if bias_idx_left - 1 >= 0:
sBias[(warp_idx, bias_idx_left - 1)] = self.bias_dtype(
left_pad_value
)
num_left_padding_blocks = min(
self.num_bias_blocks_padded
- num_bias_blocks
- num_right_padding_blocks,
self.num_bias_blocks_padded,
)
for i in cutlass.range(num_left_padding_blocks, unroll_full=True):
bias_left_pad_idx = i * 32 + lane_idx
bias_frg = cute.make_rmem_tensor((4,), dtype=self.bias_dtype)
bias_frg.fill(self.bias_dtype(left_pad_value))
cute.autovec_copy(bias_frg, sBias_row_vec4[None, bias_left_pad_idx])
cute.arch.sync_threads()
s2g_thr_copy = s2g_tiled_copy.get_slice(tidx)
# (seqlen, rel_extent_padded)
mBias_cur = seqlen_info.offset_batch_Q(mBias, batch_idx, dim=3)[
None, None, head_idx
]
# (rows_per_cta, rel_extent_padded)
gBias = cute.local_tile(mBias_cur, self.cta_out_tiler, (m_block, 0))
cBias = cute.make_identity_tensor(self.cta_out_tiler)
# (V, M, N)
tBsBias = s2g_thr_copy.partition_S(sBias)
tBgBias = s2g_thr_copy.partition_D(gBias)
tBcBias = s2g_thr_copy.partition_D(cBias)
if (
const_expr(self.num_s2g_threads == self.num_threads)
or warp_idx < self.num_s2g_threads // 32
):
num_rows_per_store = tBgBias.shape[1]
for m in cutlass.range_constexpr(num_rows_per_store):
local_m_idx = tBcBias[0, m, 0][0]
store_m_idx = local_m_idx + m_block * self.rows_per_cta
local_m_idx_in_bounds = (
const_expr(self.rows_per_cta % 8 == 0)
or local_m_idx < self.rows_per_cta
)
store_m_idx_in_bounds = (
store_m_idx // self.qhead_per_kvhead_packgqa
< seqlen_info.seqlen_q
)
if local_m_idx_in_bounds and store_m_idx_in_bounds:
cute.copy(
s2g_tiled_copy,
tBsBias[None, m, None],
tBgBias[None, m, None],
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,416 @@
"""Search feasible SM90 fwd/bwd attention configs for given (head_dim, head_dim_v).
Enumerates tile sizes, swap modes, atom layouts, and staging options.
Checks GMMA divisibility, register budget, and shared memory budget.
Usage:
python flash_attn/cute/sm90_config_search.py --headdim 128
python flash_attn/cute/sm90_config_search.py --mode fwd --headdim 192-128
python flash_attn/cute/sm90_config_search.py --mode bwd --headdim 192 --tile-n 64,96
"""
import math
# H100 hardware limits
SMEM_LIMIT = 224 * 1024 # 228 KB minus ~3 KB for LSE, dPsum, mbarriers
REG_LIMITS = {2: 216, 3: 128} # per-WG budget: 2WG=240-24, 3WG=160-32
THREADS_PER_WG = 128
def _divisors(n):
return [d for d in range(1, n + 1) if n % d == 0]
def _acc_regs(M, N, num_wg):
"""Accumulator registers per thread per WG."""
return M * N // (num_wg * THREADS_PER_WG)
def _check_mma(M, N, num_wg, atom_layout_m, swap_AB):
"""Check MMA feasibility. Returns regs per WG, or None if infeasible.
GMMA atom M=64. Swap exchanges (M, N) and atom layout.
Requires: M divisible by (atom_layout_m * 64), N by (atom_layout_n * 8).
"""
if swap_AB:
M, N = N, M
atom_layout_m = num_wg // atom_layout_m
atom_layout_n = num_wg // atom_layout_m
if M % (atom_layout_m * 64) != 0 or N % (atom_layout_n * 8) != 0:
return None
return _acc_regs(M, N, num_wg)
def _mma_traffic(M_eff, N_eff, K_red, num_wg, wg_n, is_rs=False):
"""Total SMEM read traffic for one MMA (all WGs combined).
num_instr = (M_eff / 64) * wg_n instructions total.
Each reads A(64, K_red) and B(N_eff/wg_n, K_red) from smem (bf16).
"""
num_instr = (M_eff // 64) * wg_n
A_per = 64 * K_red * 2 if not is_rs else 0
B_per = (N_eff // wg_n) * K_red * 2
return num_instr * (A_per + B_per)
# ============================================================================
# Backward
# ============================================================================
def _check_bwd_config(
hdim,
hdimv,
tile_m,
tile_n,
num_wg,
SdP_swapAB,
dKV_swapAB,
dQ_swapAB,
AtomLayoutMSdP,
AtomLayoutNdKV,
AtomLayoutMdQ,
):
reg_limit = REG_LIMITS[num_wg]
# MMA feasibility
regs_SdP = _check_mma(tile_m, tile_n, num_wg, AtomLayoutMSdP, SdP_swapAB)
regs_dK = _check_mma(tile_n, hdim, num_wg, AtomLayoutNdKV, dKV_swapAB)
regs_dV = _check_mma(tile_n, hdimv, num_wg, AtomLayoutNdKV, dKV_swapAB)
regs_dQ = _check_mma(tile_m, hdim, num_wg, AtomLayoutMdQ, dQ_swapAB)
if any(r is None for r in (regs_SdP, regs_dK, regs_dV, regs_dQ)):
return None
# Peak regs: max(S+dP, dQ) + dK + dV
total_regs = max(2 * regs_SdP, regs_dQ) + regs_dK + regs_dV
if total_regs > reg_limit:
return None
# SMEM
mma_dkv_is_rs = (
AtomLayoutMSdP == 1
and AtomLayoutNdKV == num_wg
and SdP_swapAB
and not dKV_swapAB
)
Q_stage, PdS_stage = 2, 1
for dO_stage in (2, 1):
sQ = tile_m * hdim * 2 * Q_stage
sK = tile_n * hdim * 2
sV = tile_n * hdimv * 2
sdO = tile_m * hdimv * 2 * dO_stage
sPdS = tile_m * tile_n * 2 * PdS_stage
sP = sPdS if not mma_dkv_is_rs else 0
sdQaccum = tile_m * hdim * 4
smem = sQ + sK + sV + sdO + sP + sPdS + sdQaccum
if smem <= SMEM_LIMIT:
break
else:
return None
# SMEM traffic
def _swap(a, b, s):
return (b, a) if s else (a, b)
def _wg_n(al_m, s):
return al_m if s else num_wg // al_m
M_s, N_s = _swap(tile_m, tile_n, SdP_swapAB)
wn_SdP = _wg_n(AtomLayoutMSdP, SdP_swapAB)
traffic_S = _mma_traffic(M_s, N_s, hdim, num_wg, wn_SdP)
traffic_dP = _mma_traffic(M_s, N_s, hdimv, num_wg, wn_SdP)
wn_dKV = _wg_n(AtomLayoutNdKV, dKV_swapAB)
M_dv, N_dv = _swap(tile_n, hdimv, dKV_swapAB)
traffic_dV = _mma_traffic(M_dv, N_dv, tile_m, num_wg, wn_dKV, is_rs=mma_dkv_is_rs)
M_dk, N_dk = _swap(tile_n, hdim, dKV_swapAB)
traffic_dK = _mma_traffic(M_dk, N_dk, tile_m, num_wg, wn_dKV, is_rs=mma_dkv_is_rs)
M_dq, N_dq = _swap(tile_m, hdim, dQ_swapAB)
wn_dQ = _wg_n(AtomLayoutMdQ, dQ_swapAB)
traffic_dQ = _mma_traffic(M_dq, N_dq, tile_n, num_wg, wn_dQ)
traffic_P_store = tile_m * tile_n * 2 if not mma_dkv_is_rs else 0
traffic_dS_store = tile_m * tile_n * 2
traffic_dQ_smem = tile_m * hdim * 4 * 2 # store + TMA load
smem_traffic = (
traffic_S
+ traffic_dP
+ traffic_dV
+ traffic_dK
+ traffic_dQ
+ traffic_P_store
+ traffic_dS_store
+ traffic_dQ_smem
)
return dict(
tile_m=tile_m,
tile_n=tile_n,
num_wg=num_wg,
Q_stage=Q_stage,
dO_stage=dO_stage,
PdS_stage=PdS_stage,
SdP_swapAB=SdP_swapAB,
dKV_swapAB=dKV_swapAB,
dQ_swapAB=dQ_swapAB,
AtomLayoutMSdP=AtomLayoutMSdP,
AtomLayoutNdKV=AtomLayoutNdKV,
AtomLayoutMdQ=AtomLayoutMdQ,
mma_dkv_is_rs=mma_dkv_is_rs,
regs_SdP=regs_SdP,
regs_dK=regs_dK,
regs_dV=regs_dV,
regs_dQ=regs_dQ,
total_regs=total_regs,
reg_limit=reg_limit,
smem_bytes=smem,
smem_kb=smem / 1024,
smem_traffic=smem_traffic,
smem_traffic_kb=smem_traffic / 1024,
smem_traffic_per_block=smem_traffic / (tile_m * tile_n),
)
def find_feasible_bwd_configs(
head_dim,
head_dim_v=None,
tile_m_choices=(64, 80, 96, 112, 128),
tile_n_choices=(64, 80, 96, 112, 128),
):
if head_dim_v is None:
head_dim_v = head_dim
hdim = int(math.ceil(head_dim / 32) * 32)
hdimv = int(math.ceil(head_dim_v / 32) * 32)
results = []
for num_wg in (2, 3):
divs = _divisors(num_wg)
for tile_m in tile_m_choices:
for tile_n in tile_n_choices:
for SdP_swap in (False, True):
if (tile_n if SdP_swap else tile_m) % 64 != 0:
continue
for dKV_swap in (False, True):
if not dKV_swap and tile_n % 64 != 0:
continue
if dKV_swap and (hdim % 64 != 0 or hdimv % 64 != 0):
continue
for dQ_swap in (False, True):
if (hdim if dQ_swap else tile_m) % 64 != 0:
continue
for a1 in divs:
for a2 in divs:
for a3 in divs:
cfg = _check_bwd_config(
hdim,
hdimv,
tile_m,
tile_n,
num_wg,
SdP_swap,
dKV_swap,
dQ_swap,
a1,
a2,
a3,
)
if cfg is not None:
results.append(cfg)
results.sort(
key=lambda c: (-c["tile_n"], -c["tile_m"], c["smem_traffic_per_block"])
)
return results
def print_bwd_configs(configs, max_results=20):
if not configs:
print("No feasible configs found!")
return
n = min(len(configs), max_results)
print(f"Found {len(configs)} feasible configs (showing top {n}):\n")
hdr = (
f"{'wg':>2} {'tm':>3} {'tn':>3} "
f"{'SdP':>3} {'dKV':>3} {'dQ':>3} "
f"{'aSdP':>4} {'adKV':>4} {'adQ':>4} "
f"{'Qs':>2} {'dOs':>3} "
f"{'rS':>3} {'rdK':>3} {'rdV':>3} {'rdQ':>3} {'tot':>4}/{'':<3} "
f"{'smem':>5} {'traffic':>7} {'tr/blk':>6}"
)
print(hdr)
print("-" * len(hdr))
B = lambda b: "T" if b else "F"
for c in configs[:max_results]:
print(
f"{c['num_wg']:>2} {c['tile_m']:>3} {c['tile_n']:>3} "
f"{B(c['SdP_swapAB']):>3} {B(c['dKV_swapAB']):>3} {B(c['dQ_swapAB']):>3} "
f"{c['AtomLayoutMSdP']:>4} {c['AtomLayoutNdKV']:>4} {c['AtomLayoutMdQ']:>4} "
f"{c['Q_stage']:>2} {c['dO_stage']:>3} "
f"{c['regs_SdP']:>3} {c['regs_dK']:>3} {c['regs_dV']:>3} {c['regs_dQ']:>3} "
f"{c['total_regs']:>4}/{c['reg_limit']:<3} "
f"{c['smem_kb']:>4.0f}K "
f"{c['smem_traffic_kb']:>6.0f}K "
f"{c['smem_traffic_per_block']:>6.1f}"
)
# ============================================================================
# Forward
# ============================================================================
def _check_fwd_config(hdim, hdimv, tile_n, num_wg, pv_is_rs, overlap_wg):
reg_limit = REG_LIMITS[num_wg]
tile_m = num_wg * 64
if tile_n % 8 != 0:
return None
regs_S = _acc_regs(tile_m, tile_n, num_wg)
regs_O = _acc_regs(tile_m, hdimv, num_wg)
regs_P = regs_S // 2 # bf16 = half of f32
if overlap_wg:
total_regs = regs_S + regs_P + regs_O
else:
total_regs = regs_S + regs_O
if total_regs > reg_limit:
return None
# SMEM: 1 stage Q, 2 stages K/V, O overlaps Q, sP if not RS
sQ = tile_m * hdim * 2
sK = tile_n * hdim * 2 * 2
sV = tile_n * hdimv * 2 * 2
sO = tile_m * hdimv * 2
sP = tile_m * tile_n * 2 if not pv_is_rs else 0
smem = max(sQ, sO) + sK + sV + sP
if smem > SMEM_LIMIT:
return None
# SMEM traffic: num_instr = num_wg (all WGs in M, wg_n=1)
traffic_S = num_wg * (64 * hdim * 2 + tile_n * hdim * 2)
A_pv = 64 * tile_n * 2 if not pv_is_rs else 0
traffic_O = num_wg * (A_pv + hdimv * tile_n * 2)
traffic_P_store = tile_m * tile_n * 2 if not pv_is_rs else 0
smem_traffic = traffic_S + traffic_O + traffic_P_store
return dict(
tile_m=tile_m,
tile_n=tile_n,
num_wg=num_wg,
pv_is_rs=pv_is_rs,
overlap_wg=overlap_wg,
regs_S=regs_S,
regs_O=regs_O,
regs_P=regs_P,
total_regs=total_regs,
reg_limit=reg_limit,
smem_bytes=smem,
smem_kb=smem / 1024,
smem_traffic=smem_traffic,
smem_traffic_kb=smem_traffic / 1024,
smem_traffic_per_block=smem_traffic / (tile_m * tile_n),
)
def find_feasible_fwd_configs(
head_dim, head_dim_v=None, tile_n_choices=(64, 80, 96, 112, 128, 144, 160, 176, 192)
):
if head_dim_v is None:
head_dim_v = head_dim
hdim = int(math.ceil(head_dim / 32) * 32)
hdimv = int(math.ceil(head_dim_v / 32) * 32)
results = []
for num_wg in (2, 3):
for tile_n in tile_n_choices:
for pv_is_rs in (True, False):
for overlap_wg in (True, False):
cfg = _check_fwd_config(
hdim, hdimv, tile_n, num_wg, pv_is_rs, overlap_wg
)
if cfg is not None:
results.append(cfg)
results.sort(key=lambda c: (-c["tile_n"], c["smem_traffic_per_block"]))
return results
def print_fwd_configs(configs, max_results=20):
if not configs:
print("No feasible configs found!")
return
n = min(len(configs), max_results)
print(f"Found {len(configs)} feasible configs (showing top {n}):\n")
hdr = (
f"{'wg':>2} {'tm':>3} {'tn':>3} "
f"{'RS':>2} {'olap':>4} "
f"{'rS':>3} {'rP':>3} {'rO':>3} {'tot':>4}/{'':<3} "
f"{'smem':>5} {'traffic':>7} {'tr/blk':>6}"
)
print(hdr)
print("-" * len(hdr))
B = lambda b: "T" if b else "F"
for c in configs[:max_results]:
print(
f"{c['num_wg']:>2} {c['tile_m']:>3} {c['tile_n']:>3} "
f"{B(c['pv_is_rs']):>2} {B(c['overlap_wg']):>4} "
f"{c['regs_S']:>3} {c['regs_P']:>3} {c['regs_O']:>3} "
f"{c['total_regs']:>4}/{c['reg_limit']:<3} "
f"{c['smem_kb']:>4.0f}K "
f"{c['smem_traffic_kb']:>6.0f}K "
f"{c['smem_traffic_per_block']:>6.1f}"
)
# ============================================================================
# CLI
# ============================================================================
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Search feasible SM90 MMA configs")
parser.add_argument("--mode", choices=["fwd", "bwd", "both"], default="both")
parser.add_argument(
"--headdim",
type=str,
default="128",
help="Head dim, or hdim-hdimv (e.g. 192-128)",
)
parser.add_argument(
"--tile-m", type=str, default="64,80,96,112,128", help="Bwd tile_m choices"
)
parser.add_argument(
"--tile-n",
type=str,
default=None,
help="tile_n choices (default: fwd up to 192, bwd up to 128)",
)
parser.add_argument("-n", "--num-results", type=int, default=30)
args = parser.parse_args()
parts = args.headdim.split("-")
hdim = int(parts[0])
hdimv = int(parts[1]) if len(parts) > 1 else hdim
TN_FWD = "64,80,96,112,128,144,160,176,192"
TN_BWD = "64,80,96,112,128"
if args.mode in ("fwd", "both"):
tn = tuple(int(x) for x in (args.tile_n or TN_FWD).split(","))
print(f"=== FWD configs: hdim={hdim}, hdimv={hdimv} ===\n")
print_fwd_configs(find_feasible_fwd_configs(hdim, hdimv, tn), args.num_results)
print()
if args.mode in ("bwd", "both"):
tm = tuple(int(x) for x in args.tile_m.split(","))
tn = tuple(int(x) for x in (args.tile_n or TN_BWD).split(","))
print(f"=== BWD configs: hdim={hdim}, hdimv={hdimv} ===\n")
print_bwd_configs(
find_feasible_bwd_configs(hdim, hdimv, tm, tn), args.num_results
)
@@ -0,0 +1,759 @@
# Copyright (c) 2025, Tri Dao.
import math
import operator
from dataclasses import dataclass
from typing import Tuple
import cutlass
import cutlass.cute as cute
from cutlass import Boolean, Float32
from quack import layout_utils
from quack.cute_dsl_utils import ParamsBase
import sglang.jit_kernel.flash_attn.cute.utils as utils
from sglang.jit_kernel.flash_attn.cute.seqlen_info import SeqlenInfoQK
from sglang.jit_kernel.flash_attn.cute.utils import AuxData
@cute.jit
def call_score_mod(
score_mod: cutlass.Constexpr,
score,
batch_idx,
head_idx,
q_idx,
kv_idx,
seqlen_info,
aux_data: AuxData,
):
aux_tensors = aux_data.tensors if aux_data.tensors is not None else ()
# Compatibility shim for pre-aux_scalars score_mod callables.
if cutlass.const_expr(aux_data.scalars is not None):
return score_mod(
score,
batch_idx,
head_idx,
q_idx=q_idx,
kv_idx=kv_idx,
seqlen_info=seqlen_info,
aux_tensors=aux_tensors,
aux_scalars=aux_data.scalars,
)
return score_mod(
score,
batch_idx,
head_idx,
q_idx=q_idx,
kv_idx=kv_idx,
seqlen_info=seqlen_info,
aux_tensors=aux_tensors,
)
@cute.jit
def call_score_mod_bwd(
score_mod_bwd: cutlass.Constexpr,
grad,
score,
batch_idx,
head_idx,
q_idx,
kv_idx,
seqlen_info,
aux_data: AuxData,
):
aux_tensors = aux_data.tensors if aux_data.tensors is not None else ()
# Compatibility shim for pre-aux_scalars score_mod_bwd callables.
if cutlass.const_expr(aux_data.scalars is not None):
return score_mod_bwd(
grad,
score,
batch_idx,
head_idx,
q_idx=q_idx,
kv_idx=kv_idx,
seqlen_info=seqlen_info,
aux_tensors=aux_tensors,
aux_scalars=aux_data.scalars,
)
return score_mod_bwd(
grad,
score,
batch_idx,
head_idx,
q_idx=q_idx,
kv_idx=kv_idx,
seqlen_info=seqlen_info,
aux_tensors=aux_tensors,
)
@dataclass
class Softmax(ParamsBase):
scale_log2: Float32
num_rows: cutlass.Constexpr[int]
row_max: cute.Tensor
row_sum: cute.Tensor
arch: cutlass.Constexpr[int] = 80
softmax_scale: Float32 | None = None
@staticmethod
def create(
scale_log2: Float32,
num_rows: cutlass.Constexpr[int],
arch: cutlass.Constexpr[int] = 80,
softmax_scale: Float32 | None = None,
):
row_max = cute.make_rmem_tensor(num_rows, Float32)
row_sum = cute.make_rmem_tensor(num_rows, Float32)
return Softmax(scale_log2, num_rows, row_max, row_sum, arch, softmax_scale)
def reset(self) -> None:
self.row_max.fill(-Float32.inf)
self.row_sum.fill(0.0)
def _compute_row_max(
self, acc_S_row: cute.TensorSSA, init_val: float | Float32 | None = None
) -> Float32:
return utils.fmax_reduce(acc_S_row, init_val, arch=self.arch)
def _compute_row_sum(
self, acc_S_row_exp: cute.TensorSSA, init_val: float | Float32 | None = None
) -> Float32:
return utils.fadd_reduce(acc_S_row_exp, init_val, arch=self.arch)
@cute.jit
def online_softmax(
self,
acc_S: cute.Tensor,
is_first: cutlass.Constexpr[bool] = False,
check_inf: cutlass.Constexpr[bool] = True,
) -> cute.Tensor:
"""Apply online softmax and return the row_scale to rescale O.
:param acc_S: acc_S tensor
:type acc_S: cute.Tensor
:param is_first: is first n_block
:type is_first: cutlass.Constexpr
"""
# Change acc_S to M,N layout view.
acc_S_mn = layout_utils.reshape_acc_to_mn(acc_S)
row_scale = cute.make_fragment_like(self.row_max, Float32)
row_max = self.row_max
row_sum = self.row_sum
scale_log2 = self.scale_log2
arch = self.arch
# Each iteration processes one row of acc_S
for r in cutlass.range(cute.size(row_max), unroll_full=True):
acc_S_row = acc_S_mn[r, None].load() # (n_block_size)
row_max_cur = utils.fmax_reduce(
acc_S_row,
init_val=row_max[r] if cutlass.const_expr(not is_first) else None,
arch=arch,
)
row_max_cur = cute.arch.warp_reduction_max(row_max_cur, threads_in_group=4)
# Update row_max before changing row_max_cur to safe value for -inf
row_max_prev = row_max[r]
row_max[r] = row_max_cur
if cutlass.const_expr(check_inf):
row_max_cur = 0.0 if row_max_cur == -Float32.inf else row_max_cur
if cutlass.const_expr(is_first):
row_max_cur_scaled = row_max_cur * scale_log2
acc_S_row_exp = cute.math.exp2(
acc_S_row * scale_log2 - row_max_cur_scaled, fastmath=True
)
acc_S_row_sum = utils.fadd_reduce(
acc_S_row_exp, init_val=None, arch=arch
)
row_scale[r] = 1.0
else:
row_max_cur_scaled = row_max_cur * scale_log2
acc_S_row_exp = cute.math.exp2(
acc_S_row * scale_log2 - row_max_cur_scaled, fastmath=True
)
# row_scale[r] = cute.math.exp2(row_max_prev * self.scale_log2 - row_max_cur_scaled)
row_scale[r] = cute.math.exp2(
(row_max_prev - row_max_cur) * scale_log2, fastmath=True
)
acc_S_row_sum = utils.fadd_reduce(
acc_S_row_exp, init_val=row_sum[r] * row_scale[r], arch=arch
)
row_sum[r] = acc_S_row_sum
acc_S_mn[r, None].store(acc_S_row_exp)
return row_scale
@cute.jit
def finalize(
self, final_scale: Float32 = 1.0, sink_val: Float32 | cute.Tensor | None = None
) -> cute.Tensor:
"""Finalize the online softmax by computing the scale and logsumexp."""
if cutlass.const_expr(
sink_val is not None and isinstance(sink_val, cute.Tensor)
):
assert cute.size(sink_val) == cute.size(self.row_sum)
row_sum = self.row_sum
row_max = self.row_max
scale_log2 = self.scale_log2
# quad reduction for row_sum as we didn't do it during each iteration of online softmax
row_sum.store(utils.warp_reduce(row_sum.load(), operator.add, width=4))
row_scale = cute.make_fragment_like(row_max, Float32)
for r in cutlass.range(cute.size(row_sum), unroll_full=True):
if cutlass.const_expr(sink_val is not None):
sink_val_cur = (
sink_val if not isinstance(sink_val, cute.Tensor) else sink_val[r]
)
LOG2_E = math.log2(math.e)
if row_max[r] == -Float32.inf:
# Fully-masked / empty row (can happen with SplitKV when a split's
# blocks are all outside the local window)
row_max[r] = sink_val_cur * (LOG2_E / scale_log2)
row_sum[r] = 1.0
else:
row_sum[r] += cute.math.exp2(
sink_val_cur * LOG2_E - row_max[r] * scale_log2, fastmath=True
)
# if row_sum is zero or nan, set acc_O_mn_row to 1.0
acc_O_mn_row_is_zero_or_nan = row_sum[r] == 0.0 or row_sum[r] != row_sum[r]
row_scale[r] = (
cute.arch.rcp_approx(
row_sum[r] if not acc_O_mn_row_is_zero_or_nan else 1.0
)
) * final_scale
row_sum_cur = row_sum[r]
LN2 = math.log(2.0)
row_sum[r] = (
(row_max[r] * scale_log2 + cute.math.log2(row_sum_cur, fastmath=True))
* LN2
if not acc_O_mn_row_is_zero_or_nan
else -Float32.inf
)
return row_scale
@cute.jit
def rescale_O(self, acc_O: cute.Tensor, row_scale: cute.Tensor) -> None:
"""Scale each row of acc_O by the given scale tensor.
:param acc_O: input tensor
:type acc_O: cute.Tensor
:param row_scale: row_scale tensor
:type row_scale: cute.Tensor
"""
acc_O_mn = layout_utils.reshape_acc_to_mn(acc_O)
assert cute.size(row_scale) == cute.size(acc_O_mn, mode=[0])
for r in cutlass.range(cute.size(row_scale), unroll_full=True):
acc_O_mn[r, None].store(acc_O_mn[r, None].load() * row_scale[r])
@dataclass
class SoftmaxSm100(Softmax):
rescale_threshold: cutlass.Constexpr[float] = 0.0
max_offset: cutlass.Constexpr[int] = 0
@staticmethod
def create(
scale_log2: Float32,
rescale_threshold: cutlass.Constexpr[float] = 0.0,
softmax_scale: Float32 | None = None,
max_offset: cutlass.Constexpr[int] = 0,
):
num_rows = 1
arch = 100
row_max = cute.make_rmem_tensor(num_rows, Float32)
row_sum = cute.make_rmem_tensor(num_rows, Float32)
return SoftmaxSm100(
scale_log2,
num_rows,
row_max,
row_sum,
arch,
softmax_scale,
rescale_threshold=rescale_threshold,
max_offset=max_offset,
)
@cute.jit
def compute_row_max_local(
self, acc_S_row: cute.TensorSSA, is_first: Boolean
) -> Float32:
if cutlass.const_expr(is_first):
row_max_new = self._compute_row_max(acc_S_row)
else:
row_max_old = self.row_max[0]
row_max_new = self._compute_row_max(acc_S_row, init_val=row_max_old)
return row_max_new
@cute.jit
def update_row_max_from_local(
self,
row_max_new: Float32,
is_first: Boolean,
) -> Tuple[Float32, Float32]:
if cutlass.const_expr(is_first):
row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
acc_scale = 0.0
else:
row_max_old = self.row_max[0]
row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
acc_scale_ = (row_max_old - row_max_safe) * self.scale_log2
acc_scale = cute.math.exp2(acc_scale_)
if cutlass.const_expr(self.rescale_threshold > 0.0):
if acc_scale_ >= -self.rescale_threshold:
row_max_new = row_max_old
row_max_safe = row_max_old
acc_scale = 1.0
self.row_max[0] = row_max_new
return row_max_safe, acc_scale
@cute.jit
def update_row_max(
self, acc_S_row: cute.TensorSSA, is_first: int
) -> Tuple[Float32, Float32]:
if cutlass.const_expr(is_first):
row_max_new = self._compute_row_max(acc_S_row)
row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
acc_scale = 0.0
else:
row_max_old = self.row_max[0]
row_max_new = self._compute_row_max(acc_S_row, init_val=row_max_old)
row_max_safe = row_max_new if row_max_new != -cutlass.Float32.inf else 0.0
acc_scale_ = (row_max_old - row_max_safe) * self.scale_log2
acc_scale = cute.math.exp2(acc_scale_, fastmath=True)
if cutlass.const_expr(self.rescale_threshold > 0.0):
if acc_scale_ >= -self.rescale_threshold:
row_max_new = row_max_old
row_max_safe = row_max_old
acc_scale = 1.0
self.row_max[0] = row_max_new
return row_max_safe, acc_scale
def update_row_sum(
self, acc_S_row_exp: cute.TensorSSA, row_scale: Float32, is_first: int = False
) -> None:
init_val = (
self.row_sum[0] * row_scale if cutlass.const_expr(not is_first) else None
)
# self.row_sum[0] = self._compute_row_sum(acc_S_row_exp, init_val=self.row_sum[0] * row_scale)
self.row_sum[0] = self._compute_row_sum(acc_S_row_exp, init_val=init_val)
# tmp = self._compute_row_sum(acc_S_row_exp)
# self.row_sum[0] = self.row_sum[0] * row_scale + tmp
@cute.jit
def scale_subtract_rowmax(
self,
acc_S_row: cute.Tensor,
row_max: Float32,
):
assert (
cute.size(acc_S_row.shape) % 2 == 0
), "acc_S_row must have an even number of elements"
row_max_scaled = row_max * self.scale_log2
max_offset = Float32(self.max_offset)
bias = max_offset - row_max_scaled
for i in cutlass.range(0, cute.size(acc_S_row.shape), 2, unroll_full=True):
acc_S_row[i], acc_S_row[i + 1] = cute.arch.fma_packed_f32x2(
(acc_S_row[i], acc_S_row[i + 1]),
(self.scale_log2, self.scale_log2),
(bias, bias),
)
@cute.jit
def apply_exp2_convert(
self,
acc_S_row: cute.Tensor,
acc_S_row_converted: cute.Tensor,
ex2_emu_freq: cutlass.Constexpr[int] = 0,
ex2_emu_res: cutlass.Constexpr[int] = 4,
ex2_emu_start_frg: cutlass.Constexpr[int] = 0,
):
assert (
cute.size(acc_S_row.shape) % 2 == 0
), "acc_S_row must have an even number of elements"
frg_tile = 32
assert frg_tile % 2 == 0
frg_cnt = cute.size(acc_S_row) // frg_tile
assert cute.size(acc_S_row) % frg_tile == 0
acc_S_row_frg = cute.logical_divide(acc_S_row, cute.make_layout(frg_tile))
acc_S_row_converted_frg = cute.logical_divide(
acc_S_row_converted, cute.make_layout(frg_tile)
)
for j in cutlass.range_constexpr(frg_cnt):
for k in cutlass.range_constexpr(0, cute.size(acc_S_row_frg, mode=[0]), 2):
# acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
# acc_S_row_frg[k + 1, j] = cute.math.exp2(acc_S_row_frg[k + 1, j], fastmath=True)
if cutlass.const_expr(ex2_emu_freq == 0):
acc_S_row_frg[k, j] = cute.math.exp2(
acc_S_row_frg[k, j], fastmath=True
)
acc_S_row_frg[k + 1, j] = cute.math.exp2(
acc_S_row_frg[k + 1, j], fastmath=True
)
else:
if cutlass.const_expr(
k % ex2_emu_freq < ex2_emu_freq - ex2_emu_res
or j >= frg_cnt - 1
or j < ex2_emu_start_frg
):
acc_S_row_frg[k, j] = cute.math.exp2(
acc_S_row_frg[k, j], fastmath=True
)
acc_S_row_frg[k + 1, j] = cute.math.exp2(
acc_S_row_frg[k + 1, j], fastmath=True
)
else:
# acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = utils.e2e_asm2(acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j])
acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = (
utils.ex2_emulation_2(
acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j]
)
)
acc_S_row_converted_frg[None, j].store(
acc_S_row_frg[None, j].load().to(acc_S_row_converted.element_type)
)
@cute.jit
def scale_apply_exp2_convert(
self,
acc_S_row: cute.Tensor,
row_max: Float32,
acc_S_row_converted: cute.Tensor,
):
assert (
cute.size(acc_S_row.shape) % 2 == 0
), "acc_S_row must have an even number of elements"
minus_row_max_scaled = -row_max * self.scale_log2
for i in cutlass.range_constexpr(0, cute.size(acc_S_row.shape), 2):
acc_S_row[i], acc_S_row[i + 1] = cute.arch.fma_packed_f32x2(
(acc_S_row[i], acc_S_row[i + 1]),
(self.scale_log2, self.scale_log2),
(minus_row_max_scaled, minus_row_max_scaled),
)
# for i in cutlass.range_constexpr(0, cute.size(acc_S_row.shape), 2):
# acc_S_row[i], acc_S_row[i + 1] = cute.arch.fma_packed_f32x2(
# (acc_S_row[i], acc_S_row[i + 1]),
# (self.scale_log2, self.scale_log2),
# (minus_row_max_scaled, minus_row_max_scaled),
# )
# acc_S_row[i] = cute.math.exp2(acc_S_row[i], fastmath=True)
# acc_S_row[i + 1] = cute.math.exp2(acc_S_row[i + 1], fastmath=True)
frg_tile = 32
assert frg_tile % 2 == 0
frg_cnt = cute.size(acc_S_row) // frg_tile
assert cute.size(acc_S_row) % frg_tile == 0
acc_S_row_frg = cute.logical_divide(acc_S_row, cute.make_layout(frg_tile))
acc_S_row_converted_frg = cute.logical_divide(
acc_S_row_converted, cute.make_layout(frg_tile)
)
for j in cutlass.range_constexpr(frg_cnt):
for k in cutlass.range_constexpr(0, cute.size(acc_S_row_frg, mode=[0]), 2):
# acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j] = (
# cute.arch.fma_packed_f32x2(
# (acc_S_row_frg[k, j], acc_S_row_frg[k + 1, j]),
# (self.scale_log2, self.scale_log2),
# (minus_row_max_scaled, minus_row_max_scaled),
# )
# )
# acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
# acc_S_row_frg[k + 1, j] = cute.math.exp2(acc_S_row_frg[k + 1, j], fastmath=True)
acc_S_row_frg[k, j] = cute.math.exp2(acc_S_row_frg[k, j], fastmath=True)
acc_S_row_frg[k + 1, j] = cute.math.exp2(
acc_S_row_frg[k + 1, j], fastmath=True
)
acc_S_row_converted_frg[None, j].store(
acc_S_row_frg[None, j].load().to(acc_S_row_converted.element_type)
)
@cute.jit
def floor_if_packed(
q_idx,
qhead_per_kvhead: cutlass.Constexpr[int],
) -> cute.Tensor:
"""Convert q_idx to packed format for Pack-GQA."""
if cutlass.const_expr(qhead_per_kvhead == 1):
return q_idx
return q_idx // qhead_per_kvhead
@cute.jit
def apply_score_mod_inner(
score_tensor,
index_tensor,
score_mod: cutlass.Constexpr,
batch_idx,
head_idx,
softmax_scale,
vec_size: cutlass.Constexpr,
qk_acc_dtype: cutlass.Constexpr,
aux_data: AuxData,
fastdiv_mods,
seqlen_info: SeqlenInfoQK,
constant_q_idx: cutlass.Constexpr,
qhead_per_kvhead: cutlass.Constexpr[int] = 1,
transpose_indices: cutlass.Constexpr[bool] = False,
):
"""Shared implementation for applying score modification.
Args:
score_tensor: The scores to modify (acc_S for flash_fwd, tSrS_t2r for sm100)
index_tensor: Index positions (tScS for flash_fwd, tScS_t2r for sm100)
score_mod: The score modification function to apply
batch_idx: Batch index
head_idx: Head index
softmax_scale: Scale to apply
vec_size: Vector size for processing elements
qk_acc_dtype: Data type for accumulator
aux_tensors: Optional aux_tensors for FlexAttention
aux_scalars: Optional runtime scalar captures for FlexAttention
fastdiv_mods: Tuple of (seqlen_q_divmod, seqlen_k_divmod) for wrapping
seqlen_info: Sequence length info
constant_q_idx: If provided, use this constant for all q_idx values
If None, compute q_idx per-element
qhead_per_kvhead_packgqa: Pack-GQA replication factor. Divide q_idx by this
when greater than 1 so score mods see logical heads.
transpose_indices: If True, swap q_idx/kv_idx in index_tensor (for bwd kernel where S is transposed)
"""
# Index positions in the index_tensor tuple
# Forward: index_tensor[...][0] = q_idx, index_tensor[...][1] = kv_idx
# Backward (transposed): index_tensor[...][0] = kv_idx, index_tensor[...][1] = q_idx
if cutlass.const_expr(transpose_indices):
q_idx_pos = cutlass.const_expr(1)
kv_idx_pos = cutlass.const_expr(0)
else:
q_idx_pos = cutlass.const_expr(0)
kv_idx_pos = cutlass.const_expr(1)
n_vals = cutlass.const_expr(cute.size(score_tensor.shape))
score_vec = cute.make_rmem_tensor(vec_size, qk_acc_dtype)
kv_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
# SSA values for batch (constant across all elements)
batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32).broadcast_to(
(vec_size,)
)
# Handle q_idx based on whether it's constant
q_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
# For Pack-GQA with non-constant q_idx, we need per-element head indices
# since a thread may process multiple query head indices
if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
head_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
for i in cutlass.range(0, n_vals, vec_size, unroll_full=True):
for j in cutlass.range(vec_size, unroll_full=True):
score_vec[j] = score_tensor[i + j] * softmax_scale
# Extract head offset from packed q_idx for Pack-GQA
if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
q_idx_packed = index_tensor[i + j][q_idx_pos]
# Building up the logical q_head idx: final_q_head = kv_head * qhead_per_kvhead + (q_physical % qhead_per_kvhead)
q_idx_logical = q_idx_packed // qhead_per_kvhead
head_offset = q_idx_packed - q_idx_logical * qhead_per_kvhead
head_idx_vec[j] = head_idx * qhead_per_kvhead + head_offset
# If we will do loads we mod, in order to not read OOB
if cutlass.const_expr(
aux_data.tensors is not None and fastdiv_mods is not None
):
if cutlass.const_expr(constant_q_idx is None):
seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
q_idx_floored = floor_if_packed(
index_tensor[i + j][q_idx_pos], qhead_per_kvhead
)
_, q_idx_wrapped = divmod(q_idx_floored, seqlen_q_divmod)
q_idx_vec[j] = q_idx_wrapped
else:
_, seqlen_k_divmod = fastdiv_mods
_, kv_idx_wrapped = divmod(
index_tensor[i + j][kv_idx_pos], seqlen_k_divmod
)
kv_idx_vec[j] = kv_idx_wrapped
else:
# No bounds checking - direct indexing
if constant_q_idx is None:
q_idx_vec[j] = floor_if_packed(
index_tensor[i + j][q_idx_pos], qhead_per_kvhead
)
kv_idx_vec[j] = index_tensor[i + j][kv_idx_pos]
# Convert to SSA for score_mod call
score_ssa = score_vec.load()
kv_idx_ssa = kv_idx_vec.load()
if cutlass.const_expr(constant_q_idx is None):
q_idx_ssa = q_idx_vec.load()
else:
# NB we do not apply Pack-GQA division here, as constant_q_idx is assumed to already be logical
q_idx_const = constant_q_idx
q_idx_ssa = utils.scalar_to_ssa(q_idx_const, cutlass.Int32).broadcast_to(
(vec_size,)
)
# Compute head_idx_ssa: per-element for Pack-GQA with non-constant q_idx, constant otherwise
if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
head_idx_ssa = head_idx_vec.load()
else:
head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32).broadcast_to(
(vec_size,)
)
post_mod_scores = call_score_mod(
score_mod,
score_ssa,
batch_idx_ssa,
head_idx_ssa,
q_idx_ssa,
kv_idx_ssa,
seqlen_info,
aux_data,
)
# Write back modified scores
score_vec.store(post_mod_scores)
for j in cutlass.range(vec_size, unroll_full=True):
score_tensor[i + j] = score_vec[j]
@cute.jit
def apply_score_mod_bwd_inner(
grad_tensor,
score_tensor,
index_tensor,
score_mod_bwd: cutlass.Constexpr,
batch_idx,
head_idx,
softmax_scale,
vec_size: cutlass.Constexpr,
qk_acc_dtype: cutlass.Constexpr,
aux_data: AuxData,
fastdiv_mods,
seqlen_info,
constant_q_idx: cutlass.Constexpr,
qhead_per_kvhead: cutlass.Constexpr[int] = 1,
transpose_indices: cutlass.Constexpr[bool] = False,
):
"""Apply backward score modification (joint graph).
Args:
grad_tensor: in/out: dlogits rewritten in-place with d(scaled_scores)
score_tensor: pre-mod scores (unscaled QK tile), scaled by softmax_scale internally
index_tensor: Index positions (same as forward)
score_mod_bwd: The backward score modification function (joint graph)
batch_idx: Batch index
head_idx: Head index
softmax_scale: Scale to apply to score_tensor
vec_size: Vector size for processing elements
qk_acc_dtype: Data type for accumulator
aux_tensors: Optional aux_tensors for FlexAttention
aux_scalars: Optional runtime scalar captures for FlexAttention
fastdiv_mods: Tuple of (seqlen_q_divmod, seqlen_k_divmod) for wrapping
seqlen_info: Sequence length info
constant_q_idx: If provided, use this constant for all q_idx values
qhead_per_kvhead: Pack-GQA replication factor
transpose_indices: If True, swap q_idx/kv_idx in index_tensor
"""
# Index positions in the index_tensor tuple
# Forward: index_tensor[...][0] = q_idx, index_tensor[...][1] = kv_idx
# Backward (transposed): index_tensor[...][0] = kv_idx, index_tensor[...][1] = q_idx
if cutlass.const_expr(transpose_indices):
q_idx_pos = cutlass.const_expr(1)
kv_idx_pos = cutlass.const_expr(0)
else:
q_idx_pos = cutlass.const_expr(0)
kv_idx_pos = cutlass.const_expr(1)
n_vals = cutlass.const_expr(cute.size(grad_tensor.shape))
grad_vec = cute.make_rmem_tensor(vec_size, qk_acc_dtype)
score_vec = cute.make_rmem_tensor(vec_size, qk_acc_dtype)
kv_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
batch_idx_ssa = utils.scalar_to_ssa(batch_idx, cutlass.Int32).broadcast_to(
(vec_size,)
)
q_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
# For Pack-GQA with non-constant q_idx, we need per-element head indices
if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
head_idx_vec = cute.make_rmem_tensor(vec_size, cutlass.Int32)
for i in cutlass.range(0, n_vals, vec_size, unroll_full=True):
for j in cutlass.range(vec_size, unroll_full=True):
grad_vec[j] = grad_tensor[i + j]
# Scale score so joint graph sees same value as forward score_mod
score_vec[j] = score_tensor[i + j] * softmax_scale
if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
q_idx_packed = index_tensor[i + j][q_idx_pos]
q_idx_logical = q_idx_packed // qhead_per_kvhead
head_offset = q_idx_packed - q_idx_logical * qhead_per_kvhead
head_idx_vec[j] = head_idx * qhead_per_kvhead + head_offset
if cutlass.const_expr(
aux_data.tensors is not None and fastdiv_mods is not None
):
if cutlass.const_expr(constant_q_idx is None):
seqlen_q_divmod, seqlen_k_divmod = fastdiv_mods
q_idx_floored = floor_if_packed(
index_tensor[i + j][q_idx_pos], qhead_per_kvhead
)
_, q_idx_wrapped = divmod(q_idx_floored, seqlen_q_divmod)
q_idx_vec[j] = q_idx_wrapped
else:
_, seqlen_k_divmod = fastdiv_mods
_, kv_idx_wrapped = divmod(
index_tensor[i + j][kv_idx_pos], seqlen_k_divmod
)
kv_idx_vec[j] = kv_idx_wrapped
else:
# No bounds checking - direct indexing
if constant_q_idx is None:
q_idx_vec[j] = floor_if_packed(
index_tensor[i + j][q_idx_pos], qhead_per_kvhead
)
kv_idx_vec[j] = index_tensor[i + j][kv_idx_pos]
grad_ssa = grad_vec.load()
score_ssa = score_vec.load()
kv_idx_ssa = kv_idx_vec.load()
if cutlass.const_expr(constant_q_idx is None):
q_idx_ssa = q_idx_vec.load()
else:
q_idx_ssa = utils.scalar_to_ssa(constant_q_idx, cutlass.Int32).broadcast_to(
(vec_size,)
)
if cutlass.const_expr(qhead_per_kvhead > 1 and constant_q_idx is None):
head_idx_ssa = head_idx_vec.load()
else:
head_idx_ssa = utils.scalar_to_ssa(head_idx, cutlass.Int32).broadcast_to(
(vec_size,)
)
grad_out_ssa = call_score_mod_bwd(
score_mod_bwd,
grad_ssa,
score_ssa,
batch_idx_ssa,
head_idx_ssa,
q_idx_ssa,
kv_idx_ssa,
seqlen_info,
aux_data,
)
grad_vec.store(grad_out_ssa)
for j in cutlass.range(vec_size, unroll_full=True):
grad_tensor[i + j] = grad_vec[j]
@@ -0,0 +1,580 @@
import math
from contextlib import nullcontext
from functools import wraps
from typing import Optional
import torch
import torch.nn.functional as F
from einops import rearrange, repeat
from torch._guards import active_fake_mode
from torch._subclasses.fake_tensor import FakeTensorMode
class IndexFirstAxis(torch.autograd.Function):
@staticmethod
def forward(ctx, input, indices):
ctx.save_for_backward(indices)
assert input.ndim >= 2
ctx.first_axis_dim, other_shape = input.shape[0], input.shape[1:]
second_dim = other_shape.numel()
return torch.gather(
rearrange(input, "b ... -> b (...)"),
0,
repeat(indices, "z -> z d", d=second_dim),
).reshape(-1, *other_shape)
@staticmethod
def backward(ctx, grad_output):
(indices,) = ctx.saved_tensors
assert grad_output.ndim >= 2
other_shape = grad_output.shape[1:]
grad_output = rearrange(grad_output, "b ... -> b (...)")
grad_input = torch.zeros(
[ctx.first_axis_dim, grad_output.shape[1]],
device=grad_output.device,
dtype=grad_output.dtype,
)
grad_input.scatter_(
0, repeat(indices, "z -> z d", d=grad_output.shape[1]), grad_output
)
return grad_input.reshape(ctx.first_axis_dim, *other_shape), None
index_first_axis = IndexFirstAxis.apply
class IndexPutFirstAxis(torch.autograd.Function):
@staticmethod
def forward(ctx, values, indices, first_axis_dim):
ctx.save_for_backward(indices)
assert indices.ndim == 1
assert values.ndim >= 2
output = torch.zeros(
first_axis_dim, *values.shape[1:], device=values.device, dtype=values.dtype
)
output[indices] = values
return output
@staticmethod
def backward(ctx, grad_output):
(indices,) = ctx.saved_tensors
grad_values = grad_output[indices]
return grad_values, None, None
index_put_first_axis = IndexPutFirstAxis.apply
def unpad_input(hidden_states, attention_mask, unused_mask=None):
all_masks = (
(attention_mask + unused_mask) if unused_mask is not None else attention_mask
)
seqlens_in_batch = all_masks.sum(dim=-1, dtype=torch.int32)
used_seqlens_in_batch = attention_mask.sum(dim=-1, dtype=torch.int32)
in_fake_mode = active_fake_mode() is not None
if not in_fake_mode:
indices = torch.nonzero(all_masks.flatten(), as_tuple=False).flatten()
max_seqlen_in_batch = seqlens_in_batch.max().item()
else:
# torch.nonzero and .item() are not supported in FakeTensorMode
batch_size, seqlen = attention_mask.shape
indices = torch.arange(batch_size * seqlen, device=hidden_states.device)
max_seqlen_in_batch = seqlen
cu_seqlens = F.pad(torch.cumsum(seqlens_in_batch, dim=0, dtype=torch.int32), (1, 0))
return (
index_first_axis(rearrange(hidden_states, "b s ... -> (b s) ..."), indices),
indices,
cu_seqlens,
max_seqlen_in_batch,
used_seqlens_in_batch,
)
def pad_input(hidden_states, indices, batch, seqlen):
output = index_put_first_axis(hidden_states, indices, batch * seqlen)
return rearrange(output, "(b s) ... -> b s ...", b=batch)
def generate_random_padding_mask(
max_seqlen, batch_size, device, mode="random", zero_lengths=False, min_seqlen=None
):
assert mode in ["full", "random", "third"]
min_seqlen = min_seqlen if min_seqlen is not None else 0 if zero_lengths else 1
if mode == "full":
lengths = torch.full(
(batch_size, 1), max_seqlen, device=device, dtype=torch.int32
)
elif mode == "random":
lengths = torch.randint(
max(min_seqlen, max_seqlen - 20),
max_seqlen + 1,
(batch_size, 1),
device=device,
)
else:
lengths = torch.randint(
max(min_seqlen, max_seqlen // 3),
max_seqlen + 1,
(batch_size, 1),
device=device,
)
if zero_lengths:
for i in range(batch_size):
if i % 5 == 0:
lengths[i] = 0
lengths[-1] = 0
padding_mask = (
repeat(torch.arange(max_seqlen, device=device), "s -> b s", b=batch_size)
< lengths
)
return padding_mask
def generate_qkv(
q,
k,
v,
query_padding_mask=None,
key_padding_mask=None,
qv=None,
kvpacked=False,
qkvpacked=False,
query_unused_mask=None,
key_unused_mask=None,
):
assert not (kvpacked and qkvpacked)
batch_size, seqlen_q, nheads, d = q.shape
d_v = v.shape[-1]
_, seqlen_k, nheads_k, _ = k.shape
assert k.shape == (batch_size, seqlen_k, nheads_k, d)
assert v.shape == (batch_size, seqlen_k, nheads_k, d_v)
if query_unused_mask is not None or key_unused_mask is not None:
assert not kvpacked
assert not qkvpacked
if query_padding_mask is not None:
q_unpad, indices_q, cu_seqlens_q, max_seqlen_q, seqused_q = unpad_input(
q, query_padding_mask, query_unused_mask
)
output_pad_fn = lambda output_unpad: pad_input(
output_unpad, indices_q, batch_size, seqlen_q
)
qv_unpad = (
rearrange(qv, "b s ... -> (b s) ...")[indices_q] if qv is not None else None
)
else:
q_unpad = rearrange(q, "b s h d -> (b s) h d")
cu_seqlens_q = torch.arange(
0,
(batch_size + 1) * seqlen_q,
step=seqlen_q,
dtype=torch.int32,
device=q_unpad.device,
)
seqused_q = None
max_seqlen_q = seqlen_q
output_pad_fn = lambda output_unpad: rearrange(
output_unpad, "(b s) h d -> b s h d", b=batch_size
)
qv_unpad = rearrange(qv, "b s ... -> (b s) ...") if qv is not None else None
if key_padding_mask is not None:
k_unpad, indices_k, cu_seqlens_k, max_seqlen_k, seqused_k = unpad_input(
k, key_padding_mask, key_unused_mask
)
v_unpad, *_ = unpad_input(v, key_padding_mask, key_unused_mask)
else:
k_unpad = rearrange(k, "b s h d -> (b s) h d")
v_unpad = rearrange(v, "b s h d -> (b s) h d")
cu_seqlens_k = torch.arange(
0,
(batch_size + 1) * seqlen_k,
step=seqlen_k,
dtype=torch.int32,
device=k_unpad.device,
)
seqused_k = None
max_seqlen_k = seqlen_k
if qkvpacked:
assert (query_padding_mask == key_padding_mask).all()
assert nheads == nheads_k
qkv_unpad = torch.stack([q_unpad, k_unpad, v_unpad], dim=1)
qkv = torch.stack([q, k, v], dim=2)
if query_padding_mask is not None:
dqkv_pad_fn = lambda dqkv_unpad: pad_input(
dqkv_unpad, indices_q, batch_size, seqlen_q
)
else:
dqkv_pad_fn = lambda dqkv_unpad: rearrange(
dqkv_unpad, "(b s) t h d -> b s t h d", b=batch_size
)
return (
qkv_unpad.detach().requires_grad_(),
cu_seqlens_q,
max_seqlen_q,
qkv.detach().requires_grad_(),
output_pad_fn,
dqkv_pad_fn,
)
elif kvpacked:
kv_unpad = torch.stack([k_unpad, v_unpad], dim=1)
kv = torch.stack([k, v], dim=2)
dq_pad_fn = output_pad_fn
if key_padding_mask is not None:
dkv_pad_fn = lambda dkv_unpad: pad_input(
dkv_unpad, indices_k, batch_size, seqlen_k
)
else:
dkv_pad_fn = lambda dkv_unpad: rearrange(
dkv_unpad, "(b s) t h d -> b s t h d", b=batch_size
)
return (
q_unpad.detach().requires_grad_(),
kv_unpad.detach().requires_grad_(),
cu_seqlens_q,
cu_seqlens_k,
max_seqlen_q,
max_seqlen_k,
q.detach().requires_grad_(),
kv.detach().requires_grad_(),
output_pad_fn,
dq_pad_fn,
dkv_pad_fn,
)
else:
dq_pad_fn = output_pad_fn
if key_padding_mask is not None:
dk_pad_fn = lambda dk_unpad: pad_input(
dk_unpad, indices_k, batch_size, seqlen_k
)
else:
dk_pad_fn = lambda dk_unpad: rearrange(
dk_unpad, "(b s) h d -> b s h d", b=batch_size
)
return (
q_unpad.detach().requires_grad_(),
k_unpad.detach().requires_grad_(),
v_unpad.detach().requires_grad_(),
qv_unpad.detach() if qv is not None else None,
cu_seqlens_q,
cu_seqlens_k,
seqused_q,
seqused_k,
max_seqlen_q,
max_seqlen_k,
q.detach().requires_grad_(),
k.detach().requires_grad_(),
v.detach().requires_grad_(),
qv.detach() if qv is not None else None,
output_pad_fn,
dq_pad_fn,
dk_pad_fn,
)
def construct_local_mask(
seqlen_q,
seqlen_k,
window_size=(None, None),
sink_token_length=0,
query_padding_mask=None,
key_padding_mask=None,
key_leftpad=None,
device=None,
):
row_idx = rearrange(
torch.arange(seqlen_q, device=device, dtype=torch.long), "s -> s 1"
)
col_idx = torch.arange(seqlen_k, device=device, dtype=torch.long)
if key_leftpad is not None:
key_leftpad = rearrange(key_leftpad, "b -> b 1 1 1")
col_idx = repeat(col_idx, "s -> b 1 1 s", b=key_leftpad.shape[0])
col_idx = torch.where(col_idx >= key_leftpad, col_idx - key_leftpad, 2**32)
sk = (
seqlen_k
if key_padding_mask is None
else rearrange(key_padding_mask.sum(-1), "b -> b 1 1 1")
)
sq = (
seqlen_q
if query_padding_mask is None
else rearrange(query_padding_mask.sum(-1), "b -> b 1 1 1")
)
if window_size[0] is None:
return col_idx > row_idx + sk - sq + window_size[1]
else:
sk = torch.full_like(col_idx, seqlen_k) if key_padding_mask is None else sk
if window_size[1] is None:
local_mask_left = col_idx > sk
else:
local_mask_left = col_idx > torch.minimum(
row_idx + sk - sq + window_size[1], sk
)
return torch.logical_or(
local_mask_left,
torch.logical_and(
col_idx < row_idx + sk - sq - window_size[0],
col_idx >= sink_token_length,
),
)
def construct_chunk_mask(
seqlen_q,
seqlen_k,
attention_chunk,
query_padding_mask=None,
key_padding_mask=None,
key_leftpad=None,
device=None,
):
row_idx = rearrange(
torch.arange(seqlen_q, device=device, dtype=torch.long), "s -> s 1"
)
col_idx = torch.arange(seqlen_k, device=device, dtype=torch.long)
if key_leftpad is not None:
key_leftpad = rearrange(key_leftpad, "b -> b 1 1 1")
col_idx = repeat(col_idx, "s -> b 1 1 s", b=key_leftpad.shape[0])
col_idx = torch.where(col_idx >= key_leftpad, col_idx - key_leftpad, 2**32)
sk = (
seqlen_k
if key_padding_mask is None
else rearrange(key_padding_mask.sum(-1), "b -> b 1 1 1")
)
sq = (
seqlen_q
if query_padding_mask is None
else rearrange(query_padding_mask.sum(-1), "b -> b 1 1 1")
)
sk = torch.full_like(col_idx, seqlen_k) if key_padding_mask is None else sk
col_limit_left_chunk = row_idx + sk - sq - (row_idx + sk - sq) % attention_chunk
return torch.logical_or(
col_idx < col_limit_left_chunk,
col_idx >= col_limit_left_chunk + attention_chunk,
)
def attention_ref(
q,
k,
v,
query_padding_mask=None,
key_padding_mask=None,
key_leftpad=None,
attn_bias=None,
dropout_p=0.0,
dropout_mask=None,
causal=False,
qv=None,
q_descale=None,
k_descale=None,
v_descale=None,
window_size=(None, None),
attention_chunk=0,
sink_token_length=0,
learnable_sink: Optional[torch.Tensor] = None,
softcap=0.0,
upcast=True,
reorder_ops=False,
intermediate_dtype=None,
return_lse=False,
gather_kv_indices=None,
rel_bias: Optional[torch.Tensor] = None, # [b, seqlen_q, h, rel_extent]
cu_seqlens_q: Optional[torch.Tensor] = None,
cu_seqlens_k: Optional[torch.Tensor] = None,
seqused_q: Optional[torch.Tensor] = None,
seqused_k: Optional[torch.Tensor] = None,
):
assert v is not None
has_qk = q is not None and k is not None
assert has_qk or qv is not None
if causal:
window_size = (window_size[0], 0)
dtype_og = v.dtype
q_shape = q.shape if q is not None else qv.shape
if upcast:
q, k, v, qv = [t.float() if t is not None else None for t in (q, k, v, qv)]
if q_descale is not None:
q_descale = repeat(q_descale, "b h -> b 1 (h g) 1", g=q_shape[2] // v.shape[2])
q, qv = [
(t.float() * q_descale).to(t.dtype) if t is not None else None
for t in (q, qv)
]
if k_descale is not None:
k = (k.float() * rearrange(k_descale, "b h -> b 1 h 1")).to(dtype=k.dtype)
if v_descale is not None:
v = (v.float() * rearrange(v_descale, "b h -> b 1 h 1")).to(dtype=v.dtype)
seqlen_q, seqlen_k = q_shape[1], v.shape[1]
k, v = [
(
repeat(t, "b s h d -> b s (h g) d", g=q_shape[2] // t.shape[2])
if t is not None
else None
)
for t in (k, v)
]
d = q_shape[-1] # == dv for qv
dv = v.shape[-1]
softmax_scale = 1.0 / math.sqrt(d if qv is None or q is None else d + dv)
if has_qk:
scores = torch.einsum(
"bthd,bshd->bhts",
q if reorder_ops else q * softmax_scale,
k * softmax_scale if reorder_ops else k,
)
if qv is not None:
qv_scores = torch.einsum(
"bthd,bshd->bhts",
qv if reorder_ops else qv * softmax_scale,
v * softmax_scale if reorder_ops else v,
)
scores = qv_scores if not has_qk else scores + qv_scores
if softcap > 0:
scores = torch.tanh(scores / softcap) * softcap
if key_padding_mask is not None:
scores.masked_fill_(
rearrange(~key_padding_mask, "b s -> b 1 1 s"), float("-inf")
)
local_mask = None
if window_size[0] is not None or window_size[1] is not None:
local_mask = construct_local_mask(
seqlen_q,
seqlen_k,
window_size,
sink_token_length,
query_padding_mask,
key_padding_mask,
key_leftpad=key_leftpad,
device=v.device,
)
if attention_chunk > 0:
chunk_mask = construct_chunk_mask(
seqlen_q,
seqlen_k,
attention_chunk,
query_padding_mask,
key_padding_mask,
key_leftpad=key_leftpad,
device=v.device,
)
local_mask = (
torch.logical_or(local_mask, chunk_mask)
if local_mask is not None
else chunk_mask
)
if gather_kv_indices is not None:
batch = q_shape[0]
topk_len = gather_kv_indices.shape[2]
if topk_len < seqlen_k:
topk_index_mask = torch.full(
(batch, seqlen_q, seqlen_k), False, device="cuda"
).scatter_(-1, gather_kv_indices, True)
scores.masked_fill_(
rearrange(~topk_index_mask, "b t s -> b 1 t s"), float("-inf")
)
if local_mask is not None:
scores.masked_fill_(local_mask, float("-inf"))
if attn_bias is not None:
scores = scores + attn_bias
if rel_bias is not None:
# Reference for Inkling sheared bias: gather rel_bias[i, h, i - j] into the [t, s] score grid.
rel_extent = rel_bias.shape[-1]
if cu_seqlens_q is not None:
seqlens_q = cu_seqlens_q[1:] - cu_seqlens_q[:-1]
elif seqused_q is not None:
seqlens_q = seqused_q
else:
seqlens_q = torch.full(
(q.shape[0],), seqlen_q, device=q.device, dtype=torch.long
)
if cu_seqlens_k is not None:
seqlens_k = cu_seqlens_k[1:] - cu_seqlens_k[:-1]
elif seqused_k is not None:
seqlens_k = seqused_k
else:
seqlens_k = torch.full(
(q.shape[0],), seqlen_k, device=q.device, dtype=torch.long
)
seqlen_offset = (seqlens_k - seqlens_q).to(torch.long) # [b]
q_idx = torch.arange(seqlen_q, device=q.device, dtype=torch.long)
kv_idx = torch.arange(seqlen_k, device=q.device, dtype=torch.long)
rel_dist = (
q_idx.unsqueeze(1) - kv_idx.unsqueeze(0) + seqlen_offset.view(-1, 1, 1)
) # [b, seqlen_q, seqlen_k]
safe_dist = rel_dist.clamp(0, rel_extent - 1)
is_within_window = (rel_dist >= 0) & (rel_dist < rel_extent)
idx = safe_dist.unsqueeze(2).expand(-1, -1, rel_bias.shape[2], -1)
abs_bias = rel_bias.gather(dim=-1, index=idx) # [b, seqlen_q, h, seqlen_k]
abs_bias = rearrange(abs_bias, "b t h s -> b h t s")
abs_bias = abs_bias.masked_fill(
rearrange(~is_within_window, "b t s -> b 1 t s"), 0.0
)
scores = scores + abs_bias
# After all masks are applied, before softmax:
# scores shape: [b, h, t, s]
lse = torch.logsumexp(scores, dim=-1) # [b, h, t]
if learnable_sink is None:
attention = torch.softmax(scores, dim=-1).to(v.dtype)
else:
scores_fp32 = scores.to(torch.float32)
logits_max = torch.amax(scores_fp32, dim=-1, keepdim=True)
learnable_sink = rearrange(learnable_sink, "h -> h 1 1")
logits_or_sinks_max = torch.maximum(learnable_sink, logits_max)
unnormalized_scores = torch.exp(scores_fp32 - logits_or_sinks_max)
normalizer = unnormalized_scores.sum(dim=-1, keepdim=True) + torch.exp(
learnable_sink - logits_or_sinks_max
)
# LSE with sink: log(Z) = log(normalizer) + max
lse = (torch.log(normalizer.squeeze(-1)) + logits_or_sinks_max.squeeze(-1)).to(
dtype_og
)
attention = (unnormalized_scores / normalizer).to(v.dtype)
if query_padding_mask is not None:
attention = attention.masked_fill(
rearrange(~query_padding_mask, "b s -> b 1 s 1"), 0.0
)
if key_padding_mask is not None:
attention = attention.masked_fill(
rearrange(~key_padding_mask, "b s -> b 1 1 s"), 0.0
)
if local_mask is not None:
attention = attention.masked_fill(
torch.all(local_mask, dim=-1, keepdim=True), 0.0
)
dropout_scaling = 1.0 / (1 - dropout_p)
if dropout_mask is not None:
attention_drop = attention.masked_fill(~dropout_mask, 0.0)
else:
attention_drop = attention
if intermediate_dtype is not None:
attention_drop = attention_drop.to(intermediate_dtype).to(attention_drop.dtype)
output = torch.einsum("bhts,bshd->bthd", attention_drop, v * dropout_scaling)
if query_padding_mask is not None:
output.masked_fill_(rearrange(~query_padding_mask, "b s -> b s 1 1"), 0.0)
if return_lse:
return output.to(dtype_og), attention.to(dtype_og), lse.to(dtype_og)
return output.to(dtype=dtype_og), attention.to(dtype=dtype_og)
def maybe_fake_tensor_mode(fake: bool = True):
"""
One way to populate/pre-compile cache is to use torch fake tensor mode,
which does not allocate actual GPU tensors but retains tensor shape/dtype
metadata for cute.compile.
"""
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
with FakeTensorMode() if fake else nullcontext():
return fn(*args, **kwargs)
return wrapper
return decorator
def is_fake_mode() -> bool:
return active_fake_mode() is not None
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,289 @@
import math
import operator
from dataclasses import dataclass
from typing import Optional, Type
import cutlass
import cutlass.cute as cute
import cutlass.pipeline as pipeline
from cutlass import Boolean, Int32, Uint32, const_expr
from cutlass.cute.nvgpu import cpasync
from quack.cute_dsl_utils import ParamsBase
from sglang.jit_kernel.flash_attn.cute import utils
from sglang.jit_kernel.flash_attn.cute.utils import warp_reduce
@dataclass
class CpasyncGatherKVManager(ParamsBase):
mIndexTopk: cute.Tensor
sBitmask: Optional[cute.Tensor]
cta_rank_in_cluster: Int32
thread_idx: Int32
warp_idx: Int32
topk_length: Int32
seqlen_k_limit: Int32
tile_n: Int32
num_threads: cutlass.Constexpr[Int32]
hdim: cutlass.Constexpr[Int32]
hdim_v: cutlass.Constexpr[Int32]
num_hdimv_splits: cutlass.Constexpr[Int32]
cta_group_size: cutlass.Constexpr[Int32]
gmem_threads_per_row: cutlass.Constexpr[Int32]
topk_indices_per_thread: Int32
async_copy_elems: Int32
gmem_tiled_copy_KV: cute.TiledCopy
gmem_thr_copy_KV: cute.TiledCopy
rTopk: cute.Tensor
rTopkHalf: cute.Tensor
# for bitmask
rTopk_NonInterleaved: cute.Tensor
pipeline_bitmask: Optional[pipeline.PipelineAsync]
cpasync_barrier: Optional[pipeline.NamedBarrier]
disable_bitmask: cutlass.Constexpr[Boolean]
@staticmethod
def create(
mIndexTopk: cute.Tensor,
cta_rank_in_cluster: Int32,
thread_idx: Int32,
warp_idx: Int32,
topk_length: Int32,
seqlen_k_limit: Int32,
tile_n: cutlass.Constexpr[Int32],
hdim: cutlass.Constexpr[Int32],
hdim_v: cutlass.Constexpr[Int32],
num_hdimv_splits: cutlass.Constexpr[Int32],
num_threads: cutlass.Constexpr[Int32],
dtype: Type[cutlass.Numeric],
cta_group_size: cutlass.Constexpr[Int32],
cpasync_barrier: Optional[pipeline.NamedBarrier] = None,
disable_bitmask: cutlass.Constexpr[Boolean] = True,
sBitmask: Optional[cute.Tensor] = None,
pipeline_bitmask: Optional[pipeline.PipelineAsync] = None,
):
assert tile_n % num_threads == 0
assert num_threads == 128
assert hdim % 64 == 0
assert (hdim_v // num_hdimv_splits // cta_group_size) % 64 == 0
assert num_threads % cute.arch.WARP_SIZE == 0
universal_copy_bits = 128
async_copy_elems = universal_copy_bits // dtype.width
dtype_bytes = dtype.width // 8
# assumes hdim is never part of transposed operand
gmem_k_block_size = math.gcd(
hdim,
hdim_v // num_hdimv_splits // cta_group_size,
128 // dtype_bytes,
)
assert gmem_k_block_size % async_copy_elems == 0
gmem_threads_per_row = gmem_k_block_size // async_copy_elems
assert cute.arch.WARP_SIZE % gmem_threads_per_row == 0
atom_async_copy = cute.make_copy_atom(
cpasync.CopyG2SOp(cache_mode=cpasync.LoadCacheMode.GLOBAL),
dtype,
num_bits_per_copy=universal_copy_bits,
)
thr_layout = cute.make_ordered_layout(
(num_threads // gmem_threads_per_row, gmem_threads_per_row),
order=(1, 0),
)
val_layout = cute.make_layout((1, async_copy_elems))
gmem_tiled_copy_KV = cute.make_tiled_copy_tv(
atom_async_copy, thr_layout, val_layout
)
gmem_thr_copy_KV = gmem_tiled_copy_KV.get_slice(thread_idx)
topk_indices_per_thread = tile_n // num_threads
rTopk = cute.make_rmem_tensor((topk_indices_per_thread,), Int32)
rTopkHalf = cute.make_rmem_tensor((topk_indices_per_thread,), Int32)
rTopk_NonInterleaved = cute.make_rmem_tensor((topk_indices_per_thread,), Int32)
return CpasyncGatherKVManager(
mIndexTopk,
sBitmask,
cta_rank_in_cluster,
thread_idx,
warp_idx,
topk_length,
seqlen_k_limit,
tile_n,
num_threads,
hdim,
hdim_v,
num_hdimv_splits,
cta_group_size,
gmem_threads_per_row,
topk_indices_per_thread,
async_copy_elems,
gmem_tiled_copy_KV,
gmem_thr_copy_KV,
rTopk,
rTopkHalf,
rTopk_NonInterleaved,
pipeline_bitmask,
cpasync_barrier,
disable_bitmask,
)
@cute.jit
def load_index_topk(
self,
n_block: Int32,
transpose: bool,
):
entries_per_thread = self.topk_indices_per_thread
rTopk = self.rTopk if const_expr(transpose) else self.rTopkHalf
for i in cutlass.range_constexpr(entries_per_thread):
row = (
i * self.num_threads
+ (self.thread_idx % self.gmem_threads_per_row)
* (self.num_threads // self.gmem_threads_per_row)
+ (self.thread_idx // self.gmem_threads_per_row)
)
# need this if not offset in load_X
# if const_expr(not transpose):
# row += self.cta_rank_in_cluster * (self.tile_n//self.cta_group_size)
# row = row % self.tile_n
row_idx = n_block * self.tile_n + row
rTopk[i] = self.mIndexTopk[row_idx]
if const_expr(not transpose and not self.disable_bitmask):
row_non_interleaved = i * self.num_threads + self.thread_idx
row_idx_non_interleaved = n_block * self.tile_n + row_non_interleaved
self.rTopk_NonInterleaved[0] = self.mIndexTopk[row_idx_non_interleaved]
@cute.jit
def compute_bitmask(
self,
producer_state_bitmask,
):
assert self.pipeline_bitmask is not None, "pipeline_bitmask not provided"
assert self.cpasync_barrier is not None, "cpasync barrier not provided"
lane_idx = cute.arch.lane_idx()
assert cute.size(self.rTopk_NonInterleaved) == 1
bitmask = Uint32(0)
# Step 1. Construct per-thread bitmask
topk_idx = self.rTopk_NonInterleaved[0]
is_valid = topk_idx >= 0 and topk_idx < self.seqlen_k_limit
if is_valid:
bitmask = Uint32(1 << lane_idx)
# Step 2. Warp shuffle bitwise OR = add since indices are exclusive.
bitmask = warp_reduce(bitmask, operator.add)
self.pipeline_bitmask.producer_acquire(producer_state_bitmask)
# store to smem and sync threads
if lane_idx == 0:
self.sBitmask[self.warp_idx, producer_state_bitmask.index] = bitmask
self.cpasync_barrier.arrive_and_wait()
self.pipeline_bitmask.producer_commit(producer_state_bitmask)
producer_state_bitmask.advance()
return producer_state_bitmask
@cute.jit
def compute_X_ptr(
self,
mX: cute.Tensor,
transpose: bool,
d_offset: int = 0,
):
entries_per_thread = self.topk_indices_per_thread
tPrXPtr = cute.make_rmem_tensor((entries_per_thread,), cutlass.Int64)
tPrRowValid = cute.make_rmem_tensor((entries_per_thread,), cutlass.Int32)
rTopk = self.rTopk if const_expr(transpose) else self.rTopkHalf
for i in cutlass.range_constexpr(entries_per_thread):
topk_idx = rTopk[i]
if const_expr(not self.disable_bitmask):
row_valid = topk_idx >= 0 and topk_idx < self.seqlen_k_limit
tPrRowValid[i] = row_valid
if const_expr(not transpose):
tPrXPtr[i] = utils.elem_pointer(mX, (topk_idx, d_offset)).toint()
else:
tPrXPtr[i] = utils.elem_pointer(mX, (d_offset, topk_idx)).toint()
return tPrXPtr, tPrRowValid
@cute.jit
def load_X(
self,
mX: cute.Tensor,
sX: cute.Tensor,
transpose: bool,
K_or_V: str,
d_offset: int = 0,
):
assert K_or_V in ("K", "V")
cta_tile_n = (
self.tile_n if const_expr(transpose) else self.tile_n // self.cta_group_size
)
head_dim = (
self.hdim
if const_expr(K_or_V == "K")
else self.hdim_v // self.num_hdimv_splits
)
if const_expr(transpose):
head_dim = head_dim // self.cta_group_size
order = (1, 0) if const_expr(transpose) else (0, 1)
sX_nd_layout = cute.make_ordered_layout((cta_tile_n, head_dim), order=order)
sX_nd = cute.composition(sX, sX_nd_layout)
cX = cute.make_identity_tensor((cta_tile_n, head_dim))
tXsX = self.gmem_thr_copy_KV.partition_D(sX_nd)
tXcX = self.gmem_thr_copy_KV.partition_S(cX)
tPrXPtr, tPrRowValid = self.compute_X_ptr(mX, transpose, d_offset)
if const_expr(not transpose):
offset = self.cta_rank_in_cluster * (
self.gmem_threads_per_row // self.cta_group_size
)
else:
offset = 0
for m in cutlass.range_constexpr(cute.size(tXsX, mode=[1])):
if const_expr(not self.disable_bitmask):
row_valid = utils.shuffle_sync(
tPrRowValid[m // self.gmem_threads_per_row],
(m + offset) % self.gmem_threads_per_row,
width=self.gmem_threads_per_row,
)
should_load = cute.make_fragment_like(tXsX[(0, None), m, 0], Boolean)
should_load.fill(Boolean(row_valid))
x_ptr_i64 = utils.shuffle_sync(
tPrXPtr[m // self.gmem_threads_per_row],
(m + offset) % self.gmem_threads_per_row,
width=self.gmem_threads_per_row,
)
x_gmem_ptr = cute.make_ptr(
mX.element_type, x_ptr_i64, cute.AddressSpace.gmem, assumed_align=16
)
mX_cur = cute.make_tensor(x_gmem_ptr, cute.make_layout((head_dim,)))
mX_cur_copy = cute.tiled_divide(mX_cur, (self.async_copy_elems,))
for k in cutlass.range_constexpr(cute.size(tXsX, mode=[2])):
ki = tXcX[0, 0, k][1] // self.async_copy_elems
mX_cur_copy_ki = mX_cur_copy[None, ki]
tXsX_k = tXsX[None, m, k]
mX_cur_copy_ki = cute.make_tensor(
mX_cur_copy_ki.iterator, tXsX_k.layout
)
cute.copy(
self.gmem_tiled_copy_KV,
mX_cur_copy_ki,
tXsX_k,
pred=should_load if const_expr(not self.disable_bitmask) else None,
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,357 @@
"""CUDA-JIT all-reduce kernels for Inkling symmetric-memory buffers.
The producer writes its local shard into the symmetric buffer, and the reduced
result remains there so callers do not need staging or copy-out kernels.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.jit_kernel.utils import cache_once, empty_sentinel, load_jit, make_cpp_args
if TYPE_CHECKING:
from tvm_ffi.module import Module
@cache_once
def _jit_inkling_all_reduce_module(dtype: torch.dtype, world_size: int) -> Module:
args = make_cpp_args(dtype, world_size)
return load_jit(
"inkling_all_reduce",
*args,
cuda_files=["inkling/inkling_all_reduce.cuh"],
cuda_wrappers=[
("two_shot_all_reduce", f"inkling_two_shot_all_reduce<{args}>"),
("two_shot_all_reduce_fused", f"inkling_two_shot_all_reduce_fused<{args}>"),
("multimem_one_shot_fused", f"inkling_multimem_one_shot_fused<{args}>"),
("multimem_full_oneshot", f"inkling_multimem_full_oneshot<{args}>"),
("multimem_push_oneshot", f"inkling_multimem_push_oneshot<{args}>"),
],
)
# Barrier resources for the fused kernels:
# * flags: a DEDICATED symmetric uint32 buffer, zero-initialized once at
# setup: `world_size` single-leader slots (one per peer), then
# world_size * MAX_BARRIER_BLOCKS per-(writer, block) slots for the
# per-block barrier (v5's multi-block flavor).
# * state: a device-LOCAL uint32 buffer: the 5 words
# [arrival0, arrival1, release0, release1, xepoch] padded to 8, then
# MAX_BARRIER_BLOCKS per-block epochs; persists across calls and advances
# under CUDA-graph replay.
# Keep these sizes aligned with the CUDA barrier implementation.
MAX_BARRIER_BLOCKS = 256
STATE_SIZE = 8 + MAX_BARRIER_BLOCKS
def flags_numel(world_size: int) -> int:
return world_size * (1 + MAX_BARRIER_BLOCKS)
# Tuned (kernel, num_blocks, block_size) per reduction row count. Kernels:
# "v5"=push one-shot with per-block barriers (single barrier, out-of-place),
# "mm"=torch multimem, "v2"=two-shot explicit,
# "v3"=two-shot multimem (single-leader barriers), "v3b"=v3 with per-block
# barriers, and "v4"=full one-shot. nb/bs are 0 for "mm". Tables are keyed
# by world size; TP4 is the fallback.
_AR_TUNED_TP4 = {
1: ("v5", 1, 1024),
2: ("v5", 1, 1024),
3: ("v5", 8, 512),
4: ("v5", 8, 512),
6: ("v5", 8, 1024),
8: ("v5", 8, 1024),
12: ("v5", 8, 512),
16: ("v5", 8, 512),
24: ("v5", 8, 1024),
32: ("v5", 8, 1024),
48: ("v5", 48, 1024),
64: ("v5", 48, 1024),
96: ("v5", 64, 1024),
128: ("mm", 0, 0),
192: ("mm", 0, 0),
256: ("v3b", 64, 1024),
384: ("v3b", 32, 1024),
512: ("v3b", 32, 1024),
768: ("v3b", 48, 512),
1024: ("v3b", 32, 1024),
1536: ("v3", 64, 512),
2048: ("v3", 64, 512),
3072: ("v3", 96, 512),
4096: ("v3", 96, 512),
6144: ("v3", 64, 512),
8192: ("v3", 32, 1024),
12288: ("v3", 96, 512),
16384: ("v3", 96, 512),
}
# TP8 uses full one-shot for the smallest shapes, multimem through the
# medium-sized range, and two-shot multimem for larger reductions.
_AR_TUNED_TP8 = {
1: ("v4", 1, 1024),
2: ("v4", 1, 1024),
3: ("mm", 0, 0),
4: ("mm", 0, 0),
6: ("mm", 0, 0),
8: ("mm", 0, 0),
12: ("mm", 0, 0),
16: ("mm", 0, 0),
24: ("mm", 0, 0),
32: ("mm", 0, 0),
48: ("mm", 0, 0),
64: ("mm", 0, 0),
96: ("mm", 0, 0),
128: ("mm", 0, 0),
192: ("mm", 0, 0),
256: ("mm", 0, 0),
384: ("mm", 0, 0),
512: ("mm", 0, 0),
768: ("mm", 0, 0),
1024: ("v3", 32, 512),
1536: ("v3", 16, 1024),
2048: ("v3", 32, 512),
3072: ("v3", 48, 512),
4096: ("v3", 48, 512),
6144: ("v3", 64, 512),
8192: ("v3", 96, 256),
12288: ("v3", 64, 512),
16384: ("v3", 64, 512),
}
_AR_TUNED = {4: _AR_TUNED_TP4, 8: _AR_TUNED_TP8}
_AR_TUNED_TOKENS = sorted(_AR_TUNED_TP4) # same token grid for every table
assert all(
set(t) == set(_AR_TUNED_TP4) for t in _AR_TUNED.values()
), "all tuned tables must share the same token grid"
def select_ar_config(num_tokens: int, world_size: int = 4):
"""Return (kernel, num_blocks, block_size) for a ``[num_tokens, hidden]``
reduction, from the autotuned table for ``world_size`` (round up to the
nearest tested shape). Untuned world sizes fall back to the TP4 table.
``kernel`` is one of "v5"/"v4"/"mm"/"v2"/"v3"/"v3b"."""
table = _AR_TUNED.get(world_size, _AR_TUNED_TP4)
for t in _AR_TUNED_TOKENS:
if num_tokens <= t:
return table[t]
return table[_AR_TUNED_TOKENS[-1]]
def compile_inkling_all_reduce(dtype: torch.dtype, world_size: int) -> None:
"""Warm the JIT module for (dtype, world_size) so the first call is cheap."""
_jit_inkling_all_reduce_module(dtype, world_size)
def inkling_two_shot_all_reduce(
buffer: torch.Tensor,
peer_ptrs_dev: int,
rank: int,
world_size: int,
num_items: int,
) -> None:
"""Two-shot all-reduce in place over ``num_items`` elements of the symm buffer.
Args:
buffer: this rank's symm buffer (1D, contiguous, bf16), sliced to
``num_items``; used for device/dtype validation. The producer must
have already written this rank's shard into it.
peer_ptrs_dev: ``hdl.buffer_ptrs_dev`` -- device address of the array of
``world_size`` peer buffer base pointers.
rank: this rank within the TP group.
world_size: TP world size (compile-time template arg).
num_items: number of elements to reduce (multiple of 8 for bf16).
The caller is responsible for ``hdl.barrier()`` before (producers done) and
after (result visible) this call.
"""
module = _jit_inkling_all_reduce_module(buffer.dtype, world_size)
module.two_shot_all_reduce(buffer, peer_ptrs_dev, rank, num_items)
def inkling_two_shot_all_reduce_fused(
buffer: torch.Tensor,
data_ptrs_dev: int,
flag_ptrs_dev: int,
state_ptr: int,
rank: int,
world_size: int,
num_items: int,
num_blocks: int = 0,
block_size: int = 0,
shared: torch.Tensor | None = None,
) -> None:
"""Single-launch two-shot all-reduce with an in-kernel grid-level barrier.
Args:
buffer: this rank's symm data buffer (bf16), sliced to ``num_items``.
data_ptrs_dev: ``hdl.buffer_ptrs_dev`` for the data buffer.
flag_ptrs_dev: ``buffer_ptrs_dev`` of a DEDICATED symm ``uint32[world_size]``
flags buffer, zero-initialized once at setup.
state_ptr: ``data_ptr()`` of a device-local ``uint32[STATE_SIZE]`` barrier
state buffer (persists across calls; advances under graph replay).
rank, world_size: TP coordinates (world_size is a template arg).
num_items: elements to reduce (multiple of 8 for bf16).
No external barrier needed -- the kernel fences both sides itself.
"""
module = _jit_inkling_all_reduce_module(buffer.dtype, world_size)
module.two_shot_all_reduce_fused(
buffer,
data_ptrs_dev,
flag_ptrs_dev,
state_ptr,
rank,
num_items,
num_blocks,
block_size,
shared if shared is not None else empty_sentinel(buffer.device, buffer.dtype),
)
def inkling_multimem_one_shot_fused(
buffer: torch.Tensor,
multicast_ptr: int,
flag_ptrs_dev: int,
state_ptr: int,
rank: int,
world_size: int,
num_items: int,
num_blocks: int = 0,
block_size: int = 0,
per_block_barrier: bool = False,
shared: torch.Tensor | None = None,
) -> None:
"""Single-launch multimem one-shot all-reduce (NVLink multicast ld_reduce/st).
Matches torch multimem for tiny, latency-bound (decode) messages, in a kernel
we own so norm/sconv can fuse at the epilogue seam.
Args:
buffer: this rank's symm data buffer (bf16), sliced to ``num_items``.
multicast_ptr: ``hdl.multicast_ptr`` for the data buffer (must be != 0).
flag_ptrs_dev, state_ptr: dedicated barrier flags + local state buffer
(same as the fused two-shot).
rank, world_size, num_items: as above.
per_block_barrier: use per-block peer handshakes for both barriers (no
grid funnel; capped at MAX_BARRIER_BLOCKS blocks).
"""
module = _jit_inkling_all_reduce_module(buffer.dtype, world_size)
module.multimem_one_shot_fused(
buffer,
multicast_ptr,
flag_ptrs_dev,
state_ptr,
rank,
num_items,
num_blocks,
block_size,
int(per_block_barrier),
shared if shared is not None else empty_sentinel(buffer.device, buffer.dtype),
)
def inkling_multimem_full_oneshot(
in_buffer: torch.Tensor,
out_buffer: torch.Tensor,
multicast_ptr: int,
flag_ptrs_dev: int,
state_ptr: int,
rank: int,
world_size: int,
num_items: int,
num_blocks: int = 0,
block_size: int = 0,
shared: torch.Tensor | None = None,
) -> None:
"""Full one-shot all-reduce with a SINGLE (entry-only) barrier.
Every rank ld_reduces the entire range (multicast hardware sum) into its
local ``out_buffer`` -- no broadcast, no exit barrier. Fastest for tiny
latency-bound messages, but the caller MUST double-buffer ``in_buffer`` (its
reuse is not fenced by this kernel; the next AR's entry barrier orders it).
Args:
in_buffer: this rank's symm data buffer (bf16), sliced to ``num_items``.
out_buffer: local output buffer (bf16, >= num_items); receives the sum.
multicast_ptr: ``hdl.multicast_ptr`` of the in_buffer.
flag_ptrs_dev, state_ptr: barrier flags + local state (as above).
rank, world_size, num_items: as above.
"""
module = _jit_inkling_all_reduce_module(in_buffer.dtype, world_size)
module.multimem_full_oneshot(
in_buffer,
out_buffer,
multicast_ptr,
flag_ptrs_dev,
state_ptr,
rank,
num_items,
num_blocks,
block_size,
(
shared
if shared is not None
else empty_sentinel(in_buffer.device, in_buffer.dtype)
),
)
def inkling_multimem_push_oneshot(
in_buffer: torch.Tensor,
out_buffer: torch.Tensor,
mc_stage_ptr: int,
local_stage_ptr: int,
flag_ptrs_dev: int,
state_ptr: int,
rank: int,
world_size: int,
num_items: int,
num_blocks: int = 0,
block_size: int = 0,
per_block_barrier: bool = False,
shared: torch.Tensor | None = None,
) -> None:
"""One-shot PUSH all-reduce (v5) with a SINGLE mid barrier.
Each rank multicast-stores its full input into its per-rank slot of the
symmetric staging area (slot ``r`` at elem offset ``r * num_items``), the
barrier waits for all pushes to land, then each rank reduces the
``world_size`` staged shards locally (fp32 accum) into ``out_buffer``.
Drops one barrier round trip vs the two-shot kernels, and each rank holds
the full row at the epilogue seam (norm-fusion base, like v4 but scaling
past 2 rows).
Args:
in_buffer: this rank's LOCAL input (any contiguous 16B-aligned bf16
tensor -- need not be a symm buffer; it is only read locally).
out_buffer: local output buffer (bf16, >= num_items); receives the sum.
mc_stage_ptr: multicast address of the staging area (>= world_size *
num_items elems). The caller MUST double-buffer the staging area
(A/B rotation; the next AR's barrier orders the reuse, like v4).
local_stage_ptr: this GPU's local address of the same staging area.
flag_ptrs_dev, state_ptr: barrier flags + local state (as above).
rank, world_size, num_items: as above.
per_block_barrier: use the per-block peer handshake (no grid funnel;
capped at MAX_BARRIER_BLOCKS blocks) instead of the single-leader
grid barrier -- the multi-block latency winner.
"""
module = _jit_inkling_all_reduce_module(in_buffer.dtype, world_size)
module.multimem_push_oneshot(
in_buffer,
out_buffer,
mc_stage_ptr,
local_stage_ptr,
flag_ptrs_dev,
state_ptr,
rank,
num_items,
num_blocks,
block_size,
int(per_block_barrier),
(
shared
if shared is not None
else empty_sentinel(in_buffer.device, in_buffer.dtype)
),
)
@@ -0,0 +1,222 @@
"""Fused all-reduce, decode short-convolution, and RMSNorm for Inkling.
The small-batch decode kernel processes one token per block.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.jit_kernel.utils import cache_once, empty_sentinel, load_jit, make_cpp_args
if TYPE_CHECKING:
from tvm_ffi.module import Module
@cache_once
def _jit_ar_fused_module(
dtype: torch.dtype,
world_size: int,
w: int,
use_silu: bool,
use_residual: bool,
do_track: bool,
) -> Module:
args = make_cpp_args(dtype, world_size, w, use_silu, use_residual, do_track)
return load_jit(
"inkling_ar_fused_decode",
*args,
cuda_files=["inkling/inkling_ar_fused_decode.cuh"],
cuda_wrappers=[
("ar_sconv_norm", f"ArSconvNormKernel<{args}>::run"),
("ar_sconv_norm_verify", f"ArSconvNormVerifyKernel<{args}>::run"),
],
)
# Tuned vectors per thread by decode row count; round up to the next entry.
_FUSED_VPT_TUNED = {1: 1, 2: 1, 4: 1, 8: 1, 16: 1, 32: 1, 64: 1, 96: 1}
_FUSED_VPT_TOKENS = sorted(_FUSED_VPT_TUNED)
def select_fused_vpt(num_tokens: int) -> int:
for t in _FUSED_VPT_TOKENS:
if num_tokens <= t:
return _FUSED_VPT_TUNED[t]
return _FUSED_VPT_TUNED[_FUSED_VPT_TOKENS[-1]]
def compile_inkling_ar_sconv_norm(
dtype: torch.dtype,
world_size: int,
w: int,
use_silu: bool,
use_residual: bool,
do_track: bool,
) -> None:
"""Warm the JIT module so the first fused call is cheap."""
_jit_ar_fused_module(dtype, world_size, w, use_silu, use_residual, do_track)
def inkling_ar_sconv_norm(
in_partial: torch.Tensor,
residual_in: torch.Tensor,
residual_out: torch.Tensor,
hs_out: torch.Tensor,
norm_weight: torch.Tensor,
eps: float,
sconv_cache: torch.Tensor,
cache_indices: torch.Tensor,
cache_mask: torch.Tensor,
conv_weight: torch.Tensor,
mc_stage_ptr: int,
local_stage_ptr: int,
flag_ptrs_dev: int,
state_ptr: int,
rank: int,
world_size: int,
activation: str | None = None,
use_residual: bool = True,
track_mask: torch.Tensor | None = None,
track_indices: torch.Tensor | None = None,
enable_pdl: bool = True,
vecs_per_thread: int = 0,
shared: torch.Tensor | None = None,
) -> None:
"""Fused AR + decode sconv + add-RMSNorm over ``[T, D]`` decode rows.
Args:
in_partial: this rank's LOCAL partial sums (``[T, D]`` bf16, contiguous
rows, 16B-aligned) -- e.g. the MoE combine output with
``reduce=False``. Read locally only (no stage-in copy).
shared: optional LOCAL ``[T, D]`` shared-expert partials, folded into
the pushed value in registers (fp32 add, one bf16 round --
torch.add numerics), replacing the separate ``routed + shared``
add kernel at zero extra traffic. All ranks must agree on passing it.
residual_in / residual_out: the residual stream before/after the fused
add (may alias); ``hs_out``: the normed output.
norm_weight, eps: RMSNorm gamma (``[D]`` bf16) and epsilon.
sconv_cache..conv_weight, track_*: exactly the tensors
``fused_causal_conv1d_update_decode`` takes; the conv state is
shift-updated in place, identically to the unfused kernel.
mc_stage_ptr / local_stage_ptr: multicast + local address of the v5
staging rotation slot (>= world_size*T*D elems; caller rotates A/B,
same reuse-distance rule as v5).
flag_ptrs_dev / state_ptr / rank / world_size: barrier resources
(shared with the other fused AR kernels).
"""
if activation == "swish":
activation = "silu"
use_silu = activation in ("silu", "swish")
do_track = track_mask is not None
w = conv_weight.shape[1]
if do_track:
tm = track_mask.reshape(-1)
ti = track_indices
else: # dummies; DO_TRACK=false never reads them
tm = torch.empty(0, dtype=torch.bool, device=in_partial.device)
ti = torch.empty(0, dtype=torch.int64, device=in_partial.device)
module = _jit_ar_fused_module(
in_partial.dtype, world_size, w, use_silu, use_residual, do_track
)
if vecs_per_thread <= 0:
vecs_per_thread = select_fused_vpt(in_partial.shape[0])
sh = (
shared
if shared is not None
else empty_sentinel(in_partial.device, in_partial.dtype)
)
module.ar_sconv_norm(
in_partial,
residual_in,
residual_out,
hs_out,
norm_weight,
float(eps),
sconv_cache,
cache_indices,
cache_mask.reshape(-1),
conv_weight,
tm,
ti,
mc_stage_ptr,
local_stage_ptr,
flag_ptrs_dev,
state_ptr,
rank,
int(enable_pdl),
int(vecs_per_thread),
sh,
)
def inkling_ar_sconv_norm_verify(
in_partial: torch.Tensor,
residual_in: torch.Tensor,
residual_out: torch.Tensor,
hs_out: torch.Tensor,
norm_weight: torch.Tensor,
eps: float,
sconv_cache: torch.Tensor,
cache_indices: torch.Tensor,
cache_mask: torch.Tensor,
conv_weight: torch.Tensor,
inter_out: torch.Tensor,
draft_token_num: int,
mc_stage_ptr: int,
local_stage_ptr: int,
flag_ptrs_dev: int,
state_ptr: int,
rank: int,
world_size: int,
activation: str | None = None,
use_residual: bool = True,
enable_pdl: bool = True,
shared: torch.Tensor | None = None,
) -> None:
"""Target-verify fused {AR -> causal_conv1d -> save_intermediate_conv_windows
-> add+RMSNorm} over ``[B*draft_token_num, D]`` rows.
``cache_indices``/``cache_mask`` are per-SEQUENCE (``[B]``); the working
conv cache is read-only (the per-position windows go to ``inter_out``,
exactly like ``save_intermediate_conv_windows``). Cross-token conv taps are
re-reduced from the v5 staging slot, so the same rotation rules as
``inkling_ar_sconv_norm`` apply. ``shared``: optional LOCAL ``[T, D]``
shared-expert partials folded into the push (torch.add numerics).
"""
if activation == "swish":
activation = "silu"
use_silu = activation in ("silu", "swish")
w = conv_weight.shape[1]
# do_track slot in the module key is unused by the verify kernel.
module = _jit_ar_fused_module(
in_partial.dtype, world_size, w, use_silu, use_residual, False
)
sh = (
shared
if shared is not None
else empty_sentinel(in_partial.device, in_partial.dtype)
)
module.ar_sconv_norm_verify(
in_partial,
residual_in,
residual_out,
hs_out,
norm_weight,
float(eps),
sconv_cache,
cache_indices.to(torch.int32),
cache_mask,
conv_weight,
inter_out,
int(draft_token_num),
mc_stage_ptr,
local_stage_ptr,
flag_ptrs_dev,
state_ptr,
rank,
int(enable_pdl),
sh,
)
@@ -0,0 +1,349 @@
"""Fused all-reduce and scattered short-convolution for Inkling.
The kernel reduces a per-rank hidden-channel slice, applies causal convolution,
and updates the convolution and prefix caches in one launch.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.jit_kernel.utils import cache_once, load_jit, make_cpp_args
if TYPE_CHECKING:
from tvm_ffi.module import Module
@cache_once
def _jit_ar_scattered_sconv_module(
dtype: torch.dtype,
world_size: int,
w: int,
use_silu: bool,
use_residual: bool,
) -> Module:
args = make_cpp_args(dtype, world_size, w, use_silu, use_residual)
return load_jit(
"inkling_ar_scattered_sconv",
*args,
cuda_files=["inkling/inkling_ar_scattered_sconv.cuh"],
cuda_wrappers=[
("ar_scattered_sconv", f"ArScatteredSconvKernel<{args}>::run"),
("ar_banded_sconv", f"ArBandedSconvKernel<{args}>::run"),
("ar_ssconv_norm_decode", f"SsconvNormDecodeKernel<{args}>::run"),
("ar_col_decode", f"ColDecodeKernel<{args}>::run"),
],
)
def compile_inkling_ar_scattered_sconv(
dtype: torch.dtype,
world_size: int,
w: int,
use_silu: bool,
use_residual: bool,
) -> None:
"""Warm the JIT module so the first fused call is cheap."""
_jit_ar_scattered_sconv_module(dtype, world_size, w, use_silu, use_residual)
def inkling_ar_scattered_sconv(
in_buffer: torch.Tensor,
x_scratch: torch.Tensor,
sconv_cache: torch.Tensor,
safe_idx: torch.Tensor,
cache_mask: torch.Tensor,
cache_indices: torch.Tensor,
has_initial_state: torch.Tensor,
cu: torch.Tensor,
si: torch.Tensor,
weight: torch.Tensor,
track_rows: torch.Tensor,
track_mask: torch.Tensor,
track_dst: torch.Tensor,
mc_in: int,
mc_out: int,
flag_ptrs_dev: int,
state_ptr: int,
rank: int,
world_size: int,
*,
activation: str | None,
use_residual: bool,
num_blocks: int = 0,
block_size: int = 0,
per_block_barrier: bool = False,
track_from_cache: bool = False,
out_local: torch.Tensor | None = None,
norm_gamma: torch.Tensor | None = None,
norm_residual: torch.Tensor | None = None,
norm_out: torch.Tensor | None = None,
norm_eps: float = 0.0,
need_scratch: bool = True,
use_stream: bool = False,
stream_walk: int = 0,
full_update: bool = False,
cache_col0: int = 0,
) -> None:
"""Run the fused kernel. ``in_buffer`` is this rank's [T, H] view of the
input symm region (partial sums already written by the producer);
``mc_in`` / ``mc_out`` are the multicast pointers of the input and OUT
regions. On return the OUT region holds the gathered post-conv [T, H] on
every rank and ``x_scratch`` holds the reduced pre-conv [T, Hc] shard.
Tracking: empty ``track_mask`` disables it. ``track_from_cache`` (decode)
snapshots the post-update conv window to ``track_dst`` (``track_rows`` may
be empty); otherwise ``track_rows`` gathers pre-conv rows (extend).
Fused add+RMSNorm tail (decode/verify): pass ``out_local`` (this rank's
[T, H] OUT view), ``norm_gamma``/``norm_residual``/``norm_out``/``norm_eps``.
Works under either barrier mode. The residual is updated in place;
``norm_out`` receives the normed hidden.
FULL-WIDTH mode (non-scattered sconv): ``full_update=True`` with
``sconv_cache`` the replicated [slots, W-1, H] tensor, ``weight`` this
rank's contiguous [Hc, W] row slice and ``cache_col0 = rank * Hc``. Conv
still runs column-sharded; phase 3 updates/tracks ALL H cache columns on
every rank (window rows re-ld_reduced full-width) so the replicated cache
stays coherent. Verify (``need_scratch``) is unsupported full-width."""
w = weight.shape[1]
use_silu = activation in ("silu", "swish")
module = _jit_ar_scattered_sconv_module(
in_buffer.dtype, world_size, w, use_silu, use_residual
)
do_norm = norm_gamma is not None
if do_norm:
assert (
out_local is not None and norm_residual is not None and norm_out is not None
)
else:
empty = in_buffer.new_empty((0,))
out_local = norm_gamma = norm_residual = norm_out = empty
module.ar_scattered_sconv(
in_buffer,
x_scratch,
sconv_cache,
safe_idx,
cache_mask,
cache_indices,
has_initial_state,
cu,
si,
weight,
track_rows,
track_mask,
track_dst,
out_local,
norm_gamma,
norm_residual,
norm_out,
mc_in,
mc_out,
flag_ptrs_dev,
state_ptr,
rank,
num_blocks,
block_size,
per_block_barrier,
track_from_cache,
norm_eps,
need_scratch,
use_stream,
stream_walk,
full_update,
cache_col0,
)
def inkling_ar_ssconv_norm_decode(
in_partials: torch.Tensor,
residual_in: torch.Tensor,
residual_out: torch.Tensor,
hs_out: torch.Tensor,
norm_weight: torch.Tensor,
norm_eps: float,
sconv_cache: torch.Tensor,
cache_indices: torch.Tensor,
cache_mask: torch.Tensor,
conv_weight_full: torch.Tensor,
track_mask: torch.Tensor,
track_indices: torch.Tensor,
mc_stage: int,
local_stage: int,
mc_wstage: int,
local_wstage: int,
flag_ptrs_dev: int,
state_ptr: int,
rank: int,
world_size: int,
*,
activation: str | None,
use_residual: bool,
vecs_per_thread: int = 0,
) -> None:
"""ONE-SHOT decode {AR + scattered sconv + add-RMSNorm}: v5 push pattern
with the cache-window shard co-pushed so every rank convs full width from
ONE barrier. ``sconv_cache`` is the SHARDED [pool, W-1, Hc] cache (only
this rank's columns are updated/tracked); ``conv_weight_full`` must be the
UNSHARDED [D, W] taps. ``mc_stage``/``local_stage`` = one v5 rotation slot
([world, T, D]); ``mc_wstage``/``local_wstage`` = a rotating [T, W-1, D]
window-staging half. Pass empty ``track_mask`` to disable tracking
(post-update-window snapshot semantics otherwise)."""
w = conv_weight_full.shape[1]
use_silu = activation in ("silu", "swish")
module = _jit_ar_scattered_sconv_module(
in_partials.dtype, world_size, w, use_silu, use_residual
)
module.ar_ssconv_norm_decode(
in_partials,
residual_in,
residual_out,
hs_out,
norm_weight,
norm_eps,
sconv_cache,
cache_indices,
cache_mask,
conv_weight_full,
track_mask,
track_indices,
mc_stage,
local_stage,
mc_wstage,
local_wstage,
flag_ptrs_dev,
state_ptr,
rank,
vecs_per_thread,
)
def inkling_ar_col_decode(
in_buffer: torch.Tensor,
out_local: torch.Tensor,
residual_in: torch.Tensor,
residual_out: torch.Tensor,
hs_out: torch.Tensor,
norm_weight: torch.Tensor,
norm_eps: float,
sconv_cache: torch.Tensor,
cache_indices: torch.Tensor,
cache_mask: torch.Tensor,
weight_shard: torch.Tensor,
track_mask: torch.Tensor,
track_dst: torch.Tensor,
mc_in: int,
mc_out: int,
flag_ptrs_dev: int,
state_ptr: int,
rank: int,
world_size: int,
*,
activation: str | None,
use_residual: bool,
vecs_per_thread: int = 0,
) -> None:
"""Dedicated small-batch column decode: one block per token row, block-scoped two-round barriers, prefetch under the entry spin, conv from registers on the owner shard, inline cache update (+ decode track), and the full-row add+RMSNorm after the exit round. Decode-only (single-token sequences; every tap is cache prefix). in_buffer/out_local are this rank's views of the input/OUT symm regions."""
w = weight_shard.shape[1]
use_silu = activation in ("silu", "swish")
module = _jit_ar_scattered_sconv_module(
in_buffer.dtype, world_size, w, use_silu, use_residual
)
module.ar_col_decode(
in_buffer,
out_local,
residual_in,
residual_out,
hs_out,
norm_weight,
norm_eps,
sconv_cache,
cache_indices,
cache_mask,
weight_shard,
track_mask,
track_dst,
mc_in,
mc_out,
flag_ptrs_dev,
state_ptr,
rank,
vecs_per_thread,
)
def inkling_ar_banded_sconv(
in_buffer: torch.Tensor,
scratch: torch.Tensor,
sconv_cache: torch.Tensor,
safe_idx: torch.Tensor,
cache_mask: torch.Tensor,
cache_indices: torch.Tensor,
has_initial_state: torch.Tensor,
cu: torch.Tensor,
si: torch.Tensor,
weight: torch.Tensor,
track_rows: torch.Tensor,
track_mask: torch.Tensor,
track_dst: torch.Tensor,
mc_in: int,
mc_out: int,
flag_ptrs_dev: int,
state_ptr: int,
rank: int,
world_size: int,
*,
activation: str | None,
use_residual: bool,
num_blocks: int = 0,
block_size: int = 0,
per_block_barrier: bool = False,
debug_phase: int = 0,
mc_wstage: int = 0,
local_wstage: int = 0,
) -> None:
"""Token-banded fused {v3 AR + sconv}: contiguous band slices (v3-class
switch-transaction efficiency), in-kernel conv-state update + track.
``scratch`` must be [ceil(T/world) + W-1, H]. Pass empty (numel-0)
``track_rows`` to disable the track path.
Full-width mode (``sconv_cache`` [pool, W-1, H], default): the production
{v3 AR + sconv} fusion; every rank keeps the complete cache.
SCATTERED mode (``sconv_cache`` [pool, W-1, H/world] + ``mc_wstage``/
``local_wstage`` pointing at a [B, W-1, H] staging region): each rank
pushes its cache-window shard pre-barrier (full-width taps come from the
staging), convs its contiguous token band full-width, and updates/tracks
only its own cache columns. ``weight`` must be the FULL [H, W] taps."""
w = weight.shape[1]
use_silu = activation in ("silu", "swish")
module = _jit_ar_scattered_sconv_module(
in_buffer.dtype, world_size, w, use_silu, use_residual
)
module.ar_banded_sconv(
in_buffer,
scratch,
sconv_cache,
safe_idx,
cache_mask,
cache_indices,
has_initial_state,
cu,
si,
weight,
track_rows,
track_mask,
track_dst,
mc_in,
mc_out,
flag_ptrs_dev,
state_ptr,
rank,
num_blocks,
block_size,
per_block_barrier,
debug_phase,
mc_wstage,
local_wstage,
)
@@ -0,0 +1,400 @@
"""Fused target-verify attention prologue: {k/v sconv + save_windows + qk-norm
+ KV-cache store} in one kernel (csrc/tml/inkling_attn_prologue_fused.cuh)."""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.jit_kernel.utils import (
cache_once,
empty_sentinel,
is_arch_support_pdl,
load_jit,
make_cpp_args,
)
if TYPE_CHECKING:
from tvm_ffi.module import Module
@cache_once
def _jit_attn_prologue_module(
dtype: torch.dtype,
w: int,
use_silu: bool,
use_residual: bool,
use_mxfp8: bool,
) -> Module:
args = make_cpp_args(
dtype, w, use_silu, use_residual, use_mxfp8, is_arch_support_pdl()
)
return load_jit(
"inkling_attn_prologue_fused",
*args,
cuda_files=["inkling/inkling_attn_prologue_fused.cuh"],
cuda_wrappers=[
("attn_prologue", f"AttnPrologueKernel<{args}>::run"),
("attn_prologue_decode", f"AttnPrologueDecodeKernel<{args}>::run"),
("attn_prologue_extend", f"AttnPrologueExtendKernel<{args}>::run"),
],
)
def compile_inkling_attn_prologue(
dtype: torch.dtype,
w: int,
use_silu: bool,
use_residual: bool,
use_mxfp8: bool = False,
) -> None:
_jit_attn_prologue_module(dtype, w, use_silu, use_residual, use_mxfp8)
def inkling_attn_prologue_verify(
qkvr: torch.Tensor,
k_cache: torch.Tensor,
v_cache: torch.Tensor,
cache_indices: torch.Tensor,
cache_mask: torch.Tensor,
k_weight: torch.Tensor,
v_weight: torch.Tensor,
k_inter: torch.Tensor,
v_inter: torch.Tensor,
q_gamma: torch.Tensor,
k_gamma: torch.Tensor,
eps: float,
loc: torch.Tensor,
k_buf: torch.Tensor,
v_buf: torch.Tensor,
q_off: int,
k_off: int,
v_off: int,
dq: int,
dkv: int,
draft_token_num: int,
activation: str | None = None,
use_residual: bool = True,
do_store: bool = True,
mxfp8_quant: bool = False,
sfk: torch.Tensor | None = None,
sfv: torch.Tensor | None = None,
page_size: int = 128,
log_scaling_tau: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor | None]:
"""Returns fresh contiguous (q_normed, k_normed, v_conv) [T, dq/dkv];
KV rows are also scattered into k_buf/v_buf at ``loc`` (the attention call
should pass save_kv_cache=False)."""
t = qkvr.shape[0]
if mxfp8_quant:
if dq % 128 != 0 or dkv % 128 != 0:
raise ValueError("MXFP8 fused prologue requires head_dim-aligned Q/K/V.")
if sfk is None or sfv is None:
raise ValueError("MXFP8 fused prologue requires K/V scale buffers.")
sf_shape = (k_buf.shape[0] // page_size, dkv // 128, 32, page_size // 32, 4)
if sfk.shape != sf_shape or sfv.shape != sf_shape:
raise ValueError(
"MXFP8 fused prologue requires interleaved K/V scale buffers "
f"with shape {sf_shape}, got {tuple(sfk.shape)} and {tuple(sfv.shape)}."
)
if not sfk.is_contiguous() or not sfv.is_contiguous():
raise ValueError(
"MXFP8 fused prologue requires contiguous interleaved SFK/SFV."
)
q_out = torch.empty(t, dq, dtype=torch.float8_e4m3fn, device=qkvr.device)
sfq_u8 = torch.empty(
(t, dq // 128, 128 // 32), dtype=torch.uint8, device=qkvr.device
)
sfk_u8 = sfk.view(torch.uint8)
sfv_u8 = sfv.view(torch.uint8)
else:
q_out = torch.empty(t, dq, dtype=qkvr.dtype, device=qkvr.device)
sfq_u8 = torch.empty(0, dtype=torch.uint8, device=qkvr.device)
sfk_u8 = torch.empty(0, dtype=torch.uint8, device=qkvr.device)
sfv_u8 = torch.empty(0, dtype=torch.uint8, device=qkvr.device)
k_out = torch.empty(t, dkv, dtype=qkvr.dtype, device=qkvr.device)
v_out = torch.empty(t, dkv, dtype=qkvr.dtype, device=qkvr.device)
if activation == "swish":
activation = "silu"
use_silu = activation in ("silu", "swish")
w = k_weight.shape[1]
module = _jit_attn_prologue_module(
qkvr.dtype, w, use_silu, use_residual, mxfp8_quant
)
hkv = dkv // 128
module.attn_prologue(
qkvr,
k_cache,
v_cache,
cache_indices.to(torch.int32),
cache_mask,
k_weight,
v_weight,
k_inter,
v_inter,
q_gamma,
k_gamma,
float(eps),
q_out,
k_out,
v_out,
loc,
k_buf.view(-1, hkv * 128),
v_buf.view(-1, hkv * 128),
sfq_u8,
sfk_u8,
sfv_u8,
int(q_off),
int(k_off),
int(v_off),
int(draft_token_num),
int(do_store),
int(page_size),
(
log_scaling_tau.reshape(-1).float()
if log_scaling_tau is not None
else empty_sentinel(qkvr.device, torch.float32)
),
)
q_scale = sfq_u8.view(torch.float8_e8m0fnu) if mxfp8_quant else None
return q_out, k_out, v_out, q_scale
def inkling_attn_prologue_extend(
qkvr: torch.Tensor,
k_cache: torch.Tensor,
v_cache: torch.Tensor,
cache_indices: torch.Tensor,
cache_mask: torch.Tensor,
has_initial_state: torch.Tensor,
cu: torch.Tensor,
si: torch.Tensor,
k_weight: torch.Tensor,
v_weight: torch.Tensor,
track_rows: torch.Tensor,
track_mask: torch.Tensor,
track_dst: torch.Tensor,
q_gamma: torch.Tensor,
k_gamma: torch.Tensor,
eps: float,
loc: torch.Tensor,
k_buf: torch.Tensor,
v_buf: torch.Tensor,
q_off: int,
k_off: int,
v_off: int,
dq: int,
dkv: int,
activation: str | None = None,
use_residual: bool = True,
do_store: bool = True,
mxfp8_quant: bool = False,
sfk: torch.Tensor | None = None,
sfv: torch.Tensor | None = None,
page_size: int = 128,
do_cache_update: bool = True,
log_scaling_tau: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor | None]:
"""Extend (prefill) analog of ``inkling_attn_prologue_verify``: varlen
sequences via ``cu``/``si``, no window save; instead a tiny trailing
kernel does the k/v conv-cache update at sequence ends (+ the extend
prefix-cache track when ``track_mask`` is non-empty -- pass empty tensors
to disable). Returns fresh contiguous (q_normed, k_normed, v_conv) and
scatters KV rows into k_buf/v_buf at ``loc`` when ``do_store`` (the
attention call should then pass save_kv_cache=False)."""
t = qkvr.shape[0]
if mxfp8_quant:
if dq % 128 != 0 or dkv % 128 != 0:
raise ValueError("MXFP8 fused prologue requires head_dim-aligned Q/K/V.")
if sfk is None or sfv is None:
raise ValueError("MXFP8 fused prologue requires K/V scale buffers.")
sf_shape = (k_buf.shape[0] // page_size, dkv // 128, 32, page_size // 32, 4)
if sfk.shape != sf_shape or sfv.shape != sf_shape:
raise ValueError(
"MXFP8 fused prologue requires interleaved K/V scale buffers "
f"with shape {sf_shape}, got {tuple(sfk.shape)} and {tuple(sfv.shape)}."
)
if not sfk.is_contiguous() or not sfv.is_contiguous():
raise ValueError(
"MXFP8 fused prologue requires contiguous interleaved SFK/SFV."
)
q_out = torch.empty(t, dq, dtype=torch.float8_e4m3fn, device=qkvr.device)
sfq_u8 = torch.empty(
(t, dq // 128, 128 // 32), dtype=torch.uint8, device=qkvr.device
)
sfk_u8 = sfk.view(torch.uint8)
sfv_u8 = sfv.view(torch.uint8)
else:
q_out = torch.empty(t, dq, dtype=qkvr.dtype, device=qkvr.device)
sfq_u8 = torch.empty(0, dtype=torch.uint8, device=qkvr.device)
sfk_u8 = torch.empty(0, dtype=torch.uint8, device=qkvr.device)
sfv_u8 = torch.empty(0, dtype=torch.uint8, device=qkvr.device)
k_out = torch.empty(t, dkv, dtype=qkvr.dtype, device=qkvr.device)
v_out = torch.empty(t, dkv, dtype=qkvr.dtype, device=qkvr.device)
if activation == "swish":
activation = "silu"
use_silu = activation in ("silu", "swish")
w = k_weight.shape[1]
module = _jit_attn_prologue_module(
qkvr.dtype, w, use_silu, use_residual, mxfp8_quant
)
hkv = dkv // 128
module.attn_prologue_extend(
qkvr,
k_cache,
v_cache,
cache_indices.to(torch.int32),
cache_mask,
has_initial_state,
cu,
si,
k_weight,
v_weight,
track_rows,
track_mask,
track_dst,
q_gamma,
k_gamma,
float(eps),
q_out,
k_out,
v_out,
loc,
k_buf.view(-1, hkv * 128),
v_buf.view(-1, hkv * 128),
sfq_u8,
sfk_u8,
sfv_u8,
int(q_off),
int(k_off),
int(v_off),
int(do_store),
int(page_size),
int(do_cache_update),
(
log_scaling_tau.reshape(-1).float()
if log_scaling_tau is not None
else empty_sentinel(qkvr.device, torch.float32)
),
)
q_scale = sfq_u8.view(torch.float8_e8m0fnu) if mxfp8_quant else None
return q_out, k_out, v_out, q_scale
def inkling_attn_prologue_decode(
qkvr: torch.Tensor,
k_cache: torch.Tensor,
v_cache: torch.Tensor,
cache_indices: torch.Tensor,
cache_mask: torch.Tensor,
k_weight: torch.Tensor,
v_weight: torch.Tensor,
q_gamma: torch.Tensor,
k_gamma: torch.Tensor,
eps: float,
loc: torch.Tensor,
k_buf: torch.Tensor,
v_buf: torch.Tensor,
q_off: int,
k_off: int,
v_off: int,
dq: int,
dkv: int,
activation: str | None = None,
use_residual: bool = True,
track_mask: torch.Tensor | None = None,
track_indices: torch.Tensor | None = None,
do_store: bool = True,
mxfp8_quant: bool = False,
sfk: torch.Tensor | None = None,
sfv: torch.Tensor | None = None,
page_size: int = 128,
log_scaling_tau: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor | None]:
"""Decode {k/v decode-conv + conv-cache shift-update (+track) + qk-norm
(+ KV store)} in one kernel. Returns fresh (q_normed, k_normed, v_conv).
The k/v conv caches are shift-updated in place (fused_decode_update
semantics). With ``do_store`` the KV rows are scattered into k_buf/v_buf at
``loc``; MXFP8 mode also quantizes Q and writes interleaved K/V scales."""
t = qkvr.shape[0]
if mxfp8_quant:
if dq % 128 != 0 or dkv % 128 != 0:
raise ValueError(
"MXFP8 fused decode prologue requires head_dim-aligned Q/K/V."
)
if sfk is None or sfv is None:
raise ValueError("MXFP8 fused decode prologue requires K/V scale buffers.")
sf_shape = (k_buf.shape[0] // page_size, dkv // 128, 32, page_size // 32, 4)
if sfk.shape != sf_shape or sfv.shape != sf_shape:
raise ValueError(
"MXFP8 fused decode prologue requires interleaved K/V scale buffers "
f"with shape {sf_shape}, got {tuple(sfk.shape)} and {tuple(sfv.shape)}."
)
if not sfk.is_contiguous() or not sfv.is_contiguous():
raise ValueError(
"MXFP8 fused decode prologue requires contiguous interleaved SFK/SFV."
)
q_out = torch.empty(t, dq, dtype=torch.float8_e4m3fn, device=qkvr.device)
sfq_u8 = torch.empty(
(t, dq // 128, 128 // 32), dtype=torch.uint8, device=qkvr.device
)
sfk_u8 = sfk.view(torch.uint8)
sfv_u8 = sfv.view(torch.uint8)
else:
q_out = torch.empty(t, dq, dtype=qkvr.dtype, device=qkvr.device)
sfq_u8 = torch.empty(0, dtype=torch.uint8, device=qkvr.device)
sfk_u8 = torch.empty(0, dtype=torch.uint8, device=qkvr.device)
sfv_u8 = torch.empty(0, dtype=torch.uint8, device=qkvr.device)
k_out = torch.empty(t, dkv, dtype=qkvr.dtype, device=qkvr.device)
v_out = torch.empty(t, dkv, dtype=qkvr.dtype, device=qkvr.device)
if activation == "swish":
activation = "silu"
use_silu = activation in ("silu", "swish")
w = k_weight.shape[1]
do_track = track_mask is not None
if do_track:
tm, ti = track_mask.reshape(-1), track_indices
else:
tm = torch.empty(0, dtype=torch.bool, device=qkvr.device)
ti = torch.empty(0, dtype=torch.int64, device=qkvr.device)
hkv = dkv // 128
module = _jit_attn_prologue_module(
qkvr.dtype, w, use_silu, use_residual, mxfp8_quant
)
module.attn_prologue_decode(
qkvr,
k_cache,
v_cache,
cache_indices.to(torch.int32),
cache_mask,
k_weight,
v_weight,
tm,
ti,
q_gamma,
k_gamma,
float(eps),
q_out,
k_out,
v_out,
loc,
k_buf.view(-1, hkv * 128),
v_buf.view(-1, hkv * 128),
sfq_u8,
sfk_u8,
sfv_u8,
int(q_off),
int(k_off),
int(v_off),
int(do_track),
int(do_store),
int(page_size),
(
log_scaling_tau.reshape(-1).float()
if log_scaling_tau is not None
else empty_sentinel(qkvr.device, torch.float32)
),
)
q_scale = sfq_u8.view(torch.float8_e8m0fnu) if mxfp8_quant else None
return q_out, k_out, v_out, q_scale
@@ -0,0 +1,325 @@
"""Shape-specialized Inkling MoE gate top-k + renorm JIT kernels.
Three families, all specialized for the Inkling gate layout (logits
``[tokens, 258]`` fp32 = 256 routed + 2 shared experts, top-6 selection by
``sigmoid(logit) + bias``, logsigmoid renorm over selected ++ shared):
- ``inkling_gate_topk_renorm`` -- v1 warp-per-row gate (int64 indices).
- ``inkling_gate_topk_renorm_v2`` -- v2 gate: wide vector loads, int32
indices, optional PDL, in-register raw-logit carry (no re-gather).
- ``inkling_gate_gemv`` / ``inkling_gate_gemv_fused`` -- expert-per-block GEMV
of the gate linear (x [tokens, 6144] bf16 @ W [264, 6144] bf16 -> fp32
logits), standalone or with the gate epilogue fused into the same launch
(last finishing block runs it; ticket+workspace are cached per device).
NOTE: the fused/gemv wrappers cache CUDA buffers and JIT-compile on first use;
run them eagerly once before CUDA-graph capture.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.jit_kernel.utils import cache_once, load_jit
if TYPE_CHECKING:
from tvm_ffi.module import Module
_LOGITS_PAD = 264 # fp32 logits row pitch shared with the padded gate GEMM
_HIDDEN = 6144
_TOPK = 6
_N_SHARED = 2
_FUSED_MAX_TOKENS = 64
@cache_once
def _jit_module() -> Module:
return load_jit(
"inkling_gate_topk_renorm",
"fast_math",
cuda_files=["moe/inkling_gate_topk_renorm.cuh"],
cuda_wrappers=[
("inkling_gate_topk_renorm", "inkling_gate_topk_renorm"),
("inkling_gate_topk_renorm_packed", "inkling_gate_topk_renorm_packed"),
("inkling_gate_topk_renorm_v2", "inkling_gate_topk_renorm_v2"),
(
"inkling_gate_topk_renorm_v2_packed",
"inkling_gate_topk_renorm_v2_packed",
),
("inkling_gate_gemv", "inkling_gate_gemv"),
("inkling_gate_gemv_fused", "inkling_gate_gemv_fused"),
("inkling_gate_gemv_fused_packed", "inkling_gate_gemv_fused_packed"),
],
extra_cuda_cflags=["-use_fast_math"],
)
def _launch_inkling_gate_topk_renorm(
logits: torch.Tensor,
bias: torch.Tensor,
global_scale: torch.Tensor,
routed_w: torch.Tensor,
shared_w: torch.Tensor,
indices: torch.Tensor,
route_scale: float,
) -> None:
module = _jit_module()
module.inkling_gate_topk_renorm(
logits, bias, global_scale, routed_w, shared_w, indices, float(route_scale)
)
def _check_gate_inputs(
logits: torch.Tensor, bias: torch.Tensor, global_scale: torch.Tensor
) -> None:
assert logits.is_cuda and logits.dtype == torch.float32 and logits.dim() == 2
assert logits.shape[1] == 258 and logits.stride(1) == 1
assert bias.is_cuda and bias.dtype == torch.float32 and bias.shape == (256,)
assert global_scale.is_cuda and global_scale.dtype == torch.float32
assert global_scale.numel() == 1
def inkling_gate_topk_renorm(
logits: torch.Tensor,
bias: torch.Tensor,
global_scale: torch.Tensor,
route_scale: float,
*,
return_packed: bool = False,
) -> (
tuple[torch.Tensor, torch.Tensor, torch.Tensor] | tuple[torch.Tensor, torch.Tensor]
):
"""Select top-6 routed experts from 256 and renorm with 2 shared experts.
This is specialized for the Inkling fused gate layout:
``logits`` is ``[tokens, 258]`` fp32, where columns ``0:256`` are routed
experts and columns ``256:258`` are shared experts. The top-k selection key is
``sigmoid(logits[:, :256]) + bias``; renorm is over sigmoid(raw logits) for
the selected routed experts plus both shared experts.
``return_packed=True`` emits the FlashInfer routed-MoE pack instead of the
routed_w + indices pair: ``packed[t,6]`` int32 = ``(expert_id << 16) | bf16
weight bits``. Returns ``(packed, shared_w)``.
"""
_check_gate_inputs(logits, bias, global_scale)
tokens = logits.shape[0]
shared_w = torch.empty(
(tokens, _N_SHARED), dtype=torch.float32, device=logits.device
)
if return_packed:
packed = torch.empty((tokens, _TOPK), dtype=torch.int32, device=logits.device)
if tokens == 0:
return packed, shared_w
_jit_module().inkling_gate_topk_renorm_packed(
logits,
bias.contiguous(),
global_scale.contiguous(),
packed,
shared_w,
float(route_scale),
)
return packed, shared_w
routed_w = torch.empty((tokens, _TOPK), dtype=torch.float32, device=logits.device)
indices = torch.empty((tokens, _TOPK), dtype=torch.int64, device=logits.device)
if tokens == 0:
return routed_w, shared_w, indices
_launch_inkling_gate_topk_renorm(
logits,
bias.contiguous(),
global_scale.contiguous(),
routed_w,
shared_w,
indices,
route_scale,
)
return routed_w, shared_w, indices
def inkling_gate_topk_renorm_v2(
logits: torch.Tensor,
bias: torch.Tensor,
global_scale: torch.Tensor,
route_scale: float,
*,
return_packed: bool = False,
enable_pdl: bool = False,
warps_per_block: int = 0,
) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor, torch.Tensor | None]:
"""v2 gate kernel; same math as v1 but int32 indices and optional PDL.
Returns ``(routed_w, indices, shared_w, packed)`` where the unused half is
``None`` depending on ``return_packed`` -- mirroring the triton
``sigmoid_gate_topk_renorm`` contract. ``warps_per_block`` in
``{0 (auto), 1, 2, 4, 8}`` selects the launch shape.
Requires 32B-aligned logits rows: the production ``[tokens, 264]``-padded
GEMM output sliced to ``[:, :258]`` qualifies.
"""
_check_gate_inputs(logits, bias, global_scale)
assert logits.stride(0) % 8 == 0, f"rows must be 32B-aligned: {logits.stride()=}"
tokens = logits.shape[0]
shared_w = torch.empty(
(tokens, _N_SHARED), dtype=torch.float32, device=logits.device
)
if return_packed:
packed = torch.empty((tokens, _TOPK), dtype=torch.int32, device=logits.device)
if tokens > 0:
_jit_module().inkling_gate_topk_renorm_v2_packed(
logits,
bias.contiguous(),
global_scale.contiguous(),
packed,
shared_w,
float(route_scale),
bool(enable_pdl),
int(warps_per_block),
)
return None, None, shared_w, packed
routed_w = torch.empty((tokens, _TOPK), dtype=torch.float32, device=logits.device)
indices = torch.empty((tokens, _TOPK), dtype=torch.int32, device=logits.device)
if tokens > 0:
_jit_module().inkling_gate_topk_renorm_v2(
logits,
bias.contiguous(),
global_scale.contiguous(),
routed_w,
shared_w,
indices,
float(route_scale),
bool(enable_pdl),
int(warps_per_block),
)
return routed_w, indices, shared_w, None
def _check_gemv_inputs(x: torch.Tensor, weight: torch.Tensor) -> None:
assert x.is_cuda and x.dtype == torch.bfloat16 and x.dim() == 2
assert x.shape[1] == _HIDDEN and x.stride(1) == 1 and x.stride(0) == _HIDDEN
assert weight.is_cuda and weight.dtype == torch.bfloat16 and weight.dim() == 2
assert weight.shape[0] >= 258 and weight.shape[1] == _HIDDEN
assert weight.stride(1) == 1 and weight.stride(0) == _HIDDEN
def inkling_gate_gemv(
x: torch.Tensor,
weight: torch.Tensor,
*,
enable_pdl: bool = False,
experts_per_block: int = 0,
) -> torch.Tensor:
"""Gate linear as an expert-per-block GEMV: returns fp32 logits [tokens, 258].
Drop-in for ``inkling_fused_gate_linear_with_fp32_out`` (the returned view
shares the same padded [tokens, 264] layout). Meant for small token counts
where the PDL split pair (this + v2 gate) beats cublas + gate.
"""
_check_gemv_inputs(x, weight)
tokens = x.shape[0]
logits = torch.empty((tokens, _LOGITS_PAD), dtype=torch.float32, device=x.device)
if tokens > 0:
_jit_module().inkling_gate_gemv(
x, weight, logits, bool(enable_pdl), int(experts_per_block)
)
return logits[:, :258]
# Per-device (workspace [64, 264] fp32, ticket int32[1]) reused by every fused
# call. The kernel resets the ticket to zero on completion, so the buffers are
# CUDA-graph replay-safe; allocate them eagerly (warmup) before graph capture.
_fused_scratch: dict[int, tuple[torch.Tensor, torch.Tensor]] = {}
def _get_fused_scratch(device: torch.device) -> tuple[torch.Tensor, torch.Tensor]:
key = device.index if device.index is not None else torch.cuda.current_device()
scratch = _fused_scratch.get(key)
if scratch is None:
# Allocating inside CUDA graph capture would place the persistent
# buffers in the capture pool, where other graphs' replays can reuse
# (clobber) them. Call ensure_gate_gemv_fused_scratch() eagerly first
# (InklingGate.__init__ does).
assert (
not torch.cuda.is_current_stream_capturing()
), "fused gate scratch must be allocated before CUDA graph capture"
workspace = torch.empty(
(_FUSED_MAX_TOKENS, _LOGITS_PAD), dtype=torch.float32, device=device
)
ticket = torch.zeros((1,), dtype=torch.int32, device=device)
scratch = (workspace, ticket)
_fused_scratch[key] = scratch
return scratch
def ensure_gate_gemv_fused_scratch(device: torch.device) -> None:
"""Eagerly allocate the fused-gate workspace/ticket (call at model init,
before any CUDA graph capture)."""
_get_fused_scratch(device)
def inkling_gate_gemv_fused(
x: torch.Tensor,
weight: torch.Tensor,
bias: torch.Tensor,
global_scale: torch.Tensor,
route_scale: float,
*,
return_packed: bool = False,
enable_pdl: bool = False,
experts_per_block: int = 0,
) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor, torch.Tensor | None]:
"""Fully fused Inkling gate: GEMV + sigmoid+bias top-6 + renorm, one launch.
``x`` is ``[tokens, 6144]`` bf16 (tokens <= 64), ``weight`` the padded
``[264, 6144]`` bf16 gate weight. Output contract matches
``sigmoid_gate_topk_renorm``: ``(routed_w, indices, shared_w, packed)``.
"""
_check_gemv_inputs(x, weight)
tokens = x.shape[0]
assert tokens <= _FUSED_MAX_TOKENS, f"fused gate supports <= 64 tokens: {tokens=}"
assert bias.is_cuda and bias.dtype == torch.float32 and bias.shape == (256,)
assert global_scale.is_cuda and global_scale.dtype == torch.float32
workspace, ticket = _get_fused_scratch(x.device)
shared_w = torch.empty((tokens, _N_SHARED), dtype=torch.float32, device=x.device)
if return_packed:
packed = torch.empty((tokens, _TOPK), dtype=torch.int32, device=x.device)
if tokens > 0:
_jit_module().inkling_gate_gemv_fused_packed(
x,
weight,
bias.contiguous(),
global_scale.contiguous(),
workspace,
ticket,
packed,
shared_w,
float(route_scale),
bool(enable_pdl),
int(experts_per_block),
)
return None, None, shared_w, packed
routed_w = torch.empty((tokens, _TOPK), dtype=torch.float32, device=x.device)
indices = torch.empty((tokens, _TOPK), dtype=torch.int32, device=x.device)
if tokens > 0:
_jit_module().inkling_gate_gemv_fused(
x,
weight,
bias.contiguous(),
global_scale.contiguous(),
workspace,
ticket,
routed_w,
shared_w,
indices,
float(route_scale),
bool(enable_pdl),
int(experts_per_block),
)
return routed_w, indices, shared_w, None
@@ -0,0 +1,53 @@
"""CUDA-JIT latency-lean rel_logits projection for SMALL token counts, with
the optional log-scaling tau prescale folded in registers. See
csrc/tml/inkling_rel_proj.cuh; cuBLAS keeps everything above the measured
small-t band (an earlier bandwidth-oriented custom kernel lost to it at every
size)."""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.jit_kernel.utils import (
cache_once,
empty_sentinel,
is_arch_support_pdl,
load_jit,
make_cpp_args,
)
if TYPE_CHECKING:
from tvm_ffi.module import Module
@cache_once
def _jit_rel_proj_module(d_rel: int, use_pdl: bool) -> Module:
args = make_cpp_args(d_rel, use_pdl)
return load_jit(
"inkling_rel_proj",
*args,
cuda_files=["inkling/inkling_rel_proj.cuh"],
cuda_wrappers=[("run", f"rel_proj_small_t<{args}>")],
)
def rel_proj_small_t(
r: torch.Tensor,
proj: torch.Tensor,
tau: torch.Tensor | None = None,
out: torch.Tensor | None = None,
) -> torch.Tensor:
"""``r``: [t, h, d_rel] bf16, token rows possibly strided ((h*d_rel)-
contiguous inner, 16B-aligned); ``proj``: [d_rel, e] bf16 contiguous;
``tau``: optional fp32 [t] prescale (rounds r*tau to bf16 before the dot,
the shipped prescale semantics). Returns contiguous [t, h, e]."""
if out is None:
out = torch.empty(
(r.shape[0], r.shape[1], proj.shape[1]), dtype=r.dtype, device=r.device
)
module = _jit_rel_proj_module(r.shape[2], is_arch_support_pdl())
sh = tau if tau is not None else empty_sentinel(r.device, torch.float32)
module.run(r, sh, proj, out)
return out
@@ -0,0 +1,59 @@
"""CUDA-JIT vectorized per-row scale (the apply_log_scaling_tau contract):
``out[row, :] = bf16(fp32(x[row, :]) * tau[row])``. See
csrc/tml/inkling_row_scale.cuh; the scalar triton kernel remains the fallback
for non-bf16 / unaligned inputs."""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.jit_kernel.utils import (
cache_once,
is_arch_support_pdl,
load_jit,
make_cpp_args,
)
if TYPE_CHECKING:
from tvm_ffi.module import Module
@cache_once
def _jit_row_scale_module(use_pdl: bool) -> Module:
args = make_cpp_args(use_pdl)
return load_jit(
"inkling_row_scale",
*args,
cuda_files=["inkling/inkling_row_scale.cuh"],
cuda_wrappers=[
("run", f"row_scale<{args}>"),
("run_compact", f"row_compact<{args}>"),
],
)
def row_scale_bf16(
x: torch.Tensor, tau: torch.Tensor, out: torch.Tensor | None = None
) -> torch.Tensor:
"""``x``: [rows, inner] bf16, possibly row-strided (inner contiguous,
inner % 8 == 0, 16B-aligned rows); ``tau``: fp32 [rows]. Returns a fresh
contiguous scaled tensor (bit-identical to the triton kernel's output)."""
if out is None:
out = torch.empty(x.shape, dtype=x.dtype, device=x.device)
module = _jit_row_scale_module(is_arch_support_pdl())
module.run(x, tau, out)
return out
def row_compact_bf16(x: torch.Tensor, out: torch.Tensor | None = None) -> torch.Tensor:
"""Contiguous copy of row-strided ``x`` ([rows, inner] bf16, inner
contiguous, inner % 8 == 0, 16B-aligned rows) -- the tau-less flavor of
``row_scale_bf16``. Beats the TensorIterator strided copy that einsum's
reshape would otherwise run on such inputs."""
if out is None:
out = torch.empty(x.shape, dtype=x.dtype, device=x.device)
module = _jit_row_scale_module(is_arch_support_pdl())
module.run_compact(x, out)
return out
+239
View File
@@ -0,0 +1,239 @@
"""CUDA-JIT implementations of the Inkling short-convolution kernels.
Their signatures match the Triton entrypoints so model layers can select either
backend without adapting arguments.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import torch
from sglang.jit_kernel.utils import cache_once, load_jit, make_cpp_args
if TYPE_CHECKING:
from tvm_ffi.module import Module
@cache_once
def _jit_causal_conv1d_module(
w: int,
use_silu: bool,
use_residual: bool,
is_decode: bool,
dtype: torch.dtype,
) -> Module:
args = make_cpp_args(w, use_silu, use_residual, is_decode, dtype)
return load_jit(
"inkling_causal_conv1d",
*args,
cuda_files=["inkling/causal_conv1d.cuh"],
cuda_wrappers=[("causal_conv1d", f"CausalConv1dKernel<{args}>::run")],
)
def causal_conv1d(
x: torch.Tensor,
weight: torch.Tensor,
sconv_cache: torch.Tensor,
cache_mask: torch.Tensor,
safe_idx: torch.Tensor,
cu: torch.Tensor,
si: torch.Tensor,
activation: str | None = None,
use_residual: bool = True,
is_decode: bool = False,
) -> torch.Tensor:
"""Apply depthwise causal convolution to a packed token stream.
Depthwise causal conv1d over a packed ``[T, D]`` token stream, with the W-1
prefix taps gathered directly from ``sconv_cache`` (no intermediate prefix
tensor). Metadata args (cache_mask, safe_idx, cu, si) are precomputed once
per forward pass and reused across layers.
"""
if activation == "swish":
activation = "silu"
T = x.shape[0]
if T == 0:
return torch.empty_like(x)
D = x.shape[1]
W = weight.shape[1]
use_silu = activation in ("silu", "swish")
# Contiguous [T, D] output (strides (D, 1)) regardless of x's layout.
y = torch.empty(T, D, dtype=x.dtype, device=x.device)
module = _jit_causal_conv1d_module(W, use_silu, use_residual, is_decode, x.dtype)
module.causal_conv1d(x, sconv_cache, safe_idx, cache_mask, weight, cu, si, y)
return y
@cache_once
def _jit_update_sconv_cache_module(w1: int, dtype: torch.dtype) -> Module:
args = make_cpp_args(w1, dtype)
return load_jit(
"inkling_update_sconv_cache",
*args,
cuda_files=["inkling/update_sconv_cache.cuh"],
cuda_wrappers=[("update_sconv_cache", f"UpdateSconvCacheKernel<{args}>::run")],
)
def update_sconv_cache(
x: torch.Tensor,
sconv_cache: torch.Tensor,
cache_indices: torch.Tensor,
has_initial_state: torch.Tensor,
query_start_loc: torch.Tensor,
) -> None:
"""Update each sequence's convolution cache in place.
Shift-updates each sequence's conv state to the last W-1 entries of
``[old_state(gated) ++ x[start:end]]``; PAD / empty lanes are untouched. Pure
bit-exact select/copy.
"""
W1 = sconv_cache.shape[1]
module = _jit_update_sconv_cache_module(W1, x.dtype)
module.update_sconv_cache(
x, sconv_cache, cache_indices, has_initial_state, query_start_loc
)
@cache_once
def _jit_gather_scatter_sconv_module(w1: int, dtype: torch.dtype) -> Module:
args = make_cpp_args(w1, dtype)
return load_jit(
"inkling_gather_scatter_sconv",
*args,
cuda_files=["inkling/gather_scatter_sconv.cuh"],
cuda_wrappers=[("gather_scatter", f"GatherScatterSconvKernel<{args}>::run")],
)
def fused_gather_scatter_to_sconv_cache(
hidden_states: torch.Tensor,
sconv_cache: torch.Tensor,
track_conv_indices: torch.Tensor,
mask: torch.Tensor,
dst_indices: torch.Tensor,
) -> None:
"""Gather selected hidden-state rows into the convolution cache.
Scatters masked rows ``hidden_states[track_conv_indices[b, w]]`` into
``sconv_cache[dst_indices[b], w]`` in-place; masked-out lanes untouched.
Bit-exact copy. (track int32, dst int64, per the model contract.)
"""
W1 = sconv_cache.shape[1]
module = _jit_gather_scatter_sconv_module(W1, hidden_states.dtype)
module.gather_scatter(
hidden_states, sconv_cache, track_conv_indices, mask, dst_indices
)
@cache_once
def _jit_fused_decode_update_module(
w: int, use_silu: bool, use_residual: bool, do_track: bool, dtype: torch.dtype
) -> Module:
args = make_cpp_args(w, use_silu, use_residual, do_track, dtype)
return load_jit(
"inkling_fused_decode_update",
*args,
cuda_files=["inkling/fused_decode_update.cuh"],
cuda_wrappers=[
("fused_decode_update", f"FusedDecodeUpdateKernel<{args}>::run")
],
)
def fused_causal_conv1d_update_decode(
x: torch.Tensor,
weight: torch.Tensor,
sconv_cache: torch.Tensor,
cache_indices: torch.Tensor,
cache_mask: torch.Tensor,
activation: str | None = None,
use_residual: bool = True,
track_mask: torch.Tensor | None = None,
track_indices: torch.Tensor | None = None,
) -> torch.Tensor:
"""Apply decode convolution and update its cache in one kernel.
Decode conv (W-1 cached taps + current token) fused with the cache shift-update
(+ optional prefix-cache track-copy). Returns a contiguous ``[T, D]`` output.
"""
if activation == "swish":
activation = "silu"
T, D = x.shape
W = weight.shape[1]
use_silu = activation in ("silu", "swish")
do_track = track_mask is not None
cm = cache_mask.reshape(-1)
y = torch.empty(T, D, dtype=x.dtype, device=x.device)
if do_track:
tm = track_mask.reshape(-1)
ti = track_indices
else: # dummy tensors satisfy the signature; DO_TRACK=false never reads them
tm = torch.empty(0, dtype=torch.bool, device=x.device)
ti = torch.empty(0, dtype=torch.int64, device=x.device)
module = _jit_fused_decode_update_module(
W, use_silu, use_residual, do_track, x.dtype
)
module.fused_decode_update(x, sconv_cache, cache_indices, cm, weight, y, tm, ti)
return y
@cache_once
def _jit_draft_extend_sconv_module(
w1: int, do_track: bool, dtype: torch.dtype
) -> Module:
args = make_cpp_args(w1, do_track, dtype)
return load_jit(
"inkling_draft_extend_sconv",
*args,
cuda_files=["inkling/draft_extend_sconv.cuh"],
cuda_wrappers=[("draft_extend", f"DraftExtendSconvKernel<{args}>::run")],
)
def fused_draft_extend_sconv_cache(
hidden_states: torch.Tensor,
sconv_cache: torch.Tensor,
cache_indices: torch.Tensor,
num_accepted_tokens: torch.Tensor,
draft_token_num: int,
do_tracking: bool = False,
crossed: torch.Tensor | None = None,
track_step: torch.Tensor | None = None,
mamba_track_indices: torch.Tensor | None = None,
) -> None:
"""Update draft-extend convolution state in place.
Selects each sequence's length-(W-1) conv-state window from the virtual
``[sconv_cache[ci] ++ hidden[b]]`` stream at ``num_accepted_tokens[b]`` (and, if
tracking, at ``track_step[b]`` into ``mamba_track_indices[b]`` where crossed).
Bit-exact copy.
"""
W1 = sconv_cache.shape[1]
module = _jit_draft_extend_sconv_module(W1, do_tracking, hidden_states.dtype)
dev = hidden_states.device
if do_tracking:
cr, ts, mti = crossed, track_step, mamba_track_indices
else: # dummies; DO_TRACK=false never reads them
cr = torch.empty(0, dtype=torch.bool, device=dev)
ts = torch.empty(0, dtype=torch.int32, device=dev)
mti = torch.empty(0, dtype=torch.int64, device=dev)
module.draft_extend(
hidden_states,
sconv_cache,
cache_indices,
num_accepted_tokens,
int(draft_token_num),
cr,
ts,
mti,
)
@@ -0,0 +1,93 @@
"""fused_moe_preprocess must be bit-identical to the torch.sort-based path,
and the grouped GEMM must produce identical results under both block_size_m
configs (the block schedule and kernel config are chosen together).
"""
import pytest
import torch
from sglang.srt.layers.moe.moe_runner.triton_utils.inkling_moe import (
SMALL_M_BLOCK_SIZE_M,
compute_grouped_gemm_metadata,
fused_moe_preprocess,
get_src2dst,
grouped_gemm_triton,
)
requires_cuda = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA only")
E = 256
TOPK = 6
def _reference(topk_ids_flat: torch.Tensor):
reorder_topk_ids, reorder_ids = torch.sort(
topk_ids_flat.to(torch.int16), stable=True
)
src2dst = get_src2dst(reorder_ids)
meta = compute_grouped_gemm_metadata(
reorder_topk_ids, E, block_size_m=SMALL_M_BLOCK_SIZE_M
)
return (src2dst, *meta, reorder_topk_ids)
def _ids(tokens: int, seed: int, skew: bool = False) -> torch.Tensor:
torch.manual_seed(seed)
if skew: # all tokens on few experts (stresses multi-block experts)
return torch.randint(0, 3, (tokens * TOPK,), dtype=torch.int32, device="cuda")
return (
torch.stack([torch.randperm(E, device="cuda")[:TOPK] for _ in range(tokens)])
.view(-1)
.to(torch.int32)
)
@requires_cuda
@pytest.mark.parametrize("tokens", [1, 2, 7, 32, 64, 170, 341]) # n = 6*T <= 2048
@pytest.mark.parametrize("skew", [False, True])
def test_matches_sort_path(tokens: int, skew: bool):
ids = _ids(tokens, seed=tokens, skew=skew)
ref = _reference(ids)
got = fused_moe_preprocess(ids, E)
names = [
"src2dst",
"num_tokens_per_expert",
"expert_token_offs",
"expert_block_offs",
"expert_block_schedule",
"reorder_topk_ids",
]
for tag, g, r in zip(names, got, ref):
assert g.shape == r.shape, (tag, g.shape, r.shape)
assert torch.equal(g.long(), r.long()), (
tag,
g[: min(16, g.numel())],
r[: min(16, r.numel())],
)
@requires_cuda
@pytest.mark.parametrize("tokens", [1, 16, 64])
def test_grouped_gemm_small_config_matches(tokens: int):
"""GEMM output must be identical whichever (block_size_m, config) runs."""
torch.manual_seed(tokens)
ids = _ids(tokens, seed=tokens)
m, k, n = tokens * TOPK, 768, 1024
a = (torch.randn(m, k, device="cuda") * 0.05).to(torch.bfloat16)
b = (torch.randn(E, n, k, device="cuda") * 0.02).to(torch.bfloat16)
sorted_ids, _ = torch.sort(ids.to(torch.int16), stable=True)
meta128 = compute_grouped_gemm_metadata(sorted_ids, E)
out128 = grouped_gemm_triton(a, b, E, *meta128)
pre = fused_moe_preprocess(ids, E)
out16 = grouped_gemm_triton(a, b, E, *pre[1:5], block_size_m=SMALL_M_BLOCK_SIZE_M)
# both are fp32-accumulated bf16 tensor-core dots; BLOCK_K differs so
# accumulation grouping may differ by a few ulp
torch.testing.assert_close(out16.float(), out128.float(), atol=1e-3, rtol=1e-3)
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-x"]))
@@ -0,0 +1,70 @@
"""fused_decode_sconv_metadata must be bit-identical to the unfused prep.
The unfused reference is the exact op sequence `_prepare_decode_sconv_metadata`
used to launch: two arange calls + ones + precompute_helion_decode_metadata
(!= PAD, &, clamp, long, arange x2).
"""
import pytest
import torch
from sglang.srt.models.inkling_common.kernels.sconv import (
PAD_SLOT_ID,
fused_decode_sconv_metadata,
precompute_helion_decode_metadata,
)
requires_cuda = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA only")
# cross the BLOCK=1024 grid boundary and hit odd sizes
BATCH_SIZES = [1, 2, 3, 17, 64, 160, 257, 1023, 1024, 1025]
def _reference(B: int, cache_indices: torch.Tensor):
device = cache_indices.device
query_start_loc = torch.arange(B + 1, dtype=torch.int32, device=device)
has_initial_state = torch.ones(B, dtype=torch.bool, device=device)
precomputed = precompute_helion_decode_metadata(
B=B, W=4, cache_indices=cache_indices, has_initial_state=has_initial_state
)
return query_start_loc, has_initial_state, precomputed
@requires_cuda
@pytest.mark.parametrize("b", BATCH_SIZES)
@pytest.mark.parametrize("idx_dtype", [torch.int32, torch.int64])
def test_matches_unfused(b: int, idx_dtype: torch.dtype):
torch.manual_seed(b)
cache_indices = torch.randint(0, 4096, (b,), dtype=idx_dtype, device="cuda")
# sprinkle PAD slots (cudagraph padding lanes)
pad = torch.rand(b, device="cuda") < 0.25
cache_indices[pad] = PAD_SLOT_ID
ref_qsl, ref_his, ref_meta = _reference(b, cache_indices)
qsl, his, meta = fused_decode_sconv_metadata(B=b, cache_indices=cache_indices)
for tag, got, ref in (
("query_start_loc", qsl, ref_qsl),
("has_initial_state", his, ref_his),
("cache_mask", meta["cache_mask"], ref_meta["cache_mask"]),
("safe_idx", meta["safe_idx"], ref_meta["safe_idx"]),
("cu", meta["cu"], ref_meta["cu"]),
("si", meta["si"], ref_meta["si"]),
):
assert got.dtype == ref.dtype, (tag, got.dtype, ref.dtype)
assert got.shape == ref.shape, (tag, got.shape, ref.shape)
assert torch.equal(got, ref), tag
@requires_cuda
def test_all_pad():
cache_indices = torch.full((8,), PAD_SLOT_ID, dtype=torch.int32, device="cuda")
_, _, meta = fused_decode_sconv_metadata(B=8, cache_indices=cache_indices)
assert not meta["cache_mask"].any()
assert (meta["safe_idx"] == 0).all()
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-x"]))
@@ -0,0 +1,174 @@
"""fused_extend_sconv_metadata must be bit-identical to the unfused prep.
The unfused reference is the exact op sequence _prepare_extend_common_metadata
+ precompute_helion_extend_metadata used to launch: zeros + cumsum + slice-copy
(or arange + ones for verify) + the has_initial_state compare, then != PAD, &,
clamp, long, to(int64), arange, searchsorted, clamp, to(int32).
"""
import pytest
import torch
from sglang.srt.models.inkling_common.kernels.sconv import (
HIS_ONES,
HIS_PREFIX,
HIS_SEQ_MINUS_EXT,
HIS_ZEROS,
PAD_SLOT_ID,
fused_extend_sconv_metadata,
precompute_helion_extend_metadata,
)
requires_cuda = pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA only")
# cross si tiles (BLOCK_T=256) and the single-tile B bound
BATCH_SIZES = [1, 2, 7, 64, 257, 1023]
def _ref_extend(B, extend_seq_lens, his_mode, his_src, cache_indices, T):
device = cache_indices.device
query_start_loc = torch.zeros(B + 1, dtype=torch.int32, device=device)
query_start_loc[1:] = extend_seq_lens.cumsum(dim=0)
if his_mode == HIS_ZEROS:
has_initial_state = torch.zeros(B, dtype=torch.bool, device=device)
elif his_mode == HIS_PREFIX:
has_initial_state = his_src > 0
else: # HIS_SEQ_MINUS_EXT
has_initial_state = (his_src[:B] - extend_seq_lens) > 0
meta = precompute_helion_extend_metadata(
B=B,
T=T,
W=4,
cache_indices=cache_indices,
has_initial_state=has_initial_state,
query_start_loc=query_start_loc,
)
return query_start_loc, has_initial_state, meta
def _ref_verify(B, draft_token_num, cache_indices):
device = cache_indices.device
query_start_loc = torch.arange(
0, (B + 1) * draft_token_num, draft_token_num, dtype=torch.int32, device=device
)
has_initial_state = torch.ones(B, dtype=torch.bool, device=device)
meta = precompute_helion_extend_metadata(
B=B,
T=B * draft_token_num,
W=4,
cache_indices=cache_indices,
has_initial_state=has_initial_state,
query_start_loc=query_start_loc,
)
return query_start_loc, has_initial_state, meta
def _assert_equal(got, ref):
for tag, g, r in (
("query_start_loc", got[0], ref[0]),
("has_initial_state", got[1], ref[1]),
("cache_mask", got[2]["cache_mask"], ref[2]["cache_mask"]),
("safe_idx", got[2]["safe_idx"], ref[2]["safe_idx"]),
("cu", got[2]["cu"], ref[2]["cu"]),
("si", got[2]["si"], ref[2]["si"]),
):
assert g.dtype == r.dtype, (tag, g.dtype, r.dtype)
assert g.shape == r.shape, (tag, g.shape, r.shape)
assert torch.equal(g, r), tag
def _cache_indices(b, idx_dtype):
ci = torch.randint(0, 4096, (b,), dtype=idx_dtype, device="cuda")
pad = torch.rand(b, device="cuda") < 0.25
ci[pad] = PAD_SLOT_ID
return ci
@requires_cuda
@pytest.mark.parametrize("b", BATCH_SIZES)
@pytest.mark.parametrize("his_mode", [HIS_ZEROS, HIS_PREFIX, HIS_SEQ_MINUS_EXT])
@pytest.mark.parametrize("lens_dtype", [torch.int32, torch.int64])
def test_extend_matches_unfused(b, his_mode, lens_dtype):
torch.manual_seed(b * 10 + his_mode)
lens = torch.randint(0, 33, (b,), dtype=lens_dtype, device="cuda")
lens[torch.rand(b, device="cuda") < 0.2] = 0 # zero-length sequences
T = int(lens.sum().item())
cache_indices = _cache_indices(b, torch.int32)
if his_mode == HIS_PREFIX:
his_src = torch.randint(0, 3, (b,), dtype=lens_dtype, device="cuda")
elif his_mode == HIS_SEQ_MINUS_EXT:
his_src = lens + torch.randint(0, 2, (b,), dtype=lens_dtype, device="cuda")
else:
his_src = None
ref = _ref_extend(b, lens, his_mode, his_src, cache_indices, T)
got = fused_extend_sconv_metadata(
B=b,
T=T,
cache_indices=cache_indices,
his_mode=his_mode,
extend_seq_lens=lens,
his_src=his_src,
)
assert got is not None
_assert_equal(got, ref)
@requires_cuda
@pytest.mark.parametrize("b", BATCH_SIZES)
@pytest.mark.parametrize("draft_token_num", [1, 9])
def test_verify_matches_unfused(b, draft_token_num):
torch.manual_seed(b)
cache_indices = _cache_indices(b, torch.int64)
ref = _ref_verify(b, draft_token_num, cache_indices)
got = fused_extend_sconv_metadata(
B=b,
T=b * draft_token_num,
cache_indices=cache_indices,
his_mode=HIS_ONES,
draft_token_num=draft_token_num,
)
assert got is not None
_assert_equal(got, ref)
@requires_cuda
def test_cu_not_spanning_T():
"""Dummy capture sequences: cu stops short of T; trailing si rows clamp to
B-1 exactly like the reference's searchsorted + clamp."""
b = 5
lens = torch.tensor([3, 0, 4, 0, 2], dtype=torch.int64, device="cuda")
T = int(lens.sum().item()) + 17
cache_indices = _cache_indices(b, torch.int32)
seq_lens = lens + 1
ref = _ref_extend(b, lens, HIS_SEQ_MINUS_EXT, seq_lens, cache_indices, T)
got = fused_extend_sconv_metadata(
B=b,
T=T,
cache_indices=cache_indices,
his_mode=HIS_SEQ_MINUS_EXT,
extend_seq_lens=lens,
his_src=seq_lens,
)
assert got is not None
_assert_equal(got, ref)
@requires_cuda
def test_fallback_past_batch_bound():
b = 1024 # > _FUSED_EXTEND_MAX_B
lens = torch.ones(b, dtype=torch.int64, device="cuda")
got = fused_extend_sconv_metadata(
B=b,
T=b,
cache_indices=_cache_indices(b, torch.int32),
his_mode=HIS_ZEROS,
extend_seq_lens=lens,
)
assert got is None
if __name__ == "__main__":
import sys
sys.exit(pytest.main([__file__, "-v", "-x"]))
@@ -187,42 +187,58 @@ inline void launchFusedActivationQuant(
tensorrt_llm::QuantizationSFLayout sfLayout,
bool disableFp4FastMath,
cudaStream_t stream) {
constexpr uint32_t BLOCK_SIZE = 128; // == innerHalf/16 for inter=2048 (one SF block per thread)
dim3 const grid(m), block(BLOCK_SIZE);
auto launch = [&](auto layoutTag, auto fastMathTag) {
fusedActivationQuantKernel<BLOCK_SIZE, decltype(layoutTag)::value, decltype(fastMathTag)::value>
<<<grid, block, 0, stream>>>(
m,
innerHalf,
innerDim,
gateUp,
loraDelta,
loraInputOut,
expandedIdxToPermutedIdx,
globalScaleInv,
weightOutput,
scaleOutput,
perTokenScaleOutput);
};
auto withFastMath = [&](auto layoutTag) {
if (disableFp4FastMath) {
launch(layoutTag, std::integral_constant<bool, true>{});
// One SF block per thread, no stride loop: BLOCK_SIZE must cover innerHalf/16 (a fixed
// 128 left cols [2048,inter) unwritten at Inkling EP8's inter=3072 -> NaN from the down GEMM).
uint32_t const numVecs = static_cast<uint32_t>(innerHalf) / 16;
auto dispatchBlock = [&](auto blockTag) {
constexpr uint32_t BLOCK_SIZE = decltype(blockTag)::value;
dim3 const grid(m), block(BLOCK_SIZE);
auto launch = [&](auto layoutTag, auto fastMathTag) {
fusedActivationQuantKernel<BLOCK_SIZE, decltype(layoutTag)::value, decltype(fastMathTag)::value>
<<<grid, block, 0, stream>>>(
m,
innerHalf,
innerDim,
gateUp,
loraDelta,
loraInputOut,
expandedIdxToPermutedIdx,
globalScaleInv,
weightOutput,
scaleOutput,
perTokenScaleOutput);
};
auto withFastMath = [&](auto layoutTag) {
if (disableFp4FastMath) {
launch(layoutTag, std::integral_constant<bool, true>{});
} else {
launch(layoutTag, std::integral_constant<bool, false>{});
}
};
if (sfLayout == tensorrt_llm::QuantizationSFLayout::SWIZZLED_128x4) {
withFastMath(
std::integral_constant<
tensorrt_llm::QuantizationSFLayout,
tensorrt_llm::QuantizationSFLayout::SWIZZLED_128x4>{});
} else if (sfLayout == tensorrt_llm::QuantizationSFLayout::LINEAR) {
withFastMath(
std::integral_constant<tensorrt_llm::QuantizationSFLayout, tensorrt_llm::QuantizationSFLayout::LINEAR>{});
} else {
launch(layoutTag, std::integral_constant<bool, false>{});
withFastMath(
std::integral_constant<
tensorrt_llm::QuantizationSFLayout,
tensorrt_llm::QuantizationSFLayout::SWIZZLED_8x4>{});
}
};
if (sfLayout == tensorrt_llm::QuantizationSFLayout::SWIZZLED_128x4) {
withFastMath(
std::integral_constant<
tensorrt_llm::QuantizationSFLayout,
tensorrt_llm::QuantizationSFLayout::SWIZZLED_128x4>{});
} else if (sfLayout == tensorrt_llm::QuantizationSFLayout::LINEAR) {
withFastMath(
std::integral_constant<tensorrt_llm::QuantizationSFLayout, tensorrt_llm::QuantizationSFLayout::LINEAR>{});
if (numVecs <= 128) {
dispatchBlock(std::integral_constant<uint32_t, 128>{});
} else if (numVecs <= 256) {
dispatchBlock(std::integral_constant<uint32_t, 256>{});
} else if (numVecs <= 512) {
dispatchBlock(std::integral_constant<uint32_t, 512>{});
} else {
withFastMath(
std::integral_constant<tensorrt_llm::QuantizationSFLayout, tensorrt_llm::QuantizationSFLayout::SWIZZLED_8x4>{});
// Callers guard on numVecs <= 512 and fall back to the unfused chain.
dispatchBlock(std::integral_constant<uint32_t, 1024>{});
}
}
@@ -3116,9 +3116,9 @@ class FP4BlockScaleLoraLauncher {
static int const fuseActQuant = envFlag("SGLANG_OPT_FUSED_MOE_ACTIVATION_QUANT_FUSE") ? 1 : 0;
static int const actOptMode = envFlag("SGLANG_OPT_FUSED_MOE_ACTIVATION_VEC") ? 1 : 0;
if (fuseActQuant) {
if (fuseActQuant && inter / 16 <= 512) {
// Fused: gate_up (interleaved) + lora_delta -> act_fp4/sf/per_token + activation_lora_input,
// without materializing activated_bf16. inter must be a multiple of 16 (always true here).
// without materializing activated_bf16. >512 SF vecs/row falls to the unfused chain below.
flashinfer::sgl_fused_act_quant::launchFusedActivationQuant(
num_tokens * top_k,
inter,
@@ -533,6 +533,9 @@ void Runner::run(
ptrCtaIdxXyToBatchIdx,
ptrCtaIdxXyToMnLimit,
ptrNumNonExitingCtas,
#if SGLANG_FLASHINFER_HAS_PERMUTED_BIAS_ROW_IDX
/* permutedIdxToBiasRowIdx */ nullptr,
#endif
bmm1Workspace,
stream,
device,
@@ -712,6 +715,9 @@ void Runner::run(
ptrCtaIdxXyToBatchIdx,
ptrCtaIdxXyToMnLimit,
ptrNumNonExitingCtas,
#if SGLANG_FLASHINFER_HAS_PERMUTED_BIAS_ROW_IDX
/* permutedIdxToBiasRowIdx */ nullptr,
#endif
bmm2Workspace,
stream,
device,
@@ -24,6 +24,12 @@ def gen_sgl_trtllm_gen_fused_moe_sm100_module():
flashinfer_data_dir = Path(flashinfer.__file__).resolve().parent / "data"
flashinfer_csrc_dir = flashinfer_data_dir / "csrc"
flashinfer_include_dir = flashinfer_data_dir / "include"
kernel_runner_header = (
flashinfer_include_dir / "flashinfer/trtllm/batched_gemm/KernelRunner.h"
)
has_permuted_bias_row_idx = (
"permutedIdxToBiasRowIdx" in kernel_runner_header.read_text()
)
include_path = f"{ArtifactPath.TRTLLM_GEN_BMM}/include"
header_name = "flashinferMetaInfo"
@@ -85,6 +91,7 @@ def gen_sgl_trtllm_gen_fused_moe_sm100_module():
"-DENABLE_FP8",
"-DENABLE_FP4",
"-DCUTLASS_ENABLE_GDC_FOR_SM100=1",
f"-DSGLANG_FLASHINFER_HAS_PERMUTED_BIAS_ROW_IDX={int(has_permuted_bias_row_idx)}",
f'-DTLLM_GEN_GEMM_CUBIN_PATH=\\"{ArtifactPath.TRTLLM_GEN_BMM}\\"',
]
+ nvcc_flags,
@@ -23,15 +23,6 @@ def _jit_module(dtype: torch.dtype) -> Module:
)
def supports_merged_align(virtual_num_experts: int) -> bool:
"""Commit-1 kernel only implements the (64, 1024] bucket-count branch.
The bucket count is virtual_num_experts + 1 (the +1 sentinel bucket). Other
regimes (small-batch <=64, v2 >1024) keep the old path."""
num_buckets = virtual_num_experts + 1
return 64 < num_buckets <= 1024
def moe_lora_merged_align(
topk_ids: torch.Tensor,
token_lora_mapping: torch.Tensor,
@@ -7,6 +7,7 @@ from sglang.jit_kernel.utils.arch import (
)
from sglang.jit_kernel.utils.common import (
cache_once,
empty_sentinel,
get_ci_test_range,
is_hip_runtime,
is_musa_runtime,
@@ -16,6 +17,7 @@ from sglang.jit_kernel.utils.common import (
from sglang.jit_kernel.utils.compile import KERNEL_PATH, load_jit, make_cpp_args
__all__ = [
"empty_sentinel",
"should_run_full_tests",
"get_ci_test_range",
"cache_once",
+9
View File
@@ -41,6 +41,15 @@ def cache_once(fn: F) -> F:
return wrapper # type: ignore
@functools.lru_cache(maxsize=None)
def empty_sentinel(device: torch.device, dtype: torch.dtype) -> torch.Tensor:
"""Cached 0-element tensor for optional-tensor FFI slots (the numel-0
"not present" convention). Allocating a fresh empty per call costs
~1.2us CPU on eager paths; the sentinel is never read, so one cached
instance per (device, dtype) is safe to share."""
return torch.empty(0, dtype=dtype, device=device)
@cache_once
def is_hip_runtime() -> bool:
return bool(torch.version.hip)
@@ -25,6 +25,7 @@ import logging
import triton
import triton.language as tl
from sglang.kernels.ops.attention.score_mod import unpack_aux_tensors
from sglang.srt.utils import is_hip
_is_hip = is_hip()
@@ -129,6 +130,11 @@ def _fwd_kernel_stage1(
Lv: tl.constexpr,
xai_temperature_len: tl.constexpr,
PAGE_SIZE: tl.constexpr,
SCORE_MOD: tl.constexpr = None,
Aux0=None,
aux0_stride_t=0,
aux0_stride_h=0,
aux0_len=0,
):
# int64 to avoid overflow of flat offsets into Mid_O when
# batch * num_head * max_kv_splits * head_dim exceeds 2**31.
@@ -206,6 +212,20 @@ def _fwd_kernel_stage1(
if xai_temperature_len > 0:
qk *= xai_temperature_reg
if SCORE_MOD is not None:
qk = SCORE_MOD(
qk,
cur_batch_seq_len - 1,
offs_n,
cur_batch,
cur_head,
offs_n < split_kv_end,
Aux0,
aux0_stride_t,
aux0_stride_h,
aux0_len,
)
qk = tl.where(offs_n < split_kv_end, qk, float("-inf"))
if PAGE_SIZE == 1:
@@ -275,6 +295,8 @@ def _decode_att_m_fwd(
logit_cap,
xai_temperature_len=-1,
page_size: int = 1,
score_mod=None,
aux_tensors=None,
):
BLOCK = 64
# [TODO] work around SGPR limit on MI3xx
@@ -311,6 +333,10 @@ def _decode_att_m_fwd(
v_buffer, page_size
)
aux0, aux0_stride_t, aux0_stride_h, aux0_len = unpack_aux_tensors(
score_mod, aux_tensors
)
_fwd_kernel_stage1[grid](
q,
k_buffer,
@@ -346,6 +372,11 @@ def _decode_att_m_fwd(
Lk=Lk,
Lv=Lv,
PAGE_SIZE=page_size,
SCORE_MOD=score_mod,
Aux0=aux0,
aux0_stride_t=aux0_stride_t,
aux0_stride_h=aux0_stride_h,
aux0_len=aux0_len,
)
@@ -389,6 +420,11 @@ def _fwd_grouped_kernel_stage1(
HAS_MLA: tl.constexpr = False,
USE_PDL: tl.constexpr = False,
PAGE_SIZE: tl.constexpr = 1,
SCORE_MOD: tl.constexpr = None,
Aux0=None,
aux0_stride_t=0,
aux0_stride_h=0,
aux0_len=0,
):
# int64 to avoid overflow of flat offsets into Mid_O when
# batch * num_head * max_kv_splits * head_dim exceeds 2**31.
@@ -500,6 +536,20 @@ def _fwd_grouped_kernel_stage1(
if xai_temperature_len > 0:
qk *= xai_temperature_reg[:, None]
if SCORE_MOD is not None:
qk = SCORE_MOD(
qk,
cur_batch_seq_len - 1,
offs_n[None, :],
cur_batch,
cur_head[:, None],
mask_h[:, None] & (offs_n[None, :] < split_kv_end),
Aux0,
aux0_stride_t,
aux0_stride_h,
aux0_len,
)
qk = tl.where(
mask_h[:, None] & (offs_n[None, :] < split_kv_end), qk, float("-inf")
)
@@ -574,6 +624,8 @@ def _decode_grouped_att_m_fwd(
has_mla=False,
use_pdl=False,
page_size: int = 1,
score_mod=None,
aux_tensors=None,
):
BLOCK = 32
Lk = k_buffer.shape[-1]
@@ -623,6 +675,10 @@ def _decode_grouped_att_m_fwd(
v_buffer, page_size
)
aux0, aux0_stride_t, aux0_stride_h, aux0_len = unpack_aux_tensors(
score_mod, aux_tensors
)
_fwd_grouped_kernel_stage1[grid](
q,
k_buffer,
@@ -663,6 +719,11 @@ def _decode_grouped_att_m_fwd(
HAS_MLA=has_mla,
USE_PDL=use_pdl,
PAGE_SIZE=page_size,
SCORE_MOD=score_mod,
Aux0=aux0,
aux0_stride_t=aux0_stride_t,
aux0_stride_h=aux0_stride_h,
aux0_len=aux0_len,
**extra_kargs,
)
@@ -814,6 +875,8 @@ def decode_attention_fwd_normal(
sinks=None,
xai_temperature_len=-1,
page_size: int = 1,
score_mod=None,
aux_tensors=None,
):
_decode_att_m_fwd(
q,
@@ -829,6 +892,8 @@ def decode_attention_fwd_normal(
logit_cap,
xai_temperature_len,
page_size=page_size,
score_mod=score_mod,
aux_tensors=aux_tensors,
)
_decode_softmax_reducev_fwd(
attn_logits,
@@ -863,6 +928,8 @@ def decode_attention_fwd_grouped(
has_mla=False,
use_pdl=False,
page_size: int = 1,
score_mod=None,
aux_tensors=None,
):
_decode_grouped_att_m_fwd(
q,
@@ -880,6 +947,8 @@ def decode_attention_fwd_grouped(
has_mla=has_mla,
use_pdl=use_pdl,
page_size=page_size,
score_mod=score_mod,
aux_tensors=aux_tensors,
)
_decode_softmax_reducev_fwd(
attn_logits,
@@ -916,6 +985,8 @@ def decode_attention_fwd(
has_mla=False,
use_pdl=False,
page_size: int = 1,
score_mod=None,
aux_tensors=None,
):
assert max_kv_splits == attn_logits.shape[2]
assert q.shape[0] <= kv_indptr.shape[0] - 1
@@ -944,6 +1015,8 @@ def decode_attention_fwd(
sinks=sinks,
xai_temperature_len=xai_temperature_len,
page_size=page_size,
score_mod=score_mod,
aux_tensors=aux_tensors,
)
else:
# GQA/MQA/MLA
@@ -966,4 +1039,6 @@ def decode_attention_fwd(
has_mla=has_mla,
use_pdl=use_pdl,
page_size=page_size,
score_mod=score_mod,
aux_tensors=aux_tensors,
)
@@ -24,6 +24,7 @@ from sglang.kernels.ops.attention.decode_attention import _extract_kv_strides
from sglang.kernels.ops.attention.prefill_attention import (
context_attention_fwd,
)
from sglang.kernels.ops.attention.score_mod import unpack_aux_tensors
from sglang.srt.utils import is_cuda, is_gfx95_supported, is_hip
_is_cuda = is_cuda()
@@ -295,6 +296,11 @@ def _fwd_kernel(
STORE_TRANSPOSE: tl.constexpr,
HAS_SINK: tl.constexpr,
PAGE_SIZE: tl.constexpr = 1,
SCORE_MOD: tl.constexpr = None,
Aux0=None,
aux0_stride_t=0,
aux0_stride_h=0,
aux0_len=0,
):
cur_seq = tl.program_id(0)
cur_head = tl.program_id(1)
@@ -450,6 +456,22 @@ def _fwd_kernel(
if xai_temperature_len > 0:
qk *= xai_temperature_reg[:, None]
if SCORE_MOD is not None:
qk = SCORE_MOD(
qk,
(cur_seq_len_prefix + cur_block_m * BLOCK_M + offs_m)[:, None],
start_n + offs_n[None, :],
(cur_seq_extend_start_idx + cur_block_m * BLOCK_M + offs_m)[
:, None
],
cur_head,
final_mask,
Aux0,
aux0_stride_t,
aux0_stride_h,
aux0_len,
)
qk = tl.where(final_mask, qk, float("-inf"))
row_max = tl.max(qk, 1)
@@ -565,6 +587,22 @@ def _fwd_kernel(
if xai_temperature_len > 0:
qk *= xai_temperature_reg[:, None]
if SCORE_MOD is not None:
qk = SCORE_MOD(
qk,
(cur_seq_len_prefix + cur_block_m * BLOCK_M + offs_m)[:, None],
cur_seq_len_prefix + start_n + offs_n[None, :],
(cur_seq_extend_start_idx + cur_block_m * BLOCK_M + offs_m)[
:, None
],
cur_head,
final_mask,
Aux0,
aux0_stride_t,
aux0_stride_h,
aux0_len,
)
qk = tl.where(final_mask, qk, float("-inf"))
row_max = tl.max(qk, 1)
@@ -646,6 +684,8 @@ def extend_attention_fwd(
skip_prefix=False,
skip_extend=False,
page_size: int = 1,
score_mod=None,
aux_tensors=None,
):
"""
q_extend, k_extend, v_extend, o_extend: contiguous tensors
@@ -656,6 +696,8 @@ def extend_attention_fwd(
written to it (used by DCP to merge partial attention across ranks).
``skip_prefix`` / ``skip_extend`` skip the prefix-KV / current-chunk stage
respectively so DCP can compute those two parts separately.
``score_mod`` / ``aux_tensors`` add a custom term to the attention logits;
see triton_ops/score_mod.py for the contract.
"""
Lq, Lk, Lv = (
q_extend.shape[-1],
@@ -695,6 +737,10 @@ def extend_attention_fwd(
v_buffer, page_size
)
aux0, aux0_stride_t, aux0_stride_h, aux0_len = unpack_aux_tensors(
score_mod, aux_tensors
)
_fwd_kernel[grid](
q_extend,
k_extend,
@@ -751,6 +797,11 @@ def extend_attention_fwd(
HAS_SINK=HAS_SINK,
STORE_TRANSPOSE=_is_hip,
PAGE_SIZE=page_size,
SCORE_MOD=score_mod,
Aux0=aux0,
aux0_stride_t=aux0_stride_t,
aux0_stride_h=aux0_stride_h,
aux0_len=aux0_len,
num_warps=num_warps,
num_stages=num_stages,
**extra_kargs,
@@ -838,6 +889,11 @@ def _fwd_kernel_unified(
USE_CUSTOM_MASK: tl.constexpr,
HAS_SINK: tl.constexpr,
PAGE_SIZE: tl.constexpr = 1,
SCORE_MOD: tl.constexpr = None,
Aux0=None,
aux0_stride_t=0,
aux0_stride_h=0,
aux0_len=0,
):
"""
Unified 1-stage kernel for deterministic extend attention.
@@ -1026,6 +1082,20 @@ def _fwd_kernel_unified(
if xai_temperature_len > 0:
qk *= xai_temperature_reg[:, None]
if SCORE_MOD is not None:
qk = SCORE_MOD(
qk,
(cur_seq_prefix_len + cur_block_m * BLOCK_M + offs_m)[:, None],
start_n + offs_n[None, :],
(cur_seq_q_start_idx + cur_block_m * BLOCK_M + offs_m)[:, None],
cur_head,
final_mask,
Aux0,
aux0_stride_t,
aux0_stride_h,
aux0_len,
)
qk = tl.where(final_mask, qk, float("-inf"))
# Online softmax
@@ -1101,6 +1171,8 @@ def extend_attention_fwd_unified(
window_start_pos=None,
xai_temperature_len=-1,
page_size: int = 1,
score_mod=None,
aux_tensors=None,
):
"""
Unified 1-stage extend attention for deterministic inference.
@@ -1162,6 +1234,10 @@ def extend_attention_fwd_unified(
v_buffer, page_size
)
aux0, aux0_stride_t, aux0_stride_h, aux0_len = unpack_aux_tensors(
score_mod, aux_tensors
)
_fwd_kernel_unified[grid](
q,
o,
@@ -1204,6 +1280,11 @@ def extend_attention_fwd_unified(
USE_CUSTOM_MASK=USE_CUSTOM_MASK,
HAS_SINK=HAS_SINK,
PAGE_SIZE=page_size,
SCORE_MOD=score_mod,
Aux0=aux0,
aux0_stride_t=aux0_stride_t,
aux0_stride_h=aux0_stride_h,
aux0_len=aux0_len,
num_warps=num_warps,
num_stages=num_stages,
**extra_kargs,
@@ -0,0 +1,70 @@
import torch
import triton
import triton.language as tl
@triton.jit
def _apply_log_scaling_tau_kernel(
x_ptr,
tau_ptr, # [rows] fp32 (flattened per-row scale)
out_ptr, # [rows, inner] contiguous, same dtype as x
x_row_stride,
inner,
total,
BLOCK: tl.constexpr,
):
pid = tl.program_id(0)
offs = pid.to(tl.int64) * BLOCK + tl.arange(0, BLOCK)
mask = offs < total
row = offs // inner
col = offs % inner
x = tl.load(x_ptr + row * x_row_stride + col, mask=mask).to(tl.float32)
tau = tl.load(tau_ptr + row, mask=mask)
y = x * tau
tl.store(out_ptr + offs, y.to(out_ptr.dtype.element_ty), mask=mask)
def apply_log_scaling_tau(x: torch.Tensor, tau: torch.Tensor) -> torch.Tensor:
"""out = (x.float() * tau).to(x.dtype) with tau broadcast per leading row,
fused into one launch. x may carry a leading-dim stride (the q slice of the
fused qkvr output); its trailing dims must be contiguous. No dynamo: the
torch.compile'd predecessor's call sites spanned enough rank /
dispatch-key / 0-1 specialization variants (target + de-tied MTP heads) to
exceed the recompile limit, which crashed (fullgraph) or wedged capture
(raised limit)."""
rows = x.shape[0]
inner = x.numel() // rows if rows else 0
inner_contiguous = x.stride(-1) == 1 and (
x.dim() == 2 or x.stride(-2) == x.shape[-1] * x.stride(-1)
)
if rows == 0 or inner == 0 or not inner_contiguous:
return (x.float() * tau).to(x.dtype)
if (
x.is_cuda
and x.dtype == torch.bfloat16
and inner % 8 == 0
and x.data_ptr() % 16 == 0
and (x.stride(0) * 2) % 16 == 0
):
# Vectorized JIT kernel (16B loads, one row divide per vector) --
# bit-identical output (same fp32-mul + bf16-round), ~2-3x the
# scalar triton kernel below at every size.
from sglang.jit_kernel.inkling_row_scale import row_scale_bf16
x2d = torch.as_strided(x, (rows, inner), (x.stride(0), 1))
return row_scale_bf16(x2d, tau.reshape(rows).float()).view(x.shape)
out = torch.empty(x.shape, dtype=x.dtype, device=x.device)
total = rows * inner
BLOCK = 1024
_apply_log_scaling_tau_kernel[(triton.cdiv(total, BLOCK),)](
x,
tau.reshape(rows).to(torch.float32),
out,
x.stride(0),
inner,
total,
BLOCK=BLOCK,
)
return out
@@ -322,6 +322,216 @@ def _fused_metadata_kernel_ps1_no_swa(
tl.store(page_table + pt_offsets, page_index, mask=mask, cache_modifier=".cg")
@triton.jit
def _draft_extend_metadata_kernel(
# Input tensors
seq_lens,
seq_lens_stride_0,
extend_seq_lens,
extend_seq_lens_stride_0,
req_to_token,
req_to_token_stride_0,
req_to_token_stride_1,
req_pool_indices,
req_pool_indices_stride_0,
# Output buffers
cache_seqlens_int32,
cache_seqlens_int32_stride_0,
cu_seqlens_k,
cu_seqlens_k_stride_0,
cu_seqlens_q,
cu_seqlens_q_stride_0,
page_table,
page_table_stride_0,
page_table_stride_1,
full_to_swa_index_mapping,
swa_page_table,
out_cache_loc,
swa_out_cache_loc,
# Scalar parameters
B,
max_seq_pages,
tokens_per_req,
PAGE_SIZE_ONE: tl.constexpr,
SHIFT: tl.constexpr,
BLOCK_COLS: tl.constexpr,
HAS_SWA: tl.constexpr,
OUT_BLOCK: tl.constexpr,
):
pid_b = tl.program_id(0) # batch index
pid_c = tl.program_id(1) # column chunk index
# 1. Prefix sums (only one block does them): cache_seqlens + cu_seqlens_k
# from seq_lens, cu_seqlens_q from extend_seq_lens.
if pid_b == 0 and pid_c == 0:
acc_k = 0
acc_q = 0
for idx in range(B):
seq = tl.load(seq_lens + idx * seq_lens_stride_0).to(tl.int32)
tl.store(cache_seqlens_int32 + idx * cache_seqlens_int32_stride_0, seq)
tl.store(cu_seqlens_k + idx * cu_seqlens_k_stride_0, acc_k)
acc_k += seq
ext = tl.load(extend_seq_lens + idx * extend_seq_lens_stride_0).to(tl.int32)
tl.store(cu_seqlens_q + idx * cu_seqlens_q_stride_0, acc_q)
acc_q += ext
tl.store(cu_seqlens_k + B * cu_seqlens_k_stride_0, acc_k)
tl.store(cu_seqlens_q + B * cu_seqlens_q_stride_0, acc_q)
# 2. SWA write-loc translation for this request's extend tokens. Runs
# before the seq_len early-return so padded rows (seq_len 0) keep
# swa_out_cache_loc consistent with out_cache_loc.
if HAS_SWA:
if pid_c == 0:
tok_idx = tl.arange(0, OUT_BLOCK)
tok_mask = tok_idx < tokens_per_req
tok_offsets = pid_b * tokens_per_req + tok_idx
full_locs = tl.load(out_cache_loc + tok_offsets, mask=tok_mask, other=0)
swa_locs = tl.load(
full_to_swa_index_mapping + full_locs, mask=tok_mask, other=0
)
tl.store(swa_out_cache_loc + tok_offsets, swa_locs, mask=tok_mask)
# 3. Page-table gather for this batch row and column chunk, self-guarded
# on the device-side seq_len (no host max; tails keep stale values the
# attention kernels never read past cache_seqlens).
if max_seq_pages == 0:
return
seq_len = tl.load(seq_lens + pid_b * seq_lens_stride_0).to(tl.int32)
if PAGE_SIZE_ONE:
num_live_pages = seq_len
else:
num_live_pages = (seq_len + (1 << SHIFT) - 1) >> SHIFT
num_live_pages = tl.minimum(num_live_pages, max_seq_pages)
if pid_c * BLOCK_COLS >= num_live_pages:
return
row_idx = tl.load(req_pool_indices + pid_b * req_pool_indices_stride_0)
row_offset = row_idx * req_to_token_stride_0
col_offsets = pid_c * BLOCK_COLS + tl.arange(0, BLOCK_COLS)
mask = col_offsets < num_live_pages
if PAGE_SIZE_ONE:
col_idx = col_offsets
else:
col_idx = col_offsets << SHIFT
rt_offsets = row_offset + col_idx * req_to_token_stride_1
page_index = tl.load(
req_to_token + rt_offsets, mask=mask, other=0, cache_modifier=".cg"
)
if PAGE_SIZE_ONE:
page_table_val = page_index
else:
page_table_val = page_index >> SHIFT
pt_offsets = pid_b * page_table_stride_0 + col_offsets * page_table_stride_1
tl.store(page_table + pt_offsets, page_table_val, mask=mask, cache_modifier=".cg")
if HAS_SWA:
swa_loc = tl.load(full_to_swa_index_mapping + page_index, mask=mask, other=0)
if PAGE_SIZE_ONE:
swa_page_table_val = swa_loc
else:
swa_page_table_val = swa_loc >> SHIFT
tl.store(
swa_page_table + pt_offsets,
swa_page_table_val.to(tl.int32),
mask=mask,
cache_modifier=".cg",
)
def draft_extend_set_metadata(
cache_seqlens_int32: torch.Tensor,
cu_seqlens_k: torch.Tensor,
cu_seqlens_q: torch.Tensor,
page_table: torch.Tensor,
req_to_token: torch.Tensor,
req_pool_indices: torch.Tensor,
seq_lens: torch.Tensor,
extend_seq_lens: torch.Tensor,
page_size: int,
full_to_swa_index_mapping: Optional[torch.Tensor] = None,
swa_page_table: Optional[torch.Tensor] = None,
out_cache_loc: Optional[torch.Tensor] = None,
swa_out_cache_loc: Optional[torch.Tensor] = None,
):
"""Fused, graph-recordable DRAFT_EXTEND_V2 metadata update (one launch):
1. cache_seqlens = seq_lens (int32 cast)
2. cu_seqlens_k = pad(cumsum(cache_seqlens))
3. cu_seqlens_q = pad(cumsum(extend_seq_lens))
4. page_table[:, :pages(seq_len)] = req_to_token[pool_idx, ::page_size] // page_size
5. (SWA pools) swa_page_table likewise via the full->swa lookup, and
swa_out_cache_loc = full_to_swa_index_mapping[out_cache_loc]
The page gathers self-guard on the device-side seq_lens (no host max);
row tails keep stale values that attention kernels never read past
cache_seqlens, matching the eager replay path's bounded writes.
"""
assert (
page_size > 0 and (page_size & (page_size - 1)) == 0
), f"page_size must be a power of two, got {page_size}"
batch_size = cache_seqlens_int32.shape[0]
max_seq_pages = page_table.shape[1]
has_swa = full_to_swa_index_mapping is not None
if has_swa:
assert swa_page_table is not None
assert swa_page_table.shape == page_table.shape
assert swa_page_table.stride() == page_table.stride()
assert out_cache_loc is not None and swa_out_cache_loc is not None
num_out_tokens = out_cache_loc.shape[0]
assert swa_out_cache_loc.shape[0] == num_out_tokens
assert num_out_tokens > 0 and num_out_tokens % batch_size == 0
tokens_per_req = num_out_tokens // batch_size
out_block = triton.next_power_of_2(tokens_per_req)
else:
tokens_per_req = 0
out_block = 1
BLOCK_COLS = 256
grid = (batch_size, max(1, triton.cdiv(max_seq_pages, BLOCK_COLS)))
_draft_extend_metadata_kernel[grid](
seq_lens,
seq_lens.stride(0),
extend_seq_lens,
extend_seq_lens.stride(0),
req_to_token,
req_to_token.stride(0),
req_to_token.stride(1),
req_pool_indices,
req_pool_indices.stride(0),
cache_seqlens_int32,
cache_seqlens_int32.stride(0),
cu_seqlens_k,
cu_seqlens_k.stride(0),
cu_seqlens_q,
cu_seqlens_q.stride(0),
page_table,
page_table.stride(0),
page_table.stride(1),
full_to_swa_index_mapping,
swa_page_table,
out_cache_loc,
swa_out_cache_loc,
batch_size,
max_seq_pages,
tokens_per_req,
PAGE_SIZE_ONE=page_size == 1,
SHIFT=(page_size).bit_length() - 1 if page_size > 1 else 0,
BLOCK_COLS=BLOCK_COLS,
num_warps=8,
num_stages=3,
HAS_SWA=has_swa,
OUT_BLOCK=out_block,
)
def normal_decode_set_metadata(
cache_seqlens_int32: torch.Tensor,
cu_seqlens_k: torch.Tensor,
@@ -0,0 +1,56 @@
# 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.
# ==============================================================================
"""Generic score_mod for the Triton attention kernels, mirroring FA4's
``score_mod``/``aux_tensors``. A Triton score_mod is a ``@triton.jit`` function
inlined into the kernels as a constexpr argument:
score_mod(qk, q_pos, kv_pos, q_idx, head, mask,
Aux0, aux0_stride_t, aux0_stride_h, aux0_len) -> qk
The kernels pre-broadcast q_pos/kv_pos/q_idx/head to ``qk``'s shape, so an
elementwise score_mod works at every call site. ``aux_tensors`` supports one
3D tensor ``[num_q_tokens, num_q_heads, D]`` with a contiguous last dim.
"""
import triton
import triton.language as tl
def unpack_aux_tensors(score_mod, aux_tensors):
if score_mod is None:
return None, 0, 0, 0
assert (
aux_tensors is not None and len(aux_tensors) == 1
), "Triton score_mod currently requires exactly one aux tensor"
aux0 = aux_tensors[0]
assert aux0.dim() == 3 and aux0.stride(2) == 1, (
f"aux_tensors[0] must be 3D with a contiguous last dim, "
f"got shape={tuple(aux0.shape)} stride={aux0.stride()}"
)
return aux0, aux0.stride(0), aux0.stride(1), aux0.shape[2]
@triton.jit
def relative_bias_score_mod(
qk, q_pos, kv_pos, q_idx, head, mask, Aux0, aux0_stride_t, aux0_stride_h, aux0_len
):
"""Add ``Aux0[q_idx, head, q_pos - kv_pos]`` when 0 <= q_pos - kv_pos < aux0_len."""
rel_dist = q_pos - kv_pos
rel_idx = tl.minimum(tl.maximum(rel_dist, 0), aux0_len - 1)
bias = tl.load(
Aux0 + q_idx * aux0_stride_t + head * aux0_stride_h + rel_idx,
mask=mask & (rel_dist >= 0) & (rel_dist < aux0_len),
other=0.0,
)
return qk + bias
@@ -147,7 +147,7 @@ def _gate_up_lora_b_kernel(
# Store result to output matrix
partial_sum *= scaling
partial_sum = partial_sum.to(x.dtype.element_ty)
partial_sum = partial_sum.to(output.dtype.element_ty)
output_ptr = (
output
+ n_start * output_stride_1
@@ -214,6 +214,7 @@ def gate_up_lora_b_fwd(
)
and s * r >= _CUBLAS_MIN_S_RANK
and gate_up_lora_b.shape[0] == 1
and x.dtype == gate_up_lora_b.dtype
): # single-adapter fast path: only valid with one resident slot
return _gate_up_lora_b_cublas(
x, gate_up_lora_b, batch_info, output_dim, base_output
@@ -236,18 +236,17 @@ def qkv_lora_b_fwd(
and batch_info.max_len >= _CUBLAS_MIN_MAX_LEN
and qkv_lora_b.shape[0]
== 1 # single-adapter fast path: only valid with one resident slot
and x.dtype == qkv_lora_b.dtype
):
return _qkv_lora_b_cublas(
x, qkv_lora_b, batch_info, output_offset_cpu, base_output, n_slices
)
BLOCK_S = 16
BLOCK_R = triton.next_power_of_2(r)
# BLOCK_OUT stays 64: with the 1-adapter cuBLAS dispatch the Triton path
# only runs for decode-sized batches, where 128 halves the grid (96->48
# programs on Kimi r16 bs64) and slows the kernel ~60% (11.4->18.5us, B200).
# Re-swept for the store path on GB200: 32 vs 64 is within noise (one preset
# marginally each way), so the single value is kept for both writebacks.
# Pad to >=16 for Triton MMA K>=16 (rank<16 adapters); k_offset < K=r masks the
# padded contraction rows to 0, so the result is unchanged.
BLOCK_R = max(16, triton.next_power_of_2(r))
# Keep one output tile size for both writeback paths.
BLOCK_OUT = 64
grid_b = (
@@ -43,6 +43,7 @@ def _sgemm_lora_a_kernel(
BLOCK_K: tl.constexpr,
SPLIT_K: tl.constexpr = 1,
ENABLE_PDL: tl.constexpr = False,
PADDED_RANK: tl.constexpr = False,
):
"""
Computes a segmented batched matrix multiplication for the LoRA A matrix.
@@ -81,7 +82,8 @@ def _sgemm_lora_a_kernel(
return
# Adjust N (stack_num * max_rank) to this adapter's actual rank.
N = tl.minimum(N, rank * stack_num)
if not PADDED_RANK:
N = tl.minimum(N, rank * stack_num)
# The tile in output matrix will have (pid_s, pid_n) as id
num_pid_n = tl.cdiv(N, BLOCK_N)
@@ -212,11 +214,12 @@ def sgemm_lora_a_fwd(
launch_kwargs = {}
if split_k > 1:
# out_alloc_stream (SGLANG_OPT_LORA_OVERLAP_MAIN_ALLOC) is intentionally NOT honored here:
# torch.zeros launches its memset on the alloc stream, which would race the side-stream
# shrink without extra ordering. No current config exercises split-K together with the
# two-stream main-alloc overlap (qwen3.5 leaves split-K off; kimi is single-stream-coherent).
output = torch.zeros((S, R), device=x.device, dtype=torch.float32)
if out_alloc_stream is not None:
with torch.cuda.stream(out_alloc_stream):
output = torch.empty((S, R), device=x.device, dtype=torch.float32)
output.zero_()
else:
output = torch.zeros((S, R), device=x.device, dtype=torch.float32)
launch_kwargs = {
"num_warps": 2 if split_k <= 4 else 4,
"num_stages": 3,
@@ -267,3 +270,67 @@ def sgemm_lora_a_fwd(
# split_k>1 returns the fp32 accumulator directly; the LoRA-B expand casts x to the weight dtype
# on-load (fused), dropping the standalone fp32->bf16 copy kernel. split_k==1 already returns x.dtype.
return output
def shared_sink_sgemm_lora_a_fwd(
x: torch.Tensor,
weights: torch.Tensor,
batch_info: LoRABatchInfo,
*,
stack_num: int,
padded_rank: bool,
out_alloc_stream=None,
) -> torch.Tensor:
"""Shared-sink shrink with the measured fixed-width schedule."""
assert x.is_contiguous()
assert weights.is_contiguous()
assert x.ndim == 2
assert weights.ndim == 3
num_tokens = x.shape[0]
rank_width = weights.shape[-2]
input_width = weights.shape[-1]
assert x.shape[-1] == input_width
block_s = 16
block_k = 256
block_rank = 16
grid = (
triton.cdiv(batch_info.max_len, block_s) * triton.cdiv(rank_width, block_rank),
batch_info.bs,
)
if out_alloc_stream is None:
output = torch.empty((num_tokens, rank_width), device=x.device, dtype=x.dtype)
else:
with torch.cuda.stream(out_alloc_stream):
output = torch.empty(
(num_tokens, rank_width), device=x.device, dtype=x.dtype
)
_sgemm_lora_a_kernel[grid](
x,
weights,
output,
rank_width,
input_width,
stack_num,
x.stride(0),
x.stride(1),
weights.stride(0),
weights.stride(1),
weights.stride(2),
output.stride(0),
output.stride(1),
batch_info.seg_lens,
batch_info.seg_indptr,
batch_info.weight_indices,
batch_info.lora_ranks,
batch_info.permutation,
batch_info.permutation is not None,
block_s,
block_rank,
block_k,
PADDED_RANK=padded_rank,
)
return output
@@ -66,6 +66,10 @@ def _sgemm_lora_b_kernel(
# For fused output scaling
scalings,
ENABLE_PDL: tl.constexpr = False,
APPLY_SCALING: tl.constexpr = True,
PADDED_RANK: tl.constexpr = True,
FLAT_GRID: tl.constexpr = False,
ATOMIC_ADD: tl.constexpr = True,
):
"""
Computes a segmented batched matrix multiplication for the LoRA B matrix
@@ -84,9 +88,16 @@ def _sgemm_lora_b_kernel(
the base model's output for a fused add operation.
"""
pid_s = tl.program_id(axis=0)
pid_n = tl.program_id(axis=1)
batch_id = tl.program_id(axis=2)
if FLAT_GRID:
pid = tl.program_id(axis=0)
batch_id = tl.program_id(axis=1)
num_pid_n = tl.cdiv(N, BLOCK_N)
pid_s = pid // num_pid_n
pid_n = pid % num_pid_n
else:
pid_s = tl.program_id(axis=0)
pid_n = tl.program_id(axis=1)
batch_id = tl.program_id(axis=2)
w_index = tl.load(weight_indices + batch_id)
rank = tl.load(lora_ranks + w_index)
@@ -98,7 +109,9 @@ def _sgemm_lora_b_kernel(
if pid_s * BLOCK_S >= seg_len: # also covers seg_len == 0
return
seg_start = tl.load(seg_indptr + batch_id)
scaling = tl.load(scalings + w_index)
scaling = tl.load(scalings + w_index) if APPLY_SCALING else 1.0
if not PADDED_RANK:
K = tl.minimum(K, rank)
s_offset = tl.arange(0, BLOCK_S) + pid_s * BLOCK_S
n_offset = tl.arange(0, BLOCK_N) + pid_n * BLOCK_N
@@ -122,19 +135,24 @@ def _sgemm_lora_b_kernel(
)
output_mask = (s_offset[:, None] < seg_len) & n_mask
x_tile = tl.load(
x_ptrs,
mask=(s_offset[:, None] < seg_len) & (k_offset[None, :] < K),
other=0.0,
)
w_tile = tl.load(
w_ptrs,
mask=(k_offset[:, None] < K) & n_mask,
other=0.0,
)
# cast fused: the split-K shrink returns fp32, plain path bf16 (no-op)
partial_sum = tl.dot(x_tile.to(w_tile.dtype), w_tile) * scaling
partial_sum = tl.zeros((BLOCK_S, BLOCK_N), dtype=tl.float32)
for k in range(0, tl.cdiv(K, BLOCK_K)):
k_remaining = K - k * BLOCK_K
x_tile = tl.load(
x_ptrs,
mask=(s_offset[:, None] < seg_len) & (k_offset[None, :] < k_remaining),
other=0.0,
)
w_tile = tl.load(
w_ptrs,
mask=(k_offset[:, None] < k_remaining) & n_mask,
other=0.0,
)
# The split-K shrink returns fp32; cast it on-load to the weight dtype.
partial_sum += tl.dot(x_tile.to(w_tile.dtype), w_tile)
x_ptrs += BLOCK_K * x_stride_1
w_ptrs += BLOCK_K * w_stride_2
partial_sum *= scaling
# All input reads are done; hint the runtime to launch the dependent kernel.
if ENABLE_PDL:
@@ -143,7 +161,11 @@ def _sgemm_lora_b_kernel(
# Store result to output matrix (cast to the OUTPUT dtype: x may be the fp32
# split-K shrink accumulator while base_output is bf16)
partial_sum = partial_sum.to(output.dtype.element_ty)
tl.atomic_add(output_ptr, partial_sum, mask=output_mask, sem="relaxed")
if ATOMIC_ADD:
tl.atomic_add(output_ptr, partial_sum, mask=output_mask, sem="relaxed")
else:
partial_sum += tl.load(output_ptr, mask=output_mask, other=0.0)
tl.store(output_ptr, partial_sum, mask=output_mask)
def sgemm_lora_b_fwd(
@@ -174,11 +196,14 @@ def sgemm_lora_b_fwd(
)
and S * R >= _CUBLAS_MIN_S_RANK
and weights.shape[0] == 1
and x.dtype == weights.dtype
): # single-adapter fast path: only valid with one resident slot
return _sgemm_lora_b_cublas(x, weights, batch_info, base_output)
# Block shapes
BLOCK_S = 16
BLOCK_R = triton.next_power_of_2(R)
# Pad to >=16 for Triton MMA K>=16 (rank<16 adapters); k_offset < K=R masks the
# padded contraction rows to 0, so the result is unchanged.
BLOCK_R = max(16, triton.next_power_of_2(R))
BLOCK_N = 256
grid = (
@@ -221,3 +246,66 @@ def sgemm_lora_b_fwd(
**pdl_kwargs,
)
return output
def shared_sink_sgemm_lora_b_fwd(
x: torch.Tensor,
weights: torch.Tensor,
batch_info: LoRABatchInfo,
base_output: torch.Tensor = None,
*,
apply_scaling: bool,
padded_rank: bool,
) -> torch.Tensor:
"""Shared-sink expand with the measured fixed-width schedule."""
assert x.is_contiguous()
assert weights.is_contiguous()
assert x.ndim == 2
assert weights.ndim == 3
num_tokens = x.shape[0]
output_width = weights.shape[-2]
rank_width = weights.shape[-1]
assert x.shape[-1] == rank_width
block_s = 16
block_rank = 16
block_n = 256
grid = (
triton.cdiv(batch_info.max_len, block_s) * triton.cdiv(output_width, block_n),
batch_info.bs,
)
output = (
torch.zeros((num_tokens, output_width), device=x.device, dtype=x.dtype)
if base_output is None
else base_output
)
_sgemm_lora_b_kernel[grid](
x,
weights,
output,
output_width,
rank_width,
x.stride(0),
x.stride(1),
weights.stride(0),
weights.stride(1),
weights.stride(2),
output.stride(0),
output.stride(1),
batch_info.seg_lens,
batch_info.seg_indptr,
batch_info.weight_indices,
batch_info.lora_ranks,
batch_info.permutation,
batch_info.permutation is not None,
block_s,
block_n,
block_rank,
batch_info.scalings,
APPLY_SCALING=apply_scaling,
PADDED_RANK=padded_rank,
FLAT_GRID=True,
ATOMIC_ADD=False,
)
return output
@@ -452,3 +452,52 @@ def fused_conv_window_scatter_with_mask(
dst_req_size,
BLOCK_SIZE=BLOCK_SIZE,
)
def scatter_mamba_states_after_mtp_verify(
mamba_caches,
state_indices_tensor: torch.Tensor,
last_correct_step_indices: torch.Tensor,
mamba_track_indices: torch.Tensor | None,
mamba_steps_to_track: torch.Tensor | None,
) -> None:
"""Scatter per-step verify states (ssm + all conv types) into the
persistent caches, plus the interval-crossing track slots."""
ssm_states = mamba_caches.temporal
intermediate_state_cache = mamba_caches.intermediate_ssm
if ssm_states.numel() > 0:
fused_mamba_state_scatter_with_mask(
ssm_states,
intermediate_state_cache,
state_indices_tensor,
last_correct_step_indices,
)
for conv_states, intermediate_conv_window_cache in zip(
mamba_caches.conv, mamba_caches.intermediate_conv_window
):
fused_conv_window_scatter_with_mask(
conv_states,
intermediate_conv_window_cache,
state_indices_tensor,
last_correct_step_indices,
)
if mamba_track_indices is not None:
assert mamba_steps_to_track is not None
if ssm_states.numel() > 0:
fused_mamba_state_scatter_with_mask(
ssm_states,
intermediate_state_cache,
mamba_track_indices,
mamba_steps_to_track,
)
for conv_states, intermediate_conv_window_cache in zip(
mamba_caches.conv, mamba_caches.intermediate_conv_window
):
fused_conv_window_scatter_with_mask(
conv_states,
intermediate_conv_window_cache,
mamba_track_indices,
mamba_steps_to_track,
)
@@ -326,14 +326,12 @@ def _invoke_moe_lora_shrink_splitk(
N = weight.shape[1]
K = weight.shape[2]
BLOCK_SIZE_M = config["BLOCK_SIZE_M"]
BLOCK_SIZE_N = triton.next_power_of_2(N)
BLOCK_SIZE_N = min(128, triton.next_power_of_2(N))
BLOCK_SIZE_K = 256
GROUP_SIZE_M = config.get("GROUP_SIZE_M", 1)
num_m_blocks = triton.cdiv(sorted_token_ids.shape[0], BLOCK_SIZE_M)
num_n_blocks = triton.cdiv(
N, BLOCK_SIZE_N
) # == 1, BLOCK_SIZE_N == next_pow2(N) >= N
num_n_blocks = triton.cdiv(N, BLOCK_SIZE_N)
base_grid = num_m_blocks * num_n_blocks
# Single source of truth shared with the caller's zero-intermediate decision:
# split-K accumulation REQUIRES a pre-zeroed output, so the predicted and
@@ -378,26 +376,19 @@ def _get_moe_lora_shrink_split_k(
sorted_token_ids: torch.Tensor,
config: dict[str, Any],
) -> int:
"""Rank-tiered split-K occupancy fill (PR #26899).
"""Choose split-K from rank and available occupancy.
The K reduction (e.g. 7168 / 256 = 28 iters) dominates this skinny-N grouped
GEMV, so splitting K stays useful well past full SM occupancy -- a plain
`1 if base_grid >= num_sm else ...` rule collapses SPLIT_K too early and
costs up to ~2x at the decode/prefill border. Skinnier ranks want more
splits (their output tile carries less work). The target / tiers were picked
from an offline per-M B200 sweep over E in {48,96,384}, N in {16,32,64};
this heuristic lands within ~5% of the per-shape tuned optimum across the
decode regime.
Skinny output ranks benefit from more K splits because each output tile
carries less work. Block sizes must mirror _invoke_moe_lora_shrink_splitk.
Block sizes must mirror _invoke_moe_lora_shrink_splitk (BLOCK_SIZE_N =
next_pow2(N) -> one N block; BLOCK_SIZE_K = 256).
"""
N = weight.shape[1]
K = weight.shape[2]
block_size_m = config["BLOCK_SIZE_M"]
block_size_n = min(128, triton.next_power_of_2(N))
block_size_k = 256
num_m_blocks = triton.cdiv(sorted_token_ids.shape[0], block_size_m)
base_grid = num_m_blocks # num_n_blocks == 1: BLOCK_SIZE_N == next_pow2(N) >= N
base_grid = num_m_blocks * triton.cdiv(N, block_size_n)
target = 512 if N <= 16 else 384 if N <= 32 else 256
max_split_k = max(1, K // block_size_k)
return max(1, min(triton.cdiv(target, base_grid), max_split_k, 8))
@@ -416,17 +407,10 @@ def _align_block_size_jit(
block_size: int,
num_experts: int,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""CUDA JIT align_block_size for num_experts > 1024 (up to 8191).
"""CUDA JIT alignment for up to 8191 experts.
Uses the v2 kernel from moe_align_kernel.cu which supports large expert
counts via per-thread multi-expert processing and a two-level warp scan,
replacing the previous pure-PyTorch fallback that had excessive CPU overhead
from 15+ individual kernel launches and torch.argsort.
The JIT kernel uses a +1 offset convention: topk_ids are shifted by +1 so
that the EP sentinel value (-1) maps to bucket 0. The kernel internally
handles histogram, padded prefix-sum, expert_ids assignment, and token
scattering in just 23 CUDA kernel launches.
Expert IDs are shifted by one so ``-1`` maps to a sentinel bucket. The
fused allocation stays int4-aligned for the kernel's vectorized clear.
"""
assert num_experts <= 8191, (
f"_align_block_size_jit supports at most 8191 experts "
@@ -642,6 +626,10 @@ def _merged_experts_fused_moe_lora_add_impl(
stage: str = "all",
intermediate_buffer: torch.Tensor | None = None,
expand_wait_event: "torch.cuda.Event | None" = None,
broadcast_intermediate: bool = False,
prewarm_a_routing: bool = True,
prewarm_b_routing: bool = True,
zero_intermediate: bool = False,
) -> "torch.Tensor | None":
"""
1. Prepare virtual expert routing metadata from topk_ids + token_lora_mapping * num_experts.
@@ -657,12 +645,14 @@ def _merged_experts_fused_moe_lora_add_impl(
- ``"expand"``: routing-B + LoRA-B expand/add only; requires ``intermediate_buffer`` =
the tensor produced by the ``"shrink"`` stage.
EP: when `local_num_experts` (< global) is given, this rank only computes the
delta for the experts it owns. We keep the GLOBAL expert ids + global contiguous
weights (so the merged-weight reshape stays a free view) and mask non-owned
[token, k] slots to the -1 sentinel inside `_fused_virtual_topk_ids_kernel`; the
grid shrinks via the per-rank trim in `_get_routing`. Slicing the weight's expert
dim instead would force the reshape to copy every step (non-contiguous fold).
``prewarm_a_routing`` and ``prewarm_b_routing`` let a staged caller skip
routing for a weight that it replaces with a dense operation. The B flag
also controls the automatic expand-route prewarm performed by ``"shrink"``.
``broadcast_intermediate`` is an expand-only mode where one rank vector per
token is reused for every routed expert.
EP accepts either global weights/IDs with a local range or already-localized
weights/IDs from the standard dispatcher.
"""
max_loras, _, max_lora_rank, _ = lora_a.shape
# Global per-expert dim of the LoRA weights. lora_a may be shared-outer (expert
@@ -759,24 +749,18 @@ def _merged_experts_fused_moe_lora_add_impl(
if cached is not None:
return cached
# Fused LoRA-local align: one kernel does inline virtual id + EP skip +
# compact (local experts) + single-block scatter, replacing the 3-kernel
# (_fused_virtual_topk_ids + moe_align + count_and_sort) pipeline. Two
# single-adapter (max_loras==1) regimes are fused; everything else falls back:
# - per-expert EP path (ep_local): compact local-expert histogram.
# - shared-outer path (shared_outer): lora-id routing (compute_virtual_id
# uses base=0; the kernel + launcher already size num_experts_for_weight=1
# and have no bucket-count blocker, so it just needs compact=False — compact
# + shared_outer would mis-map the id as base-offset). This is the opt1
# align/sort fusion: shared-outer used to fall through to the unfused
# _fused_virtual_topk_ids + moe_align_block_size_small_batch pair (~10.2us/
# layer at decode bs16); now it takes the single fused launch.
# Decode-only: the fused kernel's single-block scatter targets the small
# decode batch; prefill (>= 512 tokens) keeps the multi-block old path.
# Shared-outer routing has one bucket per adapter, so the same merged
# align remains valid for multi-LoRA. Compact EP routing stays single-slot.
compact_merged = ep_local and not shared_outer and max_loras == 1
bucket_experts = (
local_num_experts
if compact_merged
else (1 if shared_outer else num_experts) * max_loras
)
if (
lora_envs.SGLANG_OPT_LORA_FUSED_MERGED_ALIGN.get()
and max_loras == 1
and (shared_outer or ep_local)
and (shared_outer or compact_merged)
and bucket_experts + 1 <= 1024
and topk_ids.shape[0] < 512
):
from sglang.jit_kernel.trtllm_lora_temp.moe_lora_merged_align import (
@@ -799,9 +783,7 @@ def _merged_experts_fused_moe_lora_add_impl(
local_expert_offset,
local_num_experts,
do_skip=True,
# compact local-expert histogram is only valid for the per-expert EP
# path; shared_outer routes by lora id (base=0) so it must stay global.
compact=not shared_outer,
compact=compact_merged,
)
result = (
sorted_token_ids,
@@ -874,6 +856,12 @@ def _merged_experts_fused_moe_lora_add_impl(
"expand",
"routing",
), f"invalid stage {stage!r}"
if broadcast_intermediate:
assert stage == "expand"
assert use_direct_expand_add
assert intermediate_buffer is not None
assert intermediate_buffer.ndim == 2
assert intermediate_buffer.shape[0] == token_lora_mapping.shape[0]
lora_a_virtual = _merge_lora_expert_weight(lora_a)
lora_b_virtual = _merge_lora_expert_weight(lora_b)
num_experts_a = lora_a.shape[1]
@@ -891,20 +879,32 @@ def _merged_experts_fused_moe_lora_add_impl(
a_cfg = _get_shrink_stage_config(lora_a_virtual, token_lora_mapping.shape[0])
if lora_envs.SGLANG_OPT_LORA_SHRINK_TUNE.get():
a_cfg = {**a_cfg, "BLOCK_SIZE_M": 16}
_get_routing(
topk_ids,
token_lora_mapping,
num_experts_a,
experts_shared_outer_loras_a,
a_cfg["BLOCK_SIZE_M"],
)
_get_routing(
topk_ids,
token_lora_mapping,
num_experts_b,
experts_shared_outer_loras_b,
b_stage_config["BLOCK_SIZE_M"],
)
# Match the actual shrink-stage override below. Without this, callers
# that admit prefill-shaped batches into a side stream prewarm block-32
# routing here, then miss the cache when shrink switches to the B-stage
# block size. The miss allocates routing buffers on the side stream
# during capture, violating the allocation guarantee of stage='routing'.
if (
lora_envs.SGLANG_OPT_LORA_PREFILL_ROUTING_REUSE.get()
and token_lora_mapping.shape[0] >= 512
):
a_cfg["BLOCK_SIZE_M"] = b_stage_config["BLOCK_SIZE_M"]
if prewarm_a_routing:
_get_routing(
topk_ids,
token_lora_mapping,
num_experts_a,
experts_shared_outer_loras_a,
a_cfg["BLOCK_SIZE_M"],
)
if prewarm_b_routing:
_get_routing(
topk_ids,
token_lora_mapping,
num_experts_b,
experts_shared_outer_loras_b,
b_stage_config["BLOCK_SIZE_M"],
)
return None
intermediate = intermediate_buffer
@@ -913,21 +913,14 @@ def _merged_experts_fused_moe_lora_add_impl(
lora_a_virtual, token_lora_mapping.shape[0]
)
if lora_envs.SGLANG_OPT_LORA_SHRINK_TUNE.get():
# GB200 hand-tune knob (test-only) on top of PR #26899's heuristic config. The launcher
# pins BLOCK_SIZE_N (next_pow2(rank)) and BLOCK_SIZE_K (256), so only M/warps/stages apply.
# Test-only override; the launcher fixes N and K block sizes.
a_stage_config = {
**a_stage_config,
"BLOCK_SIZE_M": 16,
"num_warps": 4,
"num_stages": 4,
}
# F1-① prefill routing reuse: the A stage routes with BLOCK_SIZE_M 32 at prefill
# but the B stage with the tuned fused-moe config (typically 64), so the
# (num_experts, shared_outer, block_size) routing_cache key never matches across
# stages and the align/sort pipeline reruns per stage (4x/layer at prefill).
# Matching the A stage's routing block to the B stage's collapses them to one
# align/sort per layer-forward. Decode (<512 tokens) keeps the opt1 fused
# merged-align path and its tuned shrink block untouched.
# Match routing block sizes so prefill stages can share cached alignment.
if (
lora_envs.SGLANG_OPT_LORA_PREFILL_ROUTING_REUSE.get()
and token_lora_mapping.shape[0] >= 512
@@ -957,8 +950,10 @@ def _merged_experts_fused_moe_lora_add_impl(
# non-owned blocks (never reads them), but a shared-outer expand routes by lora id
# and would read them into the real (all-reduced) output -> must zero. split_k > 1
# also needs a zeroed buffer for its accumulation.
zero_intermediate = intermediate_split_k > 1 or (
ep_local and experts_shared_outer_loras_b
must_zero_intermediate = (
zero_intermediate
or intermediate_split_k > 1
or (ep_local and experts_shared_outer_loras_b)
)
if intermediate is None:
intermediate = (
@@ -967,14 +962,14 @@ def _merged_experts_fused_moe_lora_add_impl(
dtype=hidden_states.dtype,
device=hidden_states.device,
)
if zero_intermediate
if must_zero_intermediate
else torch.empty(
intermediate_shape,
dtype=hidden_states.dtype,
device=hidden_states.device,
)
)
elif zero_intermediate:
elif must_zero_intermediate:
# Caller-provided buffer (allocated on the consumer stream): zero it in-stream.
intermediate.zero_()
@@ -993,7 +988,7 @@ def _merged_experts_fused_moe_lora_add_impl(
if stage == "shrink":
# Pre-warm the routing-B cache on this (side) stream so the later "expand" stage
# launches no routing kernels — they overlap finalize together with the shrink.
if routing_cache is not None:
if routing_cache is not None and prewarm_b_routing:
_get_routing(
topk_ids,
token_lora_mapping,
@@ -1041,8 +1036,13 @@ def _merged_experts_fused_moe_lora_add_impl(
b_stage_config,
mul_routed_weight,
fuse_sum_all_reduce,
broadcast_intermediate=broadcast_intermediate,
)
else:
assert not broadcast_intermediate, (
"broadcasted LoRA-A intermediates require the rank-specialized "
"direct expand kernel"
)
invoke_fused_moe_kernel(
intermediate_flat,
lora_b_virtual,
@@ -1131,6 +1131,10 @@ def merged_experts_fused_moe_lora_add(
stage: str = "all",
intermediate_buffer: torch.Tensor | None = None,
expand_wait_event: "torch.cuda.Event | None" = None,
broadcast_intermediate: bool = False,
prewarm_a_routing: bool = True,
prewarm_b_routing: bool = True,
zero_intermediate: bool = False,
) -> "torch.Tensor | None":
"""Public API: wraps the registered op with routing_cache support."""
return _merged_experts_fused_moe_lora_add_impl(
@@ -1153,4 +1157,8 @@ def merged_experts_fused_moe_lora_add(
stage=stage,
intermediate_buffer=intermediate_buffer,
expand_wait_event=expand_wait_event,
broadcast_intermediate=broadcast_intermediate,
prewarm_a_routing=prewarm_a_routing,
prewarm_b_routing=prewarm_b_routing,
zero_intermediate=zero_intermediate,
)
@@ -12,6 +12,7 @@
# limitations under the License.
# ==============================================================================
import torch
import triton
import triton.language as tl
@@ -94,3 +95,636 @@ def rotate_input_ids(
BLOCK_SIZE=BLOCK_SIZE,
)
return input_ids
@triton.jit
def stash_append_boundary_state_kernel(
# flat sources (decode: predict + verify FULL hiddens; prefill: rotated
# input_ids + target FULL hiddens)
src_tokens_ptr,
src_hiddens_ptr, # [num_src_rows, hidden]
src_row_ends_ptr, # [bs] exclusive end row of each request's source segment
num_available_ptr, # [bs] committed rows at the segment tail (accept_lens / extend len)
req_pool_indices_ptr, # [bs]
# stash (per request, rolling last `front` committed (token, base-hidden)
# pairs; slot j of a request at boundary B holds position B - front + j)
stash_tokens_ptr, # [req_pool_size, front] int64
stash_hiddens_ptr, # [req_pool_size, front, hidden]
stash_valid_lens_ptr, # [req_pool_size] int32, count of valid tail slots
front: tl.constexpr,
hidden_dim: tl.constexpr,
SET_VALID: tl.constexpr, # prefill: valid = m; decode: valid = min(valid + m, front)
BLOCK_H: tl.constexpr,
):
"""Roll the per-request boundary stash forward by m = min(available, front)
newly committed (token, base-hidden) pairs taken from the source tail
rows [end - m, end). Kept old pairs shift down (reads stay ahead of
writes, ascending order)."""
pid = tl.program_id(0)
rpi = tl.load(req_pool_indices_ptr + pid).to(tl.int64)
end = tl.load(src_row_ends_ptr + pid).to(tl.int64)
avail = tl.load(num_available_ptr + pid).to(tl.int64)
m = tl.minimum(avail, front)
keep = front - m
h_off = tl.arange(0, BLOCK_H)
# 1) Shift the kept tail of the old stash to the front: new[i] = old[i + m]
for i in range(0, keep):
src_t = tl.load(stash_tokens_ptr + rpi * front + i + m)
tl.store(stash_tokens_ptr + rpi * front + i, src_t)
for hb in range(0, hidden_dim, BLOCK_H):
hmask = (hb + h_off) < hidden_dim
src_h = tl.load(
stash_hiddens_ptr + (rpi * front + i + m) * hidden_dim + hb + h_off,
mask=hmask,
)
tl.store(
stash_hiddens_ptr + (rpi * front + i) * hidden_dim + hb + h_off,
src_h,
mask=hmask,
)
# 2) Append the m newest committed pairs from the source tail.
for i in range(0, m):
row = end - m + i
dst = rpi * front + keep + i
tok = tl.load(src_tokens_ptr + row)
tl.store(stash_tokens_ptr + dst, tok)
for hb in range(0, hidden_dim, BLOCK_H):
hmask = (hb + h_off) < hidden_dim
src_h = tl.load(src_hiddens_ptr + row * hidden_dim + hb + h_off, mask=hmask)
tl.store(
stash_hiddens_ptr + dst * hidden_dim + hb + h_off, src_h, mask=hmask
)
if SET_VALID:
valid = m
else:
valid = tl.minimum(tl.load(stash_valid_lens_ptr + rpi).to(tl.int64) + m, front)
tl.store(stash_valid_lens_ptr + rpi, valid.to(tl.int32))
def stash_append_boundary_state_triton(
src_tokens,
src_hiddens,
src_row_ends,
num_available,
req_pool_indices,
stash_tokens,
stash_hiddens,
stash_valid_lens,
set_valid: bool,
):
"""Append newly committed (token, base-hidden) pairs to the rolling
boundary stash (see kernel docstring). Decode: sources are (predict,
verify FULL hiddens) with ends = i*W + accept_lens. Prefill: sources are
(post-rotation input_ids, target FULL hiddens) with ends = start + len."""
bs = req_pool_indices.shape[0]
if bs == 0:
return
stash_append_boundary_state_kernel[(bs,)](
src_tokens,
src_hiddens,
src_row_ends,
num_available,
req_pool_indices,
stash_tokens,
stash_hiddens,
stash_valid_lens,
front=stash_tokens.shape[1],
hidden_dim=stash_hiddens.shape[2],
SET_VALID=set_valid,
BLOCK_H=1024,
)
@triton.jit
def fill_widened_draft_extend_inputs_kernel(
# outputs: the widened per-request window buffers, width = W + front
input_ids_ptr, # [bs * width]
hidden_ptr, # [bs * width, hidden]
# sources
predict_ptr, # [bs * W] verify-sampled successor per verify row
verify_hidden_ptr, # [bs * W, hidden] target verify hiddens (FULL capture)
stash_tokens_ptr, # [req_pool_size, front]
stash_hiddens_ptr, # [req_pool_size, front, hidden]
stash_valid_lens_ptr, # [req_pool_size]
seq_lens_ptr, # [bs] PRE-verify seq_lens (window base = seq_lens - front)
req_pool_indices_ptr, # [bs]
draft_token_num: tl.constexpr, # W
front: tl.constexpr, # F_total
hidden_dim: tl.constexpr,
BLOCK_H: tl.constexpr,
):
"""Materialize the widened depth-0 window's input tokens and hiddens: front
rows (j < front) source from stash slot j, original rows (j >= front) from
predict/verify hiddens; data-invalid front rows are zeroed. Locs/positions
are computed separately by compute_widened_draft_extend_locs_positions."""
pid = tl.program_id(0)
rpi = tl.load(req_pool_indices_ptr + pid).to(tl.int64)
seq_len = tl.load(seq_lens_ptr + pid).to(tl.int64)
valid_len = tl.load(stash_valid_lens_ptr + rpi).to(tl.int64)
# Rows below this hold no usable stash data (unseeded slot or p < 0).
first_valid = tl.maximum(tl.maximum(front - valid_len, front - seq_len), 0)
h_off = tl.arange(0, BLOCK_H)
width = draft_token_num + front
for j in range(0, width):
row = pid * width + j
if j >= front:
src = pid * draft_token_num + j - front
tok = tl.load(predict_ptr + src)
tl.store(input_ids_ptr + row, tok)
for hb in range(0, hidden_dim, BLOCK_H):
hmask = (hb + h_off) < hidden_dim
src_h = tl.load(
verify_hidden_ptr + src * hidden_dim + hb + h_off, mask=hmask
)
tl.store(hidden_ptr + row * hidden_dim + hb + h_off, src_h, mask=hmask)
else:
if j >= first_valid:
tok = tl.load(stash_tokens_ptr + rpi * front + j)
tl.store(input_ids_ptr + row, tok)
for hb in range(0, hidden_dim, BLOCK_H):
hmask = (hb + h_off) < hidden_dim
src_h = tl.load(
stash_hiddens_ptr + (rpi * front + j) * hidden_dim + hb + h_off,
mask=hmask,
)
tl.store(
hidden_ptr + row * hidden_dim + hb + h_off, src_h, mask=hmask
)
else:
tl.store(input_ids_ptr + row, 0)
for hb in range(0, hidden_dim, BLOCK_H):
hmask = (hb + h_off) < hidden_dim
tl.store(
hidden_ptr + row * hidden_dim + hb + h_off, 0.0, mask=hmask
)
def fill_widened_draft_extend_inputs_triton(
input_ids,
hidden_states,
predict,
verify_hiddens,
stash_tokens,
stash_hiddens,
stash_valid_lens,
seq_lens,
req_pool_indices,
draft_token_num: int,
):
"""Fill the widened window's input tokens and hiddens in place (see kernel
docstring). Must run AFTER verify sampling (reads predict / hiddens) and
BEFORE the stash update for this iteration (the stash is still based at
the pre-verify boundary)."""
bs = req_pool_indices.shape[0]
if bs == 0:
return
fill_widened_draft_extend_inputs_kernel[(bs,)](
input_ids,
hidden_states,
predict,
verify_hiddens,
stash_tokens,
stash_hiddens,
stash_valid_lens,
seq_lens,
req_pool_indices,
draft_token_num=draft_token_num,
front=stash_tokens.shape[1],
hidden_dim=stash_hiddens.shape[2],
BLOCK_H=1024,
)
@triton.jit
def _wide_row_softmax_partials_kernel(
logits_ptr, # [bs, vocab] fp32
temperatures_ptr, # [bs, 1] fp32 (dummy when HAS_TEMPS is False)
partial_max_ptr, # [bs, nblocks] fp32
partial_sum_ptr, # [bs, nblocks] fp32
vocab,
nblocks,
HAS_TEMPS: tl.constexpr,
BLOCK: tl.constexpr,
):
row = tl.program_id(0)
blk = tl.program_id(1)
offs = blk * BLOCK + tl.arange(0, BLOCK)
mask = offs < vocab
z = tl.load(
logits_ptr + row.to(tl.int64) * vocab + offs, mask=mask, other=-float("inf")
)
if HAS_TEMPS:
z = z / tl.load(temperatures_ptr + row)
m = tl.max(z, axis=0)
s = tl.sum(tl.exp(z - m), axis=0)
tl.store(partial_max_ptr + row * nblocks + blk, m)
tl.store(partial_sum_ptr + row * nblocks + blk, s)
@triton.jit
def _wide_row_softmax_finalize_kernel(
partial_max_ptr,
partial_sum_ptr,
row_max_ptr, # [bs] fp32
row_sum_ptr, # [bs] fp32
nblocks,
NBLOCK_POW2: tl.constexpr,
):
row = tl.program_id(0)
offs = tl.arange(0, NBLOCK_POW2)
mask = offs < nblocks
m = tl.load(partial_max_ptr + row * nblocks + offs, mask=mask, other=-float("inf"))
s = tl.load(partial_sum_ptr + row * nblocks + offs, mask=mask, other=0.0)
gm = tl.max(m, axis=0)
gs = tl.sum(s * tl.exp(m - gm), axis=0)
tl.store(row_max_ptr + row, gm)
tl.store(row_sum_ptr + row, gs)
@triton.jit
def _wide_row_softmax_write_kernel(
logits_ptr,
temperatures_ptr,
row_max_ptr,
row_sum_ptr,
out_ptr, # [bs, out_row_stride] fp32; row i written at i * out_row_stride
vocab,
out_row_stride,
HAS_TEMPS: tl.constexpr,
BLOCK: tl.constexpr,
):
row = tl.program_id(0)
blk = tl.program_id(1)
offs = blk * BLOCK + tl.arange(0, BLOCK)
mask = offs < vocab
z = tl.load(
logits_ptr + row.to(tl.int64) * vocab + offs, mask=mask, other=-float("inf")
)
if HAS_TEMPS:
z = z / tl.load(temperatures_ptr + row)
gm = tl.load(row_max_ptr + row)
gs = tl.load(row_sum_ptr + row)
q = tl.exp(z - gm) / gs
tl.store(out_ptr + row.to(tl.int64) * out_row_stride + offs, q, mask=mask)
def wide_row_softmax_triton(
logits: torch.Tensor,
temperatures,
out: torch.Tensor,
) -> torch.Tensor:
"""Column-parallel softmax over very wide fp32 rows, optionally with
per-row temperature (q = softmax(logits / T)), written into ``out``
(any row stride >= vocab). torch.softmax gives one block per row, which
serializes a single wide draft-vocab row onto one SM."""
bs, vocab = logits.shape
BLOCK = 4096
nblocks = triton.cdiv(vocab, BLOCK)
partial_max = torch.empty((bs, nblocks), dtype=torch.float32, device=logits.device)
partial_sum = torch.empty((bs, nblocks), dtype=torch.float32, device=logits.device)
row_max = torch.empty((bs,), dtype=torch.float32, device=logits.device)
row_sum = torch.empty((bs,), dtype=torch.float32, device=logits.device)
has_temps = temperatures is not None
dummy = row_max
_wide_row_softmax_partials_kernel[(bs, nblocks)](
logits,
temperatures if has_temps else dummy,
partial_max,
partial_sum,
vocab,
nblocks,
HAS_TEMPS=has_temps,
BLOCK=BLOCK,
)
_wide_row_softmax_finalize_kernel[(bs,)](
partial_max,
partial_sum,
row_max,
row_sum,
nblocks,
NBLOCK_POW2=triton.next_power_of_2(nblocks),
)
_wide_row_softmax_write_kernel[(bs, nblocks)](
logits,
temperatures if has_temps else dummy,
row_max,
row_sum,
out,
vocab,
out.stride(0),
HAS_TEMPS=has_temps,
BLOCK=BLOCK,
)
return out
@triton.jit
def compute_widened_draft_extend_locs_positions_kernel(
seq_lens_ptr,
req_pool_indices_ptr,
req_to_token_ptr,
stash_valid_lens_ptr,
locs_ptr, # [bs * width] int64
positions_ptr, # [bs * width] int64
req_to_token_stride,
front,
num_warmup_tokens,
width,
WIDTH_BLOCK: tl.constexpr,
):
"""Per-request widened-window locs + positions: pos = seq_len - front + j;
rows below first_valid = max(front - stash_valid, front - seq_len, 0) hold
no stash data (positions zeroed), and the first num_warmup_tokens valid
front rows write to sacrificial loc 0."""
pid = tl.program_id(0)
offs = tl.arange(0, WIDTH_BLOCK)
wmask = offs < width
offs64 = offs.to(tl.int64)
seq_len = tl.load(seq_lens_ptr + pid).to(tl.int64)
rpi = tl.load(req_pool_indices_ptr + pid).to(tl.int64)
valid_len = tl.load(stash_valid_lens_ptr + rpi).to(tl.int64)
pos = seq_len - front + offs64
first_valid = tl.maximum(tl.maximum(front - valid_len, front - seq_len), 0)
data_valid = offs64 >= first_valid
write_real = offs64 >= tl.minimum(first_valid + num_warmup_tokens, front)
tok = tl.load(
req_to_token_ptr + rpi * req_to_token_stride + tl.maximum(pos, 0),
mask=wmask,
other=0,
).to(tl.int64)
locs = tl.where(write_real, tok, 0)
positions = tl.where(data_valid, pos, 0)
base = pid.to(tl.int64) * width
tl.store(locs_ptr + base + offs, locs, mask=wmask)
tl.store(positions_ptr + base + offs, positions, mask=wmask)
def compute_widened_draft_extend_locs_positions_triton(
seq_lens,
req_pool_indices,
req_to_token,
stash_valid_lens,
draft_token_num: int,
num_front_tokens: int,
num_warmup_tokens: int,
):
width = draft_token_num + num_front_tokens
bs = seq_lens.shape[0]
locs = torch.empty((bs * width,), dtype=torch.int64, device=seq_lens.device)
positions = torch.empty((bs * width,), dtype=torch.int64, device=seq_lens.device)
if bs > 0:
compute_widened_draft_extend_locs_positions_kernel[(bs,)](
seq_lens,
req_pool_indices,
req_to_token,
stash_valid_lens,
locs,
positions,
req_to_token.stride(0),
num_front_tokens,
num_warmup_tokens,
width,
WIDTH_BLOCK=triton.next_power_of_2(width),
)
return locs, positions
@triton.jit
def fill_draft_extend_prepare_buffers_kernel(
# persistent per-token buffers (length max_num_token, int64)
input_ids_ptr,
positions_ptr,
out_cache_loc_ptr,
# per-token sources (length num_tokens)
src_input_ids_ptr,
src_positions_ptr,
src_out_cache_loc_ptr,
# persistent per-request buffers (length max_bs)
seq_lens_ptr, # int32
req_pool_indices_ptr, # int64
num_correct_drafts_ptr, # int32
num_accept_tokens_ptr, # int32
select_index_ptr, # int64
temperatures_ptr, # float32 [max_bs, 1] (dummy when HAS_TEMPS is False)
# per-request sources (length raw_bs)
src_seq_lens_ptr,
src_req_pool_indices_ptr,
src_num_correct_drafts_ptr,
src_num_accept_tokens_ptr,
src_temperatures_ptr, # dummy when HAS_TEMPS is False
# chain hidden window, flat [num_tokens * hidden] (dummies when HAS_HIDDEN
# is False)
hidden_states_ptr,
src_hidden_states_ptr,
# gathered-buffer mirrors (dummies when HAS_GATHERED is False)
global_num_tokens_ptr,
global_num_tokens_for_logprob_ptr,
# scalars
num_tokens,
max_num_token,
raw_bs,
bs,
max_bs,
num_tokens_per_bs,
num_front_tokens,
seq_len_fill_value,
hidden_numel,
num_global,
num_token_programs,
HAS_TEMPS: tl.constexpr,
HAS_HIDDEN: tl.constexpr,
HAS_GATHERED: tl.constexpr,
BLOCK_TOK: tl.constexpr,
BLOCK_HIDDEN: tl.constexpr,
GLOBAL_BLOCK: tl.constexpr,
):
"""The whole draft-extend prepare() buffer population in one launch;
program roles split by flat program id:
- [0, num_token_programs): input_ids / positions / out_cache_loc; rows
< num_tokens take the source, the tail up to max_num_token is zeroed.
- [num_token_programs, +max_bs): one program per request row. Real rows
[0, raw_bs) take source values; padded rows [raw_bs, bs) take the pad
sentinels the graphs rely on (seq_len fill value, num_accept_tokens = -1,
temperatures = 1.0); rows >= bs are untouched except seq_lens, which is
fully reset. select_index = i*window + front + num_correct_drafts
(padded rows keep their stale num_correct_drafts, whose gather result
is discarded).
- num_token_programs + max_bs: the DP gathered-buffer fills.
- the rest: flat copy of the chain hidden window's real rows (the padded
tail is never read).
"""
pid = tl.program_id(0)
if pid < num_token_programs:
tok_offs = pid * BLOCK_TOK + tl.arange(0, BLOCK_TOK)
store_mask = tok_offs < max_num_token
copy_mask = tok_offs < num_tokens
tok = tl.load(src_input_ids_ptr + tok_offs, mask=copy_mask, other=0).to(
tl.int64
)
tl.store(input_ids_ptr + tok_offs, tok, mask=store_mask)
tok = tl.load(src_positions_ptr + tok_offs, mask=copy_mask, other=0).to(
tl.int64
)
tl.store(positions_ptr + tok_offs, tok, mask=store_mask)
tok = tl.load(src_out_cache_loc_ptr + tok_offs, mask=copy_mask, other=0).to(
tl.int64
)
tl.store(out_cache_loc_ptr + tok_offs, tok, mask=store_mask)
elif pid < num_token_programs + max_bs:
i = pid - num_token_programs
is_real = i < raw_bs
is_pad = (i >= raw_bs) & (i < bs)
in_bs = i < bs
sl = tl.load(src_seq_lens_ptr + i, mask=is_real, other=seq_len_fill_value)
tl.store(seq_lens_ptr + i, sl.to(tl.int32))
rpi = tl.load(src_req_pool_indices_ptr + i, mask=is_real, other=0).to(tl.int64)
tl.store(req_pool_indices_ptr + i, rpi, mask=is_real)
# The stale count must be read BEFORE the real-row store below.
ncd_stale = tl.load(num_correct_drafts_ptr + i, mask=is_pad, other=0).to(
tl.int64
)
ncd_src = tl.load(src_num_correct_drafts_ptr + i, mask=is_real, other=0).to(
tl.int64
)
tl.store(num_correct_drafts_ptr + i, ncd_src.to(tl.int32), mask=is_real)
ncd = tl.where(is_real, ncd_src, ncd_stale)
# Padded rows get -1 so the sconv commit skips their live mamba slots.
nat = tl.load(src_num_accept_tokens_ptr + i, mask=is_real, other=-1).to(
tl.int32
)
tl.store(num_accept_tokens_ptr + i, nat, mask=in_bs)
si = i.to(tl.int64) * num_tokens_per_bs + num_front_tokens + ncd
tl.store(select_index_ptr + i, si, mask=in_bs)
if HAS_TEMPS:
t = tl.load(src_temperatures_ptr + i, mask=is_real, other=1.0)
tl.store(temperatures_ptr + i, t, mask=in_bs)
elif pid == num_token_programs + max_bs:
if HAS_GATHERED:
g_offs = tl.arange(0, GLOBAL_BLOCK)
g_mask = g_offs < num_global
g_vals = tl.zeros((GLOBAL_BLOCK,), dtype=tl.int32) + bs * num_tokens_per_bs
tl.store(global_num_tokens_ptr + g_offs, g_vals, mask=g_mask)
tl.store(global_num_tokens_for_logprob_ptr + g_offs, g_vals, mask=g_mask)
else:
if HAS_HIDDEN:
h_base = pid - num_token_programs - max_bs - 1
h_offs = h_base.to(tl.int64) * BLOCK_HIDDEN + tl.arange(0, BLOCK_HIDDEN)
h_mask = h_offs < hidden_numel
h_vals = tl.load(src_hidden_states_ptr + h_offs, mask=h_mask)
tl.store(hidden_states_ptr + h_offs, h_vals, mask=h_mask)
def fill_draft_extend_prepare_buffers_triton(
input_ids,
positions,
out_cache_loc,
src_input_ids,
src_positions,
src_out_cache_loc,
seq_lens,
req_pool_indices,
num_correct_drafts,
num_accept_tokens,
select_index,
temperatures,
src_seq_lens,
src_req_pool_indices,
src_num_correct_drafts,
src_num_accept_tokens,
src_temperatures,
hidden_states,
src_hidden_states,
global_num_tokens,
global_num_tokens_for_logprob,
raw_bs,
bs,
num_tokens_per_bs,
num_front_tokens,
seq_len_fill_value,
):
max_num_token = input_ids.shape[0]
num_tokens = src_input_ids.shape[0]
max_bs = seq_lens.shape[0]
has_temps = temperatures is not None
has_hidden = src_hidden_states is not None
has_gathered = global_num_tokens is not None
BLOCK_TOK = 1024
BLOCK_HIDDEN = 2048
num_token_programs = triton.cdiv(max_num_token, BLOCK_TOK)
if has_hidden:
hidden_numel = num_tokens * hidden_states.shape[1]
num_hidden_programs = triton.cdiv(hidden_numel, BLOCK_HIDDEN)
else:
hidden_numel = 0
num_hidden_programs = 0
if has_gathered:
num_global = global_num_tokens.shape[0]
global_block = triton.next_power_of_2(num_global)
else:
num_global = 0
global_block = 1
grid = (num_token_programs + max_bs + 1 + num_hidden_programs,)
fill_draft_extend_prepare_buffers_kernel[grid](
input_ids,
positions,
out_cache_loc,
src_input_ids,
src_positions,
src_out_cache_loc,
seq_lens,
req_pool_indices,
num_correct_drafts,
num_accept_tokens,
select_index,
temperatures if has_temps else seq_lens,
src_seq_lens,
src_req_pool_indices,
src_num_correct_drafts,
src_num_accept_tokens,
src_temperatures if has_temps else seq_lens,
hidden_states if has_hidden else seq_lens,
src_hidden_states if has_hidden else seq_lens,
global_num_tokens if has_gathered else seq_lens,
global_num_tokens_for_logprob if has_gathered else seq_lens,
num_tokens,
max_num_token,
raw_bs,
bs,
max_bs,
num_tokens_per_bs,
num_front_tokens,
seq_len_fill_value,
hidden_numel,
num_global,
num_token_programs,
HAS_TEMPS=has_temps,
HAS_HIDDEN=has_hidden,
HAS_GATHERED=has_gathered,
BLOCK_TOK=BLOCK_TOK,
BLOCK_HIDDEN=BLOCK_HIDDEN,
GLOBAL_BLOCK=global_block,
)
+51
View File
@@ -824,6 +824,57 @@ def _deepseek_v4_overrides(server_args: Any, hf_config: Any) -> dict:
return overrides
@_register_for(
"InklingForConditionalGeneration",
"InklingForConditionalGenerationMTP",
)
def _inkling_overrides(server_args: Any, hf_config: Any) -> dict:
"""Inkling architecture defaults: SWA / mamba KV-pool ratios tuned for the
hybrid-SWA layout, the extra-buffer mamba strategy, and the unified radix
tree (which Inkling requires models/inkling.py asserts it). The full-graph
prefill default is set separately (inline, before cuda-graph resolution)
see ServerArgs.__post_init__ / _apply_inkling_prefill_cuda_graph_default. The
server-arg defaults each yield to an explicit user value (compared against
the ServerArgs class default); the prefill declaration is materialized
before _parse_cuda_graph_config folds cuda_graph_backend_prefill into
prefill.backend, and an explicit --cuda-graph-backend-prefill /
--disable-prefill-cuda-graph still wins. The unified-radix env write follows
the MiniMax-M3 handler precedent (env is not a resolvable server-arg)."""
from sglang.srt.server_args import ServerArgs
overrides: Dict[str, Any] = {}
# NOTE: the full-graph prefill default is NOT set here. cuda-graph config is
# resolved in __post_init__ before declarations are materialized, so a
# cuda_graph_backend_prefill declared here lands too late (the breakable
# default would already have been auto-disabled for this multimodal arch).
# It is set inline before _handle_cuda_graph_config instead.
if server_args.swa_full_tokens_ratio == ServerArgs.swa_full_tokens_ratio:
overrides["swa_full_tokens_ratio"] = 0.1
if server_args.mamba_full_memory_ratio == ServerArgs.mamba_full_memory_ratio:
overrides["mamba_full_memory_ratio"] = 0.1
# Inkling requires the extra-buffer mamba strategy (inkling.py asserts
# enable_mamba_extra_buffer()); the generic "auto" resolution does not cover
# Inkling, so pin it here. Yields to an explicit --mamba-scheduler-strategy.
if server_args.mamba_radix_cache_strategy == ServerArgs.mamba_radix_cache_strategy:
overrides["mamba_radix_cache_strategy"] = "extra_buffer"
# Inkling attention runs only on the fa4 (Blackwell) or triton backends --
# models/inkling_common/attn.py asserts attention_backend in {fa4, triton}.
# The generic resolver would otherwise pick trtllm_mha (SM100) / fa3
# (Hopper), so a bare launch fails on the first attention forward. Pin a
# supported default when the user left every attention-backend flag unset
# (mirrors the MiniMax-M3 SM100 fa4-default above); an explicit
# --attention-backend / --prefill/decode-attention-backend still wins.
if server_args.is_attention_backend_not_set():
inkling_attn_backend = "fa4" if is_sm100_supported() else "triton"
overrides["attention_backend"] = inkling_attn_backend
logger.info(
f"Use {inkling_attn_backend} as the attention backend for Inkling "
"(requires fa4 or triton)."
)
envs.SGLANG_ENABLE_UNIFIED_RADIX_TREE.set(True)
return overrides
@_register_for("NemotronHForCausalLM", "NemotronHPuzzleForCausalLM")
def _nemotron_h_overrides(server_args: Any, hf_config: Any) -> dict:
"""NemotronH quantization / MoE runner / attention backend defaults
+10
View File
@@ -9,6 +9,12 @@ from sglang.srt.configs.dots_vlm import DotsVLMConfig
from sglang.srt.configs.exaone import ExaoneConfig
from sglang.srt.configs.falcon_h1 import FalconH1Config
from sglang.srt.configs.granitemoehybrid import GraniteMoeHybridConfig
from sglang.srt.configs.inkling import (
InklingAudioConfig,
InklingMMConfig,
InklingModelConfig,
InklingVisionConfig,
)
from sglang.srt.configs.interns2preview import InternS2PreviewConfig
from sglang.srt.configs.janus_pro import MultiModalityConfig
from sglang.srt.configs.jet_nemotron import JetNemotronConfig
@@ -86,6 +92,10 @@ __all__ = [
"MiniMaxM3VLConfig",
"Step3p7Config",
"Qwen3ASRConfig",
"InklingAudioConfig",
"InklingMMConfig",
"InklingModelConfig",
"InklingVisionConfig",
"UnlimitedVLConfig",
"ZayaConfig",
]
+7
View File
@@ -6,6 +6,8 @@ from sglang.srt.configs import (
BailingHybridConfig,
FalconH1Config,
GraniteMoeHybridConfig,
InklingMMConfig,
InklingModelConfig,
InternS2PreviewConfig,
JetNemotronConfig,
JetVLMConfig,
@@ -76,6 +78,11 @@ def mamba2_config(model_config: ModelConfig):
| ZayaConfig,
):
return config
if isinstance(config, InklingModelConfig):
return config if config.mamba2_cache_params is not None else None
if isinstance(config, InklingMMConfig):
text_config = config.text_config
return text_config if text_config.mamba2_cache_params is not None else None
if isinstance(config, NemotronH_Nano_VL_V2_Config):
return config.llm_config
+431
View File
@@ -0,0 +1,431 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Literal, Optional
import torch
from transformers import CONFIG_MAPPING
from transformers.configuration_utils import PretrainedConfig
from sglang.srt.configs.mamba_utils import BaseLinearStateParams
class InklingModelConfig(PretrainedConfig):
model_type = "inkling_model"
keys_to_ignore_at_inference = ["past_key_values"]
def __init__(
self,
*,
vocab_size: int = 201024,
hidden_size: int = 1536,
intermediate_size: int = 768,
dense_intermediate_size: Optional[int] = None,
num_hidden_layers: int = 16,
num_attention_heads: int = 12,
num_key_value_heads: int = 4,
head_dim: Optional[int] = None,
v_head_dim: Optional[int] = None,
d_rel: int = 16,
rel_extent: int = 1024,
local_layer_ids: Optional[list[int]] = None,
sliding_window_size: int = 512,
swa_num_attention_heads: Optional[int] = None,
swa_num_key_value_heads: Optional[int] = None,
swa_head_dim: Optional[int] = None,
swa_v_head_dim: Optional[int] = None,
mtp_local_layer_ids: Optional[list[int]] = None,
mtp_local_extent: Optional[int] = None,
mtp_swa_num_attention_heads: Optional[int] = None,
mtp_swa_num_key_value_heads: Optional[int] = None,
mtp_swa_head_dim: Optional[int] = None,
rms_norm_eps: float = 1e-6,
hidden_act: str = "silu",
q_bias: bool = False,
o_bias: bool = False,
use_embed_norm: bool = False,
use_sconv: bool = False,
sconv_kernel_size: int = 4,
chain_hidden_post_norm: bool = False,
dense_mlp_idx: int = 0,
n_routed_experts: int = 0,
n_shared_experts: int = 0,
num_experts_per_tok: int = 1,
route_scale: float = 1.0,
use_gate_bias: bool = False,
use_global_scale: bool = False,
norm_after_topk: bool = True,
gate_activation: Literal["sigmoid", "softmax"] = "sigmoid",
shared_expert_sink: bool = False,
shared_experts_size: int = 1,
inference_moe_w13_interleaved: bool = True,
log_scaling_n_floor: int | None = None,
log_scaling_alpha: float = 0.1,
unpadded_vocab_size: Optional[int] = None,
padded_vocab_size: Optional[int] = None,
logits_mup_width_multiplier: Optional[float] = None,
final_logit_softcapping: Optional[float] = None,
num_nextn_predict_layers: int = 8,
tie_word_embeddings: bool = False,
**kwargs: Any,
) -> None:
if head_dim is None:
head_dim = hidden_size // num_attention_heads
if v_head_dim is None:
v_head_dim = head_dim
if swa_num_attention_heads is None:
swa_num_attention_heads = num_attention_heads
if swa_num_key_value_heads is None:
swa_num_key_value_heads = num_key_value_heads
if swa_head_dim is None:
swa_head_dim = head_dim
if swa_v_head_dim is None:
swa_v_head_dim = swa_head_dim
if dense_intermediate_size is None:
dense_intermediate_size = intermediate_size
if local_layer_ids is None:
local_layer_ids = []
# Per-depth banded MTP attention: a depth listed in mtp_local_layer_ids
# is a sliding-window block with its own window (mtp_local_extent);
# other depths stay full-attention.
if mtp_local_layer_ids is None:
mtp_local_layer_ids = []
if mtp_local_extent is None:
mtp_local_extent = sliding_window_size
if mtp_swa_num_attention_heads is None:
mtp_swa_num_attention_heads = swa_num_attention_heads
if mtp_swa_num_key_value_heads is None:
mtp_swa_num_key_value_heads = swa_num_key_value_heads
if mtp_swa_head_dim is None:
mtp_swa_head_dim = swa_head_dim
if mtp_local_layer_ids:
local_id_set = set(mtp_local_layer_ids)
assert len(local_id_set) == len(
mtp_local_layer_ids
), f"mtp_local_layer_ids must be unique: {mtp_local_layer_ids}"
assert all(0 <= i < num_nextn_predict_layers for i in local_id_set), (
f"mtp_local_layer_ids must be in [0, {num_nextn_predict_layers}): "
f"{mtp_local_layer_ids}"
)
# The draft KV pool and the sconv conv-state cache are still sized
# from the trunk's swa geometry; a head geometry that differs is not
# wired through yet.
assert (
mtp_swa_num_key_value_heads == swa_num_key_value_heads
and mtp_swa_head_dim == swa_head_dim
), (
"banded MTP head geometry must match the trunk swa geometry: "
f"kv_heads {mtp_swa_num_key_value_heads} vs "
f"{swa_num_key_value_heads}, head_dim {mtp_swa_head_dim} vs "
f"{swa_head_dim}"
)
if padded_vocab_size is None:
padded_vocab_size = vocab_size
vocab_size = (
unpadded_vocab_size
if (
unpadded_vocab_size is not None
and unpadded_vocab_size < padded_vocab_size
)
else vocab_size
)
self.vocab_size = vocab_size
self.padded_vocab_size = padded_vocab_size
self.hidden_size = hidden_size
self.intermediate_size = intermediate_size
self.dense_intermediate_size = dense_intermediate_size
self.num_hidden_layers = num_hidden_layers
self.num_attention_heads = num_attention_heads
self.num_key_value_heads = num_key_value_heads
self.head_dim = head_dim
self.v_head_dim = v_head_dim
self.d_rel = d_rel
self.rel_extent = rel_extent
self.local_layer_ids = local_layer_ids
self.sliding_window_size = sliding_window_size
self.swa_num_attention_heads = swa_num_attention_heads
self.swa_num_key_value_heads = swa_num_key_value_heads
self.swa_head_dim = swa_head_dim
self.swa_v_head_dim = swa_v_head_dim
self.mtp_local_layer_ids = mtp_local_layer_ids
self.mtp_local_extent = mtp_local_extent
self.mtp_swa_num_attention_heads = mtp_swa_num_attention_heads
self.mtp_swa_num_key_value_heads = mtp_swa_num_key_value_heads
self.mtp_swa_head_dim = mtp_swa_head_dim
self.rms_norm_eps = rms_norm_eps
self.hidden_act = hidden_act
self.q_bias = q_bias
self.o_bias = o_bias
self.use_embed_norm = use_embed_norm
self.use_sconv = use_sconv
self.sconv_kernel_size = sconv_kernel_size
self.chain_hidden_post_norm = chain_hidden_post_norm
self.dense_mlp_idx = dense_mlp_idx
self.n_routed_experts = n_routed_experts
self.num_experts = n_routed_experts
self.n_shared_experts = n_shared_experts
self.num_shared_experts = n_shared_experts
self.num_experts_per_tok = num_experts_per_tok
self.route_scale = route_scale
self.use_gate_bias = use_gate_bias
self.use_global_scale = use_global_scale
self.norm_after_topk = norm_after_topk
self.gate_activation = gate_activation
self.shared_expert_sink = shared_expert_sink
self.shared_experts_size = shared_experts_size
self.inference_moe_w13_interleaved = inference_moe_w13_interleaved
self.log_scaling_n_floor = log_scaling_n_floor
self.log_scaling_alpha = log_scaling_alpha
self.unpadded_vocab_size = self.vocab_size
self.logits_mup_width_multiplier = logits_mup_width_multiplier
self.final_logit_softcapping = final_logit_softcapping
self.num_nextn_predict_layers = num_nextn_predict_layers
super().__init__(
tie_word_embeddings=tie_word_embeddings,
**kwargs,
)
@property
def conv_layer_ids(self) -> list[int]:
return list(range(self.num_hidden_layers))
@property
def linear_layer_ids(self) -> list[int]:
return self.conv_layer_ids
@property
def full_attention_layer_ids(self) -> list[int]:
return list(range(self.num_hidden_layers))
@property
def mamba_chunk_size(self) -> int:
# Floor at 64: mamba_cache_chunk_size = max(mamba_chunk_size, page_size),
# and a floor of 1 lets the radix tree adopt another request's KV at
# tiny shared prefixes, whose different kernel-rounding perturbs decode logits.
return 64
@property
def mamba2_cache_params(self) -> Optional[InklingConvCacheParams]:
from sglang.srt.runtime_context import get_parallel
try:
tp_size = get_parallel().attn_tp_size
except (AssertionError, RuntimeError):
tp_size = 1
def tp_local_kv_conv_dim(num_kv_heads: int, head_dim: int) -> int:
return max(1, num_kv_heads // tp_size) * head_dim
full_kv_conv_dim = tp_local_kv_conv_dim(self.num_key_value_heads, self.head_dim)
local_kv_conv_dim = tp_local_kv_conv_dim(
self.swa_num_key_value_heads, self.swa_head_dim
)
stream_dim = self.hidden_size
from sglang.srt.runtime_context import get_server_args
if get_server_args().enable_scattered_sconv:
# Scattered sconv: the attn/mlp output sconvs run on the [T, H/P]
# hidden shard, so their conv-state caches shard with them.
assert (
self.hidden_size % tp_size == 0
), f"hidden_size {self.hidden_size} not divisible by attn tp {tp_size}"
stream_dim = self.hidden_size // tp_size
conv_len = self.sconv_kernel_size - 1
shape = InklingConvStateShape(
conv=[
(conv_len, full_kv_conv_dim),
(conv_len, full_kv_conv_dim),
(conv_len, local_kv_conv_dim),
(conv_len, local_kv_conv_dim),
(conv_len, stream_dim),
(conv_len, stream_dim),
],
temporal=(0, 0, 0),
)
dtype = InklingStateDType(conv=torch.bfloat16, temporal=torch.bfloat16)
return InklingConvCacheParams(
shape=shape, layers=self.conv_layer_ids, dtype=dtype
)
class InklingAudioConfig(PretrainedConfig):
model_type = "inkling_audio_model"
def __init__(
self,
*,
decoder_dmodel: Optional[int] = None,
n_mel_bins: int = 80,
mel_vocab_size: int = 16,
dmel_min_value: float = -1.5,
dmel_max_value: float = 2.0,
use_audio_norm: bool = False,
audio_mode: Literal["dmel", "flow"] = "dmel",
**kwargs: Any,
) -> None:
self.decoder_dmodel = decoder_dmodel
self.n_mel_bins = n_mel_bins
self.mel_vocab_size = mel_vocab_size
self.dmel_min_value = dmel_min_value
self.dmel_max_value = dmel_max_value
self.use_audio_norm = use_audio_norm
self.audio_mode = audio_mode
super().__init__(**kwargs)
class InklingVisionConfig(PretrainedConfig):
model_type = "inkling_vision_model"
def __init__(
self,
*,
vision_encoder_type: Literal["linear", "hmlp"] = "hmlp",
decoder_dmodel: Optional[int] = None,
patch_size: int = 16,
temporal_patch_size: int = 1,
n_channels: int = 3,
n_layers: int = 1,
use_vision_norm: bool = False,
**kwargs: Any,
) -> None:
self.vision_encoder_type = vision_encoder_type
self.decoder_dmodel = decoder_dmodel
self.patch_size = patch_size
self.temporal_patch_size = temporal_patch_size
self.n_channels = n_channels
self.n_layers = n_layers
self.use_vision_norm = use_vision_norm
super().__init__(**kwargs)
class InklingMMConfig(PretrainedConfig):
model_type = "inkling_mm_model"
keys_to_ignore_at_inference = ["past_key_values"]
sub_configs = {
"text_config": InklingModelConfig,
"audio_config": InklingAudioConfig,
"vision_config": InklingVisionConfig,
}
def __init__(
self,
*,
text_config: Optional[dict[str, Any] | InklingModelConfig] = None,
audio_config: Optional[dict[str, Any] | InklingAudioConfig] = None,
vision_config: Optional[dict[str, Any] | InklingVisionConfig] = None,
mtp_config: Optional[dict[str, Any]] = None,
tie_word_embeddings: bool = False,
**kwargs: Any,
) -> None:
self.mtp_config = mtp_config
self.text_config = (
text_config
if isinstance(text_config, InklingModelConfig)
else InklingModelConfig(**(text_config or {}))
)
if isinstance(mtp_config, dict) and mtp_config.get("local_layer_ids"):
# Banded MTP head: the checkpoint declares its sliding-window draft
# depths on mtp_config. Canonicalize onto text_config so every
# consumer (hybrid layer-id split, draft pool routing, the MTP block
# construction) reads one source of truth.
self.text_config.mtp_local_layer_ids = list(mtp_config["local_layer_ids"])
if mtp_config.get("local_extent") is not None:
self.text_config.mtp_local_extent = mtp_config["local_extent"]
self.audio_config = (
audio_config
if isinstance(audio_config, InklingAudioConfig)
else InklingAudioConfig(**(audio_config or {}))
)
self.vision_config = (
vision_config
if isinstance(vision_config, InklingVisionConfig)
else InklingVisionConfig(**(vision_config or {}))
)
super().__init__(
tie_word_embeddings=tie_word_embeddings,
**kwargs,
)
def get_text_config(self, *args: Any, **kwargs: Any) -> InklingModelConfig:
return self.text_config
@property
def vocab_size(self) -> int:
return self.text_config.vocab_size
@property
def hidden_size(self) -> int:
return self.text_config.hidden_size
@property
def num_hidden_layers(self) -> int:
return self.text_config.num_hidden_layers
@property
def num_attention_heads(self) -> int:
return self.text_config.num_attention_heads
@property
def num_key_value_heads(self) -> int:
return self.text_config.num_key_value_heads
@property
def head_dim(self) -> int:
return self.text_config.head_dim
@property
def full_attention_layer_ids(self) -> list[int]:
return self.text_config.full_attention_layer_ids
@property
def linear_layer_ids(self) -> list[int]:
return self.text_config.linear_layer_ids
@property
def conv_layer_ids(self) -> list[int]:
return self.text_config.conv_layer_ids
@property
def mamba_chunk_size(self) -> int:
return self.text_config.mamba_chunk_size
@property
def mamba2_cache_params(self) -> Optional[InklingConvCacheParams]:
return self.text_config.mamba2_cache_params
@dataclass(kw_only=True, frozen=True)
class InklingConvStateShape:
conv: list[tuple[int, int]]
temporal: tuple[int, int, int]
# Conv tuples read (K-1, dim) — the overlapping dedup view would alias
# along the dim axis, so the dedup conv-intermediate layout must stay off.
disable_conv_window_dedup: bool = True
@dataclass(kw_only=True, frozen=True)
class InklingStateDType:
conv: torch.dtype = torch.bfloat16
temporal: torch.dtype = torch.bfloat16
@dataclass(kw_only=True, frozen=True)
class InklingConvCacheParams(BaseLinearStateParams):
dtype: InklingStateDType = field(default_factory=InklingStateDType)
shape: InklingConvStateShape
for _model_type, _config_cls in {
"inkling_model": InklingModelConfig,
"inkling_audio_model": InklingAudioConfig,
"inkling_vision_model": InklingVisionConfig,
"inkling_mm_model": InklingMMConfig,
}.items():
try:
CONFIG_MAPPING.register(_model_type, _config_cls)
except Exception:
CONFIG_MAPPING._extra_content[_model_type] = _config_cls
+8
View File
@@ -137,6 +137,10 @@ class Mamba2StateShape:
conv: list[tuple[int, int]]
temporal: tuple[int, int, int]
# Conv tuples read (dim, K-1) — the window axis is last, which the
# deduplicated conv-intermediate layout requires.
disable_conv_window_dedup: bool = False
intermediate_size: int
conv_dim: int
ssm_state_size: int
@@ -217,6 +221,10 @@ class KimiLinearStateShape:
conv: List[tuple[int, int]]
temporal: tuple[int, int, int]
# Conv tuples read (K-1, dim) — the overlapping dedup view would alias
# along the dim axis, so the dedup conv-intermediate layout must stay off.
disable_conv_window_dedup: bool = True
num_heads: int
head_dim: int
num_k_heads: int

Some files were not shown because too many files have changed in this diff Show More