refactor: replace magic layout ints with TensorLayout enum

- Add TensorLayout enum (C++ + Python) to replace magic layout ints
- Add C10_CUDA_CHECK post-launch error checking to all kernel entries
- Add CUDAGuard + freqs_cis shape validation to rotary_emb.cu
- Cache SM count to eliminate per-call cudaDeviceGetAttribute
- Add DISPATCH_CAUSAL_MASK macro to deduplicate dispatcher if/else
- Convert mask type hints from X|None to Optional[X]
This commit is contained in:
2026-08-01 11:05:52 +08:00
parent 3639b50b4a
commit 7feeb0b93e
9 changed files with 91 additions and 59 deletions
+2
View File
@@ -25,6 +25,7 @@ from astrai.extension.attention_backend import (
get_backend, get_backend,
) )
from astrai.extension.attention_ops import ( from astrai.extension.attention_ops import (
TensorLayout,
attn_decode, attn_decode,
attn_paged_decode, attn_paged_decode,
attn_prefill, attn_prefill,
@@ -37,6 +38,7 @@ __all__ = [
"AttentionBackend", "AttentionBackend",
"CudaBackend", "CudaBackend",
"TorchNativeBackend", "TorchNativeBackend",
"TensorLayout",
"attention", "attention",
"attn_backend", "attn_backend",
"get_backend", "get_backend",
+19 -6
View File
@@ -12,11 +12,24 @@ Interface (all functions):
mask: 2D [batch, kv_len] or 3D [batch, q_len, kv_len] (bool, True=keep) mask: 2D [batch, kv_len] or 3D [batch, q_len, kv_len] (bool, True=keep)
""" """
import enum
from typing import Optional
import torch import torch
from astrai.extension.loader import _available, _modules from astrai.extension.loader import _available, _modules
class TensorLayout(enum.IntEnum):
"""Q/K/V tensor layout, mirrors the C++ ``TensorLayout`` enum in ``attn_common.h``.
Kernels internally operate on BHLD; BLHD inputs are transposed at entry.
"""
BHLD = 0 # [batch, n_heads, seq_len, head_dim]
BLHD = 1 # [batch, seq_len, n_heads, head_dim]
def _check_available(name: str): def _check_available(name: str):
if not _available.get(name): if not _available.get(name):
raise RuntimeError( raise RuntimeError(
@@ -29,7 +42,7 @@ def attn_decode(
q: torch.Tensor, q: torch.Tensor,
k: torch.Tensor, k: torch.Tensor,
v: torch.Tensor, v: torch.Tensor,
mask: torch.Tensor | None = None, mask: Optional[torch.Tensor] = None,
is_causal: bool = False, is_causal: bool = False,
) -> torch.Tensor: ) -> torch.Tensor:
"""GQA decode attention (q_len == 1). """GQA decode attention (q_len == 1).
@@ -47,7 +60,7 @@ def attn_decode(
_check_available("attn_decode") _check_available("attn_decode")
causal_offset = (k.size(1) - 1) if is_causal else -1 causal_offset = (k.size(1) - 1) if is_causal else -1
return _modules["attn_decode"].attn_decode( return _modules["attn_decode"].attn_decode(
q, k, v, mask=mask, causal_offset=causal_offset, layout=1 q, k, v, mask=mask, causal_offset=causal_offset, layout=TensorLayout.BLHD
) )
@@ -55,7 +68,7 @@ def attn_prefill(
q: torch.Tensor, q: torch.Tensor,
k: torch.Tensor, k: torch.Tensor,
v: torch.Tensor, v: torch.Tensor,
mask: torch.Tensor | None = None, mask: Optional[torch.Tensor] = None,
is_causal: bool = False, is_causal: bool = False,
) -> torch.Tensor: ) -> torch.Tensor:
"""GQA prefill attention (q_len > 1). """GQA prefill attention (q_len > 1).
@@ -73,7 +86,7 @@ def attn_prefill(
_check_available("attn_prefill") _check_available("attn_prefill")
causal_offset = (k.size(1) - q.size(1)) if is_causal else -1 causal_offset = (k.size(1) - q.size(1)) if is_causal else -1
return _modules["attn_prefill"].attn_prefill( return _modules["attn_prefill"].attn_prefill(
q, k, v, mask=mask, causal_offset=causal_offset, layout=1 q, k, v, mask=mask, causal_offset=causal_offset, layout=TensorLayout.BLHD
) )
@@ -84,7 +97,7 @@ def attn_paged_decode(
v_cache: torch.Tensor, v_cache: torch.Tensor,
page_size: int, page_size: int,
kv_len: int, kv_len: int,
mask: torch.Tensor | None = None, mask: Optional[torch.Tensor] = None,
is_causal: bool = False, is_causal: bool = False,
) -> torch.Tensor: ) -> torch.Tensor:
"""Paged GQA decode attention (q_len == 1, direct page-table access). """Paged GQA decode attention (q_len == 1, direct page-table access).
@@ -113,5 +126,5 @@ def attn_paged_decode(
kv_len, kv_len,
mask=mask, mask=mask,
causal_offset=causal_offset, causal_offset=causal_offset,
layout=1, layout=TensorLayout.BLHD,
) )
+8
View File
@@ -1,5 +1,13 @@
#pragma once #pragma once
// Tensor layout for Q/K/V tensors passed to attention kernels.
// Internally, kernels always operate on BHLD [batch, n_heads, seq_len, head_dim].
// When the caller passes BLHD, dims 1 and 2 are transposed at entry.
enum TensorLayout : int {
BHLD = 0, // [batch, n_heads, seq_len, head_dim]
BLHD = 1, // [batch, seq_len, n_heads, head_dim]
};
template<typename T, typename AT = float> template<typename T, typename AT = float>
struct AttentionParams { struct AttentionParams {
+3 -2
View File
@@ -16,11 +16,12 @@ torch::Tensor attn_decode(
TORCH_CHECK(p.head_dim % 32 == 0, "head_dim must be multiple of 32"); TORCH_CHECK(p.head_dim % 32 == 0, "head_dim must be multiple of 32");
auto O = torch::empty_strided(q.sizes(), q.strides(), q.options()); auto O = torch::empty_strided(q.sizes(), q.strides(), q.options());
auto O_view = (layout == 1) ? O.transpose(1, 2) : O; auto O_view = (layout == BLHD) ? O.transpose(1, 2) : O;
p.o = (bf16*)O_view.data_ptr(); p.o = (bf16*)O_view.data_ptr();
alloc_split_partials(p); alloc_split_partials(p);
DISPATCH_HEAD_DIM(p.head_dim, dispatch_decode, p); DISPATCH_HEAD_DIM(p.head_dim, dispatch_decode, p);
C10_CUDA_CHECK(cudaGetLastError());
return O; return O;
} }
@@ -32,6 +33,6 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
py::arg("mask") = py::none(), py::arg("mask") = py::none(),
py::arg("causal_offset") = -1, py::arg("causal_offset") = -1,
py::arg("scale") = 0.0, py::arg("scale") = 0.0,
py::arg("layout") = 0, py::arg("layout") = (int64_t)BHLD,
"GQA decode (tensor-core head-packing on sm_80+, scalar fallback)"); "GQA decode (tensor-core head-packing on sm_80+, scalar fallback)");
} }
+41 -45
View File
@@ -14,18 +14,50 @@
#include "attn_paged_decode_split_kv_mma.cuh" #include "attn_paged_decode_split_kv_mma.cuh"
#endif #endif
// Cached SM count — cudaDeviceGetAttribute is a host-side call that was
// invoked on every decode/paged-decode launch. Cache per-device so multi-GPU
// setups with heterogeneous GPUs still get the right count, while the common
// single-GPU path hits the cache after the first call.
inline int get_sm_count() {
int dev = 0;
cudaGetDevice(&dev);
static int cached_dev = -1;
static int cached_count = 0;
if (dev != cached_dev) {
cudaDeviceGetAttribute(&cached_count, cudaDevAttrMultiProcessorCount, dev);
cached_dev = dev;
}
return cached_count;
}
// Split-KV: compute number of splits to fill all SMs for small-batch decode. // Split-KV: compute number of splits to fill all SMs for small-batch decode.
// Caps splits so each split processes at least `min_tiles_per_split` tiles, // Caps splits so each split processes at least `min_tiles_per_split` tiles,
// avoiding excessive loop/prologue overhead when tiles are small. // avoiding excessive loop/prologue overhead when tiles are small.
inline int compute_num_splits(int base_blocks, int tiles_total, inline int compute_num_splits(int base_blocks, int tiles_total,
int min_tiles_per_split = 1) { int min_tiles_per_split = 1) {
int sm_count = 0; int sm_count = get_sm_count();
cudaDeviceGetAttribute(&sm_count, cudaDevAttrMultiProcessorCount, 0);
int n = (2 * sm_count + base_blocks - 1) / base_blocks; int n = (2 * sm_count + base_blocks - 1) / base_blocks;
int max_by_work = tiles_total / min_tiles_per_split; int max_by_work = tiles_total / min_tiles_per_split;
return std::max(1, std::min(n, std::min(max_by_work, MAX_SPLITS))); return std::max(1, std::min(n, std::min(max_by_work, MAX_SPLITS)));
} }
// Dispatch IsCausal × HasMask — eliminates the duplicated 4-way if/else
// ladder that appeared in each dispatch_* function. FN must be a function
// template <int HEAD_DIM, bool IsCausal, bool HasMask>; HEAD_DIM is forwarded
// as the first template argument so callers only spell it once.
//
// Usage: DISPATCH_CAUSAL_MASK(is_causal, has_mask, launch_decode_mma, HEAD_DIM, p, group_size);
#define DISPATCH_CAUSAL_MASK(is_causal, has_mask, FN, HEAD_DIM, ...) \
do { \
if (is_causal) { \
if (has_mask) FN<HEAD_DIM, true, true>(__VA_ARGS__); \
else FN<HEAD_DIM, true, false>(__VA_ARGS__); \
} else { \
if (has_mask) FN<HEAD_DIM, false, true>(__VA_ARGS__); \
else FN<HEAD_DIM, false, false>(__VA_ARGS__); \
} \
} while (0)
// ====================================================================== // ======================================================================
// Prefill // Prefill
// ====================================================================== // ======================================================================
@@ -56,21 +88,9 @@ static inline void dispatch_prefill(AttentionParams<bf16>& p) {
bool has_mask = (p.use_mask && p.mask); bool has_mask = (p.use_mask && p.mask);
#ifndef ASTRAI_NO_MMA #ifndef ASTRAI_NO_MMA
if (is_causal) { DISPATCH_CAUSAL_MASK(is_causal, has_mask, launch_prefill_mma, HEAD_DIM, p);
if (has_mask) launch_prefill_mma<HEAD_DIM, true, true>(p);
else launch_prefill_mma<HEAD_DIM, true, false>(p);
} else {
if (has_mask) launch_prefill_mma<HEAD_DIM, false, true>(p);
else launch_prefill_mma<HEAD_DIM, false, false>(p);
}
#else #else
if (is_causal) { DISPATCH_CAUSAL_MASK(is_causal, has_mask, launch_prefill_scalar, HEAD_DIM, p);
if (has_mask) launch_prefill_scalar<HEAD_DIM, true, true>(p);
else launch_prefill_scalar<HEAD_DIM, true, false>(p);
} else {
if (has_mask) launch_prefill_scalar<HEAD_DIM, false, true>(p);
else launch_prefill_scalar<HEAD_DIM, false, false>(p);
}
#endif #endif
} }
@@ -116,21 +136,9 @@ static inline void dispatch_decode(AttentionParams<bf16>& p) {
int group_size = p.q_head / p.kv_head; int group_size = p.q_head / p.kv_head;
#ifndef ASTRAI_NO_MMA #ifndef ASTRAI_NO_MMA
if (is_causal) { DISPATCH_CAUSAL_MASK(is_causal, has_mask, launch_decode_mma, HEAD_DIM, p, group_size);
if (has_mask) launch_decode_mma<HEAD_DIM, true, true>(p, group_size);
else launch_decode_mma<HEAD_DIM, true, false>(p, group_size);
} else {
if (has_mask) launch_decode_mma<HEAD_DIM, false, true>(p, group_size);
else launch_decode_mma<HEAD_DIM, false, false>(p, group_size);
}
#else #else
if (is_causal) { DISPATCH_CAUSAL_MASK(is_causal, has_mask, launch_decode_scalar, HEAD_DIM, p, group_size);
if (has_mask) launch_decode_scalar<HEAD_DIM, true, true>(p, group_size);
else launch_decode_scalar<HEAD_DIM, true, false>(p, group_size);
} else {
if (has_mask) launch_decode_scalar<HEAD_DIM, false, true>(p, group_size);
else launch_decode_scalar<HEAD_DIM, false, false>(p, group_size);
}
#endif #endif
attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p); attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p);
@@ -174,21 +182,9 @@ static inline void dispatch_paged_decode(PagedAttentionParams<bf16>& p) {
int group_size = p.q_head / p.kv_head; int group_size = p.q_head / p.kv_head;
#ifndef ASTRAI_NO_MMA #ifndef ASTRAI_NO_MMA
if (is_causal) { DISPATCH_CAUSAL_MASK(is_causal, has_mask, launch_paged_decode_mma, HEAD_DIM, p, group_size);
if (has_mask) launch_paged_decode_mma<HEAD_DIM, true, true>(p, group_size);
else launch_paged_decode_mma<HEAD_DIM, true, false>(p, group_size);
} else {
if (has_mask) launch_paged_decode_mma<HEAD_DIM, false, true>(p, group_size);
else launch_paged_decode_mma<HEAD_DIM, false, false>(p, group_size);
}
#else #else
if (is_causal) { DISPATCH_CAUSAL_MASK(is_causal, has_mask, launch_paged_decode_scalar, HEAD_DIM, p, group_size);
if (has_mask) launch_paged_decode_scalar<HEAD_DIM, true, true>(p, group_size);
else launch_paged_decode_scalar<HEAD_DIM, true, false>(p, group_size);
} else {
if (has_mask) launch_paged_decode_scalar<HEAD_DIM, false, true>(p, group_size);
else launch_paged_decode_scalar<HEAD_DIM, false, false>(p, group_size);
}
#endif #endif
paged_attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p); paged_attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p);
+2 -2
View File
@@ -37,7 +37,7 @@ inline void alloc_split_partials(P& p) {
// ---- Shared Q-dims + strides extraction ---- // ---- Shared Q-dims + strides extraction ----
template <typename P> template <typename P>
inline void extract_q_dims_and_strides(torch::Tensor& q, int64_t layout, P& p) { inline void extract_q_dims_and_strides(torch::Tensor& q, int64_t layout, P& p) {
if (layout == 1) q = q.transpose(1, 2); if (layout == BLHD) q = q.transpose(1, 2);
p.batch = (int)q.size(0); p.batch = (int)q.size(0);
p.q_head = (int)q.size(1); p.q_head = (int)q.size(1);
p.q_len = (int)q.size(2); p.q_len = (int)q.size(2);
@@ -109,7 +109,7 @@ inline void attn_pack_params(
extract_q_dims_and_strides(q, layout, p); extract_q_dims_and_strides(q, layout, p);
if (layout == 1) k = k.transpose(1, 2), v = v.transpose(1, 2); if (layout == BLHD) k = k.transpose(1, 2), v = v.transpose(1, 2);
p.kv_head = (int)k.size(1); p.kv_head = (int)k.size(1);
p.kv_len = (int)k.size(2); p.kv_len = (int)k.size(2);
+3 -2
View File
@@ -18,11 +18,12 @@ torch::Tensor attn_paged_decode(
page_size, kv_len, mask, causal_offset, scale, layout, p); page_size, kv_len, mask, causal_offset, scale, layout, p);
auto O = torch::empty_strided(q.sizes(), q.strides(), q.options()); auto O = torch::empty_strided(q.sizes(), q.strides(), q.options());
auto O_view = (layout == 1) ? O.transpose(1, 2) : O; auto O_view = (layout == BLHD) ? O.transpose(1, 2) : O;
p.o = (bf16*)O_view.data_ptr(); p.o = (bf16*)O_view.data_ptr();
alloc_split_partials(p); alloc_split_partials(p);
DISPATCH_HEAD_DIM(p.head_dim, dispatch_paged_decode, p); DISPATCH_HEAD_DIM(p.head_dim, dispatch_paged_decode, p);
C10_CUDA_CHECK(cudaGetLastError());
return O; return O;
} }
@@ -37,6 +38,6 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
py::arg("mask") = py::none(), py::arg("mask") = py::none(),
py::arg("causal_offset") = -1, py::arg("causal_offset") = -1,
py::arg("scale") = 0.0, py::arg("scale") = 0.0,
py::arg("layout") = 0, py::arg("layout") = (int64_t)BHLD,
"Paged GQA decode — split-KV with direct page-table access."); "Paged GQA decode — split-KV with direct page-table access.");
} }
+3 -2
View File
@@ -15,10 +15,11 @@ torch::Tensor attn_prefill(
TORCH_CHECK(p.head_dim % 16 == 0, "head_dim must be multiple of 16"); TORCH_CHECK(p.head_dim % 16 == 0, "head_dim must be multiple of 16");
auto O = torch::empty_strided(q.sizes(), q.strides(), q.options()); auto O = torch::empty_strided(q.sizes(), q.strides(), q.options());
auto O_view = (layout == 1) ? O.transpose(1, 2) : O; auto O_view = (layout == BLHD) ? O.transpose(1, 2) : O;
p.o = (bf16*)O_view.data_ptr(); p.o = (bf16*)O_view.data_ptr();
DISPATCH_HEAD_DIM(p.head_dim, dispatch_prefill, p); DISPATCH_HEAD_DIM(p.head_dim, dispatch_prefill, p);
C10_CUDA_CHECK(cudaGetLastError());
return O; return O;
} }
@@ -30,6 +31,6 @@ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
py::arg("mask") = py::none(), py::arg("mask") = py::none(),
py::arg("causal_offset") = -1, py::arg("causal_offset") = -1,
py::arg("scale") = 0.0, py::arg("scale") = 0.0,
py::arg("layout") = 0, py::arg("layout") = (int64_t)BHLD,
"GQA prefill (tensor-core mma on sm_80+, scalar fallback)"); "GQA prefill (tensor-core mma on sm_80+, scalar fallback)");
} }
+10
View File
@@ -1,4 +1,6 @@
#include <torch/extension.h> #include <torch/extension.h>
#include <c10/cuda/CUDAGuard.h>
#include <c10/cuda/CUDAException.h>
#include <cuda_bf16.h> #include <cuda_bf16.h>
__global__ void rotary_emb_kernel( __global__ void rotary_emb_kernel(
@@ -46,6 +48,8 @@ torch::Tensor rotary_emb(
torch::Tensor x, torch::Tensor x,
torch::Tensor freqs_cis torch::Tensor freqs_cis
) { ) {
const at::cuda::OptionalCUDAGuard device_guard(device_of(x));
TORCH_CHECK(x.is_cuda(), "x must be on CUDA"); TORCH_CHECK(x.is_cuda(), "x must be on CUDA");
TORCH_CHECK(freqs_cis.is_cuda(), "freqs_cis must be on CUDA"); TORCH_CHECK(freqs_cis.is_cuda(), "freqs_cis must be on CUDA");
TORCH_CHECK(x.scalar_type() == torch::kBFloat16, "x must be bf16"); TORCH_CHECK(x.scalar_type() == torch::kBFloat16, "x must be bf16");
@@ -53,6 +57,7 @@ torch::Tensor rotary_emb(
TORCH_CHECK(x.is_contiguous(), "x must be contiguous"); TORCH_CHECK(x.is_contiguous(), "x must be contiguous");
TORCH_CHECK(freqs_cis.dim() == 4, "freqs_cis must be 4D [batch, seq_len, dim/2, 2]"); TORCH_CHECK(freqs_cis.dim() == 4, "freqs_cis must be 4D [batch, seq_len, dim/2, 2]");
TORCH_CHECK(freqs_cis.is_contiguous(), "freqs_cis must be contiguous"); TORCH_CHECK(freqs_cis.is_contiguous(), "freqs_cis must be contiguous");
TORCH_CHECK(freqs_cis.scalar_type() == torch::kFloat32, "freqs_cis must be f32");
int batch = x.size(0); int batch = x.size(0);
int seq_len = x.size(1); int seq_len = x.size(1);
@@ -60,6 +65,10 @@ torch::Tensor rotary_emb(
int head_dim = x.size(3); int head_dim = x.size(3);
TORCH_CHECK(head_dim % 2 == 0, "head_dim must be even"); TORCH_CHECK(head_dim % 2 == 0, "head_dim must be even");
TORCH_CHECK(freqs_cis.size(0) == batch, "freqs_cis batch mismatch");
TORCH_CHECK(freqs_cis.size(1) == seq_len, "freqs_cis seq_len mismatch");
TORCH_CHECK(freqs_cis.size(2) == head_dim / 2, "freqs_cis dim/2 mismatch");
TORCH_CHECK(freqs_cis.size(3) == 2, "freqs_cis last dim must be 2 [cos, sin]");
auto out = torch::empty_like(x); auto out = torch::empty_like(x);
@@ -74,6 +83,7 @@ torch::Tensor rotary_emb(
reinterpret_cast<__nv_bfloat16*>(out.data_ptr()), reinterpret_cast<__nv_bfloat16*>(out.data_ptr()),
batch, seq_len, n_heads, head_dim batch, seq_len, n_heads, head_dim
); );
C10_CUDA_CHECK(cudaGetLastError());
return out; return out;
} }