#include "attn_decode_split_kv.cuh" #include "attn_entry_utils.cuh" #ifndef ASTRAI_NO_MMA #include "attn_decode_split_kv_mma.cuh" #endif // Decode has only batch*kv_head independent tasks; without split-K the grid is // tiny (e.g. 16 blocks) and leaves most SMs idle. Pick the smallest split count // that fills the device (~2 blocks/SM), capped by the tile count, min work per // split (at least 8 tiles), and 32. static int decode_num_splits(const AttentionParams& p, int tiles_total) { int sm_count = 0; cudaDeviceGetAttribute(&sm_count, cudaDevAttrMultiProcessorCount, 0); int base_blocks = p.kv_head * p.batch; int desired = 2 * (sm_count > 0 ? sm_count : 64); int n = (desired + base_blocks - 1) / base_blocks; int max_by_work = tiles_total / 8; return std::max(1, std::min({n, tiles_total, 32, max_by_work})); } // Scalar fallback: one warp per query head, split-KV across grid.z. static void launch_scalar_decode(AttentionParams& p) { int group_size = p.q_head / p.kv_head; int chunks_total = (p.kv_len + DC_CHUNK - 1) / DC_CHUNK; p.num_splits = decode_num_splits(p, chunks_total); auto fopt = torch::TensorOptions().dtype(torch::kFloat32).device(torch::kCUDA); auto o_part = torch::empty({p.batch, p.q_head, p.num_splits, p.head_dim}, fopt); auto ml_part = torch::empty({p.batch, p.q_head, p.num_splits, 2}, fopt); p.o_part = o_part.data_ptr(); p.ml_part = ml_part.data_ptr(); size_t smem = DC_CHUNK * p.head_dim * sizeof(bf16); attn_decode_split_kv_kernel<<>>(p); attn_decode_combine_kernel<<>>(p); } #ifndef ASTRAI_NO_MMA // Tensor-core head-packing requires 1 < G <= 16 (the MMA M dim) and no mask. static bool decode_use_mma(const AttentionParams& p) { int G = p.q_head / p.kv_head; return !p.use_mask && G > 1 && G <= 16; } template static void launch_mma_decode(AttentionParams& p) { int tiles_total = (p.kv_len + BC - 1) / BC; p.num_splits = decode_num_splits(p, tiles_total); auto fopt = torch::TensorOptions().dtype(torch::kFloat32).device(torch::kCUDA); auto o_part = torch::empty({p.batch, p.q_head, p.num_splits, p.head_dim}, fopt); auto ml_part = torch::empty({p.batch, p.q_head, p.num_splits, 2}, fopt); p.o_part = o_part.data_ptr(); p.ml_part = ml_part.data_ptr(); attn_decode_split_kv_mma_kernel <<>>(p); attn_decode_combine_kernel<<>>(p); } #endif template static void dispatch_decode(AttentionParams& p) { #ifndef ASTRAI_NO_MMA if (decode_use_mma(p)) { launch_mma_decode(p); return; } #endif launch_scalar_decode(p); } torch::Tensor attn_decode( torch::Tensor q, torch::Tensor k, torch::Tensor v, c10::optional mask, bool is_causal = false, int64_t causal_offset = 0, c10::optional scale = c10::nullopt ) { AttentionParams p; attn_pack_params(q, k, v, mask, is_causal, causal_offset, scale, p); TORCH_CHECK(p.q_len == 1, "Q seq_len must be 1"); TORCH_CHECK(p.head_dim % 32 == 0, "head_dim must be multiple of 32"); auto O = torch::empty_like(q); p.o = (bf16*)O.data_ptr(); switch (p.head_dim) { case 32: dispatch_decode<32>(p); break; case 64: dispatch_decode<64>(p); break; case 128: dispatch_decode<128>(p); break; case 256: dispatch_decode<256>(p); break; default: TORCH_CHECK(false, "decode: unsupported head_dim ", p.head_dim, " (supported: 32, 64, 128, 256)"); } return O; } PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { m.def("attn_decode", &attn_decode, py::arg("q"), py::arg("k"), py::arg("v"), py::arg("mask") = py::none(), py::arg("is_causal") = false, py::arg("causal_offset") = 0, py::arg("scale") = py::none(), "GQA decode (tensor-core head-packing on sm_80+, scalar fallback)"); }