FrankFundel commited on
Commit
b2071ea
·
1 Parent(s): 308669f

Select attention kernels for the allocated GPU architecture

Browse files
app/README.md CHANGED
@@ -100,8 +100,11 @@ resolved and its CUDA modules are registered during startup so the platform can
100
  restore them efficiently for each generation. Input decoding and resizing run
101
  before the GPU lease, and MP4 encoding runs after it; only model inference is in
102
  the decorated generation callback. The app uses Hugging Face's prebuilt
103
- FlashAttention 3 kernel when compatible and falls back to installed
104
- FlashAttention or PyTorch SDPA otherwise. Set `WHATMOVES_DISABLE_FA3=1` to skip
 
 
 
105
  FlashAttention 3. A generation uses an immutable snapshot of the selected
106
  target, source intervals, masks, and settings present when **Generate video**
107
  was pressed; you may continue editing while it runs. The completed video is
 
100
  restore them efficiently for each generation. Input decoding and resizing run
101
  before the GPU lease, and MP4 encoding runs after it; only model inference is in
102
  the decorated generation callback. The app uses Hugging Face's prebuilt
103
+ FlashAttention 3 kernel on Hopper GPUs, installed FlashAttention 2 on supported
104
+ Ampere/Hopper GPUs, and PyTorch SDPA on Blackwell (including ZeroGPU's RTX Pro
105
+ 6000). Compatibility is checked on the allocated GPU before any kernel launch;
106
+ the selected backend and GPU model appear in the timing logs.
107
+ Set `WHATMOVES_DISABLE_FA3=1` to skip
108
  FlashAttention 3. A generation uses an immutable snapshot of the selected
109
  target, source intervals, masks, and settings present when **Generate video**
110
  was pressed; you may continue editing while it runs. The completed video is
tests/test_attention_backend.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU-only regression tests for runtime CUDA kernel selection.
2
+
3
+ Run with: python -m unittest discover -s tests -v
4
+ """
5
+
6
+ import importlib.util
7
+ import os
8
+ from pathlib import Path
9
+ import sys
10
+ from types import SimpleNamespace
11
+ import unittest
12
+ from unittest.mock import Mock, patch
13
+
14
+ import torch
15
+
16
+
17
+ class AttentionBackendTests(unittest.TestCase):
18
+ def setUp(self):
19
+ self.fa3 = SimpleNamespace(flash_attn_varlen_func=Mock())
20
+ spec = importlib.util.spec_from_file_location(
21
+ "attention_under_test",
22
+ Path(__file__).resolve().parents[1] / "wan/modules/attention.py",
23
+ )
24
+ self.module = importlib.util.module_from_spec(spec)
25
+ # Importing a prebuilt extension must not inspect ZeroGPU's startup
26
+ # proxy or launch any CUDA work. Hardware is selected at inference.
27
+ with (
28
+ patch.dict(os.environ, {"WHATMOVES_DISABLE_FA3": "0"}),
29
+ patch.dict(sys.modules, {
30
+ "kernels": SimpleNamespace(get_kernel=Mock(return_value=self.fa3)),
31
+ "flash_attn": None,
32
+ }),
33
+ patch.object(torch.cuda, "get_device_capability",
34
+ side_effect=AssertionError("startup CUDA probe")),
35
+ ):
36
+ spec.loader.exec_module(self.module)
37
+ self.fa3.flash_attn_varlen_func.assert_not_called()
38
+
39
+ def test_architecture_and_availability_matrix(self):
40
+ cases = [
41
+ ((9, 0), True, False, None, 3),
42
+ ((9, 0), False, True, None, 2),
43
+ ((9, 0), True, True, 2, 2),
44
+ ((9, 0), True, False, 2, None),
45
+ ((8, 0), True, True, None, 2),
46
+ ((8, 9), True, True, None, 2),
47
+ ((8, 0), True, False, None, None),
48
+ ((10, 0), True, True, None, None),
49
+ ((12, 0), True, True, None, None),
50
+ ((12, 0), True, False, 3, None),
51
+ ((7, 5), True, True, None, None),
52
+ ]
53
+ for capability, fa3, fa2, requested, expected in cases:
54
+ with self.subTest(capability=capability, fa3=fa3, fa2=fa2,
55
+ requested=requested):
56
+ self.module._flash_version_for_device.cache_clear()
57
+ with (
58
+ patch.object(self.module, "FLASH_ATTN_3_AVAILABLE", fa3),
59
+ patch.object(self.module, "FLASH_ATTN_2_AVAILABLE", fa2),
60
+ patch.object(torch.cuda, "get_device_capability",
61
+ return_value=capability),
62
+ patch.object(torch.cuda, "get_device_name", return_value="test GPU"),
63
+ ):
64
+ self.assertEqual(
65
+ self.module._flash_version_for_device(
66
+ torch.device("cuda:0"), requested),
67
+ expected,
68
+ )
69
+
70
+ def test_blackwell_never_launches_imported_fa3(self):
71
+ torch.manual_seed(0)
72
+ q = torch.randn(1, 5, 2, 8)
73
+ k = torch.randn(1, 5, 2, 8)
74
+ v = torch.randn(1, 5, 2, 8)
75
+ # The mocked query supplies CUDA metadata while actual SDPA runs on
76
+ # CPU, so this checks dispatch without needing or charging for a GPU.
77
+ allocated_q = SimpleNamespace(device=torch.device("cuda:0"),
78
+ transpose=q.transpose)
79
+ expected = torch.nn.functional.scaled_dot_product_attention(
80
+ q.transpose(1, 2).to(torch.bfloat16),
81
+ k.transpose(1, 2).to(torch.bfloat16),
82
+ v.transpose(1, 2).to(torch.bfloat16),
83
+ ).transpose(1, 2).contiguous()
84
+ with (
85
+ patch.object(torch.cuda, "get_device_capability", return_value=(12, 0)),
86
+ patch.object(torch.cuda, "get_device_name", return_value="RTX Pro 6000"),
87
+ ):
88
+ for entrypoint in (self.module.attention, self.module.flash_attention):
89
+ torch.testing.assert_close(entrypoint(allocated_q, k, v), expected)
90
+ self.fa3.flash_attn_varlen_func.assert_not_called()
91
+
92
+ def test_hopper_routes_to_flash_attention(self):
93
+ q = SimpleNamespace(device=torch.device("cuda:0"))
94
+ with (
95
+ patch.object(torch.cuda, "get_device_capability", return_value=(9, 0)),
96
+ patch.object(torch.cuda, "get_device_name", return_value="H100"),
97
+ patch.object(self.module, "flash_attention", return_value="flash result") as call,
98
+ ):
99
+ self.assertEqual(self.module.attention(q, q, q), "flash result")
100
+ call.assert_called_once()
101
+
102
+ def test_cpu_does_not_query_cuda(self):
103
+ with patch.object(torch.cuda, "get_device_capability",
104
+ side_effect=AssertionError("CPU queried CUDA")):
105
+ self.assertIsNone(self.module._flash_version_for_device(torch.device("cpu")))
106
+
107
+ def test_selection_is_cached_per_device(self):
108
+ with (
109
+ patch.object(torch.cuda, "get_device_capability", return_value=(12, 0)) as probe,
110
+ patch.object(torch.cuda, "get_device_name", return_value="RTX Pro 6000"),
111
+ ):
112
+ for _ in range(5):
113
+ self.module._flash_version_for_device(torch.device("cuda:0"))
114
+ probe.assert_called_once()
115
+
116
+
117
+ if __name__ == "__main__":
118
+ unittest.main()
wan/modules/attention.py CHANGED
@@ -1,4 +1,5 @@
1
  # Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
 
2
  import logging
3
  import os
4
  import warnings
@@ -28,7 +29,7 @@ def _load_flash_attention_3():
28
  if not callable(getattr(interface, "flash_attn_varlen_func", None)):
29
  raise AttributeError("kernel has no flash_attn_varlen_func")
30
  logger.info(
31
- "flash_attention backend=fa3 source=kernels-community/vllm-flash-attn3"
32
  )
33
  return interface
34
  except Exception as error:
@@ -49,7 +50,7 @@ try:
49
 
50
  flash_attn_interface = _installed_flash_attn_interface
51
  FLASH_ATTN_3_AVAILABLE = True
52
- logger.info("flash_attention backend=fa3 source=installed")
53
  except Exception as error:
54
  logger.info(
55
  "flash_attention installed_fa3_unavailable error=%s: %s",
@@ -75,6 +76,37 @@ __all__ = [
75
  ]
76
 
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  def flash_attention(
79
  q,
80
  k,
@@ -103,6 +135,14 @@ def flash_attention(
103
  deterministic: bool. If True, slightly slower and uses more memory.
104
  dtype: torch.dtype. Apply when dtype of q/k/v is not float16/bfloat16.
105
  """
 
 
 
 
 
 
 
 
106
  half_dtypes = (torch.float16, torch.bfloat16)
107
  assert dtype in half_dtypes
108
  assert q.device.type == 'cuda' and q.size(-1) <= 256
@@ -145,7 +185,7 @@ def flash_attention(
145
  )
146
 
147
  # apply attention
148
- if (version is None or version == 3) and FLASH_ATTN_3_AVAILABLE:
149
  # Note: dropout_p, window_size are not supported in FA3 now.
150
  x = flash_attn_interface.flash_attn_varlen_func(
151
  q=q,
@@ -199,7 +239,7 @@ def attention(
199
  dtype=torch.bfloat16,
200
  fa_version=None,
201
  ):
202
- if FLASH_ATTN_2_AVAILABLE or FLASH_ATTN_3_AVAILABLE:
203
  return flash_attention(
204
  q=q,
205
  k=k,
 
1
  # Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
2
+ from functools import lru_cache
3
  import logging
4
  import os
5
  import warnings
 
29
  if not callable(getattr(interface, "flash_attn_varlen_func", None)):
30
  raise AttributeError("kernel has no flash_attn_varlen_func")
31
  logger.info(
32
+ "flash_attention fa3_loaded source=kernels-community/vllm-flash-attn3"
33
  )
34
  return interface
35
  except Exception as error:
 
50
 
51
  flash_attn_interface = _installed_flash_attn_interface
52
  FLASH_ATTN_3_AVAILABLE = True
53
+ logger.info("flash_attention fa3_loaded source=installed")
54
  except Exception as error:
55
  logger.info(
56
  "flash_attention installed_fa3_unavailable error=%s: %s",
 
76
  ]
77
 
78
 
79
+ @lru_cache(maxsize=32)
80
+ def _flash_version_for_device(device, requested_version=None):
81
+ """Select kernels on the allocated GPU, not ZeroGPU's startup proxy.
82
+
83
+ Importing FA3 does not establish hardware compatibility. Its Hopper
84
+ kernels cannot execute on Blackwell (including ZeroGPU's RTX Pro 6000),
85
+ and the CUDA extension can terminate the worker instead of raising a
86
+ catchable Python exception. Reject incompatible architectures before the
87
+ first kernel launch. PyTorch SDPA dispatches its own supported kernels.
88
+ """
89
+ if device.type != "cuda":
90
+ return None
91
+ capability = torch.cuda.get_device_capability(device)
92
+ selected = None
93
+ if (
94
+ requested_version in (None, 3)
95
+ and FLASH_ATTN_3_AVAILABLE
96
+ and capability == (9, 0)
97
+ ):
98
+ selected = 3
99
+ elif FLASH_ATTN_2_AVAILABLE and capability[0] in (8, 9):
100
+ selected = 2
101
+ logger.info(
102
+ "flash_attention selected_backend=%s gpu=%s compute_capability=%d.%d",
103
+ f"fa{selected}" if selected else "torch_sdpa",
104
+ torch.cuda.get_device_name(device),
105
+ *capability,
106
+ )
107
+ return selected
108
+
109
+
110
  def flash_attention(
111
  q,
112
  k,
 
135
  deterministic: bool. If True, slightly slower and uses more memory.
136
  dtype: torch.dtype. Apply when dtype of q/k/v is not float16/bfloat16.
137
  """
138
+ selected_version = _flash_version_for_device(q.device, version)
139
+ if selected_version is None:
140
+ return attention(
141
+ q=q, k=k, v=v, q_lens=q_lens, k_lens=k_lens,
142
+ dropout_p=dropout_p, softmax_scale=softmax_scale, q_scale=q_scale,
143
+ causal=causal, window_size=window_size,
144
+ deterministic=deterministic, dtype=dtype, fa_version=version,
145
+ )
146
  half_dtypes = (torch.float16, torch.bfloat16)
147
  assert dtype in half_dtypes
148
  assert q.device.type == 'cuda' and q.size(-1) <= 256
 
185
  )
186
 
187
  # apply attention
188
+ if selected_version == 3:
189
  # Note: dropout_p, window_size are not supported in FA3 now.
190
  x = flash_attn_interface.flash_attn_varlen_func(
191
  q=q,
 
239
  dtype=torch.bfloat16,
240
  fa_version=None,
241
  ):
242
+ if _flash_version_for_device(q.device, fa_version) is not None:
243
  return flash_attention(
244
  q=q,
245
  k=k,