feat: support HEAD_DIM=32 and split extension into loader/ops
- add case 32 to decode/prefill dispatch switch - fix swiz_col out-of-bounds for HEAD_DIM=32: XOR mask now limited to chunk count (3 for 32, 7 for >=64) instead of always 7, which produced column offsets >= LD=32 and corrupted shared memory - restructure decode dispatch to #ifndef/#else/#endif matching prefill - split astrai/extension/__init__.py into loader.py (kernel .so discovery) and ops.py (wrapper functions + torch SDPA fallback); __init__.py now re-exports the public API
This commit is contained in:
parent
9ebaea840f
commit
fd65b9bc23
|
|
@ -1,91 +1,19 @@
|
||||||
import importlib
|
"""CUDA attention kernel wrappers with torch fallback.
|
||||||
import logging
|
|
||||||
|
|
||||||
import torch
|
Public API:
|
||||||
import torch.nn.functional as F
|
- ``gqa_decode_attn`` — single-query decode attention
|
||||||
|
- ``gqa_prefill_attn`` — multi-query prefill attention
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
Each wrapper dispatches to its compiled CUDA kernel (``astrai.extension.gqa_*``)
|
||||||
|
when available, otherwise falls back to ``torch.nn.functional.scaled_dot_product_attention``.
|
||||||
|
"""
|
||||||
|
|
||||||
_available: dict[str, bool] = {}
|
from astrai.extension.loader import KERNEL_NAMES, is_available
|
||||||
_modules: dict[str, object] = {}
|
from astrai.extension.ops import gqa_decode_attn, gqa_prefill_attn
|
||||||
|
|
||||||
for _name in ["gqa_decode_attn", "gqa_prefill_attn"]:
|
__all__ = [
|
||||||
try:
|
"gqa_decode_attn",
|
||||||
_mod = importlib.import_module(f".{_name}", package=__package__)
|
"gqa_prefill_attn",
|
||||||
_available[_name] = True
|
"is_available",
|
||||||
_modules[_name] = _mod
|
"KERNEL_NAMES",
|
||||||
except ImportError:
|
]
|
||||||
_available[_name] = False
|
|
||||||
_modules[_name] = None
|
|
||||||
|
|
||||||
|
|
||||||
def _expand_kv_heads(
|
|
||||||
k: torch.Tensor, v: torch.Tensor, q_head: int
|
|
||||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
|
||||||
"""Expand K/V heads to match Q heads for GQA fallback."""
|
|
||||||
kv_head = k.size(1)
|
|
||||||
if kv_head == q_head:
|
|
||||||
return k, v
|
|
||||||
group = q_head // kv_head
|
|
||||||
k = k.repeat_interleave(group, dim=1)
|
|
||||||
v = v.repeat_interleave(group, dim=1)
|
|
||||||
return k, v
|
|
||||||
|
|
||||||
|
|
||||||
def _torch_fallback(
|
|
||||||
q: torch.Tensor,
|
|
||||||
k: torch.Tensor,
|
|
||||||
v: torch.Tensor,
|
|
||||||
mask: torch.Tensor | None,
|
|
||||||
is_causal: bool,
|
|
||||||
scale: float | None,
|
|
||||||
) -> torch.Tensor:
|
|
||||||
k, v = _expand_kv_heads(k, v, q.size(1))
|
|
||||||
attn_mask = mask[:, None, None, :] if mask is not None else None
|
|
||||||
return F.scaled_dot_product_attention(
|
|
||||||
q, k, v, attn_mask=attn_mask, is_causal=is_causal and mask is None, scale=scale
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def gqa_decode_attn(
|
|
||||||
q: torch.Tensor,
|
|
||||||
k: torch.Tensor,
|
|
||||||
v: torch.Tensor,
|
|
||||||
mask: torch.Tensor | None = None,
|
|
||||||
is_causal: bool = False,
|
|
||||||
causal_offset: int = 0,
|
|
||||||
scale: float | None = None,
|
|
||||||
) -> torch.Tensor:
|
|
||||||
if _available["gqa_decode_attn"]:
|
|
||||||
return _modules["gqa_decode_attn"].gqa_decode_attn(
|
|
||||||
q,
|
|
||||||
k,
|
|
||||||
v,
|
|
||||||
mask=mask,
|
|
||||||
is_causal=is_causal,
|
|
||||||
causal_offset=causal_offset,
|
|
||||||
scale=scale,
|
|
||||||
)
|
|
||||||
return _torch_fallback(q, k, v, mask, is_causal, scale)
|
|
||||||
|
|
||||||
|
|
||||||
def gqa_prefill_attn(
|
|
||||||
q: torch.Tensor,
|
|
||||||
k: torch.Tensor,
|
|
||||||
v: torch.Tensor,
|
|
||||||
mask: torch.Tensor | None = None,
|
|
||||||
is_causal: bool = False,
|
|
||||||
causal_offset: int = 0,
|
|
||||||
scale: float | None = None,
|
|
||||||
) -> torch.Tensor:
|
|
||||||
if _available["gqa_prefill_attn"]:
|
|
||||||
return _modules["gqa_prefill_attn"].gqa_prefill_attn(
|
|
||||||
q,
|
|
||||||
k,
|
|
||||||
v,
|
|
||||||
mask=mask,
|
|
||||||
is_causal=is_causal,
|
|
||||||
causal_offset=causal_offset,
|
|
||||||
scale=scale,
|
|
||||||
)
|
|
||||||
return _torch_fallback(q, k, v, mask, is_causal, scale)
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""Dynamic discovery and loading of compiled CUDA kernel modules.
|
||||||
|
|
||||||
|
Each kernel is registered in ``csrc/build.py`` and built into a ``.so`` placed
|
||||||
|
in this package directory. On import we try to load each one; kernels that
|
||||||
|
failed to build (or are running on a CPU-only machine) are marked unavailable
|
||||||
|
so the wrapper functions can fall back to ``torch`` SDPA.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
KERNEL_NAMES = ["gqa_decode_attn", "gqa_prefill_attn"]
|
||||||
|
|
||||||
|
_available: dict[str, bool] = {}
|
||||||
|
_modules: dict[str, object] = {}
|
||||||
|
|
||||||
|
for _name in KERNEL_NAMES:
|
||||||
|
try:
|
||||||
|
_mod = importlib.import_module(f".{_name}", package=__package__)
|
||||||
|
_available[_name] = True
|
||||||
|
_modules[_name] = _mod
|
||||||
|
except ImportError:
|
||||||
|
_available[_name] = False
|
||||||
|
_modules[_name] = None
|
||||||
|
|
||||||
|
|
||||||
|
def is_available(name: str) -> bool:
|
||||||
|
"""Return ``True`` if the compiled kernel ``name`` was loaded."""
|
||||||
|
return _available.get(name, False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_module(name: str) -> object:
|
||||||
|
"""Return the loaded kernel module for ``name``, or ``None`` if unavailable."""
|
||||||
|
return _modules.get(name)
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
"""GQA attention wrapper functions — one entry point per compiled kernel.
|
||||||
|
|
||||||
|
Each wrapper dispatches to its CUDA kernel (loaded in ``loader.py``) when
|
||||||
|
available, otherwise falls back to ``torch`` SDPA.
|
||||||
|
|
||||||
|
Add new kernel wrappers here; split into per-variant files only if this file
|
||||||
|
grows large.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
|
||||||
|
from astrai.extension.loader import _available, _modules
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_kv_heads(
|
||||||
|
k: torch.Tensor, v: torch.Tensor, q_head: int
|
||||||
|
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||||
|
"""Expand K/V heads to match Q heads for GQA fallback."""
|
||||||
|
kv_head = k.size(1)
|
||||||
|
if kv_head == q_head:
|
||||||
|
return k, v
|
||||||
|
group = q_head // kv_head
|
||||||
|
k = k.repeat_interleave(group, dim=1)
|
||||||
|
v = v.repeat_interleave(group, dim=1)
|
||||||
|
return k, v
|
||||||
|
|
||||||
|
|
||||||
|
def _torch_fallback(
|
||||||
|
q: torch.Tensor,
|
||||||
|
k: torch.Tensor,
|
||||||
|
v: torch.Tensor,
|
||||||
|
mask: torch.Tensor | None,
|
||||||
|
is_causal: bool,
|
||||||
|
scale: float | None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""Reference attention via ``scaled_dot_product_attention``."""
|
||||||
|
k, v = _expand_kv_heads(k, v, q.size(1))
|
||||||
|
attn_mask = mask[:, None, None, :] if mask is not None else None
|
||||||
|
return F.scaled_dot_product_attention(
|
||||||
|
q, k, v, attn_mask=attn_mask, is_causal=is_causal and mask is None, scale=scale
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gqa_decode_attn(
|
||||||
|
q: torch.Tensor,
|
||||||
|
k: torch.Tensor,
|
||||||
|
v: torch.Tensor,
|
||||||
|
mask: torch.Tensor | None = None,
|
||||||
|
is_causal: bool = False,
|
||||||
|
causal_offset: int = 0,
|
||||||
|
scale: float | None = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
if _available["gqa_decode_attn"]:
|
||||||
|
return _modules["gqa_decode_attn"].gqa_decode_attn(
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
mask=mask,
|
||||||
|
is_causal=is_causal,
|
||||||
|
causal_offset=causal_offset,
|
||||||
|
scale=scale,
|
||||||
|
)
|
||||||
|
return _torch_fallback(q, k, v, mask, is_causal, scale)
|
||||||
|
|
||||||
|
|
||||||
|
def gqa_prefill_attn(
|
||||||
|
q: torch.Tensor,
|
||||||
|
k: torch.Tensor,
|
||||||
|
v: torch.Tensor,
|
||||||
|
mask: torch.Tensor | None = None,
|
||||||
|
is_causal: bool = False,
|
||||||
|
causal_offset: int = 0,
|
||||||
|
scale: float | None = None,
|
||||||
|
) -> torch.Tensor:
|
||||||
|
if _available["gqa_prefill_attn"]:
|
||||||
|
return _modules["gqa_prefill_attn"].gqa_prefill_attn(
|
||||||
|
q,
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
mask=mask,
|
||||||
|
is_causal=is_causal,
|
||||||
|
causal_offset=causal_offset,
|
||||||
|
scale=scale,
|
||||||
|
)
|
||||||
|
return _torch_fallback(q, k, v, mask, is_causal, scale)
|
||||||
|
|
@ -21,13 +21,20 @@ static void dispatch_decode(GQAParams& p) {
|
||||||
gqa_decode_attn_mma_kernel<HEAD_DIM, BC><<<grid, block, smem>>>(p);
|
gqa_decode_attn_mma_kernel<HEAD_DIM, BC><<<grid, block, smem>>>(p);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
// scalar fallback (per-KV-head, one warp per query head)
|
// scalar fallback (per-KV-head, one warp per query head)
|
||||||
int group_size = p.q_head / p.kv_head;
|
int group_size = p.q_head / p.kv_head;
|
||||||
size_t smem = DC_CHUNK * p.head_dim * sizeof(bf16);
|
size_t smem = DC_CHUNK * p.head_dim * sizeof(bf16);
|
||||||
dim3 block(32, group_size);
|
dim3 block(32, group_size);
|
||||||
dim3 grid(p.batch * p.kv_head);
|
dim3 grid(p.batch * p.kv_head);
|
||||||
gqa_decode_attn_kernel<<<grid, block, smem>>>(p);
|
gqa_decode_attn_kernel<<<grid, block, smem>>>(p);
|
||||||
|
#else
|
||||||
|
// scalar fallback (per-KV-head, one warp per query head)
|
||||||
|
int group_size = p.q_head / p.kv_head;
|
||||||
|
size_t smem = DC_CHUNK * p.head_dim * sizeof(bf16);
|
||||||
|
dim3 block(32, group_size);
|
||||||
|
dim3 grid(p.batch * p.kv_head);
|
||||||
|
gqa_decode_attn_kernel<<<grid, block, smem>>>(p);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
torch::Tensor gqa_decode_attn(
|
torch::Tensor gqa_decode_attn(
|
||||||
|
|
@ -74,6 +81,9 @@ torch::Tensor gqa_decode_attn(
|
||||||
p.o = (bf16*)O.data_ptr();
|
p.o = (bf16*)O.data_ptr();
|
||||||
|
|
||||||
switch (p.head_dim) {
|
switch (p.head_dim) {
|
||||||
|
case 32:
|
||||||
|
dispatch_decode<32>(p);
|
||||||
|
break;
|
||||||
case 64:
|
case 64:
|
||||||
dispatch_decode<64>(p);
|
dispatch_decode<64>(p);
|
||||||
break;
|
break;
|
||||||
|
|
@ -85,7 +95,7 @@ torch::Tensor gqa_decode_attn(
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
TORCH_CHECK(false, "decode: unsupported head_dim ", p.head_dim,
|
TORCH_CHECK(false, "decode: unsupported head_dim ", p.head_dim,
|
||||||
" (supported: 64, 128, 256)");
|
" (supported: 32, 64, 128, 256)");
|
||||||
}
|
}
|
||||||
return O;
|
return O;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,10 +58,12 @@ __device__ __forceinline__ void ldmatrix_x2_trans(unsigned* r, const bf16* p) {
|
||||||
|
|
||||||
// XOR swizzle for shared-memory column at 8-bf16 chunk granularity.
|
// XOR swizzle for shared-memory column at 8-bf16 chunk granularity.
|
||||||
// Eliminates ldmatrix bank conflicts without LD padding: consecutive rows
|
// Eliminates ldmatrix bank conflicts without LD padding: consecutive rows
|
||||||
// land in distinct bank groups. swiz_col(d, r) = ((d>>3)^(r&7))<<3 | (d&7).
|
// land in distinct bank groups. swiz_col(d, r, mask) = ((d>>3)^(r&mask))<<3 | (d&7).
|
||||||
// Works for any d; aligned (d%8==0) simplifies to d ^ ((r&7)<<3).
|
// mask must cover log2(HEAD_DIM/8) chunk bits but stay within LD: use 7 for
|
||||||
__device__ __forceinline__ int swiz_col(int d, int r) {
|
// HEAD_DIM>=64 (8+ chunks), 3 for HEAD_DIM=32 (4 chunks). Default 7 keeps
|
||||||
return ((d >> 3) ^ (r & 7)) << 3 | (d & 7);
|
// existing HEAD_DIM>=64 call sites working unchanged.
|
||||||
|
__device__ __forceinline__ int swiz_col(int d, int r, int mask = 7) {
|
||||||
|
return ((d >> 3) ^ (r & mask)) << 3 | (d & 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
// cp.async: copy 16 bytes (8 bf16) from global to shared memory directly,
|
// cp.async: copy 16 bytes (8 bf16) from global to shared memory directly,
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,9 @@ torch::Tensor gqa_prefill_attn(
|
||||||
p.o = (bf16*)O.data_ptr();
|
p.o = (bf16*)O.data_ptr();
|
||||||
|
|
||||||
switch (p.head_dim) {
|
switch (p.head_dim) {
|
||||||
|
case 32:
|
||||||
|
dispatch_prefill<32>(p);
|
||||||
|
break;
|
||||||
case 64:
|
case 64:
|
||||||
dispatch_prefill<64>(p);
|
dispatch_prefill<64>(p);
|
||||||
break;
|
break;
|
||||||
|
|
@ -79,7 +82,7 @@ torch::Tensor gqa_prefill_attn(
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
TORCH_CHECK(false, "prefill: unsupported head_dim ", p.head_dim,
|
TORCH_CHECK(false, "prefill: unsupported head_dim ", p.head_dim,
|
||||||
" (supported: 64,128,256)");
|
" (supported: 32,64,128,256)");
|
||||||
}
|
}
|
||||||
return O;
|
return O;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ __global__ void gqa_prefill_attn_mma_kernel(GQAParams p) {
|
||||||
constexpr int KT2 = BC / 16; // P k-tiles (K=16 each)
|
constexpr int KT2 = BC / 16; // P k-tiles (K=16 each)
|
||||||
constexpr int DN8 = HEAD_DIM / 8; // O n-tiles (N=8 each)
|
constexpr int DN8 = HEAD_DIM / 8; // O n-tiles (N=8 each)
|
||||||
constexpr int LD = HEAD_DIM; // XOR swizzle (swiz_col) handles bank conflicts
|
constexpr int LD = HEAD_DIM; // XOR swizzle (swiz_col) handles bank conflicts
|
||||||
|
constexpr int SWIZ_MASK = (HEAD_DIM >= 64) ? 7 : (HEAD_DIM / 8 - 1); // chunk bits, stay within LD
|
||||||
|
|
||||||
const int warp = threadIdx.x / 32;
|
const int warp = threadIdx.x / 32;
|
||||||
const int lane = threadIdx.x % 32;
|
const int lane = threadIdx.x % 32;
|
||||||
|
|
@ -61,12 +62,12 @@ __global__ void gqa_prefill_attn_mma_kernel(GQAParams p) {
|
||||||
int qr = qrow0 + r;
|
int qr = qrow0 + r;
|
||||||
bf16 qv = (qr < p.q_len) ? p.q[q_base + qr * HEAD_DIM + d]
|
bf16 qv = (qr < p.q_len) ? p.q[q_base + qr * HEAD_DIM + d]
|
||||||
: __float2bfloat16(0.0f);
|
: __float2bfloat16(0.0f);
|
||||||
sQ[r * LD + swiz_col(d, r)] = __hmul(qv, scale_bf16);
|
sQ[r * LD + swiz_col(d, r, SWIZ_MASK)] = __hmul(qv, scale_bf16);
|
||||||
}
|
}
|
||||||
__syncwarp();
|
__syncwarp();
|
||||||
#pragma unroll
|
#pragma unroll
|
||||||
for (int kt = 0; kt < KD; kt++)
|
for (int kt = 0; kt < KD; kt++)
|
||||||
ldmatrix_x4(Qa[kt], &sQ[qrow_l * LD + swiz_col(kt * 16 + qcol_l, qrow_l)]);
|
ldmatrix_x4(Qa[kt], &sQ[qrow_l * LD + swiz_col(kt * 16 + qcol_l, qrow_l, SWIZ_MASK)]);
|
||||||
}
|
}
|
||||||
__syncthreads(); // prevent next warp from overwriting sQ prematurely
|
__syncthreads(); // prevent next warp from overwriting sQ prematurely
|
||||||
}
|
}
|
||||||
|
|
@ -106,8 +107,8 @@ __global__ void gqa_prefill_attn_mma_kernel(GQAParams p) {
|
||||||
int r = i / HEAD_DIM;
|
int r = i / HEAD_DIM;
|
||||||
int d = i % HEAD_DIM;
|
int d = i % HEAD_DIM;
|
||||||
int kc = kv0 + r;
|
int kc = kv0 + r;
|
||||||
cp_async_16(&sK[r * LD + swiz_col(d, r)], &p.k[kv_base + kc * HEAD_DIM + d]);
|
cp_async_16(&sK[r * LD + swiz_col(d, r, SWIZ_MASK)], &p.k[kv_base + kc * HEAD_DIM + d]);
|
||||||
cp_async_16(&sV[r * LD + swiz_col(d, r)], &p.v[kv_base + kc * HEAD_DIM + d]);
|
cp_async_16(&sV[r * LD + swiz_col(d, r, SWIZ_MASK)], &p.v[kv_base + kc * HEAD_DIM + d]);
|
||||||
}
|
}
|
||||||
cp_async_commit();
|
cp_async_commit();
|
||||||
cp_async_wait_all();
|
cp_async_wait_all();
|
||||||
|
|
@ -116,9 +117,9 @@ __global__ void gqa_prefill_attn_mma_kernel(GQAParams p) {
|
||||||
int r = i / HEAD_DIM, d = i % HEAD_DIM;
|
int r = i / HEAD_DIM, d = i % HEAD_DIM;
|
||||||
int kc = kv0 + r;
|
int kc = kv0 + r;
|
||||||
bf16 z = __float2bfloat16(0.0f);
|
bf16 z = __float2bfloat16(0.0f);
|
||||||
sK[r * LD + swiz_col(d, r)] = (kc < p.kv_len)
|
sK[r * LD + swiz_col(d, r, SWIZ_MASK)] = (kc < p.kv_len)
|
||||||
? p.k[kv_base + kc * HEAD_DIM + d] : z;
|
? p.k[kv_base + kc * HEAD_DIM + d] : z;
|
||||||
sV[r * LD + swiz_col(d, r)] = (kc < p.kv_len)
|
sV[r * LD + swiz_col(d, r, SWIZ_MASK)] = (kc < p.kv_len)
|
||||||
? p.v[kv_base + kc * HEAD_DIM + d] : z;
|
? p.v[kv_base + kc * HEAD_DIM + d] : z;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +138,7 @@ __global__ void gqa_prefill_attn_mma_kernel(GQAParams p) {
|
||||||
#pragma unroll
|
#pragma unroll
|
||||||
for (int kt = 0; kt < KD; kt++) {
|
for (int kt = 0; kt < KD; kt++) {
|
||||||
unsigned b[2];
|
unsigned b[2];
|
||||||
ldmatrix_x2(b, &sK[krow_l * LD + swiz_col(kt * 16 + kcol_h, krow_l)]);
|
ldmatrix_x2(b, &sK[krow_l * LD + swiz_col(kt * 16 + kcol_h, krow_l, SWIZ_MASK)]);
|
||||||
mma16816(Sacc[n8], Qa[kt], b, Sacc[n8]);
|
mma16816(Sacc[n8], Qa[kt], b, Sacc[n8]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -218,7 +219,7 @@ __global__ void gqa_prefill_attn_mma_kernel(GQAParams p) {
|
||||||
#pragma unroll
|
#pragma unroll
|
||||||
for (int dn8 = 0; dn8 < DN8; dn8++) {
|
for (int dn8 = 0; dn8 < DN8; dn8++) {
|
||||||
unsigned b[2];
|
unsigned b[2];
|
||||||
ldmatrix_x2_trans(b, &sV[vrow_l * LD + swiz_col(dn8 * 8, vrow_l)]);
|
ldmatrix_x2_trans(b, &sV[vrow_l * LD + swiz_col(dn8 * 8, vrow_l, SWIZ_MASK)]);
|
||||||
mma16816(Oacc[dn8], Pa, b, Oacc[dn8]);
|
mma16816(Oacc[dn8], Pa, b, Oacc[dn8]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue