Liny commited on
Commit
f29c645
·
1 Parent(s): bbf4ff5

fix FA on Hopper

Browse files
Files changed (4) hide show
  1. .gitignore +3 -1
  2. README.md +2 -2
  3. app.py +29 -10
  4. tests/test_attention_backend.py +126 -0
.gitignore CHANGED
@@ -1,2 +1,4 @@
1
  AGENTS.md
2
- app_examples.py
 
 
 
1
  AGENTS.md
2
+ app_examples.py
3
+ hftoken
4
+ hftoken.txt
README.md CHANGED
@@ -35,11 +35,11 @@ Gradio Space using the official Z-Image pipeline (`Tongyi-MAI/Z-Image-Turbo`) wi
35
  - `MODEL_DTYPE` (default `auto`): `bf16` if supported, else `fp16` (override with `bf16`/`fp16`/`fp32`).
36
  - `ENABLE_COMPILE` (default `true`): Enable `torch.compile` on the transformer.
37
  - `ENABLE_WARMUP` (default `false`): Run a quick warmup across resolutions after load (adds startup time).
38
- - `ATTENTION_BACKEND` (default `flash_3`): Backend for transformer attention (falls back to `flash`/`xformers`/`native`).
39
  - `OFFLOAD_TO_CPU_AFTER_RUN` (default `false`): Move the model back to CPU after each generation (useful on ZeroGPU; slower on normal GPUs).
40
  - `ENABLE_AOTI` (default `true`): Try to load ZeroGPU AoTI blocks via `spaces.aoti_blocks_load` for faster inference.
41
  - `AOTI_REPO` (default `zerogpu-aoti/Z-Image`): AoTI blocks repo.
42
- - `AOTI_VARIANT` (default `fa3`): AoTI variant.
43
  - `AOTI_ALLOW_LORA` (default `false`): Allow AoTI to load even if LoRA adapters are loaded (may crash; AoTI blocks generally don’t support LoRA).
44
  - `DEBUG` (default `false`): When set to a truthy value (`1`, `true`, `yes`, `on`), hide the Status/Debug floating panel.
45
 
 
35
  - `MODEL_DTYPE` (default `auto`): `bf16` if supported, else `fp16` (override with `bf16`/`fp16`/`fp32`).
36
  - `ENABLE_COMPILE` (default `true`): Enable `torch.compile` on the transformer.
37
  - `ENABLE_WARMUP` (default `false`): Run a quick warmup across resolutions after load (adds startup time).
38
+ - `ATTENTION_BACKEND` (default `native`): PyTorch SDPA by default; other backends fall back to `native` first. FlashAttention 3 (including `_flash_3_hub`) is only enabled on Hopper GPUs (compute capability 9.0); on Blackwell and other GPUs, the app overrides old FA3 settings with `native` before inference.
39
  - `OFFLOAD_TO_CPU_AFTER_RUN` (default `false`): Move the model back to CPU after each generation (useful on ZeroGPU; slower on normal GPUs).
40
  - `ENABLE_AOTI` (default `true`): Try to load ZeroGPU AoTI blocks via `spaces.aoti_blocks_load` for faster inference.
41
  - `AOTI_REPO` (default `zerogpu-aoti/Z-Image`): AoTI blocks repo.
42
+ - `AOTI_VARIANT` (default `fa3`): AoTI variant. FA3 blocks are skipped on non-Hopper GPUs to avoid incompatible CUDA kernels.
43
  - `AOTI_ALLOW_LORA` (default `false`): Allow AoTI to load even if LoRA adapters are loaded (may crash; AoTI blocks generally don’t support LoRA).
44
  - `DEBUG` (default `false`): When set to a truthy value (`1`, `true`, `yes`, `on`), hide the Status/Debug floating panel.
45
 
app.py CHANGED
@@ -19,7 +19,7 @@ LORA_PATH = os.environ.get("LORA_PATH", os.path.join("lora", "zit-mystic-xxx.saf
19
  HF_TOKEN = os.environ.get("HF_TOKEN")
20
  ENABLE_COMPILE = os.environ.get("ENABLE_COMPILE", "true").lower() == "true"
21
  ENABLE_WARMUP = os.environ.get("ENABLE_WARMUP", "false").lower() == "true"
22
- ATTENTION_BACKEND = os.environ.get("ATTENTION_BACKEND", "flash_3")
23
  OFFLOAD_TO_CPU_AFTER_RUN = os.environ.get("OFFLOAD_TO_CPU_AFTER_RUN", "false").lower() == "true"
24
  ENABLE_AOTI = os.environ.get("ENABLE_AOTI", "true").lower() == "true"
25
  AOTI_REPO = os.environ.get("AOTI_REPO", "zerogpu-aoti/Z-Image")
@@ -193,18 +193,33 @@ def parse_resolution(resolution: str) -> Tuple[int, int]:
193
  return 1024, 1024
194
 
195
 
 
 
 
 
 
 
 
 
196
  def set_attention_backend_safe(transformer, backend: str) -> str:
197
  global attention_backend_error
 
198
  candidates: List[str] = []
199
- if backend:
200
- candidates.append(backend)
201
- if backend.startswith("_"):
202
- candidates.append(backend.lstrip("_"))
203
- else:
204
- candidates.append(f"_{backend}")
205
- candidates.extend(["flash", "xformers", "native"])
 
 
 
 
 
 
 
206
 
207
- attention_backend_error = None
208
  errors: dict[str, Exception] = {}
209
  last_exc: Exception | None = None
210
  for name in candidates:
@@ -607,7 +622,11 @@ def init_app() -> None:
607
  if ENABLE_COMPILE and pipe is not None:
608
  ensure_on_gpu()
609
  if ENABLE_AOTI and not aoti_loaded and pipe is not None and getattr(pipe, "transformer", None) is not None:
610
- if lora_loaded and not AOTI_ALLOW_LORA:
 
 
 
 
611
  aoti_loaded = False
612
  aoti_error = "disabled: AoTI blocks are incompatible with LoRA adapters"
613
  print("AoTI disabled: LoRA adapters are loaded (AoTI blocks are incompatible with LoRA).")
 
19
  HF_TOKEN = os.environ.get("HF_TOKEN")
20
  ENABLE_COMPILE = os.environ.get("ENABLE_COMPILE", "true").lower() == "true"
21
  ENABLE_WARMUP = os.environ.get("ENABLE_WARMUP", "false").lower() == "true"
22
+ ATTENTION_BACKEND = os.environ.get("ATTENTION_BACKEND", "native")
23
  OFFLOAD_TO_CPU_AFTER_RUN = os.environ.get("OFFLOAD_TO_CPU_AFTER_RUN", "false").lower() == "true"
24
  ENABLE_AOTI = os.environ.get("ENABLE_AOTI", "true").lower() == "true"
25
  AOTI_REPO = os.environ.get("AOTI_REPO", "zerogpu-aoti/Z-Image")
 
193
  return 1024, 1024
194
 
195
 
196
+ def supports_flash_attention_3() -> bool:
197
+ # FA3 的预编译内核面向 Hopper;ZeroGPU 换成 Blackwell 后不能继续使用。
198
+ try:
199
+ return torch.cuda.is_available() and torch.cuda.get_device_capability(0) == (9, 0)
200
+ except Exception:
201
+ return False
202
+
203
+
204
  def set_attention_backend_safe(transformer, backend: str) -> str:
205
  global attention_backend_error
206
+ attention_backend_error = None
207
  candidates: List[str] = []
208
+ fa3_backends = {"flash_3", "flash_3_hub", "flash_varlen_3", "flash_3_varlen_hub"}
209
+ if backend and backend.lstrip("_") in fa3_backends and not supports_flash_attention_3():
210
+ # 设置后端不会运行 CUDA 内核,不能靠 try/except 捕获随后发生的进程退出。
211
+ attention_backend_error = f"FlashAttention 3 requires Hopper (cc 9.0); current GPU: {get_gpu_summary()}"
212
+ candidates = ["native"]
213
+ else:
214
+ if backend:
215
+ candidates.append(backend)
216
+ if backend.startswith("_"):
217
+ candidates.append(backend.lstrip("_"))
218
+ else:
219
+ candidates.append(f"_{backend}")
220
+ candidates.extend(["native", "flash", "xformers"])
221
+ candidates = list(dict.fromkeys(candidates))
222
 
 
223
  errors: dict[str, Exception] = {}
224
  last_exc: Exception | None = None
225
  for name in candidates:
 
622
  if ENABLE_COMPILE and pipe is not None:
623
  ensure_on_gpu()
624
  if ENABLE_AOTI and not aoti_loaded and pipe is not None and getattr(pipe, "transformer", None) is not None:
625
+ if AOTI_VARIANT == "fa3" and not supports_flash_attention_3():
626
+ aoti_loaded = False
627
+ aoti_error = "disabled: FA3 AoTI blocks require a Hopper GPU (cc 9.0)"
628
+ print(f"AoTI {aoti_error}")
629
+ elif lora_loaded and not AOTI_ALLOW_LORA:
630
  aoti_loaded = False
631
  aoti_error = "disabled: AoTI blocks are incompatible with LoRA adapters"
632
  print("AoTI disabled: LoRA adapters are loaded (AoTI blocks are incompatible with LoRA).")
tests/test_attention_backend.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """无需下载模型即可检查后端选择,模拟不同 GPU 和 CUDA 检测失败。"""
2
+
3
+ import ast
4
+ import contextlib
5
+ import io
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+ import unittest
9
+ from unittest.mock import Mock
10
+
11
+
12
+ APP_PATH = Path(__file__).resolve().parents[1] / "app.py"
13
+
14
+
15
+ class AttentionBackendTests(unittest.TestCase):
16
+ def setUp(self):
17
+ # 只加载待测函数,避免 app.py 导入时下载模型和启动 Gradio。
18
+ tree = ast.parse(APP_PATH.read_text(encoding="utf-8"))
19
+ names = {"get_gpu_summary", "supports_flash_attention_3", "set_attention_backend_safe", "init_app"}
20
+ functions = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name in names]
21
+ self.cuda = SimpleNamespace(
22
+ is_available=Mock(return_value=True),
23
+ get_device_capability=Mock(return_value=(12, 0)),
24
+ get_device_name=Mock(return_value="RTX PRO 6000 Blackwell"),
25
+ )
26
+ self.namespace = {"torch": SimpleNamespace(cuda=self.cuda), "List": list}
27
+ exec(compile(ast.Module(body=functions, type_ignores=[]), str(APP_PATH), "exec"), self.namespace)
28
+ self.transformer = SimpleNamespace(set_attention_backend=Mock(), layers=SimpleNamespace())
29
+
30
+ def select(self, backend):
31
+ with contextlib.redirect_stdout(io.StringIO()):
32
+ return self.namespace["set_attention_backend_safe"](self.transformer, backend)
33
+
34
+ def test_blackwell_never_selects_flash_attention_3(self):
35
+ for capability in [(10, 0), (12, 0)]:
36
+ for alias in ["flash_3", "_flash_3", "_flash_3_hub", "_flash_varlen_3", "_flash_3_varlen_hub"]:
37
+ with self.subTest(capability=capability, backend=alias):
38
+ self.cuda.get_device_capability.return_value = capability
39
+ self.transformer.set_attention_backend.reset_mock()
40
+ self.assertEqual(self.select(alias), "native")
41
+ self.transformer.set_attention_backend.assert_called_once_with("native")
42
+ self.assertIn("Hopper", self.namespace["attention_backend_error"])
43
+
44
+ def test_ampere_uses_native_for_flash_attention_3(self):
45
+ self.cuda.get_device_capability.return_value = (8, 0)
46
+ self.assertEqual(self.select("_flash_3_hub"), "native")
47
+ self.transformer.set_attention_backend.assert_called_once_with("native")
48
+
49
+ def test_hopper_preserves_requested_flash_attention_3(self):
50
+ self.cuda.get_device_capability.return_value = (9, 0)
51
+ self.assertEqual(self.select("_flash_3_hub"), "_flash_3_hub")
52
+ self.transformer.set_attention_backend.assert_called_once_with("_flash_3_hub")
53
+ self.assertIsNone(self.namespace["attention_backend_error"])
54
+
55
+ def test_unknown_gpu_uses_native(self):
56
+ self.cuda.get_device_capability.side_effect = RuntimeError("CUDA unavailable")
57
+ self.assertEqual(self.select("_flash_3_hub"), "native")
58
+ self.transformer.set_attention_backend.assert_called_once_with("native")
59
+
60
+ def test_no_cuda_uses_native_without_querying_device(self):
61
+ self.cuda.is_available.return_value = False
62
+ self.assertEqual(self.select("_flash_3_hub"), "native")
63
+ self.cuda.get_device_capability.assert_not_called()
64
+
65
+ def test_native_failure_does_not_try_incompatible_kernels(self):
66
+ self.transformer.set_attention_backend.side_effect = RuntimeError("native failed")
67
+ with self.assertRaisesRegex(RuntimeError, "native failed"):
68
+ self.select("_flash_3_hub")
69
+ self.transformer.set_attention_backend.assert_called_once_with("native")
70
+
71
+ def test_backend_setup_error_falls_back_to_native(self):
72
+ def configure(name):
73
+ if name != "native":
74
+ raise ValueError("backend unavailable")
75
+
76
+ self.transformer.set_attention_backend.side_effect = configure
77
+ self.assertEqual(self.select("missing_backend"), "native")
78
+ attempts = [call.args[0] for call in self.transformer.set_attention_backend.call_args_list]
79
+ self.assertEqual(attempts, ["missing_backend", "_missing_backend", "native"])
80
+ self.assertEqual(self.namespace["attention_backend_error"], "backend unavailable")
81
+
82
+ def test_native_selection_clears_previous_error(self):
83
+ self.select("_flash_3_hub")
84
+ self.assertEqual(self.select("native"), "native")
85
+ self.assertIsNone(self.namespace["attention_backend_error"])
86
+
87
+ def initialize(self, *, lora=False, allow_lora=False):
88
+ loader = Mock()
89
+ self.namespace.update(
90
+ pipe=SimpleNamespace(transformer=self.transformer),
91
+ ensure_models_loaded=Mock(), ENABLE_COMPILE=False, ENABLE_AOTI=True,
92
+ ENABLE_WARMUP=False, aoti_loaded=False, aoti_error=None,
93
+ lora_loaded=lora, AOTI_ALLOW_LORA=allow_lora, AOTI_VARIANT="fa3",
94
+ AOTI_REPO="zerogpu-aoti/Z-Image", ATTENTION_BACKEND="_flash_3_hub",
95
+ spaces=SimpleNamespace(aoti_blocks_load=loader),
96
+ )
97
+ with contextlib.redirect_stdout(io.StringIO()):
98
+ self.namespace["init_app"]()
99
+ return loader
100
+
101
+ def test_blackwell_skips_fa3_aoti(self):
102
+ loader = self.initialize()
103
+ loader.assert_not_called()
104
+ self.assertFalse(self.namespace["aoti_loaded"])
105
+ self.assertIn("Hopper", self.namespace["aoti_error"])
106
+
107
+ def test_allow_lora_cannot_override_hardware_guard(self):
108
+ loader = self.initialize(lora=True, allow_lora=True)
109
+ loader.assert_not_called()
110
+ self.assertFalse(self.namespace["aoti_loaded"])
111
+
112
+ def test_hopper_can_load_fa3_aoti(self):
113
+ self.cuda.get_device_capability.return_value = (9, 0)
114
+ loader = self.initialize()
115
+ loader.assert_called_once_with(self.transformer.layers, "zerogpu-aoti/Z-Image", variant="fa3")
116
+ self.assertTrue(self.namespace["aoti_loaded"])
117
+
118
+ def test_hopper_still_skips_aoti_with_lora(self):
119
+ self.cuda.get_device_capability.return_value = (9, 0)
120
+ loader = self.initialize(lora=True)
121
+ loader.assert_not_called()
122
+ self.assertIn("LoRA", self.namespace["aoti_error"])
123
+
124
+
125
+ if __name__ == "__main__":
126
+ unittest.main()