Skip to content

Commit c19cfe0

Browse files
committed
feat: add ROCm FlashAttention backend
1 parent 4a9ca42 commit c19cfe0

9 files changed

Lines changed: 685 additions & 4 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ on:
99
pull_request:
1010
branches: [ main ]
1111

12+
permissions:
13+
contents: read
14+
1215
jobs:
1316
linting:
1417
runs-on: ubuntu-latest
@@ -63,6 +66,7 @@ jobs:
6366
- name: Run Mocked Hardware Discovery Tests
6467
run: |
6568
python -m pytest rl_engine/tests/test_dispatch.py -v
69+
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest tests/test_attention_correctness.py -q -rs
6670
6771
docs:
6872
runs-on: ubuntu-latest

‎docs/getting_started/installation.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,38 @@ pip install -e ".[vllm]"
2828
Install the vLLM extra only on rollout or benchmark environments that need the
2929
vLLM runtime. Core CI and mocked integration tests do not require it.
3030

31+
### ROCm Backend
32+
33+
Use a ROCm PyTorch build that matches the installed ROCm toolchain. Then install
34+
FlashAttention with an AMD backend:
35+
36+
```bash
37+
python -m pip install ninja packaging wheel psutil einops
38+
git clone --recurse-submodules https://github.com/Dao-AILab/flash-attention.git
39+
cd flash-attention
40+
FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE \
41+
python -m pip install --no-build-isolation --no-deps .
42+
cd ..
43+
```
44+
45+
Verify the environment from the RL-Kernel checkout:
46+
47+
```bash
48+
python scripts/check_rocm_env.py --require-flash-attn
49+
FLASH_ATTENTION_TRITON_AMD_ENABLE=TRUE python - <<'PY'
50+
from flash_attn import flash_attn_func
51+
52+
print(flash_attn_func)
53+
PY
54+
```
55+
56+
RL-Kernel uses PyTorch SDPA as the default ROCm attention path. To opt in to
57+
external FlashAttention for ROCm attention dispatch, set:
58+
59+
```bash
60+
export RL_KERNEL_ROCM_ATTN_BACKEND=flash_attn
61+
```
62+
3163
## Development Dependencies
3264

3365
```bash
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright (c) 2026 RL-Kernel Contributors
3+
4+
import torch
5+
import torch.nn.functional as F
6+
7+
8+
class NativeAttentionOp:
9+
"""PyTorch SDPA fallback for FlashAttention-layout tensors."""
10+
11+
def __call__(
12+
self,
13+
q: torch.Tensor,
14+
k: torch.Tensor,
15+
v: torch.Tensor,
16+
dropout_p: float = 0.0,
17+
softmax_scale: float | None = None,
18+
causal: bool = False,
19+
) -> torch.Tensor:
20+
q_ref = q.transpose(1, 2)
21+
k_ref = k.transpose(1, 2)
22+
v_ref = v.transpose(1, 2)
23+
out = F.scaled_dot_product_attention(
24+
q_ref,
25+
k_ref,
26+
v_ref,
27+
dropout_p=dropout_p,
28+
is_causal=causal,
29+
scale=softmax_scale,
30+
)
31+
return out.transpose(1, 2)
32+
33+
34+
__all__ = ["NativeAttentionOp"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright (c) 2026 RL-Kernel Contributors
3+
4+
from .flash_attn import RocmFlashAttentionOp
5+
6+
__all__ = [
7+
"RocmFlashAttentionOp",
8+
]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright (c) 2026 RL-Kernel Contributors
3+
4+
import importlib.util
5+
import os
6+
7+
import torch
8+
9+
from rl_engine.utils.logger import logger
10+
11+
12+
def _select_flash_attn_backend() -> str:
13+
"""Select the installed FlashAttention ROCm backend before importing flash_attn."""
14+
if os.environ.get("FLASH_ATTENTION_TRITON_AMD_ENABLE", "").upper() == "TRUE":
15+
return "triton"
16+
if importlib.util.find_spec("flash_attn_2_cuda") is None:
17+
os.environ["FLASH_ATTENTION_TRITON_AMD_ENABLE"] = "TRUE"
18+
return "triton"
19+
return "ck"
20+
21+
22+
class RocmFlashAttentionOp:
23+
"""
24+
Standard FlashAttention wrapper for ROCm.
25+
Demonstrates the reference structure for adding new operator families.
26+
"""
27+
28+
def __init__(self):
29+
if torch.version.hip is None:
30+
raise RuntimeError("RocmFlashAttentionOp requires a ROCm PyTorch build.")
31+
32+
backend = _select_flash_attn_backend()
33+
try:
34+
from flash_attn import flash_attn_func
35+
36+
self.op = flash_attn_func
37+
logger.info("Successfully linked to external flash_attn library (%s backend).", backend)
38+
except ImportError as exc:
39+
raise RuntimeError(
40+
"ROCm FlashAttention requires a ROCm-compatible flash-attn installation. "
41+
"See docs/getting_started/installation.md#rocm-backend."
42+
) from exc
43+
44+
def __call__(
45+
self,
46+
q: torch.Tensor,
47+
k: torch.Tensor,
48+
v: torch.Tensor,
49+
dropout_p: float = 0.0,
50+
softmax_scale: float | None = None,
51+
causal: bool = False,
52+
) -> torch.Tensor:
53+
"""
54+
Standard attention forward pass.
55+
Args:
56+
q: (batch, seqlen, nheads, headdim)
57+
k: (batch, seqlen, nheads_k, headdim)
58+
v: (batch, seqlen, nheads_k, headdim)
59+
"""
60+
valid_dtypes = (torch.float16, torch.bfloat16)
61+
if (
62+
q.dtype not in valid_dtypes
63+
or k.dtype not in valid_dtypes
64+
or v.dtype not in valid_dtypes
65+
):
66+
raise TypeError("FlashAttention requires FP16 or BF16 for q/k/v")
67+
# PyTorch uses the CUDA device API for both CUDA and ROCm tensors.
68+
if not (q.is_cuda and k.is_cuda and v.is_cuda):
69+
raise ValueError("Inputs must be on a CUDA/ROCm GPU device")
70+
if not (q.device == k.device == v.device):
71+
raise ValueError("q, k, and v must be on the same device")
72+
73+
q, k, v = q.contiguous(), k.contiguous(), v.contiguous()
74+
75+
return self.op(q, k, v, dropout_p=dropout_p, softmax_scale=softmax_scale, causal=causal)

‎rl_engine/kernels/registry.py‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# Copyright (c) 2026 RL-Kernel Contributors
33

44
import importlib
5+
import os
56
from enum import Enum, EnumMeta
67
from typing import Any, Dict, Optional, Set, Type
78

@@ -32,13 +33,15 @@ class OpBackend(Enum, metaclass=_KernelEnumMeta):
3233
# AMD ROCm optimized stack
3334
ROCM_AITER = "rl_engine.kernels.ops.rocm.aiter.AiterOp"
3435
ROCM_CK = "rl_engine.kernels.ops.rocm.composable_kernel.CKOp"
36+
ROCM_FLASH_ATTN = "rl_engine.kernels.ops.rocm.attention.flash_attn.RocmFlashAttentionOp"
3537

3638
# GRPO loss (group reward normalization + clipped surrogate + KL)
3739
TRITON_GRPO_LOSS = "rl_engine.kernels.ops.triton.triton_grpo_loss.TritonGRPOLossOp"
3840
PYTORCH_GRPO_LOSS = "rl_engine.kernels.ops.pytorch.loss.grpo_loss.NativeGRPOLossOp"
3941

4042
# Generic fallback
4143
TRITON_GENERIC = "rl_engine.kernels.ops.triton.generic.TritonOp"
44+
PYTORCH_ATTN = "rl_engine.kernels.ops.pytorch.attention.NativeAttentionOp"
4245
PYTORCH_NATIVE = "rl_engine.kernels.ops.pytorch.loss.logp.NativeLogpOp"
4346

4447

@@ -72,24 +75,43 @@ def __init__(self):
7275
OpBackend.CUDA_FUSED_LOGP_GENERIC,
7376
OpBackend.PYTORCH_NATIVE,
7477
],
75-
"attn": [OpBackend.FLASH_ATTN, OpBackend.TRITON_GENERIC, OpBackend.PYTORCH_NATIVE],
78+
"attn": [OpBackend.FLASH_ATTN, OpBackend.TRITON_GENERIC, OpBackend.PYTORCH_ATTN],
7679
"grpo_loss": [OpBackend.TRITON_GRPO_LOSS, OpBackend.PYTORCH_GRPO_LOSS],
7780
# Default dispatch logic for new operators
7881
},
7982
"rocm": {
8083
"logp": [OpBackend.ROCM_AITER, OpBackend.TRITON_GENERIC, OpBackend.PYTORCH_NATIVE],
81-
"attn": [OpBackend.TRITON_GENERIC, OpBackend.PYTORCH_NATIVE],
84+
"attn": [
85+
OpBackend.PYTORCH_ATTN,
86+
OpBackend.ROCM_FLASH_ATTN,
87+
OpBackend.TRITON_GENERIC,
88+
],
8289
"grpo_loss": [OpBackend.TRITON_GRPO_LOSS, OpBackend.PYTORCH_GRPO_LOSS],
8390
},
8491
"cpu": {
8592
"logp": [OpBackend.PYTORCH_NATIVE],
86-
"attn": [OpBackend.PYTORCH_NATIVE],
93+
"attn": [OpBackend.PYTORCH_ATTN],
8794
"grpo_loss": [OpBackend.PYTORCH_GRPO_LOSS],
8895
},
8996
}
9097
logger.info(f"KernelRegistry initialized for {device_ctx.device_type}")
98+
self._adjust_priority_from_env()
9199
self._adjust_priority_for_hardware()
92100

101+
def _adjust_priority_from_env(self):
102+
rocm_attn_backend = os.getenv("RL_KERNEL_ROCM_ATTN_BACKEND", "").strip().lower()
103+
if rocm_attn_backend in {"flash_attn", "flash-attn", "flash_attention"}:
104+
self._priority_map["rocm"]["attn"] = [
105+
OpBackend.ROCM_FLASH_ATTN,
106+
OpBackend.PYTORCH_ATTN,
107+
OpBackend.TRITON_GENERIC,
108+
]
109+
elif rocm_attn_backend and rocm_attn_backend not in {"native", "pytorch", "sdpa"}:
110+
logger.warning(
111+
"Unknown RL_KERNEL_ROCM_ATTN_BACKEND=%s; using default ROCm attention priority.",
112+
rocm_attn_backend,
113+
)
114+
93115
def _adjust_priority_for_hardware(self):
94116
"""Prioritize the fused TMA LogP kernel only when it is compiled into the
95117
extension and the device is TMA-capable (SM90/100/120)."""

‎rl_engine/tests/test_dispatch.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import torch
55

66
from rl_engine.executors.rollout import RolloutExecutor
7-
from rl_engine.kernels.registry import kernel_registry
7+
from rl_engine.kernels.registry import KernelRegistry, OpBackend, kernel_registry
88
from rl_engine.platforms.device import device_ctx
99
from rl_engine.utils.logger import logger
1010

@@ -25,6 +25,23 @@ def test_device_and_registry():
2525
logger.info(f"Retrieved Attention Operator: {attn_op}")
2626

2727

28+
def test_rocm_attention_uses_native_sdpa_by_default(monkeypatch):
29+
monkeypatch.delenv("RL_KERNEL_ROCM_ATTN_BACKEND", raising=False)
30+
31+
registry = KernelRegistry()
32+
33+
assert registry._priority_map["rocm"]["attn"][0] == OpBackend.PYTORCH_ATTN
34+
35+
36+
def test_rocm_attention_flash_attn_opt_in(monkeypatch):
37+
monkeypatch.setenv("RL_KERNEL_ROCM_ATTN_BACKEND", " flash_attn ")
38+
39+
registry = KernelRegistry()
40+
41+
assert registry._priority_map["rocm"]["attn"][0] == OpBackend.ROCM_FLASH_ATTN
42+
assert registry._priority_map["rocm"]["attn"][1] == OpBackend.PYTORCH_ATTN
43+
44+
2845
def test_executor_flow():
2946
executor = RolloutExecutor()
3047
mock_input_ids = torch.ones((1, 16), dtype=torch.long)

0 commit comments

Comments
 (0)