mirror of
https://github.com/ROCm/composable_kernel.git
synced 2026-05-04 13:41:24 +00:00
[CK_TILE] Add SageAttention v2 forward kernel with multi-granularity quantization (#6574) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add a CK_TILE forward kernel implementing [SageAttention v2](https://arxiv.org/abs/2411.10958) — an attention algorithm that applies multi-granularity quantization to Q/K/V before computing attention, trading minimal accuracy loss for higher throughput on low-precision hardware. ### Quantization design | Tensor | Supported data types | Scale granularity options | |--------|---------------------|--------------------------| | Q | fp8 / int8 / int4 | per-tensor, per-block (128 tokens), per-warp (32 tokens), per-thread (4 tokens) | | K | fp8 / int8 / int4 | per-tensor, per-block (128 tokens), per-warp (64 tokens), per-thread (16 tokens) | | V | fp8 | per-channel (always) | | O | bf16 | — | Three precision combinations are supported: `fp8/bf16` (QKV fp8, O bf16), `i8/fp8/bf16` (QK int8, V fp8, O bf16), and `i4/fp8/bf16` (QK int4, V fp8, O bf16). ### Architecture support - **gfx9** (CDNA2/3, e.g. gfx90a, gfx942) — full tile set - **gfx950** (CDNA4) — restricted tile set (N-per-block capped at 64 for fp8-family dtypes) ### Implementation - Two pipeline variants: `QRKSVS` (synchronous) and `QRKSVS_ASYNC` (async copy) - Masking support: no mask, causal (top-left / bottom-right), and generic windowed - Batch and group (variable-length) modes - Head dimension: d=128, d_v=128 - Python codegen under `example/ck_tile/49_sageattention/codegen/` generates kernel instances per target/dtype/tile combination - Smoke tests included via `tile_example_sageattn_fwd` ### Test commands \`\`\`bash # fp8 QKV ./build/bin/tile_example_sageattn_fwd -v=1 -b=16 -h=8 -s=1024 -d=128 -kname=1 -prec=fp8bf16 -qscale=3 -init=3 # int8 QK, fp8 V ./build/bin/tile_example_sageattn_fwd -v=1 -b=16 -h=8 -s=1024 -d=128 -kname=1 -prec=i8fp8bf16 -qscale=3 -init=3 \`\`\` \`-qscale\` values: 1=per-tensor, 2=per-block, 3=per-warp, 4=per-thread
174 lines
4.7 KiB
Python
174 lines
4.7 KiB
Python
# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
# generate kernel instances to speed up compilation
|
|
|
|
import argparse
|
|
from enum import IntEnum
|
|
from pathlib import Path
|
|
import pkgutil
|
|
from typing import List, Optional
|
|
|
|
import codegen.ops
|
|
from codegen.cmake_config import GEN_DIR
|
|
|
|
|
|
class HandlerId(IntEnum):
|
|
LIST_BLOBS = 0
|
|
WRITE_BLOBS = 1
|
|
|
|
|
|
# inspect all modules under 'codegen.ops' and register API handlers
|
|
ops = []
|
|
for importer, module_name, _ in pkgutil.iter_modules(codegen.ops.__path__):
|
|
full_module_name = "%s.%s" % (codegen.ops.__name__, module_name)
|
|
ops.append(importer.find_spec(module_name).loader.load_module(module_name))
|
|
# Strip "sageattn_" so module sageattn_fwd registers as CLI key "fwd".
|
|
unwanted_prefix = "sageattn_"
|
|
handlers = dict(
|
|
[
|
|
(
|
|
(
|
|
op.__name__[len(unwanted_prefix) :]
|
|
if op.__name__.startswith(unwanted_prefix)
|
|
else op.__name__
|
|
),
|
|
(op.list_blobs, op.write_blobs),
|
|
)
|
|
for op in ops
|
|
]
|
|
)
|
|
assert 0 < len(handlers)
|
|
|
|
|
|
def write_blobs(
|
|
targets: List[str],
|
|
output_dir: Optional[str],
|
|
api_list: List[str],
|
|
filters_list: List[str],
|
|
optdim_list: List[int],
|
|
receipt,
|
|
mask_impl,
|
|
) -> None:
|
|
if output_dir is None:
|
|
output_dir = Path(__file__).parent
|
|
else:
|
|
output_dir = Path(output_dir) / GEN_DIR
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
for api, kernel_filter in zip(api_list, filters_list):
|
|
handler = handlers[api][HandlerId.WRITE_BLOBS]
|
|
handler(targets, output_dir, kernel_filter, receipt, optdim_list, mask_impl)
|
|
|
|
|
|
# list all the files that will be generated
|
|
def list_blobs(
|
|
targets: List[str],
|
|
output_file: Optional[str],
|
|
api_list: List[str],
|
|
filters_list: List[str],
|
|
optdim_list: List[int],
|
|
receipt,
|
|
mask_impl,
|
|
) -> None:
|
|
assert output_file is not None
|
|
file_path = Path(output_file)
|
|
|
|
# create an empty file / drop its contents if it exists
|
|
open(file_path, "w").close()
|
|
|
|
for api, kernel_filter in zip(api_list, filters_list):
|
|
handler = handlers[api][HandlerId.LIST_BLOBS]
|
|
handler(targets, file_path, kernel_filter, receipt, optdim_list, mask_impl)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
prog="generate",
|
|
description="Generate SageAttention CK_tile kernel/API blobs.",
|
|
)
|
|
parser.add_argument(
|
|
"--targets",
|
|
default="gfx9,gfx950",
|
|
required=False,
|
|
help="list of GPU targets, separated by comma.",
|
|
)
|
|
parser.add_argument(
|
|
"-a",
|
|
"--api",
|
|
default="fwd",
|
|
required=False,
|
|
help="Codegen API key(s), comma-separated (e.g. fwd -> module codegen.ops.sageattn_fwd).",
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--output_dir",
|
|
required=False,
|
|
help="write all the blobs into a directory",
|
|
)
|
|
parser.add_argument(
|
|
"-l", "--list_blobs", required=False, help="list all the kernels to a file"
|
|
)
|
|
# TODO: if using filter, must apply same value to output_dir and list_blobs
|
|
parser.add_argument(
|
|
"-f",
|
|
"--filter",
|
|
default="",
|
|
required=False,
|
|
help="filter out kernels that need to generate, using fnmatch module",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-m",
|
|
"--mask",
|
|
default="simplified",
|
|
required=False,
|
|
help="mask implementation, simplified/generic",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-r",
|
|
"--receipt",
|
|
default=0,
|
|
required=False,
|
|
help="Codegen receipt index. SageAttention forward currently uses receipt 0 only; "
|
|
"the value is passed through to ops (see get_product in sageattn_fwd.py).",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--optdim",
|
|
default="-1",
|
|
required=False,
|
|
help="only optimize the hdim in the list. separated by comma. -1 is the default choice. "
|
|
"e.g. --optdim=32,64,128,256",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
targets = args.targets.split(",")
|
|
api_list = args.api.split(",")
|
|
filter_list = args.filter.split(",")
|
|
filter_list.extend([""] * (len(api_list) - len(filter_list)))
|
|
optdim_list = [int(hdim) for hdim in args.optdim.split(",")]
|
|
|
|
if args.list_blobs is not None:
|
|
list_blobs(
|
|
targets,
|
|
args.list_blobs,
|
|
api_list,
|
|
filter_list,
|
|
optdim_list,
|
|
int(args.receipt),
|
|
mask_impl=args.mask,
|
|
)
|
|
else:
|
|
write_blobs(
|
|
targets,
|
|
args.output_dir,
|
|
api_list,
|
|
filter_list,
|
|
optdim_list,
|
|
int(args.receipt),
|
|
mask_impl=args.mask,
|
|
)
|