Skip to content

Commit 4069bfa

Browse files
fix(attention): repair the FROST plan-cache key arity and harden the version gate
The device element added to _key() in 63294e8 made the key 12 items while _build_fwd and _build_bwd still unpacked 11, so the first FROST call of any kind raised ValueError: too many values to unpack. Every path was affected, forward and backward, CP and non-CP. Nothing caught it because the only test that reaches _key is gated on SM100/SM103. The unpack is now starred, so further device components cannot reintroduce the same break. The key also lacked device.type, letting a CPU tensor alias cuda:0, and called torch.cuda.current_device() unguarded for a normalisation that a materialized CUDA tensor never needs -- which would raise a confusing CUDA-init error on a CPU-only host. It now keys on (type, index) directly, following _score_mod_device_key in flex_attention.py. Version handling moves to packaging.Version over distribution metadata, matching _cudnn_frontend_version_supported in fused_mla_q_uproj.py. The hand-rolled parser accepted 1.29.0rc1 as 1.29.0, and by replacing the old int() parse it had quietly relaxed the cutlass floor to admit 4.7.0rc1 as well; both are rejected again. An undeterminable version no longer reports as "0" and hard-declines a valid source install -- it defers to _select_frost_plan, which checks the plan by name. That error message now looks both versions up defensively, since it previously could raise PackageNotFoundError while formatting the very diagnostic explaining a failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
1 parent 4381310 commit 4069bfa

1 file changed

Lines changed: 47 additions & 45 deletions

File tree

‎transformer_engine/pytorch/attention/dot_product_attention/frost_attention.py‎

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@
3636
from __future__ import annotations
3737

3838
import os
39+
from importlib.metadata import PackageNotFoundError, version as get_pkg_version
3940
from typing import Optional, Tuple
4041

4142
import torch
43+
from packaging.version import InvalidVersion, Version as PkgVersion
4244

4345
__all__ = [
4446
"is_frost_attention_available",
@@ -57,12 +59,12 @@
5759
# the selected plan by NAME rather than trusting that the engine was used.
5860
_FROST_FWD_PLAN_TOKEN = "sdpa_fwd_prefill_sm100"
5961
_FROST_BWD_PLAN_TOKEN = "sdpa_bwd_sm100"
60-
_MIN_CUTLASS_DSL = (4, 7, 0)
62+
_MIN_CUTLASS_DSL = PkgVersion("4.7.0")
6163

6264
# 1.29.0 is the first release carrying the head_dim=512 BACKWARD (bprop_d512_f16_sm100). 1.28.0
6365
# ships the forward only, and the repo's own pin allows it, so without this check training would
6466
# build a forward plan and then raise on the first backward.
65-
_MIN_CUDNN_FRONTEND = (1, 29, 0)
67+
_MIN_CUDNN_FRONTEND = PkgVersion("1.29.0")
6668

6769
_SUPPORTED_ARCHS = ((10, 0), (10, 3))
6870
_MAX_HEAD_DIM = 512
@@ -86,20 +88,23 @@ def _import_cudnn():
8688
return _cudnn
8789

8890

89-
def _parse_version(raw: str) -> Tuple[int, ...]:
90-
"""Leading numeric components of a version, ignoring any suffix. Unparseable sorts lowest."""
91-
parts = []
92-
for piece in str(raw).split(".")[:3]:
93-
digits = ""
94-
for ch in piece:
95-
if not ch.isdigit():
96-
break
97-
digits += ch
98-
if not digits:
99-
break
100-
parts.append(int(digits))
101-
# Pad, or "1.29" would compare below (1, 29, 0) and be rejected as too old.
102-
return tuple(parts + [0] * (3 - len(parts))) if parts else (0, 0, 0)
91+
def _pkg_version(name: str, module=None) -> Optional[PkgVersion]:
92+
"""Installed version of a package, or None if it cannot be determined.
93+
94+
Distribution metadata first, matching the sibling check in fused_mla_q_uproj.py, with the
95+
module attribute as a fallback so a source or vendored install is not misreported as old.
96+
"""
97+
raw = None
98+
try:
99+
raw = get_pkg_version(name)
100+
except PackageNotFoundError:
101+
raw = getattr(module, "__version__", None)
102+
if not isinstance(raw, str):
103+
return None
104+
try:
105+
return PkgVersion(raw)
106+
except InvalidVersion:
107+
return None
103108

104109

105110
def is_frost_attention_available() -> Tuple[bool, str]:
@@ -128,31 +133,25 @@ def _no(reason):
128133
except ImportError as exc:
129134
return _no("nvidia-cudnn-frontend not importable: %s" % exc)
130135

131-
from importlib.metadata import PackageNotFoundError, version
132-
133-
fe_raw = getattr(_cudnn, "__version__", None)
134-
if fe_raw is None:
135-
try:
136-
fe_raw = version("nvidia-cudnn-frontend")
137-
except PackageNotFoundError:
138-
fe_raw = "0"
139-
if _parse_version(fe_raw) < _MIN_CUDNN_FRONTEND:
136+
# Decline only on positive evidence of a too-old install. An undeterminable version is left
137+
# to _select_frost_plan, which checks the plan by name and fails loudly with both versions.
138+
frontend = _pkg_version("nvidia-cudnn-frontend", _cudnn)
139+
if frontend is not None and frontend < _MIN_CUDNN_FRONTEND:
140140
return _no(
141-
"nvidia-cudnn-frontend %s does not carry the head_dim>256 backward; >= 1.29.0 is"
142-
" required (1.28.0 ships the forward only, so this would raise on the first"
143-
" backward rather than here)" % fe_raw
141+
"nvidia-cudnn-frontend %s registers no sm100 backward engine; >= %s is required"
142+
" (1.28.0 ships the d512 forward only, so this would otherwise raise on the first"
143+
" backward rather than here)" % (frontend, _MIN_CUDNN_FRONTEND)
144144
)
145145

146-
try:
147-
raw = version("nvidia-cutlass-dsl")
148-
except PackageNotFoundError:
149-
return _no("nvidia-cutlass-dsl not installed (FROST requires >= 4.7.0)")
150-
parsed = _parse_version(raw)
151-
if parsed < _MIN_CUTLASS_DSL:
146+
cutlass = _pkg_version("nvidia-cutlass-dsl")
147+
if cutlass is None:
148+
return _no("nvidia-cutlass-dsl not installed (FROST requires >= %s)" % _MIN_CUTLASS_DSL)
149+
if cutlass < _MIN_CUTLASS_DSL:
152150
# Worth being loud: this combination fails by silently declining, not by raising.
153151
return _no(
154-
"nvidia-cutlass-dsl %s is below the FROST floor 4.7.0; FROST engines would be"
155-
" silently skipped in favour of ordinary cuDNN backend plans" % raw
152+
"nvidia-cutlass-dsl %s is below the FROST floor %s; FROST engines would be"
153+
" silently skipped in favour of ordinary cuDNN backend plans"
154+
% (cutlass, _MIN_CUTLASS_DSL)
156155
)
157156

158157
_availability = (True, "")
@@ -286,17 +285,19 @@ def _select_frost_plan(graph, token: str, what: str):
286285
names = [graph.get_plan_name_at_index(i) for i in range(graph.get_execution_plan_count())]
287286
hits = [i for i, n in enumerate(names) if token in n]
288287
if not hits:
289-
from importlib.metadata import version
290-
288+
# Both versions, because either floor can cause this and blaming one misdirects. Looked
289+
# up defensively: this is the message explaining a failure, so it must not raise itself.
291290
raise RuntimeError(
292291
"no cuDNN FROST %s engine was offered (looked for %r). Candidate plans: %s."
293-
" nvidia-cudnn-frontend=%s (floor 1.29.0), nvidia-cutlass-dsl=%s (floor 4.7.0)."
292+
" nvidia-cudnn-frontend=%s (floor %s), nvidia-cutlass-dsl=%s (floor %s)."
294293
% (
295294
what,
296295
token,
297296
names[:6],
298-
getattr(_cudnn, "__version__", None) or version("nvidia-cudnn-frontend"),
299-
version("nvidia-cutlass-dsl"),
297+
_pkg_version("nvidia-cudnn-frontend", _cudnn) or "unknown",
298+
_MIN_CUDNN_FRONTEND,
299+
_pkg_version("nvidia-cutlass-dsl") or "unknown",
300+
_MIN_CUTLASS_DSL,
300301
)
301302
)
302303
graph.select_plan(hits[0])
@@ -308,7 +309,7 @@ def _select_frost_plan(graph, token: str, what: str):
308309
def _build_fwd(key) -> dict:
309310
"""Build (and JIT-compile) a forward graph. Expensive; always reached through the cache."""
310311
cudnn = _import_cudnn()
311-
b, hq, hkv, sq, skv, d, dtype, mask, scale, qs, ks = key
312+
*_device, b, hq, hkv, sq, skv, d, dtype, mask, scale, qs, ks = key
312313
io_dt = _cudnn_dtype(dtype)
313314
shq, shkv = [b, hq, sq, d], [b, hkv, skv, d]
314315

@@ -347,7 +348,7 @@ def _build_fwd(key) -> dict:
347348
def _build_bwd(key) -> dict:
348349
"""Build (and JIT-compile) a backward graph. Expensive; always reached through the cache."""
349350
cudnn = _import_cudnn()
350-
b, hq, hkv, sq, skv, d, dtype, mask, scale, qs, ks = key
351+
*_device, b, hq, hkv, sq, skv, d, dtype, mask, scale, qs, ks = key
351352
io_dt = _cudnn_dtype(dtype)
352353
shq, shkv = [b, hq, sq, d], [b, hkv, skv, d]
353354

@@ -411,8 +412,9 @@ def _key(q, k, mask, scale):
411412
return (
412413
# The graph is built under whichever device was current, so it must not be reused on
413414
# another one. Matches the C++ fused-attn cache, which keys on device_id for the same
414-
# reason. Normalise None, or "cuda" and "cuda:0" would build two plans for one device.
415-
q.device.index if q.device.index is not None else torch.cuda.current_device(),
415+
# reason. Type is included too, so a CPU tensor cannot alias cuda:0.
416+
q.device.type,
417+
q.device.index,
416418
q.shape[0],
417419
q.shape[1],
418420
k.shape[1],

0 commit comments

Comments
 (0)