a masked attention pipeline gets the default backend, not the other fast one

LTX-2 masks its text conditioning. The model loaded cleanly and died at the
first denoising step:

    ValueError: `attn_mask` is not supported for sage attention

so `auto` choosing sage there is not a slower path, it is a broken one.

The first version of this guard moved it to flash instead, and it failed
again with the same sentence and a different name:

    ValueError: `attn_mask` is not supported for flash-attn 2.

NEITHER fast backend accepts a mask, so falling back from one to the other is
not a fix. For these families the only working backend is the default SDPA,
and the guard now keeps it.

An operator who names sage or flash explicitly is still obeyed -- the override
exists so they can force a backend, and silently ignoring it would be a
different kind of wrong from silently choosing a broken one. They get the
error, which is theirs to get.

Wan and the LTX 0.9 family are unaffected and still take the fast backends;
tests pin both directions.

Suite: 15 failed, 5258 passed -- the same 15 as the baseline.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZnmWyKjWZerwsb27jZ
parent 3bd589ad
...@@ -1177,6 +1177,30 @@ def _sage_usable() -> bool: ...@@ -1177,6 +1177,30 @@ def _sage_usable() -> bool:
return False return False
#: Pipeline families that pass an ATTENTION MASK into attention.
#:
#: NEITHER of the fast backends accepts one. Both raise at the first denoising
#: step, with the same sentence and a different name:
#:
#: ValueError: `attn_mask` is not supported for sage attention
#: ValueError: `attn_mask` is not supported for flash-attn 2.
#:
#: So for these families the only working backend is the default SDPA, and
#: "fall back to the other fast one" is not a fix -- measured 2026-10-27 on
#: LTX-2.3-10Eros, which loaded cleanly, failed on sage, was moved to flash by
#: a first version of this guard, and failed again identically.
#:
#: Matched on the pipeline class name, which is what names the family. LTX-2
#: masks its text conditioning; the LTX 0.9 family is a different architecture
#: and is not affected.
_ATTN_MASK_FAMILIES = ("LTX2",)
def _pipeline_uses_attn_mask(pipe) -> bool:
cls = type(pipe).__name__ if pipe is not None else ""
return any(f in cls for f in _ATTN_MASK_FAMILIES)
def _apply_attention_backend(pipe, model_cfg: dict) -> None: def _apply_attention_backend(pipe, model_cfg: dict) -> None:
"""Switch the Wan transformer(s) to a faster attention backend (diffusers 0.38+ """Switch the Wan transformer(s) to a faster attention backend (diffusers 0.38+
dispatcher). Best-effort and per-component: if a backend isn't available it dispatcher). Best-effort and per-component: if a backend isn't available it
...@@ -1184,6 +1208,18 @@ def _apply_attention_backend(pipe, model_cfg: dict) -> None: ...@@ -1184,6 +1208,18 @@ def _apply_attention_backend(pipe, model_cfg: dict) -> None:
name = _resolve_attn_backend(model_cfg) name = _resolve_attn_backend(model_cfg)
if not name: if not name:
return return
# `auto` picked sage for a family that masks its attention. An operator who
# named 'sage' explicitly is left alone -- that is their call to make, and
# they get the error -- but auto must not choose a backend that cannot run
# the model at all.
if name in ("sage", "flash") and _pipeline_uses_attn_mask(pipe):
_explicit = str((model_cfg or {}).get("video_attention_backend")
or "").strip().lower()
if _explicit not in ("sage", "flash"):
print(f" [video][attn] {type(pipe).__name__} passes an attention "
f"mask, which neither SageAttention nor flash-attn 2 accepts "
f"— keeping the default (SDPA)")
return
applied = [] applied = []
for cn in ('transformer', 'transformer_2'): for cn in ('transformer', 'transformer_2'):
comp = getattr(pipe, cn, None) comp = getattr(pipe, cn, None)
......
"""SageAttention cannot take an attention mask, so `auto` must not pick it.
LTX-2 masks its text conditioning. The model loaded cleanly and then failed at
the first denoising step with
ValueError: `attn_mask` is not supported for sage attention
which makes sage not a slower path for that family but a broken one. The
backend resolver already gates on "can diffusers actually dispatch to sage";
this is the same gate, extended to the model.
"""
import types
import pytest
import codai.api.video as V
class _Comp:
def __init__(self):
self.backend = None
def set_attention_backend(self, name):
self.backend = name
def _pipe(cls_name):
ns = {"transformer": _Comp(), "transformer_2": None}
return type(cls_name, (), ns)()
@pytest.mark.parametrize("cls,expect", [
("LTX2Pipeline", True),
("LTX2ImageToVideoPipeline", True),
("WanPipeline", False),
("LTXPipeline", False), # the 0.9 family is a different thing
("CogVideoXPipeline", False),
])
def test_only_the_masking_families_are_recognised(cls, expect):
assert V._pipeline_uses_attn_mask(_pipe(cls)) is expect
@pytest.mark.parametrize("chosen", ["sage", "flash"])
def test_neither_fast_backend_is_given_to_a_masking_pipeline(monkeypatch, chosen):
"""Falling back from sage to flash is NOT a fix.
Both raise on an attention mask, with the same sentence and a different
name. A first version of this guard moved LTX-2 from sage to flash and it
failed identically, so the only correct answer is the default SDPA.
"""
monkeypatch.setattr(V, "_resolve_attn_backend", lambda cfg: chosen)
p = _pipe("LTX2Pipeline")
V._apply_attention_backend(p, {})
assert p.transformer.backend is None, (
f"{chosen} cannot take an attention mask either")
def test_a_non_masking_pipeline_still_gets_sage(monkeypatch):
# The whole point of choosing sage is speed; this must not regress Wan.
monkeypatch.setattr(V, "_resolve_attn_backend", lambda cfg: "sage")
p = _pipe("WanPipeline")
V._apply_attention_backend(p, {})
assert p.transformer.backend == "sage"
def test_flash_still_reaches_a_non_masking_pipeline(monkeypatch):
monkeypatch.setattr(V, "_resolve_attn_backend", lambda cfg: "flash")
p = _pipe("WanPipeline")
V._apply_attention_backend(p, {})
assert p.transformer.backend == "flash"
def test_an_operator_who_names_sage_explicitly_is_obeyed(monkeypatch):
"""Their call to make, and they get the error.
The override exists so an operator can force a backend; silently ignoring
it would be a different kind of wrong from silently choosing a broken one.
"""
monkeypatch.setattr(V, "_resolve_attn_backend", lambda cfg: "sage")
p = _pipe("LTX2Pipeline")
V._apply_attention_backend(p, {"video_attention_backend": "sage"})
assert p.transformer.backend == "sage"
def test_nothing_configured_leaves_the_pipeline_alone(monkeypatch):
monkeypatch.setattr(V, "_resolve_attn_backend", lambda cfg: None)
p = _pipe("LTX2Pipeline")
V._apply_attention_backend(p, {})
assert p.transformer.backend is None
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment