Skip to content
Open
112 changes: 91 additions & 21 deletions atom/model_ops/attention_mha.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: MIT
# Copyright (C) 2024-2025, Advanced Micro Devices, Inc. All rights reserved.

import logging
from functools import cache

import aiter
Expand Down Expand Up @@ -29,6 +30,12 @@

from .attention_mla import MLAModules

logger = logging.getLogger("atom")

# Once per process: the hint names the opt-in, not the shape, so one line says
# all a default user needs, and the decode loop pays one bool test after it.
_flydsl_off_hint_logged = False


@cache
def use_pa_decode_bf16_asm() -> bool:
Expand Down Expand Up @@ -608,19 +615,61 @@ def _shuffle_v_for_flydsl(self, v_cache, k_cache):
return v_cache
return self._view_v_cache_for_pa_decode_bf16_asm(v_cache, k_cache)

def _flydsl_takes_decode(self, q, k_cache, v_cache, attn_metadata) -> bool:
"""Without gluon decode (gfx1250): whether FlyDSL accepts this call.
def _pa_decode_kv_scales(self, k_scale, v_scale):
"""The K/V scales ``paged_attention_triton`` hands ``run_pa_decode``."""
if self.kv_cache_dtype == "bf16":
return None, None
if k_scale is not None and k_scale.numel() > 1:
return k_scale.unsqueeze(-1), v_scale.unsqueeze(-1)
return k_scale, v_scale

def _flydsl_takes_decode(
self, q, k_cache, v_cache, k_scale, v_scale, attn_metadata
):
"""Sequence count FlyDSL runs a decode routed to unified with, or a
falsy value to keep it on unified.

Two cases reach unified with a call FlyDSL may take: a GPU without
gluon decode (gfx1250), and a gluon GPU whose query length x group is
past the gluon envelope (Qwen3.5 MTP-5: qlen 6, group 8). Inside that
envelope a gluon GPU keeps its route. The decision is made per call,
on the same tensors and metadata ``run_pa_decode`` then receives, and
the count is passed down as ``flydsl_seqs``, so aiter's predicate is
asked once per call and the two cannot disagree.

With ATOM_PA_FLYDSL unset, such a call stays on unified and one info
line per process says that the flag would route it to FlyDSL.

ATOM_USE_UNIFIED_ATTN keeps the call on unified_attention.

gfx1250 with per-token FP8 KV past qlen 4 is decided by the same check;
it is not exercised by this path's tests or runs (out of scope here).
"""
if (
envs.ATOM_USE_UNIFIED_ATTN
or pa_decode_gluon_supports_arch()
or self.use_flash_layout
or self.sliding_window > 0
):
return False
num_seqs = attn_metadata.context_lens.shape[0]
return bool(
if pa_decode_gluon_supports_arch() and not gluon_decode_over_limit(
attn_metadata.max_seqlen_q, self.num_heads, self.num_kv_heads
):
return False
if not envs.ATOM_PA_FLYDSL:
global _flydsl_off_hint_logged
if not _flydsl_off_hint_logged:
_flydsl_off_hint_logged = True
logger.info(
"decode with max_seqlen_q=%d x %d q heads per kv head runs on "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this route refuses per-token fp8 kv scales, so the hint reads wrong.. mention that?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, with per-token FP8 KV that call is refused, not run on unified. Fixed in 1ef5a2b: the hint moved to paged_attention_unified. Per-token scales: the refusal itself now ends with "ATOM_PA_FLYDSL=1 lets aiter's FlyDSL pa_decode take it (needs aiter with ROCm/aiter#7206)" and no info line is logged; otherwise the one info line per process says it runs on unified. Neither appears where the flag would not help (ATOM_USE_UNIFIED_ATTN, flash layout, window, inside the gluon envelope). Tests: test_flydsl_off_names_the_opt_in_once[per_token=False/True], test_no_hint_inside_the_gluon_envelope. CPU 166 passed (with #7206 pa_decode); only failure is main's GPU-only test_batch_one_asks_for_the_ceiling.

"unified_attention because ATOM_PA_FLYDSL is unset; "
"ATOM_PA_FLYDSL=1 lets aiter's FlyDSL pa_decode take it "
"(needs aiter with ROCm/aiter#7206)",
attn_metadata.max_seqlen_q,
self.num_heads // self.num_kv_heads,
)
return False
k_scale, v_scale = self._pa_decode_kv_scales(k_scale, v_scale)
return (
flydsl_decode_seqs(
output=q,
q=q,
Expand All @@ -629,21 +678,21 @@ def _flydsl_takes_decode(self, q, k_cache, v_cache, attn_metadata) -> bool:
block_tables=attn_metadata.block_tables,
context_lens=attn_metadata.context_lens,
max_seqlen_q=attn_metadata.max_seqlen_q,
max_context_partition_num=dense_decode_splits(
num_seqs, k_cache.shape[1]
),
context_partition_size=256,
compute_type=(
torch.bfloat16
if self.kv_cache_dtype == "bf16"
else aiter.dtypes.fp8
),
q_scale=None,
k_scale=k_scale,
v_scale=v_scale,
alibi_slopes=None,
sinks=self.sinks,
sliding_window=self.sliding_window,
ps=True,
)
or False
)

@mark_trace(prefix="paged_attention_unified", torch_compile=False)
Expand All @@ -657,9 +706,20 @@ def paged_attention_unified(
of the reasons for it.
"""
attn_metadata = fwd_ctx.attn_metadata
if self._flydsl_takes_decode(q, k_cache, v_cache, attn_metadata):
flydsl_seqs = self._flydsl_takes_decode(
q, k_cache, v_cache, k_scale, v_scale, attn_metadata
)
if flydsl_seqs:
return self.paged_attention_triton(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we ask the gate once per call instead of twice per layer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in accf537: _flydsl_takes_decode returns the sequence count, paged_attention_unified passes it down as flydsl_seqs, and run_pa_decode skips its own check when given one. Same arguments as before (the check already used the tensors run_pa_decode receives), so routing is unchanged; other run_pa_decode callers (vLLM/SGLang bridges, sparse sites) still check inside. Test: test_the_gate_is_asked_once_per_call drives unified -> triton -> run_pa_decode to the plan step and asserts the gate ran once. CPU: tests/test_paged_attention_dispatch.py 166 passed with #7206 pa_decode, 165 + 1 skip with the installed aiter; the only failure is main's test_batch_one_asks_for_the_ceiling, which needs a visible GPU.

q, k, v, k_cache, v_cache, k_scale, v_scale, fwd_ctx
q,
k,
v,
k_cache,
v_cache,
k_scale,
v_scale,
fwd_ctx,
flydsl_seqs=flydsl_seqs,
)
self._reject_per_token_scales_on_unified(k_scale, v_scale, "decode")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default user has no hint here, add the ATOM_PA_FLYDSL=1 reason in this branch?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in accf537: with ATOM_PA_FLYDSL unset, a decode past the gluon envelope (e.g. Qwen3.5 MTP-5, qlen 6 x group 8) logs one info line per process saying it runs on unified_attention and that ATOM_PA_FLYDSL=1 lets FlyDSL take it (needs aiter #7206). aiter is not asked in that case. Test: test_flydsl_off_hints_once_and_skips_the_gate (3 calls -> 1 line, predicate not called).


Expand Down Expand Up @@ -696,13 +756,23 @@ def paged_attention_unified(

@mark_trace(prefix="paged_attention_triton", torch_compile=False)
def paged_attention_triton(
self, q, k, v, k_cache, v_cache, k_scale, v_scale, fwd_ctx: ForwardContext
self,
q,
k,
v,
k_cache,
v_cache,
k_scale,
v_scale,
fwd_ctx: ForwardContext,
flydsl_seqs: int | None = None,
):
"""Decode through ``run_pa_decode``: gluon, or opted-in FlyDSL.

Callers must have cleared ``gluon_decode_over_limit`` in
``_dispatch_decode`` or, on a GPU without gluon, established through
``_flydsl_takes_decode`` that FlyDSL accepts the call.
``_dispatch_decode`` or established through ``_flydsl_takes_decode``
that FlyDSL accepts the call; the latter pass the sequence count it
returned as ``flydsl_seqs``, so ``run_pa_decode`` does not ask again.
"""

attn_metadata = fwd_ctx.attn_metadata
Expand All @@ -723,9 +793,7 @@ def paged_attention_triton(
max_context_partition_num = 1
context_partition_size = 128

if k_scale is not None and k_scale.numel() > 1:
k_scale = k_scale.unsqueeze(-1)
v_scale = v_scale.unsqueeze(-1)
k_scale, v_scale = self._pa_decode_kv_scales(k_scale, v_scale)

compute_type = (
torch.bfloat16 if self.kv_cache_dtype == "bf16" else aiter.dtypes.fp8
Expand All @@ -743,8 +811,8 @@ def paged_attention_triton(
context_partition_size=context_partition_size,
compute_type=compute_type,
q_scale=None,
k_scale=None if self.kv_cache_dtype == "bf16" else k_scale,
v_scale=None if self.kv_cache_dtype == "bf16" else v_scale,
k_scale=k_scale,
v_scale=v_scale,
alibi_slopes=None,
sinks=self.sinks,
sliding_window=self.sliding_window,
Expand All @@ -755,6 +823,7 @@ def paged_attention_triton(
# max_context_length: the block-table bound is the same in graph
# and eager, so both pick the same kernel specialization.
work_plan=getattr(attn_metadata, "flydsl_work_plan", None),
flydsl_seqs=flydsl_seqs,
)

return o
Expand Down Expand Up @@ -1086,9 +1155,10 @@ def prefill_attention_triton(
def _dispatch_decode(self, max_qlen: int = 1):
"""Select the coarse decode runner from model shape and configuration.

Cache-dependent dispatch happens later: on a GPU without gluon,
``paged_attention_unified`` hands FlyDSL calls inside its supported
domain and keeps the rest on unified attention. The vLLM bridge shares
Cache-dependent dispatch happens later: on a GPU without gluon, or
past the gluon envelope on one with it, ``paged_attention_unified``
hands FlyDSL calls inside its supported domain and keeps the rest on
unified attention. The vLLM bridge shares
the constants and predicate, not this order -- its arms differ (no
unified fallback, no sliding-window arm). Both
paged kernels stop short of unified, at different points, and drafting
Expand Down
Loading
Loading