""" Copyright (c) 2025 by SpargeAttn 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. """ import importlib import torch from spas_sage_attn.utils import hyperparameter_check, get_block_map_meansim_fuse_quant, get_vanilla_qk_quant, block_map_lut_triton from spas_sage_attn.triton_kernel_example import spas_sage_attn_meansim as spas_sage_attn_meansim_triton from einops import rearrange def _has_exports(module, names): return all(hasattr(module, name) for name in names) def _load_qattn_kernel(compile_module, direct_module, attr_name, op_namespace, required_exports): try: compile_kernel = getattr(importlib.import_module(compile_module), attr_name) if _has_exports(compile_kernel, required_exports): return compile_kernel except ModuleNotFoundError as exc: if exc.name != compile_module: raise direct_kernel = importlib.import_module(direct_module) op_kernel = getattr(torch.ops, op_namespace) if _has_exports(op_kernel, required_exports): return op_kernel if _has_exports(direct_kernel, required_exports): return direct_kernel raise AttributeError(f"{direct_module} does not expose required kernels: {', '.join(required_exports)}") def _load_fused_kernel(): direct_kernel = importlib.import_module("spas_sage_attn._fused") op_kernel = torch.ops.spas_sage_attn_fused required_exports = ("transpose_pad_permute_cuda", "scale_fuse_quant_cuda") if _has_exports(op_kernel, required_exports): return op_kernel if _has_exports(direct_kernel, required_exports): return direct_kernel raise AttributeError(f"spas_sage_attn._fused does not expose required kernels: {', '.join(required_exports)}") try: _qattn_sm80 = _load_qattn_kernel( "spas_sage_attn.sm80_compile", "spas_sage_attn._qattn_sm80", "_qattn_sm80", "spas_sage_attn_qattn_sm80", ("qk_int8_sv_f16_accum_f16_block_sparse_attn_inst_buf_with_pv_threshold",), ) SM80_ENABLED = True except: SM80_ENABLED = False try: _qattn_sm89 = _load_qattn_kernel( "spas_sage_attn.sm89_compile", "spas_sage_attn._qattn_sm89", "_qattn_sm89", "spas_sage_attn_qattn_sm89", ( "qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold", "qk_int8_sv_f8_accum_f16_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold", ), ) SM89_ENABLED = True except: SM89_ENABLED = False try: _qattn_sm90 = _load_qattn_kernel( "spas_sage_attn.sm90_compile", "spas_sage_attn._qattn_sm90", "_qattn_sm90", "spas_sage_attn_qattn_sm90", ("qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold_sm90",), ) SM90_ENABLED = True except: SM90_ENABLED = False _fused = _load_fused_kernel() def get_cuda_version(): version = torch.version.cuda major, minor = version.split('.') return int(major), int(minor) def get_cuda_arch_versions(): cuda_archs = [] for i in range(torch.cuda.device_count()): major, minor = torch.cuda.get_device_capability(i) cuda_archs.append(f"sm{major}{minor}") return cuda_archs # Currently get_cuda_arch_versions cannot be traced by torch.compile _cuda_archs = get_cuda_arch_versions() def _unpack_qkv(q, k=None, v=None): if k is None and v is None and isinstance(q, list): qkv_list = q q, k, v = qkv_list qkv_list.clear() return q, k, v def _unpack_tensor(x): if isinstance(x, list): x_list = x x = x_list[0] x_list.clear() return x def _contiguous_to_dtype(x, dtype): x = _unpack_tensor(x) if x.is_contiguous() and x.dtype == dtype: return x if x.dtype == dtype: return x.contiguous() out = torch.empty_like(x, dtype=dtype, memory_format=torch.contiguous_format) out.copy_(x) return out def _quantize_v_fp8(v, arch): v = _unpack_tensor(v) b, h_kv, kv_len, head_dim = v.shape padded_len = (kv_len + 127) // 128 * 128 v_transposed_permuted = torch.empty((b, h_kv, head_dim, padded_len), dtype=v.dtype, device=v.device) _fused.transpose_pad_permute_cuda(v, v_transposed_permuted, 1) del v v_fp8 = torch.empty(v_transposed_permuted.shape, dtype=torch.float8_e4m3fn, device=v_transposed_permuted.device) v_scale = torch.empty((b, h_kv, head_dim), dtype=torch.float32, device=v_transposed_permuted.device) scale_max = 2.25 if arch in {"sm89", "sm100", "sm120", "sm121"} else 448.0 _fused.scale_fuse_quant_cuda(v_transposed_permuted, v_fp8, v_scale, kv_len, scale_max, 1) del v_transposed_permuted return v_fp8, v_scale def spas_sage_attn_meansim(q, k=None, v=None, *args, **kwargs): q, k, v = _unpack_qkv(q, k, v) arch = _cuda_archs[q.device.index] if arch in {"sm70", "sm75"}: return spas_sage_attn_meansim_triton(q, k, v, *args, **kwargs) else: return spas_sage2_attn_meansim_cuda(q, k, v, *args, **kwargs) def spas_sage2_attn_meansim_cuda(q, k=None, v=None, attn_mask=None, dropout_p=0.0, is_causal=False, scale=None, smooth_k=True, simthreshd1=0.6, cdfthreshd=0.98, pvthreshd=50, attention_sink=False, tensor_layout="HND", output_dtype=torch.float16, return_sparsity=False, recycle_q=False): q, k, v = _unpack_qkv(q, k, v) assert tensor_layout in ['HND', 'NHD'] assert q.size(-2)>=128, "seq_len should be not less than 128." q_size = q.size() q_device = q.device q_dtype = q.dtype q_heads = q.size(-3) if smooth_k: km = k.mean(dim=-2, keepdim=True) else: km = None headdim = q.size(-1) arch = _cuda_archs[q.device.index] if arch == "sm90": lut, valid_block_num, q_int8, q_scale, k_int8, k_scale = get_block_map_meansim_fuse_quant(q, k, km, is_causal=is_causal, simthreshd1=simthreshd1, cdfthreshd=cdfthreshd, return_lut=True, attention_sink=attention_sink, BLKQ=64, BLKK=128) else: lut, valid_block_num, q_int8, q_scale, k_int8, k_scale = get_block_map_meansim_fuse_quant(q, k, km, is_causal=is_causal, simthreshd1=simthreshd1, cdfthreshd=cdfthreshd, return_lut=True, attention_sink=attention_sink, BLKQ=128, BLKK=64) del k, km if scale is None: scale = 1.0 / (headdim ** 0.5) assert headdim in [64, 128], "headdim should be in [64, 128]. For other headdim, you can use padding and specify the softmax scale." pvthreshd = hyperparameter_check(pvthreshd, q_heads, q_device) if arch in {"sm89", "sm90", "sm100", "sm120", "sm121"}: v_ref = [v] v = None v_fp8, v_scale = _quantize_v_fp8(v_ref, arch) _is_causal = 1 if is_causal else 0 if recycle_q: o = q else: del q o = torch.empty(q_size, dtype=q_dtype, device=q_device) if arch in {"sm80", "sm86", "sm87"}: assert SM80_ENABLED _qattn_sm80.qk_int8_sv_f16_accum_f16_block_sparse_attn_inst_buf_with_pv_threshold(q_int8, k_int8, v, o, lut, valid_block_num, pvthreshd, q_scale, k_scale, 1, _is_causal, 1, scale, 0) del v elif arch in {"sm89", "sm100", "sm120", "sm121"}: assert SM89_ENABLED if get_cuda_version() < (12, 8): f = _qattn_sm89.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold else: f = _qattn_sm89.qk_int8_sv_f8_accum_f16_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold f(q_int8, k_int8, v_fp8, o, lut, valid_block_num, pvthreshd, q_scale, k_scale, v_scale, 1, _is_causal, 1, scale, 0) del v_fp8, v_scale elif arch == "sm90": assert SM90_ENABLED _qattn_sm90.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold_sm90(q_int8, k_int8, v_fp8, o, lut, valid_block_num, pvthreshd, q_scale, k_scale, v_scale, 1, _is_causal, 1, scale, 0) del v_fp8, v_scale else: raise ValueError(f"Unsupported CUDA architecture: {arch}") del q_int8, k_int8, q_scale, k_scale if tensor_layout == 'NHD': o = rearrange(o, '... H L D -> ... L H D') if return_sparsity: if is_causal is False: qk_sparsity = 1 - (valid_block_num.float().sum()) / (lut.size(3) * lut.size(2) * lut.size(0) * lut.size(1)) else: qk_sparsity = 1 - (valid_block_num.float().sum()) / ((lut.size(3) + 2) // 2 * lut.size(2) * lut.size(0) * lut.size(1)) del lut, valid_block_num return o, qk_sparsity.item() else: del lut, valid_block_num return o def spas_sage2_attn_meansim_topk_cuda(q, k=None, v=None, attn_mask=None, dropout_p=0.0, is_causal=False, scale=None, smooth_k=True, simthreshd1=-0.1, cdfthreshd=None, topk=0.5, pvthreshd=50, attention_sink=False, tensor_layout="HND", output_dtype=torch.float16, return_sparsity=False, recycle_q=False): q, k, v = _unpack_qkv(q, k, v) assert tensor_layout in ['HND', 'NHD'] assert q.size(-2)>=128, "seq_len should be not less than 128." q_size = q.size() q_device = q.device q_dtype = q.dtype q_heads = q.size(-3) if smooth_k: km = k.mean(dim=-2, keepdim=True) else: km = None headdim = q.size(-1) arch = _cuda_archs[q.device.index] if arch == "sm90": lut, valid_block_num, q_int8, q_scale, k_int8, k_scale = get_block_map_meansim_fuse_quant(q, k, km, is_causal=is_causal, simthreshd1=simthreshd1, cdfthreshd=cdfthreshd, topk=topk, return_lut=True, attention_sink=attention_sink, BLKQ=64, BLKK=128) else: lut, valid_block_num, q_int8, q_scale, k_int8, k_scale = get_block_map_meansim_fuse_quant(q, k, km, is_causal=is_causal, simthreshd1=simthreshd1, cdfthreshd=cdfthreshd, topk=topk, return_lut=True, attention_sink=attention_sink, BLKQ=128, BLKK=64) del k, km if scale is None: scale = 1.0 / (headdim ** 0.5) assert headdim in [64, 128], "headdim should be in [64, 128]. For other headdim, you can use padding and specify the softmax scale." pvthreshd = hyperparameter_check(pvthreshd, q_heads, q_device) if arch in {"sm89", "sm90", "sm100", "sm120", "sm121"}: v_ref = [v] v = None v_fp8, v_scale = _quantize_v_fp8(v_ref, arch) _is_causal = 1 if is_causal else 0 if recycle_q: o = q else: del q o = torch.empty(q_size, dtype=q_dtype, device=q_device) if arch in {"sm80", "sm86", "sm87"}: assert SM80_ENABLED _qattn_sm80.qk_int8_sv_f16_accum_f16_block_sparse_attn_inst_buf_with_pv_threshold(q_int8, k_int8, v, o, lut, valid_block_num, pvthreshd, q_scale, k_scale, 1, _is_causal, 1, scale, 0) del v elif arch in {"sm89", "sm100", "sm120", "sm121"}: assert SM89_ENABLED if get_cuda_version() < (12, 8): f = _qattn_sm89.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold else: f = _qattn_sm89.qk_int8_sv_f8_accum_f16_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold f(q_int8, k_int8, v_fp8, o, lut, valid_block_num, pvthreshd, q_scale, k_scale, v_scale, 1, _is_causal, 1, scale, 0) del v_fp8, v_scale elif arch == "sm90": assert SM90_ENABLED _qattn_sm90.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold_sm90(q_int8, k_int8, v_fp8, o, lut, valid_block_num, pvthreshd, q_scale, k_scale, v_scale, 1, _is_causal, 1, scale, 0) del v_fp8, v_scale else: raise ValueError(f"Unsupported CUDA architecture: {arch}") del q_int8, k_int8, q_scale, k_scale if tensor_layout == 'NHD': o = rearrange(o, '... H L D -> ... L H D') if return_sparsity: if is_causal is False: qk_sparsity = 1 - (valid_block_num.float().sum()) / (lut.size(3) * lut.size(2) * lut.size(0) * lut.size(1)) else: qk_sparsity = 1 - (valid_block_num.float().sum()) / ((lut.size(3) + 2) // 2 * lut.size(2) * lut.size(0) * lut.size(1)) del lut, valid_block_num return o, qk_sparsity.item() else: del lut, valid_block_num return o def block_sparse_attn_cuda(q, k=None, v=None, mask_id=None, dropout_p=0.0, scale=None, smooth_k=True, pvthreshd=50, attention_sink=False, tensor_layout="HND", output_dtype=torch.float16, return_sparsity=False, recycle_q=False): q, k, v = _unpack_qkv(q, k, v) mask_id = _unpack_tensor(mask_id) assert tensor_layout in ['HND', 'NHD'] if tensor_layout == 'NHD': q = rearrange(q, '... L H D -> ... H L D') k = rearrange(k, '... L H D -> ... H L D') v = rearrange(v, '... L H D -> ... H L D') arch = _cuda_archs[q.device.index] v_ref = [v] v = None v = _contiguous_to_dtype(v_ref, torch.float16) if arch in {"sm89", "sm90", "sm100", "sm120", "sm121"}: v_ref = [v] v = None v_fp8, v_scale = _quantize_v_fp8(v_ref, arch) qk_dtype = torch.float16 if q.dtype in (torch.float32, torch.float16) else torch.bfloat16 q_ref = [q] q = None q = _contiguous_to_dtype(q_ref, qk_dtype) k_ref = [k] k = None k = _contiguous_to_dtype(k_ref, qk_dtype) assert q.size(-2)>=128, "seq_len should be not less than 128." q_size = q.size() q_device = q.device q_dtype = q.dtype q_heads = q.size(-3) if smooth_k: km = k.mean(dim=-2, keepdim=True) else: km = None headdim = q.size(-1) if arch == "sm90": q_int8, q_scale, k_int8, k_scale = get_vanilla_qk_quant(q, k, km, 64, 128) else: q_int8, q_scale, k_int8, k_scale = get_vanilla_qk_quant(q, k, km, 128, 64) del k, km lut, valid_block_num = block_map_lut_triton(block_map=mask_id) del mask_id if scale is None: scale = 1.0 / (headdim ** 0.5) assert headdim in [64, 128], "headdim should be in [64, 128]. For other headdim, you can use padding and specify the softmax scale." pvthreshd = hyperparameter_check(pvthreshd, q_heads, q_device) if recycle_q: o = q else: del q o = torch.empty(q_size, dtype=q_dtype, device=q_device) if arch in {"sm80", "sm86", "sm87"}: assert SM80_ENABLED _qattn_sm80.qk_int8_sv_f16_accum_f16_block_sparse_attn_inst_buf_with_pv_threshold(q_int8, k_int8, v, o, lut, valid_block_num, pvthreshd, q_scale, k_scale, 1, False, 1, scale, 0) del v elif arch in {"sm89", "sm100", "sm120", "sm121"}: assert SM89_ENABLED if get_cuda_version() < (12, 8): f = _qattn_sm89.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold else: f = _qattn_sm89.qk_int8_sv_f8_accum_f16_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold f(q_int8, k_int8, v_fp8, o, lut, valid_block_num, pvthreshd, q_scale, k_scale, v_scale, 1, False, 1, scale, 0) del v_fp8, v_scale elif arch == "sm90": assert SM90_ENABLED _qattn_sm90.qk_int8_sv_f8_accum_f32_block_sparse_attn_inst_buf_fuse_v_scale_with_pv_threshold_sm90(q_int8, k_int8, v_fp8, o, lut, valid_block_num, pvthreshd, q_scale, k_scale, v_scale, 1, False, 1, scale, 0) del v_fp8, v_scale else: raise ValueError(f"Unsupported CUDA architecture: {arch}") del q_int8, k_int8, q_scale, k_scale if tensor_layout == 'NHD': o = rearrange(o, '... H L D -> ... L H D') if return_sparsity: qk_sparsity = 1 - (valid_block_num.float().sum()) / (lut.size(3) * lut.size(2) * lut.size(0) * lut.size(1)) del lut, valid_block_num return o, qk_sparsity.item() else: del lut, valid_block_num return o # For compatibility. spas_sage2_attn_meansim_cuda and spas_sage2_attn_meansim_topk_cuda already support sm80/86/89/90/120 spas_sage_attn_meansim_cuda = spas_sage2_attn_meansim_cuda spas_sage_attn_meansim_topk_cuda = spas_sage2_attn_meansim_topk_cuda