Keep xFormers working when flash-attn 4 is installed, and guard the varlen int32 overflow by danielhanchen · Pull Request #8957 · unslothai/unsloth · GitHub
Skip to content

Keep xFormers working when flash-attn 4 is installed, and guard the varlen int32 overflow - #8957

Merged
danielhanchen merged 7 commits into
mainfrom
fix/flash-attn-4-xformers-coexist
Aug 16, 2026
Merged

Keep xFormers working when flash-attn 4 is installed, and guard the varlen int32 overflow#8957
danielhanchen merged 7 commits into
mainfrom
fix/flash-attn-4-xformers-coexist

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Two independent robustness fixes on the attention path. Both are silent failures today: one costs a large amount of speed and memory with no message, the other kills the CUDA context.

1. flash-attn 4 silently disables xFormers

The flash-attn-4 wheel ships only flash_attn/cute/, with no flash_attn/__init__.py, so flash_attn becomes an implicit namespace package with no flash_attn_func in it. xFormers does this at import time (xformers/ops/fmha/flash.py:63-65):

elif importlib.util.find_spec("flash_attn"):
    import flash_attn
    import flash_attn.flash_attn_interface

The find_spec gate passes, the second import raises, and the error escapes import xformers.ops. HAS_XFORMERS becomes False and we quietly fall back to plain SDPA.

Measured on a B200, Qwen3-0.6B + LoRA, seq_len 8192:

attention ms/step peak memory
xFormers 547 2.69 GB
plain SDPA (what you get after installing flash-attn 4) 2154 19.02 GB

That is 3.9x slower and 7x the memory, with nothing printed. It matters now because flash-attn 2 publishes no wheel for torch 2.9 / cp313, so flash-attn 4 is what a Blackwell user reaches for.

The fix hides flash_attn* from importlib.util.find_spec for exactly one import xformers.ops, restored in a finally. It is keyed on the same spec lookup xFormers itself performs, so the detection cannot drift from the failure it prevents. No third-party file is edited.

before after
no flash-attn 4 xFormers, HAS_XFORMERS=True unchanged
flash-attn 4 present HAS_XFORMERS=False, backend sdpa, no message HAS_XFORMERS=True, 0.0.33.post2, backend xformers

A real forward and backward through run_attention(backend=XFORMERS) at 1x4096x16x128 runs with flash-attn 4 on the path after the fix.

_package_available("flash_attn") already returns False under flash-attn 4 (the distribution is named flash-attn-4, so the metadata lookup misses), so HAS_FLASH_ATTENTION stays False and the attn_implementation= delegation path correctly picks sdpa. That half was already safe.

2. Varlen backward faults on large packed rows

A packed row with many documents aborts with CUDA error: an illegal memory access was encountered, and it poisons the CUDA context, so every later op in the process fails too.

The cause is not a document-count constant. flash-attn 2's varlen backward allocates

dq_accum = zeros(total_q + 128 * n_seqs, n_heads, round_up(head_dim, 32))

and indexes it with int32, so it faults at 2^31 elements. xFormers dispatches a BlockDiagonal* bias to that same op, which is why it shows up there.

Bisected on bare xFormers with no Unsloth involved, predicted against observed last-good document count:

heads head_dim doc_len predicted last OK
16 128 1 8128 8129
16 96 1 10837 10838
16 64 1 16257 16257
8 128 1 16257 16257 ok, 16400 fails
16 128 4 7944 7944
4 128 1 32518 no failure up to 20000

Forward-only never allocates the buffer and runs clean at 20000 documents, which is why this only appears in training.

The fix predicts the overflow before dispatch and downgrades that call to SDPA with a one-time warning naming the buffer size. UNSLOTH_DISABLE_VARLEN_INT32_GUARD=1 opts out.

BEFORE  ATTENTION=FAILED  CUDA_CONTEXT=POISONED
AFTER   Unsloth: A packed row holds 8192 documents over 8192 tokens. The xformers backward
        kernel would index a 2,164,260,864-element buffer with int32 (limit 2,147,483,648) ...
        ATTENTION=SURVIVED  CUDA_CONTEXT=ALIVE

64 documents of 128 tokens, and 8128 documents of 1 token (just under the bound), both keep the xFormers kernel and warn nothing.

Compatibility

Additive only, no behaviour change when neither condition holds. Nothing is hard-required: both fixes feature-detect. No compiled-cache or codegen changes. Verified against the consumers of HAS_XFORMERS, HAS_FLASH_ATTENTION, flash_attn_func, flash_attn_varlen_func, xformers_attention and select_attention_backend across both attention stacks, the monkeypatched fast path and the attn_implementation= delegation path. Note select_attention_backend also exists in studio/backend/core/inference/diffusion_attention.py; its flash4 alias resolves through the kernels hub and never touches the flash_attn PyPI package, so it is unaffected.

Tests

25 new tests in tests/test_flash_attn_4_namespace_shadow.py (9) and tests/utils/test_varlen_int32_overflow_guard.py (16), all passing, plus 356 passing in the surrounding attention suite.

danielhanchen and others added 2 commits August 16, 2026 05:56
…overflow

Two failures found while benchmarking attention on Blackwell.

1. flash-attn 4 silently disables xFormers.

The `flash-attn-4` wheel installs its CuTe build at `flash_attn/cute/` with no
`flash_attn/__init__.py`, so `flash_attn` resolves as an implicit namespace
package with no `flash_attn_func`, no `flash_attn_varlen_func` and no
`flash_attn.flash_attn_interface`. xFormers gates on
`find_spec("flash_attn")` and then imports `flash_attn.flash_attn_interface`
unguarded, so `import xformers.ops` raises, `models/_utils.py` swallows it into
`xformers = None`, `HAS_XFORMERS` goes False and every fast-path model drops to
plain SDPA without a word. Measured on a B200 at seq_len 8192 with Qwen3-0.6B +
LoRA: 547 -> 2154 ms/step and 2.69 -> 19.02 GB peak.

`fix_flash_attn_4_namespace_shadow` imports xFormers once with `flash_attn`
hidden from `find_spec`, which sends it down the next branch of its own elif
chain exactly as on a machine with no flash-attn. A real flash-attn 2 install is
detected by `flash_attn.flash_attn_interface` resolving -- the exact import
xFormers performs -- so flash-attn 2 alone, and flash-attn 2 alongside
flash-attn 4, are both left untouched. Nothing is written to any third-party
package. If the repair cannot work the state is reported once with its cost.

2. Packed rows with thousands of documents abort the CUDA context.

flash-attn 2's varlen backward allocates
`dq_accum = zeros(total_q + 128 * n_seqs, n_heads, round_up(head_dim, 32))` and
indexes it with int32, so it faults with "CUDA error: an illegal memory access
was encountered" once that element count reaches 2**31 -- and that poisons the
context, so the run dies with an opaque error far from the cause. xFormers
dispatches a BlockDiagonal* bias to the same kernel, which is where it showed up.

Bisecting document count on a B200 puts the predicted limit within one document
of the observed one across seven shapes (16/128/len1: 8129, 16/96: 10838,
16/64: 16257, 8/128: 16257, 16/128/len2: ~8065, 16/128/len4: 7944, and 4/128 no
failure at all, correctly below the bound). Forward-only allocates no dq_accum
and ran clean at 20000 documents, so the guard is conditioned on a backward
being possible, keyed so gradient checkpointing picks the same backend in both
of its passes. Over the bound both varlen backends fall back to SDPA with a
one-time warning naming the cost; `UNSLOTH_DISABLE_VARLEN_INT32_GUARD=1` opts
out.
chatgpt-codex-connector[bot]

This comment was marked as resolved.

find_spec("flash_attn.flash_attn_interface") resolves the dotted name by importing
the parent first, so the classifier was executing flash_attn/__init__.py (and loading
flash_attn_2_cuda) during import unsloth on every machine with a real flash-attn 2,
including users who never touch flash attention. Probe the package's own search
locations on disk instead: same classification, a stat() instead of an import, and no
side effects. Adds a test that pins it for all four layouts.

Also point the remedy at flash-attn>=2.7.1 rather than >=2.6.3, since xformers enforces
FLASH_VER_MIN = 2.7.1 and a pinned 2.6.3 reproduces the failure the message is about,
and record the measured (~1s) width of the find_spec window instead of asserting the
process is single-threaded.

@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: 7506842704

ℹ️ 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".

# FLASH_VARLEN calls it directly, and XFORMERS dispatches a BlockDiagonal* bias to its
# flash-2 op. Guard both before the int32 overflow aborts the process (see
# _varlen_backward_overflows_int32 above). Pure integer arithmetic and no device sync.
if backend in (FLASH_VARLEN, XFORMERS) and not _VARLEN_INT32_GUARD_DISABLED:

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 Keep xFormers when it cannot dispatch FlashAttention

On pre-sm100 devices with fp32 Q/K/V, such as the DoRA path documented below, xFormers uses its fp32-capable CUTLASS operator rather than the fp16/bf16 flash-2 operator, so it never allocates flash-attn's dq_accum. This backend-name-only guard nevertheless downgrades oversized packed rows to dense SDPA, which can consume substantially more memory or OOM despite the original xFormers call being unaffected; condition the guard on the operator that xFormers will actually dispatch.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The dispatch premise is right, the conclusion is not, so leaving this as is.

Confirmed against the installed xformers 0.0.33.post2: ops/fmha/flash.py has SUPPORTED_DTYPES = {torch.half, torch.bfloat16} and ops/fmha/cutlass.py has {torch.float, torch.half, torch.bfloat16}, so fp32 Q/K/V do land on cutlass below sm_100 and no dq_accum is allocated. The guard does trip there.

Two reasons not to condition on it:

  1. The fallback is correct, not wrong. SDPA computes the same attention, and the one case where it could not (Gemma 2 softcapping) already raises instead of falling back. There is no wrong result and no deterministic crash, only a slower path.
  2. The overlap is empty in practice. The guard needs total_q + 128 * n_seqs >= 2**31 / (n_heads * round_up(head_dim, 32)), which at 16 heads and head_dim 128 is 1,048,576, so with n_seqs <= total_q the floor is 8128 documents of exactly one token in a single packed row. Reaching that on fp32 DoRA, below sm_100, with no flash-attn installed, is not a shape real packing produces.

Conditioning on the operator is also not a dtype test. flash.FwOp.CUDA_MINIMUM_COMPUTE_CAPABILITY = (8, 0) and cutlass.FwOp.CUDA_MAXIMUM_COMPUTE_CAPABILITY = (9, 0), and the dispatcher also weighs SUPPORTED_MAX_K, the bias type and whether the build shipped the kernel at all. Predicting its choice means mirroring _dispatch_fw_priority_list / _dispatch_bw, which move between xformers releases, so the guard would go stale silently instead of being merely conservative.

Gemma 2 hands attn_logit_softcapping to the fast kernels through flash_varlen_kwargs
only (unsloth/models/gemma2.py:157-168) and the SDPA branch of run_attention has no
softcap at all, so swapping the backend to SDPA would keep a packed Gemma 2 run alive
while training it on uncapped logits and uncapped gradients. That is worse than the
fault the guard exists to prevent, and it is reachable: at 16 heads and head_dim 256 the
threshold is around four thousand documents in a row.

When a softcap is configured, raise with the element count and the way out instead of
falling back. Everything without a softcap keeps the rescue unchanged.
danielhanchen added a commit to danielhanchen/unsloth-staging-2 that referenced this pull request Aug 16, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 8a220c3bfe

ℹ️ 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".

They imported the module as unsloth.import_fixes, which runs unsloth/__init__.py,
which refuses to import without an accelerator. So every subprocess in the file
exited 1 on Repo tests (CPU), and check = True hid the child's stderr behind a
CalledProcessError, which is why the cause never appeared in the log.

Load import_fixes.py by path instead, in the parent and in each subprocess, and
assert on the return code with stdout and stderr attached. Verified both ways:
35 passed with a GPU visible, and 35 passed under CUDA_VISIBLE_DEVICES="".
chatgpt-codex-connector[bot]

This comment was marked as resolved.

The test that checks find_spec is restored after a failed xformers import only
intercepted xformers.ops, so on a machine without xformers installed the repair
returned early at its find_spec("xformers") gate and never warned. The Repo
tests (CPU) job collects this file and installs no xformers, so the assertion
would fail there. Serve a stub spec for the top-level package as well, which
makes the case exercise the same path with and without xformers present.
@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: 2deea0729e

ℹ️ 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 text only, no code change. The measured B200 numbers, the dq_accum
formula and its int32 bound, the bisection table, the reason the classifier
never resolves a dotted flash_attn name, the two-term backward test and the
_gpu_init ordering constraint all stay.
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@unslothai unslothai deleted a comment from chatgpt-codex-connector Bot Aug 16, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

@danielhanchen
danielhanchen merged commit 2cf7a28 into main Aug 16, 2026
52 of 55 checks passed
@danielhanchen
danielhanchen deleted the fix/flash-attn-4-xformers-coexist branch August 16, 2026 09:39
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.

1 participant