File size: 6,166 Bytes
f29c645 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | """无需下载模型即可检查后端选择,模拟不同 GPU 和 CUDA 检测失败。"""
import ast
import contextlib
import io
from pathlib import Path
from types import SimpleNamespace
import unittest
from unittest.mock import Mock
APP_PATH = Path(__file__).resolve().parents[1] / "app.py"
class AttentionBackendTests(unittest.TestCase):
def setUp(self):
# 只加载待测函数,避免 app.py 导入时下载模型和启动 Gradio。
tree = ast.parse(APP_PATH.read_text(encoding="utf-8"))
names = {"get_gpu_summary", "supports_flash_attention_3", "set_attention_backend_safe", "init_app"}
functions = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name in names]
self.cuda = SimpleNamespace(
is_available=Mock(return_value=True),
get_device_capability=Mock(return_value=(12, 0)),
get_device_name=Mock(return_value="RTX PRO 6000 Blackwell"),
)
self.namespace = {"torch": SimpleNamespace(cuda=self.cuda), "List": list}
exec(compile(ast.Module(body=functions, type_ignores=[]), str(APP_PATH), "exec"), self.namespace)
self.transformer = SimpleNamespace(set_attention_backend=Mock(), layers=SimpleNamespace())
def select(self, backend):
with contextlib.redirect_stdout(io.StringIO()):
return self.namespace["set_attention_backend_safe"](self.transformer, backend)
def test_blackwell_never_selects_flash_attention_3(self):
for capability in [(10, 0), (12, 0)]:
for alias in ["flash_3", "_flash_3", "_flash_3_hub", "_flash_varlen_3", "_flash_3_varlen_hub"]:
with self.subTest(capability=capability, backend=alias):
self.cuda.get_device_capability.return_value = capability
self.transformer.set_attention_backend.reset_mock()
self.assertEqual(self.select(alias), "native")
self.transformer.set_attention_backend.assert_called_once_with("native")
self.assertIn("Hopper", self.namespace["attention_backend_error"])
def test_ampere_uses_native_for_flash_attention_3(self):
self.cuda.get_device_capability.return_value = (8, 0)
self.assertEqual(self.select("_flash_3_hub"), "native")
self.transformer.set_attention_backend.assert_called_once_with("native")
def test_hopper_preserves_requested_flash_attention_3(self):
self.cuda.get_device_capability.return_value = (9, 0)
self.assertEqual(self.select("_flash_3_hub"), "_flash_3_hub")
self.transformer.set_attention_backend.assert_called_once_with("_flash_3_hub")
self.assertIsNone(self.namespace["attention_backend_error"])
def test_unknown_gpu_uses_native(self):
self.cuda.get_device_capability.side_effect = RuntimeError("CUDA unavailable")
self.assertEqual(self.select("_flash_3_hub"), "native")
self.transformer.set_attention_backend.assert_called_once_with("native")
def test_no_cuda_uses_native_without_querying_device(self):
self.cuda.is_available.return_value = False
self.assertEqual(self.select("_flash_3_hub"), "native")
self.cuda.get_device_capability.assert_not_called()
def test_native_failure_does_not_try_incompatible_kernels(self):
self.transformer.set_attention_backend.side_effect = RuntimeError("native failed")
with self.assertRaisesRegex(RuntimeError, "native failed"):
self.select("_flash_3_hub")
self.transformer.set_attention_backend.assert_called_once_with("native")
def test_backend_setup_error_falls_back_to_native(self):
def configure(name):
if name != "native":
raise ValueError("backend unavailable")
self.transformer.set_attention_backend.side_effect = configure
self.assertEqual(self.select("missing_backend"), "native")
attempts = [call.args[0] for call in self.transformer.set_attention_backend.call_args_list]
self.assertEqual(attempts, ["missing_backend", "_missing_backend", "native"])
self.assertEqual(self.namespace["attention_backend_error"], "backend unavailable")
def test_native_selection_clears_previous_error(self):
self.select("_flash_3_hub")
self.assertEqual(self.select("native"), "native")
self.assertIsNone(self.namespace["attention_backend_error"])
def initialize(self, *, lora=False, allow_lora=False):
loader = Mock()
self.namespace.update(
pipe=SimpleNamespace(transformer=self.transformer),
ensure_models_loaded=Mock(), ENABLE_COMPILE=False, ENABLE_AOTI=True,
ENABLE_WARMUP=False, aoti_loaded=False, aoti_error=None,
lora_loaded=lora, AOTI_ALLOW_LORA=allow_lora, AOTI_VARIANT="fa3",
AOTI_REPO="zerogpu-aoti/Z-Image", ATTENTION_BACKEND="_flash_3_hub",
spaces=SimpleNamespace(aoti_blocks_load=loader),
)
with contextlib.redirect_stdout(io.StringIO()):
self.namespace["init_app"]()
return loader
def test_blackwell_skips_fa3_aoti(self):
loader = self.initialize()
loader.assert_not_called()
self.assertFalse(self.namespace["aoti_loaded"])
self.assertIn("Hopper", self.namespace["aoti_error"])
def test_allow_lora_cannot_override_hardware_guard(self):
loader = self.initialize(lora=True, allow_lora=True)
loader.assert_not_called()
self.assertFalse(self.namespace["aoti_loaded"])
def test_hopper_can_load_fa3_aoti(self):
self.cuda.get_device_capability.return_value = (9, 0)
loader = self.initialize()
loader.assert_called_once_with(self.transformer.layers, "zerogpu-aoti/Z-Image", variant="fa3")
self.assertTrue(self.namespace["aoti_loaded"])
def test_hopper_still_skips_aoti_with_lora(self):
self.cuda.get_device_capability.return_value = (9, 0)
loader = self.initialize(lora=True)
loader.assert_not_called()
self.assertIn("LoRA", self.namespace["aoti_error"])
if __name__ == "__main__":
unittest.main()
|