mxflashattn 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mxflashattn/api.py ADDED
@@ -0,0 +1,286 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ import torch
6
+
7
+ from . import reference
8
+ from .dispatch import dispatch
9
+ from .validation import validate_cumulative_lengths, validate_options, validate_qkv, validate_softmax_scale
10
+
11
+
12
+ def _resolved_scale(head_dim: int, softmax_scale: float | None) -> float:
13
+ return float(softmax_scale) if softmax_scale is not None else float(head_dim**-0.5)
14
+
15
+
16
+ def _options(
17
+ dropout_p: float,
18
+ window_size: tuple[int, int],
19
+ softcap: float,
20
+ alibi_slopes: torch.Tensor | None,
21
+ deterministic: bool,
22
+ return_attn_probs: bool,
23
+ ) -> None:
24
+ validate_options(
25
+ dropout_p=dropout_p,
26
+ window_size=window_size,
27
+ softcap=softcap,
28
+ alibi_slopes=alibi_slopes,
29
+ deterministic=deterministic,
30
+ return_attn_probs=return_attn_probs,
31
+ )
32
+
33
+
34
+ def flash_attn_func(
35
+ q: torch.Tensor,
36
+ k: torch.Tensor,
37
+ v: torch.Tensor,
38
+ dropout_p: float = 0.0,
39
+ softmax_scale: float | None = None,
40
+ causal: bool = False,
41
+ window_size: tuple[int, int] = (-1, -1),
42
+ softcap: float = 0.0,
43
+ alibi_slopes: torch.Tensor | None = None,
44
+ deterministic: bool = False,
45
+ return_attn_probs: bool = False,
46
+ ) -> torch.Tensor:
47
+ validate_qkv(q, k, v)
48
+ validate_softmax_scale(softmax_scale)
49
+ _options(dropout_p, window_size, softcap, alibi_slopes, deterministic, return_attn_probs)
50
+ return dispatch(
51
+ "flash_attn_func",
52
+ q,
53
+ (q, k, v, causal, _resolved_scale(q.shape[-1], softmax_scale)),
54
+ lambda: reference.attention_batched(q, k, v, softmax_scale=softmax_scale, causal=causal),
55
+ native_args=(q, k, v),
56
+ native_kwargs={
57
+ "dropout_p": dropout_p,
58
+ "softmax_scale": softmax_scale,
59
+ "causal": causal,
60
+ "window_size": window_size,
61
+ "softcap": softcap,
62
+ "alibi_slopes": alibi_slopes,
63
+ "deterministic": deterministic,
64
+ "return_attn_probs": return_attn_probs,
65
+ },
66
+ )
67
+
68
+
69
+ def flash_attn_varlen_func(
70
+ q: torch.Tensor,
71
+ k: torch.Tensor,
72
+ v: torch.Tensor,
73
+ cu_seqlens_q: torch.Tensor,
74
+ cu_seqlens_k: torch.Tensor,
75
+ max_seqlen_q: int,
76
+ max_seqlen_k: int,
77
+ dropout_p: float = 0.0,
78
+ softmax_scale: float | None = None,
79
+ causal: bool = False,
80
+ window_size: tuple[int, int] = (-1, -1),
81
+ softcap: float = 0.0,
82
+ alibi_slopes: torch.Tensor | None = None,
83
+ deterministic: bool = False,
84
+ return_attn_probs: bool = False,
85
+ ) -> torch.Tensor:
86
+ validate_softmax_scale(softmax_scale)
87
+ for name, tensor in (("q", q), ("k", k), ("v", v)):
88
+ if not isinstance(tensor, torch.Tensor) or tensor.ndim != 3:
89
+ raise ValueError(f"{name} must have rank 3 with layout [total_tokens, heads, dim]")
90
+ if k.shape != v.shape:
91
+ raise ValueError("k and v must have identical shapes")
92
+ if q.device != k.device or q.device != v.device:
93
+ raise ValueError("q, k, and v must be on the same device")
94
+ if q.dtype != k.dtype or q.dtype != v.dtype:
95
+ raise ValueError("q, k, and v must have the same dtype")
96
+ if q.dtype not in (torch.float16, torch.bfloat16, torch.float32):
97
+ raise TypeError("supported dtypes are float16, bfloat16, and float32 for reference execution")
98
+ if q.shape[1] <= 0 or k.shape[1] <= 0 or q.shape[1] % k.shape[1]:
99
+ raise ValueError("q heads must be a positive multiple of kv heads")
100
+ if q.shape[2] not in (64, 128) or k.shape[2] != q.shape[2]:
101
+ raise ValueError("head dimension must be 64 or 128 and match across q, k, and v")
102
+ if not isinstance(cu_seqlens_q, torch.Tensor) or not isinstance(cu_seqlens_k, torch.Tensor):
103
+ raise TypeError("cu_seqlens_q and cu_seqlens_k must be torch.Tensor values")
104
+ if cu_seqlens_q.device != q.device or cu_seqlens_k.device != q.device:
105
+ raise ValueError("cu_seqlens_q and cu_seqlens_k must be on the same device as q")
106
+ _options(dropout_p, window_size, softcap, alibi_slopes, deterministic, return_attn_probs)
107
+ q_offsets = validate_cumulative_lengths(
108
+ cu_seqlens_q, total_tokens=q.shape[0], max_seqlen=max_seqlen_q, name="cu_seqlens_q"
109
+ )
110
+ k_offsets = validate_cumulative_lengths(
111
+ cu_seqlens_k,
112
+ total_tokens=k.shape[0],
113
+ max_seqlen=max_seqlen_k,
114
+ name="cu_seqlens_k",
115
+ require_non_empty=True,
116
+ )
117
+ if len(q_offsets) != len(k_offsets):
118
+ raise ValueError("cu_seqlens_q and cu_seqlens_k must describe the same batch size")
119
+
120
+ return dispatch(
121
+ "flash_attn_varlen_func",
122
+ q,
123
+ (
124
+ q,
125
+ k,
126
+ v,
127
+ cu_seqlens_q,
128
+ cu_seqlens_k,
129
+ max_seqlen_q,
130
+ max_seqlen_k,
131
+ causal,
132
+ _resolved_scale(q.shape[-1], softmax_scale),
133
+ ),
134
+ lambda: reference.attention_varlen(
135
+ q, k, v, q_offsets, k_offsets, softmax_scale=softmax_scale, causal=causal
136
+ ),
137
+ native_args=(q, k, v, cu_seqlens_q, cu_seqlens_k, max_seqlen_q, max_seqlen_k),
138
+ native_kwargs={
139
+ "dropout_p": dropout_p,
140
+ "softmax_scale": softmax_scale,
141
+ "causal": causal,
142
+ "window_size": window_size,
143
+ "softcap": softcap,
144
+ "alibi_slopes": alibi_slopes,
145
+ "deterministic": deterministic,
146
+ "return_attn_probs": return_attn_probs,
147
+ },
148
+ )
149
+
150
+
151
+ def flash_attn_with_kvcache(
152
+ q: torch.Tensor,
153
+ k_cache: torch.Tensor,
154
+ v_cache: torch.Tensor,
155
+ k: torch.Tensor | None = None,
156
+ v: torch.Tensor | None = None,
157
+ cache_seqlens: torch.Tensor | None = None,
158
+ block_table: torch.Tensor | None = None,
159
+ softmax_scale: float | None = None,
160
+ causal: bool = False,
161
+ window_size: tuple[int, int] = (-1, -1),
162
+ softcap: float = 0.0,
163
+ alibi_slopes: torch.Tensor | None = None,
164
+ rotary_cos: torch.Tensor | None = None,
165
+ rotary_sin: torch.Tensor | None = None,
166
+ return_softmax_lse: bool = False,
167
+ ) -> torch.Tensor:
168
+ validate_softmax_scale(softmax_scale)
169
+ if rotary_cos is not None or rotary_sin is not None:
170
+ raise NotImplementedError("rotary embeddings are not supported")
171
+ if return_softmax_lse:
172
+ raise NotImplementedError("return_softmax_lse is not supported")
173
+ _options(0.0, window_size, softcap, alibi_slopes, False, False)
174
+ if (k is None) != (v is None):
175
+ raise ValueError("k and v must either both be provided or both be omitted")
176
+ if q.ndim != 4 or k_cache.ndim != 4 or v_cache.shape != k_cache.shape:
177
+ raise ValueError("q and cache tensors must have rank 4 and matching k/v cache shapes")
178
+ if q.device != k_cache.device or q.device != v_cache.device:
179
+ raise ValueError("q and cache tensors must be on the same device")
180
+ if q.dtype != k_cache.dtype or q.dtype != v_cache.dtype:
181
+ raise ValueError("q and cache tensors must have the same dtype")
182
+ if q.dtype not in (torch.float16, torch.bfloat16, torch.float32):
183
+ raise TypeError("supported dtypes are float16, bfloat16, and float32 for reference execution")
184
+ if q.shape[0] != k_cache.shape[0] and block_table is None:
185
+ raise ValueError("dense cache batch dimension must match q")
186
+ if q.shape[2] <= 0 or k_cache.shape[2] <= 0:
187
+ raise ValueError("q and kv cache head counts must be positive")
188
+ if k_cache.shape[1] <= 0:
189
+ raise ValueError("cache page size must be positive")
190
+ if q.shape[3] not in (64, 128) or k_cache.shape[3] != q.shape[3]:
191
+ raise ValueError("head dimension must be 64 or 128 and match across q and caches")
192
+ if q.shape[2] % k_cache.shape[2]:
193
+ raise ValueError("q heads must be divisible by kv cache heads")
194
+ if (k is not None and (k.shape != v.shape or k.ndim != 4)):
195
+ raise ValueError("appended k and v must have identical rank-4 shapes")
196
+ if k is not None and (k.shape[0] != q.shape[0] or k.shape[2:] != k_cache.shape[2:]):
197
+ raise ValueError("appended k/v must match batch, kv heads, and head dimension")
198
+ if k is not None and (k.device != q.device or k.dtype != q.dtype or v.device != q.device or v.dtype != q.dtype):
199
+ raise ValueError("appended k/v must match the query device and dtype")
200
+ if cache_seqlens is None:
201
+ raise ValueError("cache_seqlens is required for dense and paged caches")
202
+ if not isinstance(cache_seqlens, torch.Tensor):
203
+ raise TypeError("cache_seqlens must be a torch.Tensor")
204
+ if cache_seqlens.ndim != 1 or cache_seqlens.numel() != q.shape[0]:
205
+ raise ValueError("cache_seqlens must be one-dimensional with one value per batch item")
206
+ if cache_seqlens.dtype not in (torch.int32, torch.int64):
207
+ raise TypeError("cache_seqlens must use int32 or int64")
208
+ if cache_seqlens.device != q.device:
209
+ raise ValueError("cache_seqlens must be on the same device as q")
210
+ if block_table is not None:
211
+ if not isinstance(block_table, torch.Tensor):
212
+ raise TypeError("block_table must be a torch.Tensor")
213
+ if block_table.ndim != 2 or block_table.shape[0] != q.shape[0]:
214
+ raise ValueError("block_table must have one row per batch item")
215
+ if block_table.dtype not in (torch.int32, torch.int64):
216
+ raise TypeError("block_table must use int32 or int64")
217
+ if block_table.device != q.device:
218
+ raise ValueError("block_table must be on the same device as q")
219
+ else:
220
+ if k_cache.shape[0] != q.shape[0]:
221
+ raise ValueError("dense cache batch dimension must match q")
222
+
223
+ # Deep validation reads cache_seqlens / block_table back to the host, which
224
+ # costs a device synchronisation on every call. Inside vLLM this runs once
225
+ # per layer per decode step, so it dominates the end-to-end cost. Cheap
226
+ # shape/device/dtype checks above always run; the host-syncing checks are
227
+ # gated and enabled by the test suite via MXFLASHATTN_STRICT_VALIDATION=1.
228
+ if os.getenv("MXFLASHATTN_STRICT_VALIDATION", "0") == "1":
229
+ lengths = [int(length) for length in cache_seqlens.detach().cpu().tolist()]
230
+ if any(length < 0 for length in lengths):
231
+ raise ValueError("cache sequence lengths must be non-negative")
232
+ if any(length + (0 if k is None else k.shape[1]) == 0 for length in lengths):
233
+ raise ValueError("attention requires at least one cached or appended key/value token")
234
+ if block_table is None:
235
+ capacity = k_cache.shape[1]
236
+ if any(length + (0 if k is None else k.shape[1]) > capacity for length in lengths):
237
+ raise ValueError("cache sequence length exceeds dense cache capacity")
238
+ else:
239
+ rows = block_table.detach().cpu().tolist()
240
+ max_blocks = block_table.shape[1]
241
+ pages = k_cache.shape[0]
242
+ for length, row in zip(lengths, rows):
243
+ used = (length + (0 if k is None else k.shape[1]) + k_cache.shape[1] - 1) // k_cache.shape[1]
244
+ if used > max_blocks:
245
+ raise ValueError("block_table does not contain enough pages for cache sequence")
246
+ if any(int(page) < 0 or int(page) >= pages for page in row[:used]):
247
+ raise ValueError("block_table contains an out-of-range page index")
248
+
249
+ return dispatch(
250
+ "flash_attn_with_kvcache",
251
+ q,
252
+ (
253
+ q,
254
+ k_cache,
255
+ v_cache,
256
+ k,
257
+ v,
258
+ cache_seqlens,
259
+ block_table,
260
+ _resolved_scale(q.shape[-1], softmax_scale),
261
+ causal,
262
+ ),
263
+ lambda: reference.attention_kvcache(
264
+ q,
265
+ k_cache,
266
+ v_cache,
267
+ cache_seqlens=cache_seqlens,
268
+ block_table=block_table,
269
+ k=k,
270
+ v=v,
271
+ softmax_scale=softmax_scale,
272
+ causal=causal,
273
+ ),
274
+ native_args=(q, k_cache, v_cache),
275
+ native_kwargs={
276
+ "k": k,
277
+ "v": v,
278
+ "cache_seqlens": cache_seqlens,
279
+ "block_table": block_table,
280
+ "softmax_scale": softmax_scale,
281
+ "causal": causal,
282
+ "window_size": window_size,
283
+ "softcap": softcap,
284
+ "alibi_slopes": alibi_slopes,
285
+ },
286
+ )
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import importlib.metadata
5
+ import os
6
+ import threading
7
+ import warnings
8
+ from dataclasses import dataclass
9
+ from functools import lru_cache
10
+ from typing import Any, Callable
11
+
12
+ import torch
13
+
14
+
15
+ class BackendUnavailableError(RuntimeError):
16
+ """Raised when native execution is unavailable and fallback was not enabled."""
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DispatchInfo:
21
+ backend: str
22
+ operation: str
23
+ fallback_reason: str | None = None
24
+
25
+
26
+ _state = threading.local()
27
+
28
+
29
+ def get_last_dispatch_info() -> DispatchInfo | None:
30
+ return getattr(_state, "dispatch_info", None)
31
+
32
+
33
+ def _native_extension() -> Any | None:
34
+ try:
35
+ return importlib.import_module("mxflashattn._C")
36
+ except (ImportError, OSError):
37
+ return None
38
+
39
+
40
+ @lru_cache(maxsize=1)
41
+ def _metax_flash_attn_module() -> Any | None:
42
+ try:
43
+ module = importlib.import_module("flash_attn")
44
+ except (ImportError, OSError):
45
+ return None
46
+ try:
47
+ version = importlib.metadata.version("flash-attn")
48
+ except importlib.metadata.PackageNotFoundError:
49
+ version = str(getattr(module, "__version__", ""))
50
+ if "metax" not in version.casefold():
51
+ return None
52
+ return module
53
+
54
+
55
+ def _flash_attn_function(operation: str) -> Callable[..., torch.Tensor] | None:
56
+ if os.getenv("MXFLASHATTN_BACKEND", "auto").strip().casefold() == "aten":
57
+ return None
58
+ module = _metax_flash_attn_module()
59
+ if module is None:
60
+ return None
61
+ function = getattr(module, operation, None)
62
+ return function if callable(function) else None
63
+
64
+
65
+ def native_unavailable_reason(
66
+ tensor: torch.Tensor,
67
+ operation: str,
68
+ *,
69
+ require_aten: bool = False,
70
+ ) -> str | None:
71
+ if tensor.device.type not in {"cuda", "maca", "musa"}:
72
+ return "native_backend_requires_accelerator_tensor"
73
+ if tensor.dtype not in (torch.float16, torch.bfloat16):
74
+ return "native_backend_supports_only_float16_and_bfloat16"
75
+ if _flash_attn_function(operation) is not None and not require_aten:
76
+ return None
77
+ extension = _native_extension()
78
+ if extension is None:
79
+ if require_aten:
80
+ return "zero_softmax_scale_requires_aten_extension"
81
+ return "native_extension_not_built_or_not_importable"
82
+ if not bool(extension.is_available()):
83
+ return "native_extension_reports_unavailable_runtime"
84
+ if not hasattr(extension, operation):
85
+ return f"native_extension_does_not_implement_{operation}"
86
+ return None
87
+
88
+
89
+ def dispatch(
90
+ operation: str,
91
+ tensor: torch.Tensor,
92
+ extension_args: tuple[Any, ...],
93
+ reference: Callable[[], torch.Tensor],
94
+ *,
95
+ native_args: tuple[Any, ...],
96
+ native_kwargs: dict[str, Any],
97
+ ) -> torch.Tensor:
98
+ """Dispatch vendor-direct first, then ATen correctness, then opt-in reference.
99
+
100
+ The vendor wheel is the performance path. Any vendor import, signature, or
101
+ runtime failure is recorded and routed to the ATen extension when it is
102
+ available; it is never silently relabeled as a native FlashAttention path.
103
+ """
104
+ _state.dispatch_info = None
105
+ require_aten = native_kwargs.get("softmax_scale") == 0.0
106
+ vendor = None if require_aten else _flash_attn_function(operation)
107
+ # Preserve the actionable contract reason before attempting fallbacks. In
108
+ # particular, CPU reference tests and callers should see that native
109
+ # execution requires an accelerator rather than a generic import failure.
110
+ failure_reason: str | None = native_unavailable_reason(
111
+ tensor, operation, require_aten=require_aten
112
+ )
113
+
114
+ if vendor is not None:
115
+ try:
116
+ result = vendor(*native_args, **native_kwargs)
117
+ if operation == "flash_attn_with_kvcache" and native_kwargs.get("k") is not None:
118
+ # Documented contract: appending k/v advances the caller's
119
+ # cache_seqlens in place. Only this branch mutates it, so callers
120
+ # that pass k=None (the vLLM decode path) need no defensive copy.
121
+ native_kwargs["cache_seqlens"].add_(native_kwargs["k"].shape[1])
122
+ _state.dispatch_info = DispatchInfo(backend="vendor_direct", operation=operation)
123
+ return result
124
+ except (Exception, RuntimeError) as exc:
125
+ failure_reason = f"vendor_call_failed:{type(exc).__name__}:{exc}"
126
+
127
+ extension = _native_extension()
128
+ extension_reason = ""
129
+ if extension is not None:
130
+ try:
131
+ available = bool(extension.is_available())
132
+ except Exception as exc:
133
+ available = False
134
+ extension_reason = f"aten_availability_check_failed:{type(exc).__name__}:{exc}"
135
+ if available and hasattr(extension, operation):
136
+ try:
137
+ result = getattr(extension, operation)(*extension_args)
138
+ _state.dispatch_info = DispatchInfo(
139
+ backend="mxmac_aten_correctness",
140
+ operation=operation,
141
+ fallback_reason=failure_reason,
142
+ )
143
+ return result
144
+ except Exception as exc:
145
+ extension_reason = f"aten_call_failed:{type(exc).__name__}:{exc}"
146
+ elif extension is None:
147
+ extension_reason = "aten_extension_not_importable"
148
+ elif not hasattr(extension, operation):
149
+ extension_reason = f"aten_extension_does_not_implement_{operation}"
150
+ elif not extension_reason:
151
+ extension_reason = "aten_extension_reports_unavailable_runtime"
152
+
153
+ reason = ";".join(part for part in (failure_reason, extension_reason) if part)
154
+ if not reason:
155
+ reason = "vendor_unavailable_or_parameter_incompatible"
156
+ if os.getenv("MXFLASHATTN_ALLOW_FALLBACK", "").strip() != "1":
157
+ raise BackendUnavailableError(
158
+ f"MXFlashAttn dispatch failed ({reason}); set MXFLASHATTN_ALLOW_FALLBACK=1 "
159
+ "to explicitly allow the PyTorch reference path"
160
+ )
161
+
162
+ _state.dispatch_info = DispatchInfo(
163
+ backend="reference", operation=operation, fallback_reason=reason
164
+ )
165
+ warnings.warn(
166
+ f"MXFlashAttn is using the PyTorch reference path: {reason}",
167
+ RuntimeWarning,
168
+ stacklevel=3,
169
+ )
170
+ return reference()
171
+
172
+
mxflashattn/policy.py ADDED
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+
7
+ Phase = Literal["prefill", "decode"]
8
+ Backend = Literal["vendor", "m1_prefill_chunked", "m1_split_kv"]
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class WorkloadShape:
13
+ """Shape metadata used by the v1.4 candidate policy.
14
+
15
+ This object contains no tensors and performs no device queries. It is
16
+ deliberately suitable for local tests and for later integration with a
17
+ vLLM adapter.
18
+ """
19
+
20
+ phase: Phase
21
+ batch_size: int
22
+ query_len: int
23
+ kv_len: int
24
+ head_dim: int
25
+ gqa_ratio: int
26
+
27
+ def __post_init__(self) -> None:
28
+ if self.phase not in ("prefill", "decode"):
29
+ raise ValueError("phase must be 'prefill' or 'decode'")
30
+ if self.batch_size <= 0 or self.query_len <= 0 or self.kv_len <= 0:
31
+ raise ValueError("batch_size, query_len, and kv_len must be positive")
32
+ if self.head_dim <= 0 or self.gqa_ratio <= 0:
33
+ raise ValueError("head_dim and gqa_ratio must be positive")
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class DispatchPolicyConfig:
38
+ """Candidate thresholds, not production calibration.
39
+
40
+ The values are intentionally disabled by default. They must be calibrated
41
+ on the target C500 environment before being used for runtime dispatch.
42
+ """
43
+
44
+ enabled: bool = False
45
+ calibrated: bool = False
46
+ prefill_batch_min: int = 4
47
+ prefill_kv_len_min: int = 512
48
+ decode_batch_min: int = 16
49
+ decode_kv_len_min: int = 256
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class DispatchDecision:
54
+ backend: Backend
55
+ reason: str
56
+ eligible: bool
57
+ policy_enabled: bool
58
+ thresholds_calibrated: bool
59
+
60
+
61
+ class AdaptiveDispatchPolicy:
62
+ """Safe, tensor-free candidate policy for later runtime integration."""
63
+
64
+ def __init__(self, config: DispatchPolicyConfig | None = None) -> None:
65
+ self.config = config or DispatchPolicyConfig()
66
+
67
+ def select(self, shape: WorkloadShape) -> DispatchDecision:
68
+ config = self.config
69
+ if not config.enabled:
70
+ return DispatchDecision(
71
+ backend="vendor",
72
+ reason="policy_disabled",
73
+ eligible=False,
74
+ policy_enabled=False,
75
+ thresholds_calibrated=config.calibrated,
76
+ )
77
+ if not config.calibrated:
78
+ return DispatchDecision(
79
+ backend="vendor",
80
+ reason="thresholds_not_calibrated",
81
+ eligible=False,
82
+ policy_enabled=True,
83
+ thresholds_calibrated=False,
84
+ )
85
+
86
+ if shape.phase == "prefill":
87
+ eligible = (
88
+ shape.batch_size >= config.prefill_batch_min
89
+ and shape.kv_len >= config.prefill_kv_len_min
90
+ )
91
+ return DispatchDecision(
92
+ backend="m1_prefill_chunked" if eligible else "vendor",
93
+ reason="prefill_threshold_match" if eligible else "prefill_below_threshold",
94
+ eligible=eligible,
95
+ policy_enabled=True,
96
+ thresholds_calibrated=True,
97
+ )
98
+
99
+ eligible = (
100
+ shape.batch_size >= config.decode_batch_min
101
+ and shape.kv_len >= config.decode_kv_len_min
102
+ and shape.gqa_ratio in (2, 4, 8)
103
+ and shape.head_dim in (64, 128)
104
+ )
105
+ return DispatchDecision(
106
+ backend="m1_split_kv" if eligible else "vendor",
107
+ reason="decode_threshold_match" if eligible else "decode_shape_or_threshold_miss",
108
+ eligible=eligible,
109
+ policy_enabled=True,
110
+ thresholds_calibrated=True,
111
+ )
mxflashattn/prefill.py ADDED
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ import torch
6
+
7
+ from .reference import attention_batched
8
+ from .validation import validate_qkv, validate_softmax_scale
9
+
10
+
11
+ def prefill_chunked_reference(
12
+ q: torch.Tensor,
13
+ k: torch.Tensor,
14
+ v: torch.Tensor,
15
+ *,
16
+ chunk_size: int = 256,
17
+ softmax_scale: float | None = None,
18
+ causal: bool = False,
19
+ ) -> torch.Tensor:
20
+ """Correctness-first chunked attention prototype for Prefill.
21
+
22
+ The tensors use the public layout ``[batch, sequence, heads, head_dim]``.
23
+ KV is scanned in chunks and combined with an online softmax accumulator.
24
+ This is a reference implementation for algorithm validation, not a
25
+ C500-optimized kernel and not part of the production dispatch path.
26
+ """
27
+ validate_qkv(q, k, v)
28
+ validate_softmax_scale(softmax_scale)
29
+ if not isinstance(chunk_size, int) or chunk_size <= 0:
30
+ raise ValueError("chunk_size must be a positive integer")
31
+
32
+ batch, query_len, query_heads, head_dim = q.shape
33
+ key_len = k.shape[1]
34
+ scale = float(softmax_scale) if softmax_scale is not None else 1.0 / math.sqrt(head_dim)
35
+ groups = query_heads // k.shape[2]
36
+
37
+ # Use float accumulators to make the prototype's numerical behavior
38
+ # comparable across float16, bfloat16, and float32 inputs.
39
+ qf = q.float().transpose(1, 2)
40
+ kf = k.float().transpose(1, 2).repeat_interleave(groups, dim=1)
41
+ vf = v.float().transpose(1, 2).repeat_interleave(groups, dim=1)
42
+ output = torch.zeros((batch, query_heads, query_len, head_dim), device=q.device, dtype=torch.float32)
43
+ normalizer = torch.zeros((batch, query_heads, query_len, 1), device=q.device, dtype=torch.float32)
44
+ running_max = torch.full_like(normalizer, float("-inf"))
45
+
46
+ query_positions = torch.arange(query_len, device=q.device) + (key_len - query_len)
47
+ for start in range(0, key_len, chunk_size):
48
+ end = min(start + chunk_size, key_len)
49
+ scores = torch.matmul(qf, kf[:, :, start:end].transpose(-2, -1)) * scale
50
+
51
+ if causal:
52
+ key_positions = torch.arange(start, end, device=q.device)
53
+ blocked = key_positions.unsqueeze(0) > query_positions.unsqueeze(1)
54
+ scores = scores.masked_fill(blocked.unsqueeze(0).unsqueeze(0), float("-inf"))
55
+
56
+ chunk_max = scores.amax(dim=-1, keepdim=True)
57
+ old_valid = torch.isfinite(running_max)
58
+ chunk_valid = torch.isfinite(chunk_max)
59
+ new_max = torch.maximum(running_max, chunk_max)
60
+
61
+ old_factor = torch.where(
62
+ old_valid,
63
+ torch.exp(running_max - new_max),
64
+ torch.zeros_like(running_max),
65
+ )
66
+ exp_scores = torch.where(
67
+ torch.isfinite(scores),
68
+ torch.exp(scores - new_max),
69
+ torch.zeros_like(scores),
70
+ )
71
+ normalizer = normalizer * old_factor + exp_scores.sum(dim=-1, keepdim=True)
72
+ output = output * old_factor
73
+ output = output + torch.matmul(exp_scores, vf[:, :, start:end])
74
+ running_max = torch.where(chunk_valid | old_valid, new_max, running_max)
75
+
76
+ output = torch.where(
77
+ normalizer > 0,
78
+ output / normalizer.clamp_min(torch.finfo(output.dtype).tiny),
79
+ torch.zeros_like(output),
80
+ )
81
+ return output.transpose(1, 2).to(dtype=q.dtype)
82
+
83
+
84
+ def prefill_chunked_reference_fallback(
85
+ q: torch.Tensor,
86
+ k: torch.Tensor,
87
+ v: torch.Tensor,
88
+ *,
89
+ chunk_size: int = 256,
90
+ softmax_scale: float | None = None,
91
+ causal: bool = False,
92
+ ) -> torch.Tensor:
93
+ """Convenience wrapper used by local experiments.
94
+
95
+ The fallback provides a single switch for comparing the prototype with the
96
+ existing readable reference implementation. It is intentionally not used
97
+ by the runtime dispatcher.
98
+ """
99
+ if chunk_size >= k.shape[1]:
100
+ return attention_batched(q, k, v, softmax_scale=softmax_scale, causal=causal)
101
+ return prefill_chunked_reference(
102
+ q,
103
+ k,
104
+ v,
105
+ chunk_size=chunk_size,
106
+ softmax_scale=softmax_scale,
107
+ causal=causal,
108
+ )