Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions unsloth_zoo/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,95 @@ def replace_with_grouped_query_attention(module, source):
pass




def replace_sdpa_with_amd_aiter(source):
"""
For AMD ROCm with amd-aiter installed: replace scaled_dot_product_attention
calls with amd-aiter's Flash Attention implementation in the compiled source.

amd-aiter uses full MFMA on both QK^T and P@V matrix multiplications,
plus a fast exp2-based softmax and causal tile skip — giving ~5-9x higher
TFLOPS than PyTorch SDPA for prefill workloads.

This function rewrites the source string so that the compiled model calls
aiter.flash_attn_func instead of torch.nn.functional.scaled_dot_product_attention.

Input/output conventions:
- transformers attention: q/k/v shape = (B, n_heads, S, D) [B,H,S,D]
- aiter flash_attn_func: q/k/v shape = (B, S, n_heads, D) [B,S,H,D]
- So we must transpose 1↔2 before and after the aiter call.

Only activates on AMD ROCm when get_amd_attention_implementation() == "amd_aiter".
No-op on NVIDIA and on ROCm without amd-aiter or ROCm < 7.0.
"""
if get_amd_attention_implementation() != "amd_aiter":

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Import the AMD selector before calling it

replace_sdpa_with_amd_aiter() is called for each SDPA module, but compiler.py only imports DEVICE_TYPE from the package and never imports get_amd_attention_implementation. Once the syntax error is fixed, any SDPA-capable model that enters this loop will raise NameError here even on NVIDIA/ROCm setups where this should be a no-op.

Useful? React with 👍 / 👎.

return source

# Pattern: capture the SDPA call and all its arguments
# Matches: attn_output = torch.nn.functional.scaled_dot_product_attention(
# query_states, key_states, value_states,
# attn_mask=..., dropout_p=..., is_causal=...
# )
sdpa_call_pattern = (
r"([ ]*)([A-Za-z_][A-Za-z0-9_]*)[ ]*=[ ]*"
r"(?:[A-Za-z_.]*scaled_dot_product_attention)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid matching the disabled SDPA shim

This pattern matches any callable name ending in scaled_dot_product_attention, so on ROCm+aiter it also rewrites the disable_compile_scaled_dot_product_attention(...) calls inserted just above when the compiler decided an SDPA module was unsafe. That undoes the existing opt-out for those modules and reintroduces the SDPA/aiter call the preceding branch was trying to disable.

Useful? React with 👍 / 👎.

r"\("
r"[

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Escape the embedded newlines in these strings

This raw string is split by a physical newline, so compiler.py is not parseable (python -m py_compile unsloth_zoo/compiler.py stops here with SyntaxError: unterminated string literal). Any path that imports or uses the compiler now fails before the AMD/ROCm gate can no-op; the same escaping issue appears in the f-strings that build the replacement body below.

Useful? React with 👍 / 👎.

]*([A-Za-z_][A-Za-z0-9_]*)[ ]*," # query
r"[
]*([A-Za-z_][A-Za-z0-9_]*)[ ]*," # key
r"[
]*([A-Za-z_][A-Za-z0-9_]*)[ ]*" # value
r"([^)]*)\)" # remaining args

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle nested calls in SDPA keyword arguments

The rest_args group stops at the first ), not the closing parenthesis of scaled_dot_product_attention. On AMD+aiter, any SDPA call with a nested expression in its keywords, for example scale=getattr(self, "scaling", None), is only partially replaced and leaves the remaining arguments after the generated fallback, yielding an invalid compiled forward instead of a working SDPA fallback.

Useful? React with 👍 / 👎.

)

import re

def aiter_replacement(m):
indent = m.group(1)
out_var = m.group(2)
q_var = m.group(3)
k_var = m.group(4)
v_var = m.group(5)
rest_args = m.group(6)

# Extract is_causal from rest_args (default False if not found)
causal_match = re.search(r"is_causal\s*=\s*(True|False|[A-Za-z_][A-Za-z0-9_]*)", rest_args)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve full is_causal expressions

This regex captures only a literal or a single identifier after is_causal=, so common SDPA calls such as is_causal=self.is_causal and attention_mask is None and q_len > 1 are rewritten with causal=self. On AMD+aiter that makes the aiter kernel see the module object as a truthy causal flag instead of the original boolean expression, producing causal attention in cases where the original SDPA call would not.

Useful? React with 👍 / 👎.

is_causal = causal_match.group(1) if causal_match else "False"

# Build aiter call:
# aiter expects (B, S, H, D) but transformers uses (B, H, S, D) -> transpose 1,2
return (
f"{indent}# AMD aiter Flash Attention (MFMA + fast softmax + causal tile skip)
"
f"{indent}# Requires: pip install amd-aiter (ROCm >= 7.0)
"
f"{indent}_aiter_q = {q_var}.transpose(1, 2) # (B,H,S,D) -> (B,S,H,D)
"
f"{indent}_aiter_k = {k_var}.transpose(1, 2)
"
f"{indent}_aiter_v = {v_var}.transpose(1, 2)
"
f"{indent}_aiter_fn = get_amd_flash_attn_func()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Import the flash helper into generated modules

The replacement emits get_amd_flash_attn_func() into the compiled cache file, but create_new_function() builds that file with fixed imports plus symbols from the Transformers modeling module; it does not import this helper from unsloth_zoo.device_type. On ROCm+aiter, the rewritten forward will therefore fail with NameError at runtime as soon as it reaches the aiter branch.

Useful? React with 👍 / 👎.

"
f"{indent}if _aiter_fn is not None:
"
f"{indent} {out_var} = _aiter_fn(_aiter_q, _aiter_k, _aiter_v, causal={is_causal}).transpose(1, 2)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve SDPA mask arguments on the aiter path

The aiter branch uses only the extracted is_causal flag and discards the rest of the original SDPA arguments. On AMD systems with aiter, any attention call that carries a padding/sliding attn_mask, training dropout_p, custom scale, or enable_gqa will silently compute different attention than the original SDPA call instead of falling back to the SDPA path that still receives rest_args.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip aiter for float32 attention inputs

This branch passes q/k/v to aiter unconditionally, but the repo has forced-float32 attention patches that deliberately call SDPA with float32 tensors (for example unsloth_zoo/temporary_patches/qwen3_moe_float32.py:153-155 and :218-226) to avoid fp16 overflows, while the new helper documents aiter inputs as float16 or bfloat16. On AMD+aiter with UNSLOTH_FORCE_FLOAT32=1, the rewrite sends those float32 tensors into aiter instead of the SDPA fallback, causing unsupported-dtype failures or bypassing the stability fix.

Useful? React with 👍 / 👎.

"
f"{indent}else:
"
f"{indent} {out_var} = torch.nn.functional.scaled_dot_product_attention(
"
f"{indent} {q_var}, {k_var}, {v_var}{rest_args})"
)

new_source, n = re.subn(sdpa_call_pattern, aiter_replacement, source, flags=re.DOTALL)
if n > 0:
pass # Successfully replaced SDPA with aiter
return new_source
Comment on lines +407 to +488

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

critical

There are two critical NameError bugs, some robustness issues, and compliance concerns with repository rules in this implementation:

  1. Compile-time NameError: get_amd_attention_implementation is called on line 398 but is never imported in unsloth_zoo/compiler.py.
  2. Runtime NameError: get_amd_flash_attn_func is used in the generated code on line 446, but it is not imported in the compiled module, which will fail when the compiled module is loaded and executed.
  3. Regex Robustness & Line Structure Preservation: The regex pattern ([^)]) for rest_args will fail if any argument contains nested parentheses. We can use ((?:[^)]|([^)]))*)) to safely support one level of nested parentheses. Additionally, when patching code via regex, we must ensure replacement strings do not introduce unintended blank lines by checking if captured suffixes (like rest_args) already contain leading newlines.
  4. Causal Fast-Path Routing: When optimizing attention mechanisms by routing to fast paths (like aiter FlashAttention) that assume causal masking, we must ensure that bidirectional attention layers (where is_causal is False and attention_mask is None) are not incorrectly forced to be causal. We should explicitly verify that the call is causal before routing to the causal-only fast path.

We can resolve all of these issues cleanly by importing get_amd_attention_implementation at the start of the function, importing get_amd_flash_attn_func directly inside the generated code block, checking if is_causal is True before routing to _aiter_fn, cleaning up leading newlines in rest_args, and updating the regex patterns.

    from unsloth_zoo.device_type import get_amd_attention_implementation
    if get_amd_attention_implementation() != "amd_aiter":
        return source

    sdpa_call_pattern = (
        r"([ \t]*)([A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*"
        r"(?:[A-Za-z_.]*scaled_dot_product_attention)"
        r"\("
        r"[ \t\n]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*,"
        r"[ \t\n]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*,"
        r"[ \t\n]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*"
        r"((?:[^)]|\([^)]*\))*)\)"
    )

    import re

    def aiter_replacement(m):
        indent = m.group(1)
        out_var = m.group(2)
        q_var = m.group(3)
        k_var = m.group(4)
        v_var = m.group(5)
        rest_args = m.group(6)

        causal_match = re.search(r"is_causal\s*=\s*([^,]+)", rest_args)
        is_causal = causal_match.group(1).strip() if causal_match else "False"

        rest_args_clean = rest_args.lstrip('\r\n')
        if rest_args_clean != rest_args:
            rest_args_formatted = ", " + rest_args_clean.strip() if rest_args_clean.strip() else ""
        else:
            rest_args_formatted = rest_args

        return (
            f"{indent}# AMD aiter Flash Attention (MFMA + fast softmax + causal tile skip)\n"
            f"{indent}# Requires: pip install amd-aiter  (ROCm >= 7.0)\n"
            f"{indent}from unsloth_zoo.device_type import get_amd_flash_attn_func\n"
            f"{indent}_aiter_fn = get_amd_flash_attn_func()\n"
            f"{indent}if _aiter_fn is not None and {is_causal}:\n"
            f"{indent}    _aiter_q = {q_var}.transpose(1, 2)\n"
            f"{indent}    _aiter_k = {k_var}.transpose(1, 2)\n"
            f"{indent}    _aiter_v = {v_var}.transpose(1, 2)\n"
            f"{indent}    {out_var} = _aiter_fn(_aiter_q, _aiter_k, _aiter_v, causal=True).transpose(1, 2)\n"
            f"{indent}else:\n"
            f"{indent}    {out_var} = torch.nn.functional.scaled_dot_product_attention(\n"
            f"{indent}        {q_var}, {k_var}, {v_var}{rest_args_formatted})"
        )

    new_source, n = re.subn(sdpa_call_pattern, aiter_replacement, source, flags=re.DOTALL)
    if n > 0:
        pass
    return new_source
References
  1. When patching code via regex, ensure replacement strings do not introduce unintended blank lines by checking if captured suffixes already contain leading newlines. Verify the fix using tests that check for line structure preservation and, if applicable, live round-trip checks against the upstream source.
  2. When optimizing attention mechanisms by routing to fast paths (like FlashAttention) that assume causal masking, ensure that bidirectional attention layers (where is_causal is False and attention_mask is None) are not incorrectly forced to be causal. Explicitly verify that the call is causal or has an explicit mask before routing to a causal-only fast path.



def _get_compile_folder(use_tempfile=False):
global UNSLOTH_COMPILE_LOCATION
global UNSLOTH_COMPILE_USE_TEMP
Expand Down Expand Up @@ -3809,6 +3898,8 @@ def _def_pos(name):
disabled_scaled_dot_product_attention_modules.append(module)
pass
pass
# AMD ROCm: replace SDPA with amd-aiter Flash Attention if available
new_source = replace_sdpa_with_amd_aiter(new_source)
scaled_dot_product_attention_modules[module] = new_source
pass

Expand Down
67 changes: 67 additions & 0 deletions unsloth_zoo/device_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

__all__ = [
"is_hip",
"get_amd_attention_implementation",
"get_amd_flash_attn_func",
"get_device_type",
"DEVICE_TYPE",
"DEVICE_TYPE_TORCH",
Expand Down Expand Up @@ -213,6 +215,71 @@ def is_hip():
return bool(getattr(getattr(torch, "version", None), "hip", None))
pass


@functools.cache
def get_amd_attention_implementation():
"""
Return the best available attention implementation for AMD ROCm.

Priority:
1. "amd_aiter" — AMD aiter (pip install amd-aiter, ROCm >= 7.0 required)
2. "sdpa" — PyTorch SDPA via MIOpen (always available on ROCm)

Returns: "amd_aiter" | "sdpa"
"""
if not is_hip():
return "sdpa"

# Gate on ROCm >= 7.0 (amd-aiter has hard ABI dep on libamdhip64.so.7)
try:
rocm_version = _detect_rocm_major_minor()
if rocm_version is not None:
major = int(rocm_version.split(".")[0])
if major < 7:
return "sdpa"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use torch.version.hip before gating on ROCm major

In wheel-only ROCm 7 PyTorch installs that lack /opt/rocm/hipcc but do have rocm-smi, _detect_rocm_major_minor() can fall through to rocm-smi --showdriverversion; the ROCm SMI docs describe that flag as printing the AMDGPU module/kernel driver version, not the ROCm runtime version. A 6.x kernel driver string therefore makes this new gate return sdpa before checking aiter, even when torch.version.hip is 7.x, so the advertised AMD aiter path is disabled on supported wheel-based installs.

Useful? React with 👍 / 👎.

except Exception:
return "sdpa"

# Check for amd-aiter (AMD AI Tensor Engine for ROCm)
try:
import importlib.util # must import submodule explicitly (not bare importlib)
if importlib.util.find_spec("aiter") is not None:
import aiter as _aiter
# Validate: AMD AI tensor engine exposes flash_attn_func or FlashAttnFunc
if hasattr(_aiter, "flash_attn_func") or hasattr(_aiter, "FlashAttnFunc"):
return "amd_aiter"
except Exception:
pass

return "sdpa"


@functools.cache
def get_amd_flash_attn_func():
"""
Return the amd-aiter flash attention function, or None if unavailable.

Checks both naming conventions across amd-aiter versions:
- flash_attn_func (newer, lowercase)
- FlashAttnFunc (older, class-based callable)

Call signature: func(q, k, v, causal=True)
Shapes: q/k/v = (batch, seqlen, nheads, headdim), float16 or bfloat16
Returns: output tensor, same shape as q
"""
if get_amd_attention_implementation() != "amd_aiter":
return None
try:
import aiter as _aiter
if hasattr(_aiter, "flash_attn_func"):
return _aiter.flash_attn_func
if hasattr(_aiter, "FlashAttnFunc"):
return _aiter.FlashAttnFunc

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Call FlashAttnFunc.apply instead of returning the class

When an installed aiter build exposes only FlashAttnFunc, this returns the autograd Function class itself. The rewritten forward later calls the returned object as _aiter_fn(q, k, v, causal=...), but that class-based API needs .apply(...) with the full positional argument list, so those environments crash instead of using aiter or falling back to SDPA.

Useful? React with 👍 / 👎.

except Exception:
pass
return None


@functools.cache
def get_device_type():
if _IS_MLX:
Expand Down
Loading