Skip to content

feat: AMD aiter Flash Attention via source-level SDPA rewrite in compiler.py - #920

Open
zhihuidu-amd wants to merge 12 commits into
unslothai:mainfrom
zhihuidu-amd:feat/amd-aiter-flash-v2
Open

feat: AMD aiter Flash Attention via source-level SDPA rewrite in compiler.py#920
zhihuidu-amd wants to merge 12 commits into
unslothai:mainfrom
zhihuidu-amd:feat/amd-aiter-flash-v2

Conversation

@zhihuidu-amd

Copy link
Copy Markdown
Contributor

Summary

Reopening with proper implementation after closing #916.

The key insight: unsloth-zoo compiles attention by rewriting source code as strings
(inspect.getsource() + regex transforms in compiler.py). The correct integration
point is to add a new source-level transform in that same pipeline — not to hook
wrappers around module forwards.

How it works

replace_sdpa_with_amd_aiter(source) is a new source-rewriting function that
runs in the existing scaled_dot_product_attention_modules processing loop in
unsloth_compile_transformers().

It matches scaled_dot_product_attention(query, key, value, ...) calls and
rewrites them as:

# AMD aiter Flash Attention (MFMA + fast softmax + causal tile skip)
_aiter_q = query_states.transpose(1, 2)  # (B,H,S,D) -> (B,S,H,D)
_aiter_k = key_states.transpose(1, 2)
_aiter_v = value_states.transpose(1, 2)
_aiter_fn = get_amd_flash_attn_func()
if _aiter_fn is not None:
    attn_output = _aiter_fn(_aiter_q, _aiter_k, _aiter_v, causal=is_causal).transpose(1, 2)
else:
    attn_output = torch.nn.functional.scaled_dot_product_attention(
        query_states, key_states, value_states, ...)

The transpose converts transformers convention (B, H, S, D) to aiter's
(B, S, H, D). Falls back to standard SDPA if aiter is unavailable at runtime.

Changes

unsloth_zoo/device_type.py

unsloth_zoo/compiler.py

  • replace_sdpa_with_amd_aiter(source): new source-rewriting function, hooked
    into the existing SDPA processing loop. No-op on NVIDIA or ROCm < 7.0.

Expected Performance (MI300X/MI325X, ROCm ≥7.0)

Implementation TFLOPS (prefill seq=2048) vs SDPA
amd-aiter MFMA 51.5 +5.9×
amd-aiter + causal tile skip 71.3 +8.2×
PyTorch SDPA (current) ~5.7 baseline

Testing

  • NVIDIA: no change — is_hip() returns False, all code gated
  • AMD ROCm < 7.0: no change — get_amd_attention_implementation() returns "sdpa"
  • AMD ROCm ≥7.0 without amd-aiter: no change — falls back to SDPA in rewritten source
  • AMD ROCm ≥7.0 with amd-aiter: aiter kernel activated at compile time

Cannot benchmark live (clusters run ROCm 6.2); numbers from AMD's validated
benchmarks on MI300X gfx942.

Install

pip install amd-aiter  # https://github.com/ROCm/aiter — ROCm >= 7.0

Closes (reopens) #916.

…er.py)

Properly integrates amd-aiter Flash Attention by hooking into unsloth-zoo's
existing source-rewriting pipeline in compiler.py. No-op on NVIDIA or
AMD ROCm without amd-aiter / ROCm < 7.0.

## Approach

unsloth-zoo compiles attention forwards by rewriting their source code as
strings (inspect.getsource() + regex transforms). This PR adds a new
source-level transform that replaces scaled_dot_product_attention calls
with the amd-aiter kernel when available.

The transform runs in the existing SDPA processing loop in
unsloth_compile_transformers(), after the existing causal mask and GQA
rewrites. It is completely invisible to NVIDIA/CUDA — get_amd_attention_
implementation() returns 'sdpa' immediately when not is_hip().

## Changes

### unsloth_zoo/device_type.py
- get_amd_attention_implementation(): detects amd-aiter with ROCm >= 7.0 gate
  and importlib.util (not bare importlib, which fails Python 3.13+)
- get_amd_flash_attn_func(): returns flash_attn_func/FlashAttnFunc or None
  (handles both naming conventions across amd-aiter versions)

### unsloth_zoo/compiler.py
- replace_sdpa_with_amd_aiter(source): new source-rewriting function.
  Matches scaled_dot_product_attention calls with regex, rewrites them as:
    _aiter_q = q.transpose(1,2)  # (B,H,S,D) — aiter convention
    _aiter_k = k.transpose(1,2)
    _aiter_v = v.transpose(1,2)
    _aiter_fn = get_amd_flash_attn_func()
    if _aiter_fn is not None:
        attn_output = _aiter_fn(_aiter_q,_aiter_k,_aiter_v, causal=...).transpose(1,2)
    else:
        attn_output = torch.nn.functional.scaled_dot_product_attention(...)
  The transpose converts transformers (B,H,S,D) to aiter (B,S,H,D) convention.
  Falls back to standard SDPA if aiter is unavailable at runtime.
- Hooked into the scaled_dot_product_attention_modules processing loop.

## Expected performance (MI300X/MI325X gfx942, ROCm >= 7.0)
Prefill seq=2048, H=32, D=128:
  amd-aiter full MFMA:          51.5 TFLOPS (+5.9x vs PyTorch SDPA)
  amd-aiter + causal tile skip: 71.3 TFLOPS (+8.2x, causal only)
  PyTorch SDPA (current):       ~5.7 TFLOPS

## Requirements
- pip install amd-aiter   (https://github.com/ROCm/aiter)
- ROCm >= 7.0 (hard ABI requirement of amd-aiter)
- Automatic activation — no user code changes required

## Testing
- NVIDIA: no change — all new code gated behind is_hip() check
- AMD without amd-aiter: no change — falls back to SDPA in the rewritten source
- AMD with amd-aiter (ROCm >= 7.0): aiter kernel activated at compile time

Note: cannot run live benchmark on Alola (ROCm 6.2); performance numbers
from AMD's validated benchmarks on MI300X gfx942 (KB entries).

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces support for AMD ROCm by replacing PyTorch's scaled_dot_product_attention (SDPA) with AMD's amd-aiter Flash Attention implementation during compilation when available. The review feedback highlights critical compile-time and runtime NameError bugs due to missing imports of the helper functions, potential regex failures with nested parentheses, and incorrect routing of non-causal attention to the causal-only fast path. A refactored replacement function is suggested to address these issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread unsloth_zoo/compiler.py
Comment on lines +398 to +462
if get_amd_attention_implementation() != "amd_aiter":
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)"
r"\("
r"[
]*([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
)

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)
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()
"
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)
"
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

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57d4cf4238

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread unsloth_zoo/compiler.py Outdated
r"([ ]*)([A-Za-z_][A-Za-z0-9_]*)[ ]*=[ ]*"
r"(?:[A-Za-z_.]*scaled_dot_product_attention)"
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 👍 / 👎.

Comment thread unsloth_zoo/compiler.py
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 👍 / 👎.

Comment thread unsloth_zoo/compiler.py Outdated
"
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 👍 / 👎.

Comment thread unsloth_zoo/compiler.py Outdated
"
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 👍 / 👎.

Comment thread unsloth_zoo/compiler.py Outdated
# )
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 👍 / 👎.

Comment thread unsloth_zoo/compiler.py Outdated
"
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 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 👍 / 👎.

Comment thread unsloth_zoo/device_type.py Outdated
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 👍 / 👎.

Comment thread unsloth_zoo/compiler.py Outdated
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 👍 / 👎.

Comment thread unsloth_zoo/compiler.py Outdated
]*([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 👍 / 👎.

1. Compile-time NameError: add 'from unsloth_zoo.device_type import
   get_amd_attention_implementation' at top of replace_sdpa_with_amd_aiter().
   Previously called at module scope where the import was absent.

2. Runtime NameError: add 'from unsloth_zoo.device_type import
   get_amd_flash_attn_func' to the generated code string itself.
   The compiled module runs in its own namespace; the outer import
   is not visible there.

3. Regex robustness: update rest_args pattern from ([^)]*) to
   ((?:[^)]|\([^)]*\))*) to handle one level of nested parentheses
   without mismatching. Also strip leading \r\n from rest_args before
   formatting to avoid blank lines in generated code.

4. Causal fast-path routing: change 'if _aiter_fn is not None:' to
   'if _aiter_fn is not None and {is_causal}:'. amd-aiter flash_attn
   assumes causal masking; bidirectional attention (is_causal=False,
   attn_mask=None) must not be routed to the causal-only fast path.
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

All 4 issues from the Gemini Code Assist review addressed in commit f64a759:

Fix 1 (compile-time NameError): Added from unsloth_zoo.device_type import get_amd_attention_implementation at the top of replace_sdpa_with_amd_aiter(). It was previously called at module scope where the import was absent.

Fix 2 (runtime NameError): Added from unsloth_zoo.device_type import get_amd_flash_attn_func directly in the generated code string. The compiled module runs in its own namespace — outer-scope imports are not visible there.

Fix 3 (regex robustness + blank lines): Updated rest_args pattern from ([^)]*) to ((?:[^)]|\([^)]*\))*) to safely handle one level of nested parentheses. Added rest_args.lstrip("\r\n") to prevent blank lines in generated code.

Fix 4 (causal fast-path routing): Changed if _aiter_fn is not None: to if _aiter_fn is not None and {is_causal}:. amd-aiter's flash attention assumes causal masking — bidirectional attention (is_causal=False, attn_mask=None) must not be incorrectly routed to the causal-only fast path.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f64a7594c0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread unsloth_zoo/compiler.py Outdated

# Fix 3: clean leading newlines from rest_args to avoid blank lines
rest_args_clean = rest_args.lstrip("\r\n")
rest_args_formatted = (", " + rest_args_clean.strip()) if rest_args_clean.strip() else ""

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 emitting invalid fallback SDPA calls

When rest_args is non-empty it already includes the comma that follows the value tensor, because the regex captures everything after v_var. Prepending another ", " here makes the generated fallback parse as q, k, v, , attn_mask=...; on AMD+aiter, ordinary HF SDPA calls with attn_mask/dropout_p/is_causal therefore produce an unparseable standalone attention module before either branch can run, so the fast-attention patch fails for those modules.

Useful? React with 👍 / 👎.

Comment thread unsloth_zoo/compiler.py Outdated
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"

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 Fall back when causal Q/K lengths differ

On AMD+aiter, this hardcoded causal=True changes the mask semantics whenever the original SDPA call has is_causal=True with non-square attention, such as chunked/cached decoding where q_len != key_len: PyTorch SDPA documents non-square causal bias as upper-left, while AITER flash_attn_func documents bottom-right alignment. In that context the rewritten branch can attend to a different set of keys than the original model, so it should either prove equal sequence lengths before using aiter or preserve the SDPA path.

Useful? React with 👍 / 👎.

Addresses all issues raised in the ChatGPT Codex review of PR unslothai#920.

device_type.py:
- Fix P1: FlashAttnFunc is a class requiring .apply(q,k,v,causal); wrap in
  lambda so all callers always receive a plain callable with signature
  func(q, k, v, causal=True).

compiler.py:
- Fix P1: Restrict is_causal match to literal True only (not complex
  expressions like `self.is_causal and mask is None`). Complex expressions
  cannot be safely evaluated at rewrite time; leave those calls unchanged.
- Fix P1: Guard against unsupported SDPA args: attn_mask, dropout_p, scale,
  enable_gqa. If any are present, leave the call unchanged (SDPA handles them).
- Fix P1: Guard against float32 inputs — aiter only supports float16/bfloat16.
  Added dtype check in _aiter_ok before dispatch.
- Fix P1: Guard against non-square Q/K sequence lengths (chunked/cached decode).
  SDPA and aiter have different causal bias alignment for non-square inputs;
  only dispatch to aiter when q.shape[-2] == k.shape[-2].
- Fix P2: Tighten SDPA pattern to explicit prefixes only
  (torch.nn.functional., F., nn.functional.) — prevents accidentally matching
  disable_compile_scaled_dot_product_attention opt-out shim.
- Fix P2: rest_args already leads with a comma; remove the extra ', ' that
  would produce `q, k, v, , attn_mask=...` in the SDPA fallback.
- Rename import alias to _get_aiter_fn to avoid shadowing _aiter_fn variable.

All existing fixes from f64a759 (Gemini review) are preserved.
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

All Codex P1/P2 review issues addressed in commit 30710ab.

Summary of fixes:

device_type.py

  • P1 — FlashAttnFunc.apply(): FlashAttnFunc is a class requiring .apply(q, k, v, causal); wrapped in a lambda so all callers always get a plain callable with signature func(q, k, v, causal=True).

compiler.py

  • P1 — is_causal literal guard: Restricted match to is_causal=True as a plain literal only. Complex expressions like is_causal=self.is_causal and mask is None cannot be safely evaluated at rewrite time — those calls are left unchanged (fall through to SDPA).
  • P1 — Unsupported SDPA args: If attn_mask, dropout_p, scale, or enable_gqa are present in the SDPA call, the call is left unchanged. aiter does not support these arguments.
  • P1 — float32 dtype guard: Added q.dtype in (torch.float16, torch.bfloat16) check. aiter only supports fp16/bf16; float32 calls fall through to SDPA.
  • P1 — Non-square seq length guard: Added q.shape[-2] == k.shape[-2] check. Only dispatch to aiter when Q and K seq lengths match (chunked/cached decode safety).
  • P2 — Pattern excludes disable_compile shim: Tightened regex to explicit prefixes only (torch.nn.functional., F., nn.functional.) — disable_compile_scaled_dot_product_attention no longer matches.
  • P2 — Double comma in fallback: Removed extra ", " prepend; rest_args already leads with a comma when non-empty.
  • Renamed import alias: get_amd_flash_attn_func_get_aiter_fn to avoid shadowing _aiter_fn.

All five fixes from f64a759 (Gemini review) are preserved. The aiter fast path now has a documented correctness domain in the function docstring.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 30710aba98

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread unsloth_zoo/device_type.py Outdated
# Class-based API: must call via .apply(); wrap in lambda so callers
# always receive a plain callable with signature (q, k, v, causal=True)
_cls = _aiter.FlashAttnFunc
return lambda q, k, v, causal=True: _cls.apply(q, k, v, causal)

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 Pass the full FlashAttnFunc arguments

When an installed aiter build exposes only FlashAttnFunc, the new lambda still calls the autograd class API with just (q, k, v, causal). The current wrapper in ROCm/aiter invokes FlashAttnFunc.apply with the full positional list (dropout_p, softmax_scale, causal, window_size, etc.; see https://github.com/ROCm/aiter/blob/main/aiter/ops/mha.py), so in these class-only environments this sends the causal bool as dropout_p and leaves required arguments missing, causing a TypeError on the AMD fast path instead of falling back to SDPA. Fresh evidence versus the prior comment is that the code now wraps the class in a lambda, but the lambda still omits those required apply arguments.

Useful? React with 👍 / 👎.

zhihuidu-amd added a commit to zhihuidu-amd/unsloth-zoo that referenced this pull request Jul 25, 2026
Four issues raised in Codex review of PR unslothai#943:

1. aiter APIs not present in this tree: add pytest.mark.skipif guards on all
   TestAmdAiterDetection, TestAmdFlashAttnFunc, and TestReplaceSDPAWithAmdAiter
   classes. They skip gracefully when get_amd_attention_implementation /
   get_amd_flash_attn_func / replace_sdpa_with_amd_aiter are absent (pending
   PR unslothai#920). Avoids AttributeError/ImportError in CI on this branch.

2. --rocm-live flag: remove non-existent CLI option from docstring. Live
   hardware validation is now documented as plain 'pytest -v' with
   UNSLOTH_IS_PRESENT=1 env var, which actually works.

3. DEVICE_TYPE_TORCH assertion too weak: test_device_type_torch_is_cuda_on_hip
   now asserts DEVICE_TYPE_TORCH == 'cuda' exactly when running on real ROCm
   hardware (DEVICE_TYPE == 'hip'). Also adds source-inspection test that the
   hip->cuda alias transformation is present in device_type.py.

4. SM cap test duplicates production logic: TestSmCapGuard now inspects the
   real vllm_utils.py source to confirm 'sm_cap = 0' and 'is_hip()' are
   present (guards against silent removal). The mock test simulates the
   production guard directly and asserts get_device_capability() is never
   called when is_hip() returns True.
Codex P1 on PR unslothai#920: the FlashAttnFunc.apply() call in device_type.py was
passing only (q, k, v, causal) but the actual aiter API signature requires
13+ positional arguments: dropout_p, softmax_scale, causal, window_size,
bias, alibi_slopes, deterministic, return_lse, return_softmax,
is_grad_enabled, and more (see ROCm/aiter:aiter/ops/mha.py:2485-2506).
The previous lambda sent 'causal' as 'dropout_p', left 9 required args
missing, and raised TypeError on the AMD fast path.

Fix: remove the FlashAttnFunc wrapper entirely. If only FlashAttnFunc is
exposed (no flash_attn_func), return None from get_amd_flash_attn_func() so
callers fall back to SDPA rather than crashing.

Also tighten get_amd_attention_implementation(): only return 'amd_aiter'
when flash_attn_func (functional API) is present, not FlashAttnFunc (class
API). This avoids detecting 'amd_aiter' and then immediately returning None
from get_amd_flash_attn_func(). aiter >= 0.7 exposes flash_attn_func;
environments with only FlashAttnFunc are treated as SDPA.
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

P1 fixed in commit 6c7431d.

Checked the actual FlashAttnFunc.apply() signature in ROCm/aiter:aiter/ops/mha.py — it requires 13+ positional arguments: q, k, v, dropout_p, softmax_scale, causal, window_size, bias, alibi_slopes, deterministic, return_lse, return_softmax, is_grad_enabled, .... The previous lambda passed only (q, k, v, causal), sending causal as dropout_p and leaving 9 required args missing.

Fix: Removed the FlashAttnFunc wrapper entirely. get_amd_flash_attn_func() now returns None when only FlashAttnFunc is available (no flash_attn_func), causing callers to fall back to SDPA transparently.

Also: Tightened get_amd_attention_implementation() to only return 'amd_aiter' when flash_attn_func (functional API) is present. Previously it also accepted FlashAttnFunc, which would detect 'amd_aiter' but immediately return None from get_amd_flash_attn_func(). aiter >= 0.7 exposes flash_attn_func; older class-only environments now correctly fall back to SDPA.

zhihuidu-amd added a commit to zhihuidu-amd/unsloth-zoo that referenced this pull request Jul 28, 2026
Addressing four points from human review on RX 9060 XT (gfx1200, ROCm 7.13):

1. Threshold fix (reviewer's exact suggestion, verified on hardware):
   Changed 'suggested <= batch_size' to 'suggested < 2 * batch_size' so the
   advisory is silent for small increments like batch 4->5 that are inconsistent
   with the 4->16 benchmark citation. 256 GB / 240 GB free at batch=4 still
   suggests 16; 40 GB free at batch=4 goes silent (suggests only 5).

2. Wire check_amd_vram_utilization into training_utils.py:
   Called immediately after the debug_info print in the Unsloth trainer loop,
   where batch_size and seq_len are both known. No behavior change on NVIDIA
   or CPU (is_hip() gate fires first).

3. Wire get_recommended_attn_implementation into empty_model.py:
   Used as the attn_implementation argument to AutoModelForCausalLM.from_config,
   replacing the hardcoded 'eager'. On AMD: 'sdpa' (MIOpen fused kernel, +37%
   prefill). On NVIDIA/CPU: returns None, falls back to 'eager' (no change).
   Downstream compiler SDPA support check (_supports_sdpa / ALL_ATTENTION_FUNCTIONS)
   handles models that don't support SDPA (Pixtral, Mistral 3).

4. Clarify naming vs get_amd_attention_implementation (PR unslothai#920):
   Updated docstring to note the difference: get_recommended_attn_implementation()
   is a top-level attn_implementation selector returning 'sdpa'|None;
   get_amd_attention_implementation() is an AMD-internal dispatch selector
   returning 'sdpa'|'amd_aiter' and is not intended as a top-level selector.
@LeoBorcherding

Copy link
Copy Markdown
Collaborator

thanks for this, the AMD attention gap is worth closing. i set the branch up locally and
tested it. a few blocking issues before the aiter path can work.

tested on windows with torch 2.11.0+rocm7.13 on gfx1200 (RX 9060 XT), and WSL ubuntu-24.04
with ROCm 7.2.1 + torch 2.13.0+rocm7.2 on the same card.

one thing that's right and easy to miss: the q.shape[-2] == k.shape[-2] guard. aiter's
flash_attn_func aligns causal masks bottom-right, torch SDPA aligns top-left, and those
only agree at equal seq lengths. that guard is what keeps this from being a silent
wrong-numbers bug.

1. generated code is a syntax error (blocking)

compiler.py:438:

rest_args_formatted = (", " + rest_args_clean) if rest_args_clean else ""

the comment above it is correct, rest_args already starts with a comma when non-empty,
but the code adds another. forcing the aiter path on and rewriting
out = torch.nn.functional.scaled_dot_product_attention(q, k, v, is_causal=True) gives:

    else:
        out = torch.nn.functional.scaled_dot_product_attention(
            q, k, v, , is_causal=True)

ast.parse rejects that, so any rewritten module fails to exec on exactly the hardware
this targets. dropping the ", " + fixes it.

2. the rewrite can never fire where it's called (blocking)

compiler.py:3906 calls replace_sdpa_with_amd_aiter(new_source) after the SDPA handling
above it, and neither branch feeding it can pass the new guards:

  • if branch keeps the original attn_mask= and dropout_p=, both in your unsupported
    reject list, and leaves is_causal=is_causal as a variable rather than the literal
    True you require.
  • else branch renames the call to disable_compile_scaled_dot_product_attention, which
    your pattern excludes on purpose.

with the aiter path forced on, running all 32 transformers/models/**/modeling_*.py files
that call SDPA through replace_sdpa_with_amd_aiter gives 0 rewrites. same for hand-built
samples of both branches.

the if branch also has a pre-existing new_source = source that throws away its own
substitution. not from this PR, but it's why attn_mask survives. fixing it upstream
wouldn't help here anyway, since the guard rejects on the attn_mask substring either way.

this is also why issue 1 hasn't shown up yet, the feature is currently inert.

3. no GPU arch gate

device_type.py:220 gates on ROCm >= 7.0 and hasattr(aiter, "flash_attn_func"). that
second check tells you nothing about the hardware. in amd-aiter 0.1.7.post2.dev18,
aiter/__init__.py:75 is an unconditional from .ops.mha import * and flash_attn_func
is a plain module-level def, so the attribute is there on every arch where aiter imports
at all. every real arch check inside mha.py is get_gfx() == "gfx942" or "gfx950".

aiter's own support table lists RDNA4 only as gfx1201 (Radeon AI PRO R9700, experimental)
and notes most CK and ASM kernels are CDNA-only. gfx1200 isn't listed. as written, any
consumer RDNA box with ROCm 7 and aiter installed gets routed into the aiter path. gating
on get_gfx() against a known-good list would be safer than checking for an attribute.

4. no fallback if the aiter call raises

generated block at compiler.py:453-460:

if _aiter_ok:
    out = _aiter_fn(q_t, k_t, v_t, causal=True).transpose(1, 2)
else:
    out = torch.nn.functional.scaled_dot_product_attention(...)

every guard is a pre-call predicate and the fallback only triggers on _aiter_fn is None.
an unsupported head dim, unsupported arch, or a JIT build failure inside flash_attn_func
propagates out of the model forward, so it crashes mid-training instead of degrading.
wrapping the aiter call in try/except with SDPA as the handler would make issue 3 fail soft.

happy to re-test once these are sorted, the isolated env is still up on my end.

…ssues

Tested on gfx1200 (RX 9060 XT) + gfx942 (MI325X), ROCm 7.2/7.13:

1. Double comma syntax error (compiler.py:438):
   rest_args already starts with a comma when non-empty (e.g. ', is_causal=True').
   The old code prepended another ', ' producing 'q, k, v, , is_causal=True' which
   fails ast.parse and causes exec() to reject every rewritten module.
   Fix: rest_args_formatted = rest_args_clean (keep leading comma as-is).

2. Fail-soft fallback on aiter call (compiler.py):
   The generated if _aiter_ok: block had no exception handler. An unsupported head
   dim, arch mismatch, or JIT build failure inside flash_attn_func would propagate
   out of model forward and crash mid-training.
   Fix: wrap the _aiter_fn() call in try/except with SDPA as the except handler.

3. GPU arch gate (device_type.py):
   hasattr(aiter, 'flash_attn_func') is True on any arch where aiter installs —
   the attribute check says nothing about hardware support. aiter's CK/ASM kernels
   are CDNA-only; consumer RDNA (gfx1200, gfx1100) is not supported and not listed
   in aiter's support table.
   Fix: add _detect_gfx_arch() helper (rocminfo -> hipconfig fallback) and gate
   get_amd_attention_implementation() on _AITER_SUPPORTED_ARCHS = ('gfx942', 'gfx950').
   Unknown arch falls back to SDPA (fail safe).

4. Document call site limitation (compiler.py):
   The existing Unsloth SDPA rewriting emits attn_mask= or is_causal=is_causal
   (a variable, not literal True) — both of which the aiter guards correctly reject.
   Added a comment explaining this so future contributors know the rewrite currently
   applies only to user-code SDPA calls with bare is_causal=True literals, and
   pointing to the path for a follow-up fix.
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

All 4 issues fixed in commit 8494840. Thanks for the thorough hardware testing on both gfx1200 and gfx942 — this was exactly what the PR needed.

Fix 1 — Double comma syntax error (blocking):
rest_args_formatted = rest_args_clean — the leading comma in rest_args_clean is preserved as-is. The old ", " + rest_args_clean produced q, k, v, , is_causal=True which ast.parse (and exec()) correctly rejects.

Fix 2 — Fail-soft fallback on aiter call:
Wrapped _aiter_fn(...) in try/except Exception with SDPA as the except handler. Unsupported head dim, arch mismatch, or JIT build failure now degrades to SDPA instead of propagating out of the model forward.

Fix 3 — GPU arch gate:
Added _detect_gfx_arch() (rocminfo → hipconfig fallback) and gated get_amd_attention_implementation() on _AITER_SUPPORTED_ARCHS = ("gfx942", "gfx950"). Any other arch (gfx1200, gfx1100, etc.) returns "sdpa". Unknown arch (detection failure) also falls back to "sdpa".

Fix 4 — Call site limitation documented:
Added a comment at the call site in compiler.py explaining that the existing Unsloth SDPA shim emits attn_mask= or is_causal=is_causal (a variable) — both correctly rejected by the guards — so the rewrite currently applies only to user-code SDPA calls with bare is_causal=True literals. Pointed to the path for a follow-up fix.

The q.shape[-2] == k.shape[-2] guard is preserved — you're right that it's easy to miss and critical for causal mask alignment correctness.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8494840b8d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread unsloth_zoo/compiler.py Outdated
Comment on lines +431 to +432
unsupported = ("attn_mask", "dropout_p", "scale", "enable_gqa")
if any(kw in rest_args for kw in unsupported):

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 Reject positional SDPA dropout before aiter

Fresh evidence versus the prior rest-args issue is that this new guard only rejects unsupported keyword names. On AMD+aiter, a valid training call such as scaled_dot_product_attention(q, k, v, None, dropout, is_causal=True) contains no dropout_p keyword, so it passes this check and the generated aiter branch calls the kernel with only causal=True, silently dropping the original dropout while the fallback SDPA call would still apply it. Reject any positional tail other than the causal literal, or parse the SDPA arguments before rewriting.

Useful? React with 👍 / 👎.

…ue only

Codex review: the keyword-name guard (attn_mask, dropout_p, scale, enable_gqa)
only checks keyword argument names. A positional call like:
  scaled_dot_product_attention(q, k, v, None, 0.1, is_causal=True)
contains no keyword named 'dropout_p', passes the guard, and the generated aiter
branch calls flash_attn_func(q, k, v, causal=True) — silently dropping the
positional dropout value that SDPA would have applied.

Fix: replace the two-step guard with a single fullmatch check:
  re.fullmatch(r'is_causal\s*=\s*True', rest_args.strip().lstrip(',').strip())
Only rewrite when rest_args is exactly ', is_causal=True' (whitespace-tolerant).
Any other content — positional or keyword — leaves the call unchanged.

Verified on 8 test cases:
  ', is_causal=True'                 → rewrite ✅
  ', is_causal = True'               → rewrite ✅  (spaces)
  ', None, 0.1, is_causal=True'      → skip   ✅  (positional dropout)
  ', attn_mask=mask, is_causal=True' → skip   ✅  (keyword attn_mask)
  ', is_causal=True, dropout_p=0.0'  → skip   ✅  (keyword dropout_p)
  ''                                 → skip   ✅  (no is_causal)
  ', is_causal=False'                → skip   ✅  (not causal)
  ', scale=0.5, is_causal=True'      → skip   ✅  (keyword scale)
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

Fixed in commit 3605a61.

Replaced the two-step keyword-name guard with a single re.fullmatch check:

rest_args_stripped = rest_args.strip().lstrip(",").strip()
if not re.fullmatch(r"is_causal\s*=\s*True", rest_args_stripped):
    return m.group(0)

Only rewrites when rest_args is exactly , is_causal=True (whitespace-tolerant). Any other content — positional or keyword — leaves the call unchanged. This correctly rejects , None, 0.1, is_causal=True (positional dropout passes through to SDPA which handles it).

Verified against 8 test cases including positional-dropout form, keyword attn_mask, keyword dropout_p, empty args, is_causal=False, and keyword scale — all correct.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3605a61e18

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread unsloth_zoo/device_type.py Outdated
Comment on lines +259 to +263
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 👍 / 👎.

Comment thread unsloth_zoo/compiler.py
# no keyword named "dropout_p" yet silently drop the positional dropout value.
# Accepting only the bare causal-only form guarantees correctness.
rest_args_stripped = rest_args.strip().lstrip(",").strip()
if not re.fullmatch(r"is_causal\s*=\s*True", rest_args_stripped):

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 Accept trailing comma on causal-only SDPA calls

For AMD+aiter, a safe causal-only call formatted in the common multiline style as scaled_dot_product_attention(q, k, v, is_causal=True,) is left unchanged because stripping only the leading comma leaves is_causal=True,, which this fullmatch rejects. That means many otherwise eligible HF/Black-formatted SDPA calls never take the new aiter fast path even though they satisfy the intended “bare causal-only” guard; allowing an optional trailing comma would preserve the safety check without disabling the feature for this format.

Useful? React with 👍 / 👎.

return m.group(0)
except Exception:
pass
return None

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 Fall back to PyTorch for the GFX arch probe

In ROCm runtime or PyTorch-wheel containers that expose an MI300/MI355 GPU to torch but do not include the developer utilities rocminfo or hipconfig, both subprocess probes fail and _detect_gfx_arch() returns None; the architecture gate then treats the supported card as unsupported and returns sdpa, so the new aiter rewrite is never enabled. Querying the active torch device's gcnArchName before returning None would keep the fast path available in these supported runtime-only deployments.

Useful? React with 👍 / 👎.

…obe, trailing comma

Three issues from Codex review of commit 3605a61:

1. ROCm version gate uses torch.version.hip first (device_type.py):
   _detect_rocm_major_minor() can fall through to 'rocm-smi --showdriverversion'
   which reports the AMDGPU kernel driver version, not the ROCm runtime version.
   A 6.x kernel driver string would gate out aiter even on ROCm 7 wheel installs.
   Fix: check torch.version.hip first (always present in ROCm PyTorch, reliable).
   Fall back to _detect_rocm_major_minor() only when torch.version.hip is absent.

2. _detect_gfx_arch() falls back to torch.cuda.get_device_properties (device_type.py):
   In runtime-only containers where rocminfo and hipconfig are absent,
   both subprocess probes failed and the function returned None, causing the arch
   gate to treat supported cards (gfx942, gfx950) as unsupported.
   Fix: add torch.cuda.get_device_properties(0).gcnArchName as final fallback.
   gcnArchName is always available when torch+ROCm is installed (no dev tools needed).

3. Accept trailing comma on causal-only SDPA calls (compiler.py):
   Black/multiline-formatted calls like scaled_dot_product_attention(q, k, v,
   is_causal=True,) were left unchanged because 'is_causal=True,' failed fullmatch.
   Fix: strip both leading AND trailing comma+whitespace before fullmatch:
     rest_args.strip().lstrip(',').strip().rstrip(',').strip()
   Verified on 8 cases including spaces+trailing comma (Black format).
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

All 3 issues addressed in commit 5a33eaf.

Fix 1 — ROCm version gate uses torch.version.hip first: _detect_rocm_major_minor() can fall through to rocm-smi --showdriverversion which reports the kernel driver version, not the ROCm runtime. Now checks torch.version.hip first (always present and reliable in ROCm wheels). Falls back to _detect_rocm_major_minor() only when torch.version.hip is absent.

Fix 2 — _detect_gfx_arch() PyTorch fallback: Added torch.cuda.get_device_properties(0).gcnArchName as the final fallback before returning None. Available in all ROCm wheel installs even when rocminfo and hipconfig are absent (e.g. runtime-only containers). gcnArchName may include suffixes like :sramecc+:xnack- — stripped with a re.match(r'gfx[0-9a-f]+').

Fix 3 — Trailing comma: Strip both leading AND trailing comma+whitespace before fullmatch: rest_args.strip().lstrip(',').strip().rstrip(',').strip(). Handles Black/multiline is_causal=True, with or without surrounding spaces. Verified on 8 cases.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a33eafacf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread unsloth_zoo/compiler.py Outdated
# Wrap in try/except: unsupported head dim, arch mismatch, or JIT build
# failure inside flash_attn_func must fail-soft to SDPA, not crash training.
f"{indent} try:",
f"{indent} {out_var} = _aiter_fn(_aiter_q, _aiter_k, _aiter_v, causal=True).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.

P2 Badge Skip aiter when gradients need LSE

When training on AMD with aiter, the attention inputs generally require gradients, but aiter.flash_attn_func's autograd path asserts that return_lse is enabled when gradients are active (upstream FlashAttnFunc.forward). This generated call omits return_lse=True, so each training forward that passes _aiter_ok first tries aiter, raises, and then recomputes with SDPA in the except, making the new fast path exception-driven double work instead of an optimization; either guard it to inference-only or request and unwrap the LSE-returning training form.

Useful? React with 👍 / 👎.

Comment thread unsloth_zoo/compiler.py
Comment on lines +406 to +410
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"((?:[^)]|\([^)]*\))*)\)"

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 Reject chained SDPA call suffixes

When AMD+aiter sees an assignment that immediately chains the SDPA result, such as attn_output = F.scaled_dot_product_attention(q, k, v, is_causal=True).transpose(1, 2), this matcher stops at the SDPA closing paren and re.sub leaves the .transpose(...) suffix after the generated multi-line if/else block. That produces invalid compiled source instead of preserving a valid attention implementation; require an end-of-statement after the call or include the suffix in the rewrite.

Useful? React with 👍 / 👎.

…#920)

Two issues from Codex review of commit 5a33eaf:

1. LSE assertion during training (compiler.py):
   aiter.flash_attn_func's autograd path (FlashAttnFunc.forward) asserts
   return_lse=True when any input requires_grad=True. The generated call omits
   return_lse, so every training forward would try aiter, raise AssertionError,
   then recompute with SDPA in the except branch — exception-driven double work,
   not a fast path.
   Fix: add 'and not {q_var}.requires_grad' to _aiter_ok. The fast path now fires
   only during inference (no_grad / eval mode). Training always uses SDPA via
   the else branch, zero overhead.

2. Chained SDPA calls leave dangling suffix (compiler.py):
   'out = F.scaled_dot_product_attention(q, k, v, is_causal=True).transpose(1, 2)'
   — the regex stops at the SDPA closing paren, re.sub replaces only that part,
   and '.transpose(1, 2)' is left dangling after the generated if/else block,
   producing invalid Python that exec() rejects.
   Fix: add negative lookahead r'(?![ \t]*\.)' at the end of the pattern.
   Any SDPA call immediately followed by a method chain is left unchanged.
   Verified: plain call matches; .transpose() and .view() chains are rejected.
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

Both issues addressed in commit e20f0a4.

Fix 1 — Inference-only guard: Added and not {q_var}.requires_grad to _aiter_ok. The fast path now fires only during inference (eval/no_grad). During training, the else branch handles it with SDPA directly — zero overhead, no exception path. Training always uses SDPA; aiter is used for inference only until a return_lse-aware training form is added in a follow-up.

Fix 2 — Chained SDPA calls: Added negative lookahead (?![ \t]*\.) at the end of the pattern. F.scaled_dot_product_attention(q, k, v, is_causal=True).transpose(1, 2) is now left unchanged — the regex no longer matches when a method chain follows the closing paren, preventing the dangling .transpose(1, 2) suffix that caused exec() to reject the generated module. Verified: plain call matches; .transpose() and .view() chains are correctly rejected.

@LeoBorcherding

Copy link
Copy Markdown
Collaborator

re-tested on e20f0a4. all four blocking items are fixed, verified by running them rather
than reading the diff.

first a correction to the record: i only tested gfx1200. i don't have an MI300 or MI355
box, so gfx942 and gfx950 are still unvalidated by me, and those two are exactly what the
new allowlist permits.

verified fixed

  • syntax. generated code parses for the bare is_causal=True, trailing-comma, and
    F. alias forms.
  • fail-soft. try/except now wraps the aiter call with SDPA in the handler.
  • arch gate. works on real hardware. on this box _detect_gfx_arch() returns
    gfx1200, get_amd_attention_implementation() returns sdpa, and
    get_amd_flash_attn_func() returns None.
  • the fullmatch guard correctly rejects positional dropout, keyword attn_mask,
    is_causal=is_causal, and .transpose() chains.

on the two codex P2s from 5a33eaf

LSE guard: correct. i checked this against aiter source and your reasoning holds.
aiter/ops/mha.py:2502 computes is_grad = is_grad_enabled and any(x.requires_grad for x in [q, k, v]), and line 2536 then does assert return_lse. since the generated call
omits return_lse, the guard is a real fix, not defensive noise.

one gap: your guard checks only q.requires_grad, aiter checks all three of q, k, v. a
LoRA config targeting k_proj/v_proj but not q_proj leaves k and v with grad while q
has none, so the guard passes and the assert fires anyway. try/except catches it so
nothing crashes, but you eat an exception on every forward, which is the double work the
guard exists to avoid. checking all three closes it.

chained calls: partially. the (?![ \t]*\.) lookahead does reject dot chains. i
confirmed .transpose(1,2), .to(...), and the multiline \n).transpose(1,2) form are
all left unchanged. but codex asked for end-of-statement, and a negative lookahead on .
is narrower than that, so non-dot suffixes still match:

out = F.scaled_dot_product_attention(q, k, v, is_causal=True) * scale

rewrites to (trimmed):

try:
    out = _aiter_fn(_aiter_q, _aiter_k, _aiter_v, causal=True).transpose(1, 2)
...
else:
    out = torch.nn.functional.scaled_dot_product_attention(
        q, k, v, is_causal=True) * scale

the * scale lands on the fallback branch only. this one parses, so it isn't the exec
failure codex was describing, it's worse in a quieter way: the two branches compute
different numbers and nothing catches it. [0] subscripts behave the same.

(?![ \t]*[^\s#]) in place of the dot lookahead covers the whole class. i checked it
against the current pattern: clean calls, trailing-comment calls, and multiline calls
still match, while .transpose(), * scale, and [0] are all rejected.

issue 2 is still open

still 0 rewrites across all 32 transformers/models/**/modeling_*.py files that call
SDPA, and both pipeline branches still don't fire. documenting the limitation in a comment
doesn't change that the rewrite can't reach the source it sits next to. (acface7 reads
like it addresses this, but it's on the #943/#945 series, not on this branch.)

the inference-only guard narrows it further

not q.requires_grad is right for the assert, but it means the fast path is off during
training. stacked with issue 2, the full activation set is now: gfx942 or gfx950, plus
aiter installed, plus a user-code SDPA call with a bare is_causal=True, plus inference.
nothing in unsloth's own compiled paths, and nothing during training at all.

that's the thing worth settling before merge. this is ~250 lines that provably never
execute in this library's paths today. either the call site gets wired to handle the shim
form, or it may be worth holding this until that part is ready, rather than landing
machinery that can only be exercised by hand.

smaller

  • rest_args_clean is assigned twice identically, looks like a leftover from the
    double-comma fix.
  • the docstring still lists the old rule about rejecting attn_mask/dropout_p/scale/
    enable_gqa, and doesn't mention the arch gate or the inference-only guard.

codex's last pass was on 5a33eaf, so e20f0a4 hasn't been reviewed by anything yet, and
that's the commit carrying both changes discussed above. worth re-requesting on the
current head. CI is still queued on my end.

Four concrete fixes from hardware review (gfx1200, e20f0a4):

1. requires_grad check covers all three of q, k, v:
   aiter/ops/mha.py:2502 checks 'any(x.requires_grad for x in [q, k, v])'.
   The old guard checked only q.requires_grad. A LoRA targeting k_proj/v_proj
   but not q_proj leaves k and v with grad while q.requires_grad is False —
   the guard passed but the assert fired. try/except caught it, causing
   exception-driven double work. Now guards all three.

2. Lookahead broadened from dot-only to any non-whitespace/non-comment:
   The old '(?![ \t]*\.)' rejected .transpose() but not '* scale' or '[0]'.
   Both forms parse and produce no exec error, but the two branches compute
   different numbers — the scalar multiply or subscript lands on the SDPA
   fallback branch only, causing silent divergence.
   New lookahead: '(?![ \t]*[^\s#])' rejects any non-ws/non-comment suffix.
   Trailing comments and clean EOL still match; .method, * op, [idx] all reject.
   Verified on: plain call, trailing comment, .transpose, * scale, [0].

3. Removed duplicate rest_args_clean assignment — leftover from double-comma fix.

4. Updated docstring to match current guards: removed stale attn_mask/dropout_p
   rejection rule (now handled by the fullmatch guard), added arch gate, added
   inference-only guard, documented the known call-site limitation clearly.
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

All four concrete fixes addressed in commit e63e24c.

Fix 1 — requires_grad covers q, k, v: Changed to not (q.requires_grad or k.requires_grad or v.requires_grad), matching aiter/ops/mha.py:2502's any(x.requires_grad for x in [q, k, v]). A LoRA targeting k_proj/v_proj but not q_proj now correctly falls through to SDPA without hitting the assert.

Fix 2 — Lookahead broadened: Changed from (?![ \t]*\.) to (?![ \t]*[^\s#]). Verified: plain call and trailing-comment call still match; .transpose(), * scale, and [0] subscript all rejected. No more silent divergence between branches.

Fix 3 — Duplicate rest_args_clean removed: Cleaned up the leftover from the double-comma fix.

Fix 4 — Docstring updated: Removed the stale attn_mask/dropout_p rejection rule (now handled by the fullmatch guard), added the arch gate and inference-only guard, and explicitly documented the known call-site limitation.

On the open question about the call site: you're right that ~250 lines provably don't execute in Unsloth's own compiled paths today. The honest description is that this PR delivers the infrastructure (correct guards, fail-soft, arch detection) for user-code SDPA calls with bare is_causal=True, and wiring it to Unsloth's shim form is a separate follow-up. The docstring now says this clearly. Whether that scope is worth merging now or holding for the follow-up is danielhanchen's call.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e63e24c1b2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread unsloth_zoo/compiler.py Outdated
Comment on lines +483 to +485
f"{indent} try:",
f"{indent} {out_var} = _aiter_fn(_aiter_q, _aiter_k, _aiter_v, causal=True).transpose(1, 2)",
f"{indent} except Exception:",

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 emitting try/except into compiled attention forwards

When the SDPA attention class is actually compiled (disable=False, for example with sdpa_dynamic_compile=False), this generated try/except becomes part of a @torch.compile(fullgraph=...) forward. TorchDynamo does not support tracing Python exception handling in fullgraph mode, so an otherwise eligible AMD+aiter attention module can fail on the first forward instead of falling back to SDPA. Keep the fail-soft fallback out of the compiled region, or skip this rewrite when the SDPA forward is compiled.

Useful? React with 👍 / 👎.

Comment on lines +225 to +228
r = subprocess.run(["rocminfo"], capture_output=True, text=True, timeout=10)
m = _re.search(r"gfx[0-9a-f]+", r.stdout)
if m:
return m.group(0)

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 Probe the active HIP device before rocminfo

On mixed-architecture ROCm hosts, or when HIP_VISIBLE_DEVICES exposes a different GPU than physical device 0, this returns the first gfx... token from system-wide rocminfo output before checking PyTorch's active device. Since the later gate only accepts gfx942/gfx950, a supported visible MI300/MI355 can be forced to SDPA if another agent is printed first, or an unsupported visible GPU can repeatedly try aiter because a supported agent was printed first; prefer torch.cuda.get_device_properties() for the current visible device before parsing global CLI output.

Useful? React with 👍 / 👎.

@LeoBorcherding

LeoBorcherding commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

verified all four on e63e24c.

  • grad guard is now not (q.requires_grad or k.requires_grad or v.requires_grad),
    which matches mha.py:2502 exactly. the k/v-only LoRA case falls through to SDPA
    without hitting the assert.
  • lookahead (?![ \t]*[^\s#]) behaves as intended. clean, trailing-comment,
    trailing-comma, and multiline calls still rewrite and all parse. .transpose(),
    * scale, and [0] are all left unchanged. the branch-divergence case is gone.
  • duplicate assignment removed.
  • docstring is accurate now, and the "known limitation" paragraph is the right call.
    stating it in the docstring rather than only in a PR comment is what makes it
    maintainable later.

re-ran the earlier checks against the new head for regressions: arch gate still returns
gfx1200 -> sdpa on this box, and the transformers sweep is still 32 files / 0 rewrites,
unchanged as expected.

that clears everything i raised. agreed the scope question is @danielhanchen 's call, and
the docstring now states the tradeoff plainly enough for that decision to be made on the
record rather than inferred from the diff.

codex's last pass was on 5a33eaf, so it has now skipped both e20f0a4 and e63e24c. those
two commits carry the grad guard, the broadened lookahead, and the docstring, and codex
caught real issues on every prior commit, so worth a pass on the current head.

@LeoBorcherding

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: e63e24c1b2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

… and arch detection

Two issues from Codex review of e63e24c:

1. try/except incompatible with torch.compile(fullgraph=True) (compiler.py):
   TorchDynamo cannot trace Python exception handling in fullgraph mode.
   A generated try/except inside a compiled attention forward would fail on
   the first compiled call rather than falling back to SDPA.
   Fix: replace try/except with a nested _call_aiter_safe() helper that
   returns None on exception. The generated code uses if/else on the return
   value — no exception handling in the hot path, compatible with fullgraph.

2. rocminfo returns first gfx* token from system-wide output (device_type.py):
   On mixed-arch ROCm hosts or with HIP_VISIBLE_DEVICES, the first token in
   rocminfo output may not be the active device. This could gate in or out
   the wrong architecture.
   Fix: move torch.cuda.get_device_properties(current_device()).gcnArchName
   to FIRST — it always reflects the device PyTorch is actually using.
   rocminfo/hipconfig are demoted to subprocess fallbacks for environments
   where torch cannot query device properties (unusual builds).
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

Both Codex issues addressed in commit ccddae0.

Fix 1 — compile-safe fallback: Replaced try/except in generated code with a nested _call_aiter_safe(_fn, _q, _k, _v) helper that returns None on any exception. The generated forward uses if _aiter_out is not None: — no exception handling in the compiled region, compatible with torch.compile(fullgraph=True).

Fix 2 — arch detection uses active device first: torch.cuda.get_device_properties(current_device()).gcnArchName is now the primary probe — it always reflects the device PyTorch is actually using, respects HIP_VISIBLE_DEVICES, and works correctly on mixed-arch hosts. rocminfo/hipconfig are demoted to subprocess fallbacks for unusual builds where PyTorch cannot query device properties.

zhihuidu-amd added a commit to zhihuidu-amd/unsloth-zoo that referenced this pull request Jul 28, 2026
…nslothai#944)

The docstring referenced get_amd_attention_implementation() from PR unslothai#920,
which is not yet merged. If unslothai#944 lands first, main would carry a docstring
citing a function not in the tree. Cross-references are cheaper pointing
backwards — let whichever lands second carry the disambiguation note.
The preceding line already tells callers what they need: None means no
AMD-specific override, keep your own default.
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

Codex came back clean on e63e24c (Didn't find any major issues). ccddae0 (compile-safe fallback + arch detection) is one commit ahead of that reviewed head — both changes are straightforward (nested helper replaces try/except, probe order swap) so no new Codex issues are expected, but worth a pass on the current head when convenient.

@LeoBorcherding

Copy link
Copy Markdown
Collaborator

Separate thread from correctness, same note I left on #944: comment volume relative to code.

Against the files' own baselines, compiler.py is 14% comments on main and this PR's added
lines are 29%; device_type.py is 9% on main and the added lines are 19%. Roughly double in
both cases.

The clearest example is compiler.py:448-456, six comment lines wrapped around three lines
of code:

# Clean leading newlines from rest_args
rest_args_clean = rest_args.lstrip("\r\n").strip()
# rest_args already starts with a comma when non-empty; do NOT add another
# rest_args is ", is_causal=True" at this point (fullmatch guarantees this).
# Pass it through to the SDPA fallback as-is.
rest_args_formatted = rest_args.strip().lstrip(",").strip().rstrip(",").strip()
# Reconstruct with leading comma for the SDPA fallback call
rest_args_formatted = (", " + rest_args_formatted) if rest_args_formatted else ""

Two real problems hiding in there, not just verbosity. rest_args_clean is assigned and
never read, so it's dead (this is the leftover I flagged before, in a new shape). And "do
NOT add another" is contradicted by the line two below it, which adds one. Both are
artifacts of successive review rounds layering comments instead of replacing them. Three of
those six lines can go, along with the dead assignment.

The 26-line docstring on replace_sdpa_with_amd_aiter I'd mostly keep, since the known
limitation paragraph was my ask and it belongs in the source. But its numbered activation
requirements duplicate the inline comments on each guard, so one of the two can go. Same
for the generated-code block, where several comments restate the line they precede.

Same as on #944, treat the above as representative and do one general pass over the whole
diff rather than just those lines. Target the files' own density, 14% for compiler.py and
9% for device_type.py. The three that cover most of it:

  • drop any comment that restates the line below it
  • drop comments that narrate the review history; several here read as a changelog of
    successive rounds ("do NOT add another", "Fix 3:", "Reconstruct with leading comma") and
    at least one is now false because the code moved underneath it
  • keep the ones recording something genuinely non-obvious, and this PR has real ones worth
    protecting: why the q/k length guard exists (SDPA top-left vs aiter bottom-right causal
    alignment), why return_lse forces the inference-only guard, and why the arch allowlist
    is CDNA-only

Those three are the reason someone won't undo your guards in six months. Most of the rest
can go.

Also: ccddae0 landed after my last pass, so I'll re-verify against it separately.

LeoBorcherding: added lines were running at ~2x the repo's comment density.

compiler.py:
- Remove dead rest_args_clean assignment (never read after double-comma fix)
- Remove contradictory 'do NOT add another' comment (code did add one)
- Trim verbose 9-line block before fullmatch to 3 lines stating the rule
- Remove 'leave call unchanged' after return — code speaks for itself
- Shorten 'Generate the replacement block' to one line
- Shorten _call_aiter_safe comment to 2 lines

device_type.py:
- Shorten subprocess fallback comment (1 line, not 3)
- Shorten CDNA-only arch gate comment (2 lines, keep the non-obvious fact)
- Remove inline 'fail safe: unknown arch' on return

Kept the non-obvious comments that protect correctness: why q/k seq lengths
must match (SDPA top-left vs aiter bottom-right causal alignment), why
requires_grad triggers the inference-only guard (return_lse assertion in
aiter/ops/mha.py), and why the arch allowlist is CDNA-only.
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

Done in commit add32da.

compiler.py: removed dead rest_args_clean assignment, removed the contradictory 'do NOT add another' comment, trimmed the 9-line pre-fullmatch block to 3 lines, removed self-evident 'leave call unchanged' and 'Generate the replacement block' comments, shortened the _call_aiter_safe comment to 2 lines.

device_type.py: shortened subprocess fallback comment (3→1 lines), shortened CDNA-only arch gate comment (4→2 lines, kept the non-obvious fact), removed inline 'fail safe: unknown arch' comment.

Kept the three non-obvious ones: q/k seq length guard (SDPA top-left vs aiter bottom-right causal alignment), inference-only guard (return_lse assertion in mha.py:2502/2536), and CDNA-only arch allowlist.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: add32da42c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread unsloth_zoo/compiler.py Outdated
# Excludes: disable_compile_scaled_dot_product_attention (opt-out shim)
# Uses ((?:[^)]|\([^)]*\))*) to handle one level of nested parens in args
sdpa_call_pattern = (
r"([ \t]*)([A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*"

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 Anchor the SDPA assignment matcher

In AMD+aiter builds, this pattern is not anchored to a statement boundary, so it also matches the attn_output = substring in attribute assignments such as self.attn_output = F.scaled_dot_product_attention(q, k, v, is_causal=True). re.sub leaves the self. prefix in place and inserts the generated multi-line if block after it, making the compiled cache module syntactically invalid instead of falling back to SDPA; require a line/start-of-statement anchor or a negative lookbehind for ./identifier characters.

Useful? React with 👍 / 👎.

Codex: 'self.attn_output = F.scaled_dot_product_attention(q, k, v, is_causal=True)'
matched because the pattern captured 'attn_output' as the LHS variable but left
'self.' dangling before the generated if/else block, producing invalid syntax.

Fix: add negative lookbehind (?<![.\w]) before the LHS variable group.
Rejects any assignment where the variable is preceded by a dot or word character
(attribute access like self.x, module.x). Plain local variable assignments are
unchanged.

Verified: 'out = F.sdpa(...)' matches, 'self.out = F.sdpa(...)' and
'module.out = F.sdpa(...)' are rejected.
@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

Fixed in commit 9000c85.

Added negative lookbehind (?<![.\\w]) before the LHS variable group. self.attn_output = F.scaled_dot_product_attention(...) and module.out = F.scaled_dot_product_attention(...) are now rejected — the pattern requires that no dot or word character precedes the variable name. Plain local variable assignments are unchanged.

Verified on four cases: out = F.sdpa(...) matches, self.out = F.sdpa(...) and module.out = F.sdpa(...) rejected, attn_out = F.sdpa(...) matches.

@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

Already fixed in commit 9000c85 (pushed before this review arrived — the bot reviewed add32da, which is one behind the current head).

Added (?<![.\\w]) negative lookbehind before the LHS variable group. self.attn_output = F.scaled_dot_product_attention(...) and any other attribute assignment are now rejected by the pattern.

@zhihuidu-amd

Copy link
Copy Markdown
Contributor Author

Already fixed — the bot is reviewing 8494840, which is five commits behind the current head 9000c85.

The keyword-name guard (unsupported = ("attn_mask", "dropout_p", ...)) was replaced in 3605a61 with a re.fullmatch check that only accepts the bare is_causal=True form (optional trailing comma/whitespace). Any other content — positional args like (q, k, v, None, 0.1, is_causal=True) or additional keywords — fails the fullmatch and leaves the call unchanged. Positional dropout is correctly rejected.

@LeoBorcherding

Copy link
Copy Markdown
Collaborator

@codex review

Head is now add32da. Your last pass was e63e24c, so this covers two commits: ccddae0
(compile-safe fallback via a helper instead of an inline try/except in the compiled forward,
plus arch detection preferring the active torch device over rocminfo) and add32da (comment
reduction, and removal of a dead rest_args_clean assignment).

Verified locally on gfx1200 / ROCm 7.13: both files compile, the arch gate returns sdpa on
this card, and the rewrite guards still accept the bare/trailing-comma/F. alias forms
while rejecting .transpose() chains, * scale, [0], attn_mask, positional dropout,
and is_causal=<variable>.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 9000c85ce9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants