Files
TheRock/rocm-libraries/dnn-providers/hip-kernel-provider/rocke/library/dispatch/attention.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

678 lines
25 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""Attention / FMHA dispatcher family (path-level selection).
Worked implementation mirroring :mod:`rocke.dispatch.gemm.bf16_rcr`, backed by
:mod:`kernels.common.attention_unified` (the unified tiled FMHA
emitter).
SCOPE -- what this dispatcher decides
-------------------------------------
The load-bearing dispatch decision for unified attention is the *kernel path*:
the 2D-tiled (per-(kv_head, q_block) CTA) kernel vs the 3D split-KV kernel. That
decision is a PURE function of the problem
(:func:`rocke.helpers.attention.use_2d_kernel`, surfaced as
``UnifiedAttentionProblem.select_path``), so it can be mirrored byte-faithfully
on the C++ side. Backend coverage is gated by
:func:`attention_unified.supports_native_unified_attention` (head_size /
block_size / dtype / feature gate -- also pure).
The structural identity used for selection parity is therefore::
(path, head_size, block_size)
where ``path`` is ``"2d"`` or ``"3d"``.
DEFERRED -- arch-tuned block geometry
-------------------------------------
The 2D-tiled kernel's exact CTA geometry (``num_warps`` / ``block_m_per_warp`` /
``tile_size``) is chosen by heuristics in ``attention_unified`` that query the
running device arch (``_resolve_attention_arch``) and encode many measured,
shape-specific thresholds (see ``_select_2d_num_warps`` et al.). Those are
downstream PERFORMANCE-TUNING knobs, not a "which kernel family" decision, and
they are not reproducible CPU-only without a device. They are intentionally OUT
of the parity identity here; modelling them faithfully across C++/Python is a
separate, larger effort. This dispatcher selects the path + backend; geometry is
left to the instance builder at launch time.
"""
from __future__ import annotations
from dataclasses import asdict, dataclass
from typing import Sequence, Tuple
from rocke.core.arch import ArchTarget
from kernels.common.attention_unified import (
UnifiedAttentionProblem,
supports_native_unified_attention,
)
from rocke.dispatch.core import (
CandidateRegistry,
DispatchResult,
KernelCandidate,
KernelId,
OperatorRequest,
Ranker,
stable_json_hash,
)
_FAMILY = "attention_unified"
ATTENTION_ABI_VERSION = "hipkg-attention-unified/v1"
@dataclass(frozen=True)
class AttentionRequest(OperatorRequest):
"""Normalized scaled-dot-product-attention request."""
batch: int
nhead_q: int
nhead_k: int
seqlen_q: int
seqlen_k: int
hdim_q: int
hdim_v: int
arch: str
mask_type: int = 0 # 0=none, 1=causal/top-left, ...
use_sinks: bool = False
sliding_window: int = 0
kv_block_size: int = 16 # paged KV block_size (modulus); {16,32,64}
num_sms: int = 120
op: str = "attention"
dtype: str = "fp16"
algorithm: str = "auto"
spec_id: str = "auto"
# --- gfx950 attention_dense knobs (only consumed by the opt-in
# ``attention_dense`` candidate; ignored by the unified 2D/3D paths).
# Defaults deliver the persistent ~970-TFLOPS prefill path for large Sq:
# ``dense_persistent="auto"`` turns on the grid-stride variant once there
# is enough work to fill the persistent grid, and ``persist_decode="auto"``
# picks the L2-locality hkv-major decode where it is balance-safe. ---
dense_persistent: str = "auto" # "auto" | "on" | "off"
dense_num_persistent: int = 256
dense_persist_decode: str = "auto" # "auto" | "qb_major" | "hkv_major"
def normalized(self) -> dict:
d = asdict(self)
d["dtype"] = self.dtype.lower()
return d
def _request_errors(req: OperatorRequest) -> list[str]:
if not isinstance(req, AttentionRequest):
return [f"expected AttentionRequest, got {type(req).__name__}"]
errors: list[str] = []
if req.op != "attention":
errors.append(f"unsupported op {req.op!r}")
for field in ("batch", "nhead_q", "nhead_k", "seqlen_q", "seqlen_k", "hdim_q"):
if int(getattr(req, field)) <= 0:
errors.append(f"{field} must be positive")
if req.hdim_q != req.hdim_v:
errors.append("only hdim_q == hdim_v is supported")
if int(req.nhead_q) % int(req.nhead_k):
errors.append("nhead_q must be divisible by nhead_k (GQA grouping)")
try:
ArchTarget.from_gfx(req.arch)
except KeyError as e:
errors.append(str(e))
return errors
def _problem(req: AttentionRequest) -> UnifiedAttentionProblem:
# total_q = batch * seqlen_q (the flattened query rows). num_seqs = batch.
return UnifiedAttentionProblem(
total_q=int(req.batch) * int(req.seqlen_q),
num_seqs=int(req.batch),
num_query_heads=int(req.nhead_q),
num_kv_heads=int(req.nhead_k),
head_size=int(req.hdim_q),
block_size=int(req.kv_block_size),
max_seqlen_q=int(req.seqlen_q),
max_seqlen_k=int(req.seqlen_k),
dtype=req.dtype.lower(),
sliding_window=int(req.sliding_window),
use_sinks=bool(req.use_sinks),
num_sms=int(req.num_sms),
)
def _selector_matches(
req: AttentionRequest, candidate: KernelCandidate
) -> Tuple[bool, str]:
algorithm = req.algorithm.strip().lower()
spec_id = req.spec_id.strip().lower()
if algorithm not in ("auto", candidate.algorithm):
return False, f"request algorithm {req.algorithm!r} != {candidate.algorithm!r}"
if spec_id not in ("auto", candidate.spec_id):
return False, f"request spec_id {req.spec_id!r} != {candidate.spec_id!r}"
return True, "ok"
@dataclass(frozen=True)
class AttentionSpec:
"""The selected attention path + the dims that determine the kernel family."""
path: str # "2d" | "3d"
head_size: int
block_size: int
dtype: str
num_query_heads: int
num_kv_heads: int
name: str = "rocke_attention_unified"
# When set, this verbatim kernel_name is returned by :meth:`kernel_name`
# instead of the composed unified name. Used by the dense candidate to
# surface the concrete persistent/ragged/decode kernel it will launch.
kernel_name_override: str = ""
# Optional pinned codegen knobs a specialized candidate wants the builder to
# apply (sorted (key, value) pairs; empty for generic candidates). See
# ``attention_unified._d256_gfx950_spec_overrides``; the builder consumes them
# via ``_tiled_spec_from_problem(problem, overrides=...)``.
tiled_overrides: Tuple[Tuple[str, object], ...] = ()
def kernel_name(self) -> str:
if self.kernel_name_override:
return self.kernel_name_override
from rocke.helpers.spec import kernel_name_join
return kernel_name_join(
self.name,
self.path,
self.dtype,
f"hd{self.head_size}",
f"bs{self.block_size}",
f"gqa{self.num_query_heads}x{self.num_kv_heads}",
)
def _make_candidate(*, path: str, priority: int) -> KernelCandidate:
spec_id = f"unified_{path}"
name = f"attention_unified_{path}"
def support(req: OperatorRequest) -> Tuple[bool, str]:
errors = _request_errors(req)
if errors:
return False, "; ".join(errors)
assert isinstance(req, AttentionRequest)
ok, why = _selector_matches(req, candidate)
if not ok:
return False, why
problem = _problem(req)
ok, why = supports_native_unified_attention(problem)
if not ok:
return False, why
if problem.select_path() != path:
return False, (
f"problem routes to {problem.select_path()!r} path, not {path!r}"
)
return True, "ok"
def select(req: OperatorRequest) -> AttentionSpec:
ok, why = support(req)
if not ok:
raise ValueError(f"{name} does not support request: {why}")
assert isinstance(req, AttentionRequest)
problem = _problem(req)
return AttentionSpec(
path=path,
head_size=problem.head_size,
block_size=problem.block_size,
dtype=problem.dtype,
num_query_heads=problem.num_query_heads,
num_kv_heads=problem.num_kv_heads,
)
candidate = KernelCandidate(
name=name,
family=_FAMILY,
algorithm=f"unified_{path}",
spec_id=spec_id,
abi_version=ATTENTION_ABI_VERSION,
priority=priority,
supports=support,
select_spec=select,
signature=lambda _spec: (),
grid=lambda spec, req: (0, 0, 0), # geometry deferred (see module doc)
block=lambda spec: (0, 0, 0),
sweep_space=lambda req: (select(req),) if support(req)[0] else (),
)
return candidate
ATTENTION_REGISTRY = CandidateRegistry(_FAMILY)
ATTENTION_REGISTRY.extend(
(
# 2d and 3d are mutually exclusive per problem (select_path returns one),
# so priority only orders the two when both could match -- which they
# cannot. Equal priority keeps the registry order stable.
_make_candidate(path="2d", priority=10),
_make_candidate(path="3d", priority=10),
)
)
def _make_gfx942_dense_pipe_candidate() -> KernelCandidate:
"""Fast gfx942 fp16 prefill kernel — transposed-x8 flash with ring-sliced K.
Registered at priority 5 so it outranks the generic unified_2d candidate
(priority 10) whenever both would match the same gfx942 fp16 2D problem.
The registry sorts ascending (lower = higher precedence).
Callers can also force this path explicitly via algorithm="dense_pipe".
"""
spec_id = "gfx942_dense_pipe"
name = "attention_gfx942_dense_pipe"
def support(req: OperatorRequest) -> Tuple[bool, str]:
errors = _request_errors(req)
if errors:
return False, "; ".join(errors)
assert isinstance(req, AttentionRequest)
if req.arch != "gfx942":
return False, f"dense_pipe requires gfx942 (got {req.arch!r})"
if req.dtype != "fp16":
return False, f"dense_pipe is fp16-only (got {req.dtype!r})"
ok, why = _selector_matches(req, candidate)
if not ok:
return False, why
problem = _problem(req)
ok, why = supports_native_unified_attention(problem)
if not ok:
return False, why
if problem.select_path() != "2d":
return False, "problem routes to 3D, not 2D"
from kernels.common.attention_unified import _enable_gfx942_fp16_flash
if not _enable_gfx942_fp16_flash(problem):
return False, "gfx942 fp16 flash not eligible for this shape"
return True, "ok"
def select(req: OperatorRequest) -> AttentionSpec:
ok, why = support(req)
if not ok:
raise ValueError(f"{name} does not support request: {why}")
assert isinstance(req, AttentionRequest)
problem = _problem(req)
return AttentionSpec(
path="2d",
head_size=problem.head_size,
block_size=problem.block_size,
dtype=problem.dtype,
num_query_heads=problem.num_query_heads,
num_kv_heads=problem.num_kv_heads,
name="rocke_attention_gfx942_dense_pipe",
)
candidate = KernelCandidate(
name=name,
family=_FAMILY,
algorithm="dense_pipe",
spec_id=spec_id,
abi_version=ATTENTION_ABI_VERSION,
priority=5,
supports=support,
select_spec=select,
signature=lambda _spec: (),
grid=lambda spec, req: (0, 0, 0),
block=lambda spec: (0, 0, 0),
sweep_space=lambda req: (select(req),) if support(req)[0] else (),
)
return candidate
ATTENTION_REGISTRY.register(_make_gfx942_dense_pipe_candidate())
# block_n (KV tile) the dense candidate ships; 64 is the resource-efficient
# peak (see AttentionDenseSpec.block_n).
_DENSE_BLOCK_N = 64
def _dense_spec(req: OperatorRequest):
"""Build the ``AttentionDenseSpec`` for a request at its best-performing
config. Persistent ("auto") turns on the grid-stride ~970-TFLOPS variant once
there is enough work to fill the persistent grid (``nqb*Hq*B >= num_persistent``
-- the large-Sq prefill regime); ``persist_decode`` / ``lazy_rescale`` default
to the L2-locality hkv-major decode and always-on lazy rescale. Non-tile-
multiple self-attention lengths use the on-chip ragged path (no host pad)."""
from kernels.gfx950.attention_dense import AttentionDenseSpec, _BLOCK_M
assert isinstance(req, AttentionRequest)
sq, sk = int(req.seqlen_q), int(req.seqlen_k)
bn = _DENSE_BLOCK_N
# on-chip ragged padding for ragged self-attention lengths (seqlen_q==seqlen_kv,
# not a 256/block_n multiple). Cross-attention ragged is left to the validator.
ragged = (sq == sk) and ((sq % _BLOCK_M != 0) or (sk % bn != 0))
nqb = (sq + _BLOCK_M - 1) // _BLOCK_M
work = nqb * int(req.nhead_q) * int(req.batch)
np = int(req.dense_num_persistent)
mode = req.dense_persistent.strip().lower()
if mode == "on":
persistent = True
elif mode == "off":
persistent = False
elif mode == "auto":
persistent = work >= np # enough work to fill the persistent grid
else:
raise ValueError(
f"dense_persistent must be 'auto'/'on'/'off', got {req.dense_persistent!r}"
)
return AttentionDenseSpec(
batch=int(req.batch),
seqlen_q=sq,
seqlen_kv=sk,
num_query_heads=int(req.nhead_q),
num_kv_heads=int(req.nhead_k),
head_size=int(req.hdim_q),
causal=(int(req.mask_type) != 0),
dtype=req.dtype.lower(),
block_n=bn,
persistent=persistent,
num_persistent=np,
persist_decode=req.dense_persist_decode.strip().lower(),
ragged=ragged,
)
def dense_spec_for_request(req: AttentionRequest):
"""Public builder: the launch-ready ``AttentionDenseSpec`` for ``req`` at its
best config (see :func:`_dense_spec`). Pair with ``run_attention_dense_torch``
to execute the dispatched dense candidate."""
return _dense_spec(req)
def _make_gfx950_attention_dense_candidate() -> KernelCandidate:
"""Dense CK-1 persistent flash-attn prefill on gfx950 (bf16/fp16, causal/full).
OPT-IN ONLY: matches solely when the request explicitly names
``algorithm="attention_dense"`` (or ``spec_id="gfx950_attention_dense"``), so it
never auto-overrides the generic unified_2d path (no change to default routing).
When selected it uses the persistent best-config from :func:`_dense_spec`
(grid-stride + hkv-major + lazy for large Sq); the concrete kernel_name (incl.
``persist``/``hkvmaj``/``lazyrs``/``ragged``) is surfaced on the spec so the
launched kernel is the fast path, not the default grid. End-to-end launch is
``run_attention_dense_torch(spec=dense_spec_for_request(req), ...)``.
"""
spec_id = "gfx950_attention_dense"
name = "attention_gfx950_dense"
def support(req: OperatorRequest) -> Tuple[bool, str]:
errors = _request_errors(req)
if errors:
return False, "; ".join(errors)
assert isinstance(req, AttentionRequest)
# Opt-in: never selected under algorithm/spec_id "auto".
if req.algorithm.strip().lower() != "attention_dense" and (
req.spec_id.strip().lower() != spec_id
):
return False, "attention_dense is opt-in (algorithm='attention_dense')"
if req.arch != "gfx950":
return False, f"attention_dense requires gfx950 (got {req.arch!r})"
if req.dtype.lower() not in ("bf16", "fp16"):
return False, f"attention_dense is bf16/fp16 only (got {req.dtype!r})"
if int(req.sliding_window) or bool(req.use_sinks):
return False, "attention_dense is dense (no sliding-window / sinks)"
from kernels.gfx950.attention_dense import supports_attention_dense
try:
spec = _dense_spec(req)
except ValueError as e:
return False, str(e)
ok, why = supports_attention_dense(spec, arch=req.arch)
if not ok:
return False, why
return True, "ok"
def select(req: OperatorRequest) -> AttentionSpec:
ok, why = support(req)
if not ok:
raise ValueError(f"{name} does not support request: {why}")
assert isinstance(req, AttentionRequest)
problem = _problem(req)
dense_spec = _dense_spec(req)
return AttentionSpec(
path="2d",
head_size=problem.head_size,
block_size=problem.block_size,
dtype=problem.dtype,
num_query_heads=problem.num_query_heads,
num_kv_heads=problem.num_kv_heads,
name="rocke_attention_dense",
# surface the concrete persistent/hkvmaj/lazyrs/ragged kernel so the
# dispatched spec names the fast path it will actually launch.
kernel_name_override=dense_spec.kernel_name(),
)
candidate = KernelCandidate(
name=name,
family=_FAMILY,
algorithm="attention_dense",
spec_id=spec_id,
abi_version=ATTENTION_ABI_VERSION,
priority=3,
supports=support,
select_spec=select,
signature=lambda _spec: (),
grid=lambda spec, req: (0, 0, 0),
block=lambda spec: (0, 0, 0),
sweep_space=lambda req: (select(req),) if support(req)[0] else (),
)
return candidate
def _make_gfx950_d256_candidate() -> KernelCandidate:
"""Fast gfx950 bf16 head_size-256 prefill kernel — 32x32 transposed stack
with FA3-style softmax<->MFMA interleave (mode2/g4) + slab-padded K_lds.
Registered at priority 5 so it outranks the generic unified_2d candidate
(priority 10) for the gfx950 bf16 D256 prefill cohort. The registry sorts
ascending (lower = higher precedence); gfx950-only, so it never competes
with the gfx942 dense_pipe candidate. Callers can also force this path
explicitly via algorithm="d256_gfx950".
The cohort is the single source of truth
``kernels.common.attention_unified._d256_gfx950_cohort`` — the same predicate
the orchestrator's ``_d256_gfx950_fast`` override uses — so dispatch selection
and the built spec cannot drift. Only the arch gate differs (request arch
here vs resolved device arch there).
"""
spec_id = "gfx950_d256"
name = "attention_gfx950_d256"
def support(req: OperatorRequest) -> Tuple[bool, str]:
errors = _request_errors(req)
if errors:
return False, "; ".join(errors)
assert isinstance(req, AttentionRequest)
if req.arch != "gfx950":
return False, f"d256_gfx950 requires gfx950 (got {req.arch!r})"
if req.dtype != "bf16":
return False, f"d256_gfx950 is bf16-only (got {req.dtype!r})"
ok, why = _selector_matches(req, candidate)
if not ok:
return False, why
problem = _problem(req)
ok, why = supports_native_unified_attention(problem)
if not ok:
return False, why
if problem.select_path() != "2d":
return False, "problem routes to 3D, not 2D"
from kernels.common.attention_unified import _d256_gfx950_cohort
if not _d256_gfx950_cohort(problem):
return False, "not the gfx950 bf16 D256 prefill fast-path cohort"
return True, "ok"
def select(req: OperatorRequest) -> AttentionSpec:
ok, why = support(req)
if not ok:
raise ValueError(f"{name} does not support request: {why}")
assert isinstance(req, AttentionRequest)
problem = _problem(req)
from kernels.common.attention_unified import _d256_gfx950_spec_overrides
return AttentionSpec(
path="2d",
head_size=problem.head_size,
block_size=problem.block_size,
dtype=problem.dtype,
num_query_heads=problem.num_query_heads,
num_kv_heads=problem.num_kv_heads,
name="rocke_attention_gfx950_d256",
tiled_overrides=tuple(sorted(_d256_gfx950_spec_overrides().items())),
)
candidate = KernelCandidate(
name=name,
family=_FAMILY,
algorithm="d256_gfx950",
spec_id=spec_id,
abi_version=ATTENTION_ABI_VERSION,
priority=5,
supports=support,
select_spec=select,
signature=lambda _spec: (),
grid=lambda spec, req: (0, 0, 0),
block=lambda spec: (0, 0, 0),
sweep_space=lambda req: (select(req),) if support(req)[0] else (),
)
return candidate
def _make_d256_decode_candidate() -> KernelCandidate:
"""D256 bf16 decode candidate for gfx950 and gfx942 — 3D split-KV path.
Registered at priority 5 so it outranks the generic unified_3d candidate
(priority 10) for eligible D256 bf16 decode shapes. Callers can also force
this path explicitly via algorithm="d256_decode".
"""
from kernels.common.attention_unified import _d256_decode_cohort
spec_id = "d256_decode"
name = "attention_d256_decode"
def support(req: OperatorRequest) -> Tuple[bool, str]:
errors = _request_errors(req)
if errors:
return False, "; ".join(errors)
assert isinstance(req, AttentionRequest)
if req.arch not in ("gfx942", "gfx950"):
return False, f"d256_decode requires gfx942 or gfx950 (got {req.arch!r})"
if req.dtype != "bf16":
return False, f"d256_decode is bf16-only (got {req.dtype!r})"
ok, why = _selector_matches(req, candidate)
if not ok:
return False, why
problem = _problem(req)
ok, why = supports_native_unified_attention(problem)
if not ok:
return False, why
if not _d256_decode_cohort(problem):
return False, "problem is not in the D256 bf16 decode cohort"
if problem.select_path() != "3d":
return False, (
f"problem routes to {problem.select_path()!r} path, not '3d'"
)
return True, "ok"
def select(req: OperatorRequest) -> AttentionSpec:
ok, why = support(req)
if not ok:
raise ValueError(f"{name} does not support request: {why}")
assert isinstance(req, AttentionRequest)
problem = _problem(req)
return AttentionSpec(
path="3d",
head_size=problem.head_size,
block_size=problem.block_size,
dtype=problem.dtype,
num_query_heads=problem.num_query_heads,
num_kv_heads=problem.num_kv_heads,
name="rocke_attention_d256_decode",
)
candidate = KernelCandidate(
name=name,
family=_FAMILY,
algorithm="d256_decode",
spec_id=spec_id,
abi_version=ATTENTION_ABI_VERSION,
priority=5,
supports=support,
select_spec=select,
signature=lambda _spec: (),
grid=lambda spec, req: (0, 0, 0),
block=lambda spec: (0, 0, 0),
sweep_space=lambda req: (select(req),) if support(req)[0] else (),
)
return candidate
ATTENTION_REGISTRY.register(_make_gfx950_attention_dense_candidate())
ATTENTION_REGISTRY.register(_make_gfx950_d256_candidate())
ATTENTION_REGISTRY.register(_make_d256_decode_candidate())
def attention_candidates() -> Tuple[KernelCandidate, ...]:
return ATTENTION_REGISTRY.candidates()
def _kernel_id(
req: AttentionRequest, candidate: KernelCandidate, spec: AttentionSpec
) -> KernelId:
request_hash = stable_json_hash(req.normalized(), n=16)
spec_hash = stable_json_hash(asdict(spec), n=16)
return KernelId(
op="attention",
family=_FAMILY,
candidate=candidate.name,
algorithm=candidate.algorithm,
spec_id=candidate.spec_id,
arch=req.arch,
abi_version=candidate.abi_version,
request_hash=request_hash,
spec_hash=spec_hash,
)
def attention_sweep_space(req: OperatorRequest) -> Sequence[AttentionSpec]:
if _request_errors(req):
return ()
specs = []
seen = set()
for candidate in ATTENTION_REGISTRY.supported(req):
spec = candidate.select_spec(req)
h = stable_json_hash(asdict(spec), n=16)
if h not in seen:
seen.add(h)
specs.append(spec)
return tuple(specs)
def dispatch_attention(
req: AttentionRequest, *, ranker: Ranker | None = None
) -> DispatchResult:
"""Select the unified attention kernel PATH for ``req``.
Returns the 2D-tiled or 3D split-KV path (a pure function of the problem),
gated by the native-backend coverage predicate. The CTA geometry is left to
the instance builder (see module docstring -- deferred from parity).
"""
candidate = ATTENTION_REGISTRY.select(req, ranker=ranker)
spec = candidate.select_spec(req)
kid = _kernel_id(req, candidate, spec)
return DispatchResult(
request=req,
candidate=candidate,
spec=spec,
kernel_id=kid,
grid=candidate.grid(spec, req),
block=candidate.block(spec),
signature=tuple(candidate.signature(spec)),
explanation=(
f"selected {candidate.name} ({spec.path} path) on {req.arch}",
f"algorithm={candidate.algorithm}",
f"spec_id={candidate.spec_id}",
f"spec_hash={kid.spec_hash}",
f"request_hash={kid.request_hash}",
),
)