Keep xFormers working when flash-attn 4 is installed, and guard the varlen int32 overflow - #8957
Conversation
…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.
for more information, see https://pre-commit.ci
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.
There was a problem hiding this comment.
💡 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: |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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:
- 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.
- 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 withn_seqs <= total_qthe 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.
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
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="".
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.
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
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.

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-4wheel ships onlyflash_attn/cute/, with noflash_attn/__init__.py, soflash_attnbecomes an implicit namespace package with noflash_attn_funcin it. xFormers does this at import time (xformers/ops/fmha/flash.py:63-65):The
find_specgate passes, the second import raises, and the error escapesimport xformers.ops.HAS_XFORMERSbecomes False and we quietly fall back to plain SDPA.Measured on a B200, Qwen3-0.6B + LoRA, seq_len 8192:
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*fromimportlib.util.find_specfor exactly oneimport xformers.ops, restored in afinally. 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.HAS_XFORMERS=TrueHAS_XFORMERS=False, backendsdpa, no messageHAS_XFORMERS=True, 0.0.33.post2, backendxformersA 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 namedflash-attn-4, so the metadata lookup misses), soHAS_FLASH_ATTENTIONstays False and theattn_implementation=delegation path correctly pickssdpa. 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
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:
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=1opts out.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_attentionandselect_attention_backendacross both attention stacks, the monkeypatched fast path and theattn_implementation=delegation path. Noteselect_attention_backendalso exists instudio/backend/core/inference/diffusion_attention.py; itsflash4alias resolves through the kernels hub and never touches theflash_attnPyPI package, so it is unaffected.Tests
25 new tests in
tests/test_flash_attn_4_namespace_shadow.py(9) andtests/utils/test_varlen_int32_overflow_guard.py(16), all passing, plus 356 passing in the surrounding attention suite.