stackformers 3.9.0__tar.gz → 3.9.1__tar.gz
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.
- {stackformers-3.9.0 → stackformers-3.9.1}/PKG-INFO +1 -1
- {stackformers-3.9.0 → stackformers-3.9.1}/pyproject.toml +1 -1
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/attention/README.md +4 -2
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/attention/ops.py +8 -57
- stackformers-3.9.1/stackformers/attention/varlen_backend.py +88 -0
- stackformers-3.9.1/tests/attention/test_varlen_backend.py +122 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/uv.lock +1 -1
- {stackformers-3.9.0 → stackformers-3.9.1}/.claudeignore +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/.gitignore +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/.python-version +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/.vscode/settings.json +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/CLAUDE.md +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/Justfile +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/LICENSE +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/PLAN.md +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/README.md +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/attention/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/attention/bias.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/attention/config.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/attention/cross_attn.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/attention/protocols.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/attention/self_attn.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/config.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/cross_attender.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/decoder.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/encoder.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/feedforward/README.md +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/feedforward/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/feedforward/config.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/feedforward/factory.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/feedforward/geglu.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/feedforward/protocols.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/feedforward/relu_squared.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/feedforward/swiglu.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/layers.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/norm/README.md +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/norm/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/norm/config.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/norm/factory.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/norm/protocols.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/positional/README.md +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/positional/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/positional/config.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/positional/factory.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/positional/learned.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/positional/none.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/positional/protocols.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/positional/rope1d.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/positional/rope2d.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/presets/README.md +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/presets/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/presets/cross_attender.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/presets/decoder.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/presets/encoder.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/presets/pyramid_vision.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/sequence.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/README.md +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/config.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/factory.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/input.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/kv_reduction.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/layer.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/patch_merging.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/protocols.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/spatial_reduction.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/stackformers/spatial/window.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/attention/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/attention/test_cross_attn.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/attention/test_kernels.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/attention/test_ops.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/attention/test_self_attn.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/conftest.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/feedforward/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/feedforward/test_geglu.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/feedforward/test_relu_squared.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/feedforward/test_swiglu.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/positional/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/positional/test_learned_pos.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/positional/test_rope.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/presets/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/presets/test_cross_attender.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/presets/test_decoder.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/presets/test_encoder.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/presets/test_pyramid_vision.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/spatial/__init__.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/spatial/test_input.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/spatial/test_kv_reduction.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/spatial/test_layer.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/spatial/test_patch_merging.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/spatial/test_spatial_reduction.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/spatial/test_window.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/test_cross_attender.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/test_decoder.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/test_encoder.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/test_layers.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/test_norm.py +0 -0
- {stackformers-3.9.0 → stackformers-3.9.1}/tests/test_sequence.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackformers
|
|
3
|
-
Version: 3.9.
|
|
3
|
+
Version: 3.9.1
|
|
4
4
|
Summary: Typed, composable, SOLID transformer library for PyTorch
|
|
5
5
|
Project-URL: Homepage, https://github.com/Red-Eyed/stackformers
|
|
6
6
|
Project-URL: Repository, https://github.com/Red-Eyed/stackformers
|
|
@@ -8,9 +8,11 @@ Multi-head self-attention and cross-attention.
|
|
|
8
8
|
|
|
9
9
|
**Static vs runtime dispatch.** The attention *type* (global vs sliding-window) is determined at construction time via `window_size: int | None` in `SelfAttentionConfig`. The *backend* (padded vs packed) is determined at runtime: `SelfAttention.forward` and `CrossAttention.forward` dispatch on `SequenceInput` via `match`. This means one model handles both training (packed) and inference (padded) without swapping classes.
|
|
10
10
|
|
|
11
|
-
**Kernel ops in `ops.py`.** All low-level attention math lives in `attention/ops.py`: mask builders, `
|
|
11
|
+
**Kernel ops in `ops.py`.** All low-level attention math lives in `attention/ops.py`: mask builders, `padded_sdpa`, the packed↔padded scatter/gather helpers, and `packed_attn_or_fallback`. The modules stay thin dispatchers.
|
|
12
12
|
|
|
13
|
-
**
|
|
13
|
+
**Experimental kernel behind one interface function.** `torch.nn.attention.varlen` is an experimental API — the symbol is absent in some torch builds and its signature is not stable. Everything that touches it lives in `varlen_backend.py` behind a single function, `try_varlen_attn(...) -> Tensor | None`: the guarded import, the eligibility/bias checks, and the call itself. The rest of the package depends only on that function and never on the volatile import path, so an absent or renamed symbol degrades to the fallback instead of breaking the whole package at import time.
|
|
14
|
+
|
|
15
|
+
**Packed attention with automatic fallback.** `PackedInput` normally routes to `varlen_attn`, which requires CUDA and `float16`/`bfloat16`. `packed_attn_or_fallback` calls `try_varlen_attn`; a `None` result means take the padded path — scatter q/k/v to padded layout, run `F.scaled_dot_product_attention`, and gather the result back to packed (same weights, same output shape `(nt, h, d)`). The fallback runs **silently** for the expected CPU/float32/`torch.export` route, and **with a `UserWarning` explaining why** when the kernel was eligible but unusable: it failed to import, an attention bias is present (no bias slot), or the call raised a runtime/signature error (unsupported GPU or a changed experimental API). Dropout works on the fallback path; it is silently skipped only when `varlen_attn` is used (which does not accept a dropout argument).
|
|
14
16
|
|
|
15
17
|
**`torch.export` and `torch.compile` compatibility.** All scatter/gather helpers in `ops.py` (`_cu_to_indices`, `_packed_heads_to_padded`, `_padded_heads_to_packed`) are fully export-compatible. The key design constraint is that per-document length iteration must never use Python-side loops over data-dependent values (i.e., no `.tolist()` followed by a list comprehension). Instead, `_cu_to_indices` uses `torch.repeat_interleave` to produce `batch_idx` and derives `pos_idx` via the identity `arange(nt) - cu[batch_idx]`. This keeps the computation graph purely symbolic even when `nt` (total tokens) is a data-dependent unbacked symbol.
|
|
16
18
|
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import warnings
|
|
4
|
-
|
|
5
3
|
import torch
|
|
6
4
|
import torch.nn.functional as F
|
|
7
5
|
from torch import Tensor
|
|
8
|
-
from torch.nn.attention.varlen import varlen_attn as _varlen_attn
|
|
9
6
|
|
|
7
|
+
from stackformers.attention.varlen_backend import try_varlen_attn
|
|
10
8
|
from stackformers.sequence import PackedSequence
|
|
11
9
|
|
|
12
10
|
|
|
13
|
-
def varlen_supported(q: Tensor) -> bool:
|
|
14
|
-
return q.is_cuda and q.dtype in (torch.float16, torch.bfloat16)
|
|
15
|
-
|
|
16
|
-
|
|
17
11
|
def padding_mask(mask: Tensor, dtype: torch.dtype) -> Tensor:
|
|
18
12
|
bias = torch.zeros(mask.shape, dtype=dtype, device=mask.device)
|
|
19
13
|
bias.masked_fill_(~mask, torch.finfo(dtype).min)
|
|
@@ -34,12 +28,6 @@ def window_mask(n: int, s: int, window_size: int, causal: bool, device: torch.de
|
|
|
34
28
|
return mask
|
|
35
29
|
|
|
36
30
|
|
|
37
|
-
def varlen_window(causal: bool, window_size: int | None) -> tuple[int, int]:
|
|
38
|
-
if window_size is None:
|
|
39
|
-
return (-1, 0) if causal else (-1, -1)
|
|
40
|
-
return (window_size, 0) if causal else (window_size // 2, window_size // 2)
|
|
41
|
-
|
|
42
|
-
|
|
43
31
|
def padded_sdpa(
|
|
44
32
|
q: Tensor,
|
|
45
33
|
k: Tensor,
|
|
@@ -101,30 +89,6 @@ def _padded_heads_to_packed(x: Tensor, mask: Tensor) -> Tensor:
|
|
|
101
89
|
return x.permute(0, 2, 1, 3)[mask]
|
|
102
90
|
|
|
103
91
|
|
|
104
|
-
def packed_attn(
|
|
105
|
-
q: Tensor,
|
|
106
|
-
k: Tensor,
|
|
107
|
-
v: Tensor,
|
|
108
|
-
q_seq: PackedSequence,
|
|
109
|
-
k_seq: PackedSequence,
|
|
110
|
-
causal: bool,
|
|
111
|
-
window_size: int | None,
|
|
112
|
-
) -> Tensor:
|
|
113
|
-
win = varlen_window(causal, window_size)
|
|
114
|
-
result = _varlen_attn(
|
|
115
|
-
query=q,
|
|
116
|
-
key=k,
|
|
117
|
-
value=v,
|
|
118
|
-
cu_seq_q=q_seq.cu_seqlens.to(torch.int32),
|
|
119
|
-
cu_seq_k=k_seq.cu_seqlens.to(torch.int32),
|
|
120
|
-
max_q=q_seq.max_seqlen,
|
|
121
|
-
max_k=k_seq.max_seqlen,
|
|
122
|
-
window_size=win,
|
|
123
|
-
)
|
|
124
|
-
assert isinstance(result, Tensor)
|
|
125
|
-
return result
|
|
126
|
-
|
|
127
|
-
|
|
128
92
|
def packed_attn_or_fallback(
|
|
129
93
|
q: Tensor,
|
|
130
94
|
k: Tensor,
|
|
@@ -135,29 +99,16 @@ def packed_attn_or_fallback(
|
|
|
135
99
|
window_size: int | None,
|
|
136
100
|
bias: Tensor | None,
|
|
137
101
|
) -> Tensor:
|
|
138
|
-
"""Run varlen attention; fall back to padded SDPA when unavailable.
|
|
102
|
+
"""Run varlen attention; fall back to padded SDPA when it is unavailable.
|
|
139
103
|
|
|
140
104
|
Inputs and output are all packed: q/k/v are (nt, h, d), result is (nt_q, h, d).
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
warning
|
|
144
|
-
that do not support varlen_attn (e.g. compute capability < 8.0).
|
|
105
|
+
``try_varlen_attn`` owns the experimental kernel and its warnings; ``None`` from it means
|
|
106
|
+
take the padded SDPA path — silently for the expected CPU/float32/export route, or after
|
|
107
|
+
a warning when the kernel was eligible but unavailable.
|
|
145
108
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
"Performance may be lower.",
|
|
150
|
-
stacklevel=2,
|
|
151
|
-
)
|
|
152
|
-
elif varlen_supported(q):
|
|
153
|
-
try:
|
|
154
|
-
return packed_attn(q, k, v, q_seq, k_seq, causal, window_size)
|
|
155
|
-
except RuntimeError as exc:
|
|
156
|
-
warnings.warn(
|
|
157
|
-
f"varlen_attn is not supported on this device ({exc}); "
|
|
158
|
-
"falling back to padded SDPA. Performance may be lower.",
|
|
159
|
-
stacklevel=2,
|
|
160
|
-
)
|
|
109
|
+
out = try_varlen_attn(q, k, v, q_seq, k_seq, causal, window_size, bias)
|
|
110
|
+
if out is not None:
|
|
111
|
+
return out
|
|
161
112
|
|
|
162
113
|
# Keep b as SymInt-friendly: int(SymInt) forces specialization under torch.export.
|
|
163
114
|
# Downstream consumers (_packed_heads_to_padded) accept SymInt for shape args.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""The experimental ``varlen_attn`` kernel behind one interface function.
|
|
2
|
+
|
|
3
|
+
``torch.nn.attention.varlen`` is an experimental API: the symbol is absent in some torch
|
|
4
|
+
builds, and its call signature is not yet stable. Everything that touches it lives here —
|
|
5
|
+
the guarded import, the eligibility/bias checks, and the call itself — so the rest of the
|
|
6
|
+
package depends only on :func:`try_varlen_attn` and never on the volatile import path.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
import torch
|
|
14
|
+
from torch import Tensor
|
|
15
|
+
|
|
16
|
+
from stackformers.sequence import PackedSequence
|
|
17
|
+
|
|
18
|
+
_FALLBACK_NOTE = " Falling back to padded SDPA; performance may be lower."
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load():
|
|
22
|
+
try:
|
|
23
|
+
from torch.nn.attention.varlen import varlen_attn
|
|
24
|
+
except ImportError as exc:
|
|
25
|
+
return None, f"varlen_attn could not be imported ({exc})"
|
|
26
|
+
return varlen_attn, None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_varlen_attn, _import_error = _load()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _eligible(q: Tensor) -> bool:
|
|
33
|
+
"""Whether this query's device/dtype is one ``varlen_attn`` would handle at all."""
|
|
34
|
+
return q.is_cuda and q.dtype in (torch.float16, torch.bfloat16)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _window(causal: bool, window_size: int | None) -> tuple[int, int]:
|
|
38
|
+
if window_size is None:
|
|
39
|
+
return (-1, 0) if causal else (-1, -1)
|
|
40
|
+
return (window_size, 0) if causal else (window_size // 2, window_size // 2)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _skip(reason: str) -> None:
|
|
44
|
+
warnings.warn(reason + _FALLBACK_NOTE, stacklevel=3)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def try_varlen_attn(
|
|
49
|
+
q: Tensor,
|
|
50
|
+
k: Tensor,
|
|
51
|
+
v: Tensor,
|
|
52
|
+
q_seq: PackedSequence,
|
|
53
|
+
k_seq: PackedSequence,
|
|
54
|
+
causal: bool,
|
|
55
|
+
window_size: int | None,
|
|
56
|
+
bias: Tensor | None,
|
|
57
|
+
) -> Tensor | None:
|
|
58
|
+
"""Attempt varlen attention for a packed batch; return ``None`` to use the SDPA fallback.
|
|
59
|
+
|
|
60
|
+
q/k/v are ``(nt, h, d)``; the result is the packed ``(nt_q, h, d)`` tensor. ``None`` means
|
|
61
|
+
the caller should run the padded SDPA path instead. When varlen *would* apply (CUDA +
|
|
62
|
+
float16/bfloat16) but cannot, a ``UserWarning`` explains why: an absent kernel, an
|
|
63
|
+
attention bias (varlen_attn has no bias slot), or a runtime/signature error from the call
|
|
64
|
+
(an unsupported GPU or a changed experimental API). The expected CPU/float32/export route
|
|
65
|
+
returns ``None`` silently — that is normal, not a failure.
|
|
66
|
+
"""
|
|
67
|
+
if not _eligible(q):
|
|
68
|
+
return None
|
|
69
|
+
if _varlen_attn is None:
|
|
70
|
+
return _skip(f"varlen_attn is unavailable ({_import_error}).")
|
|
71
|
+
if bias is not None:
|
|
72
|
+
return _skip("varlen_attn does not support attention bias.")
|
|
73
|
+
try:
|
|
74
|
+
result = _varlen_attn(
|
|
75
|
+
query=q,
|
|
76
|
+
key=k,
|
|
77
|
+
value=v,
|
|
78
|
+
cu_seq_q=q_seq.cu_seqlens.to(torch.int32),
|
|
79
|
+
cu_seq_k=k_seq.cu_seqlens.to(torch.int32),
|
|
80
|
+
max_q=q_seq.max_seqlen,
|
|
81
|
+
max_k=k_seq.max_seqlen,
|
|
82
|
+
window_size=_window(causal, window_size),
|
|
83
|
+
)
|
|
84
|
+
except (RuntimeError, TypeError) as exc:
|
|
85
|
+
return _skip(f"varlen_attn call failed ({type(exc).__name__}: {exc}).")
|
|
86
|
+
if not isinstance(result, Tensor):
|
|
87
|
+
return _skip(f"varlen_attn returned {type(result).__name__}, expected Tensor.")
|
|
88
|
+
return result
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Tests for stackformers/attention/varlen_backend.py.
|
|
2
|
+
|
|
3
|
+
Covers the fallback contract of ``try_varlen_attn``: it returns the kernel's tensor on the
|
|
4
|
+
happy path and ``None`` (with a reason-bearing ``UserWarning`` when the kernel was eligible
|
|
5
|
+
but unusable) on every degraded path — absent symbol, attention bias, a runtime/signature
|
|
6
|
+
error from the call, or a non-Tensor return. The experimental kernel is faked via monkeypatch
|
|
7
|
+
so the logic is exercised without CUDA or the real (possibly-absent) ``varlen_attn``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import warnings
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
from stackformers.attention import varlen_backend as vb
|
|
18
|
+
from stackformers.sequence import PackedSequence
|
|
19
|
+
|
|
20
|
+
NT, H, DH = 5, 2, 8
|
|
21
|
+
|
|
22
|
+
Packed = tuple[torch.Tensor, torch.Tensor, torch.Tensor, PackedSequence]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def packed() -> Packed:
|
|
27
|
+
"""Two packed sequences (lengths 3 and 2) of (nt, h, d) q/k/v plus their PackedSequence."""
|
|
28
|
+
seq = PackedSequence(cu_seqlens=torch.tensor([0, 3, 5], dtype=torch.long), max_seqlen=3)
|
|
29
|
+
q = torch.randn(NT, H, DH)
|
|
30
|
+
k = torch.randn(NT, H, DH)
|
|
31
|
+
v = torch.randn(NT, H, DH)
|
|
32
|
+
return q, k, v, seq
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def force_eligible(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
37
|
+
"""Pretend the query is CUDA + float16 so the eligibility guard does not short-circuit."""
|
|
38
|
+
monkeypatch.setattr(vb, "_eligible", lambda q: True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _attempt(packed: Packed, bias: torch.Tensor | None = None) -> torch.Tensor | None:
|
|
42
|
+
q, k, v, seq = packed
|
|
43
|
+
return vb.try_varlen_attn(q, k, v, seq, seq, causal=False, window_size=None, bias=bias)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_ineligible_falls_back_silently(packed: Packed, recwarn: pytest.WarningsRecorder) -> None:
|
|
47
|
+
"""A plain CPU/float32 query is the expected fallback route — None, and no warning."""
|
|
48
|
+
out = _attempt(packed)
|
|
49
|
+
assert out is None
|
|
50
|
+
assert len(recwarn) == 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_unavailable_kernel_warns(
|
|
54
|
+
packed: Packed, force_eligible: None, monkeypatch: pytest.MonkeyPatch
|
|
55
|
+
) -> None:
|
|
56
|
+
"""An eligible query with no imported kernel falls back and reports the import reason."""
|
|
57
|
+
monkeypatch.setattr(vb, "_varlen_attn", None)
|
|
58
|
+
monkeypatch.setattr(vb, "_import_error", "varlen_attn could not be imported (no module)")
|
|
59
|
+
with pytest.warns(UserWarning, match="unavailable"):
|
|
60
|
+
out = _attempt(packed)
|
|
61
|
+
assert out is None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_attention_bias_warns(
|
|
65
|
+
packed: Packed, force_eligible: None, monkeypatch: pytest.MonkeyPatch
|
|
66
|
+
) -> None:
|
|
67
|
+
"""varlen_attn has no bias slot: an eligible query with a bias falls back with a warning."""
|
|
68
|
+
monkeypatch.setattr(vb, "_varlen_attn", lambda **kw: torch.zeros(NT, H, DH))
|
|
69
|
+
bias = torch.zeros(1, 1, NT, NT)
|
|
70
|
+
with pytest.warns(UserWarning, match="attention bias"):
|
|
71
|
+
out = _attempt(packed, bias=bias)
|
|
72
|
+
assert out is None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_signature_error_warns(
|
|
76
|
+
packed: Packed, force_eligible: None, monkeypatch: pytest.MonkeyPatch
|
|
77
|
+
) -> None:
|
|
78
|
+
"""A changed experimental signature raises TypeError, which is caught and falls back."""
|
|
79
|
+
|
|
80
|
+
def renamed_kwarg(**kw: object) -> torch.Tensor:
|
|
81
|
+
raise TypeError("unexpected keyword argument 'cu_seq_q'")
|
|
82
|
+
|
|
83
|
+
monkeypatch.setattr(vb, "_varlen_attn", renamed_kwarg)
|
|
84
|
+
with pytest.warns(UserWarning, match="call failed"):
|
|
85
|
+
out = _attempt(packed)
|
|
86
|
+
assert out is None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_runtime_error_warns(
|
|
90
|
+
packed: Packed, force_eligible: None, monkeypatch: pytest.MonkeyPatch
|
|
91
|
+
) -> None:
|
|
92
|
+
"""An unsupported-GPU RuntimeError from the call is caught and falls back."""
|
|
93
|
+
|
|
94
|
+
def unsupported(**kw: object) -> torch.Tensor:
|
|
95
|
+
raise RuntimeError("varlen_attn requires compute capability >= 8.0")
|
|
96
|
+
|
|
97
|
+
monkeypatch.setattr(vb, "_varlen_attn", unsupported)
|
|
98
|
+
with pytest.warns(UserWarning, match="call failed"):
|
|
99
|
+
out = _attempt(packed)
|
|
100
|
+
assert out is None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_non_tensor_return_warns(
|
|
104
|
+
packed: Packed, force_eligible: None, monkeypatch: pytest.MonkeyPatch
|
|
105
|
+
) -> None:
|
|
106
|
+
"""A changed return contract (non-Tensor) falls back rather than propagating bad data."""
|
|
107
|
+
monkeypatch.setattr(vb, "_varlen_attn", lambda **kw: ("not", "a", "tensor"))
|
|
108
|
+
with pytest.warns(UserWarning, match="expected Tensor"):
|
|
109
|
+
out = _attempt(packed)
|
|
110
|
+
assert out is None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_success_returns_kernel_tensor(
|
|
114
|
+
packed: Packed, force_eligible: None, monkeypatch: pytest.MonkeyPatch
|
|
115
|
+
) -> None:
|
|
116
|
+
"""The happy path returns the kernel's tensor unchanged and emits no warning."""
|
|
117
|
+
expected = torch.randn(NT, H, DH)
|
|
118
|
+
monkeypatch.setattr(vb, "_varlen_attn", lambda **kw: expected)
|
|
119
|
+
with warnings.catch_warnings():
|
|
120
|
+
warnings.simplefilter("error")
|
|
121
|
+
out = _attempt(packed)
|
|
122
|
+
assert out is expected
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|