stackformers 3.8.2__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.8.2 → stackformers-3.9.1}/PKG-INFO +1 -1
- {stackformers-3.8.2 → stackformers-3.9.1}/pyproject.toml +1 -1
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/attention/README.md +4 -2
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/attention/ops.py +8 -57
- stackformers-3.9.1/stackformers/attention/varlen_backend.py +88 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/presets/README.md +3 -1
- stackformers-3.9.1/stackformers/presets/pyramid_vision.py +173 -0
- stackformers-3.9.1/stackformers/spatial/README.md +49 -0
- stackformers-3.9.1/stackformers/spatial/__init__.py +45 -0
- stackformers-3.9.1/stackformers/spatial/config.py +138 -0
- stackformers-3.9.1/stackformers/spatial/factory.py +49 -0
- stackformers-3.9.1/stackformers/spatial/input.py +53 -0
- stackformers-3.9.1/stackformers/spatial/kv_reduction.py +69 -0
- stackformers-3.9.1/stackformers/spatial/layer.py +42 -0
- stackformers-3.9.1/stackformers/spatial/patch_merging.py +54 -0
- stackformers-3.9.1/stackformers/spatial/protocols.py +33 -0
- stackformers-3.9.1/stackformers/spatial/spatial_reduction.py +70 -0
- stackformers-3.9.1/stackformers/spatial/window.py +101 -0
- stackformers-3.9.1/tests/attention/test_varlen_backend.py +122 -0
- stackformers-3.9.1/tests/presets/test_pyramid_vision.py +71 -0
- stackformers-3.9.1/tests/spatial/__init__.py +0 -0
- stackformers-3.9.1/tests/spatial/test_input.py +41 -0
- stackformers-3.9.1/tests/spatial/test_kv_reduction.py +66 -0
- stackformers-3.9.1/tests/spatial/test_layer.py +45 -0
- stackformers-3.9.1/tests/spatial/test_patch_merging.py +37 -0
- stackformers-3.9.1/tests/spatial/test_spatial_reduction.py +81 -0
- stackformers-3.9.1/tests/spatial/test_window.py +75 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/uv.lock +1 -1
- stackformers-3.8.2/.claude/settings.json +0 -7
- {stackformers-3.8.2 → stackformers-3.9.1}/.claudeignore +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/.gitignore +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/.python-version +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/.vscode/settings.json +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/CLAUDE.md +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/Justfile +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/LICENSE +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/PLAN.md +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/README.md +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/attention/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/attention/bias.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/attention/config.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/attention/cross_attn.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/attention/protocols.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/attention/self_attn.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/config.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/cross_attender.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/decoder.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/encoder.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/feedforward/README.md +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/feedforward/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/feedforward/config.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/feedforward/factory.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/feedforward/geglu.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/feedforward/protocols.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/feedforward/relu_squared.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/feedforward/swiglu.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/layers.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/norm/README.md +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/norm/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/norm/config.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/norm/factory.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/norm/protocols.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/positional/README.md +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/positional/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/positional/config.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/positional/factory.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/positional/learned.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/positional/none.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/positional/protocols.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/positional/rope1d.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/positional/rope2d.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/presets/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/presets/cross_attender.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/presets/decoder.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/presets/encoder.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/stackformers/sequence.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/attention/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/attention/test_cross_attn.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/attention/test_kernels.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/attention/test_ops.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/attention/test_self_attn.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/conftest.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/feedforward/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/feedforward/test_geglu.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/feedforward/test_relu_squared.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/feedforward/test_swiglu.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/positional/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/positional/test_learned_pos.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/positional/test_rope.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/presets/__init__.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/presets/test_cross_attender.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/presets/test_decoder.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/presets/test_encoder.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/test_cross_attender.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/test_decoder.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/test_encoder.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/test_layers.py +0 -0
- {stackformers-3.8.2 → stackformers-3.9.1}/tests/test_norm.py +0 -0
- {stackformers-3.8.2 → 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.
|
|
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
|
|
@@ -19,7 +19,9 @@ Each preset is split into two levels:
|
|
|
19
19
|
|
|
20
20
|
**`CrossAttender`** — cross-attn → ff per layer. `x` is a fixed set of queries with no self-attention. Useful for Perceiver-style or slot-attention architectures.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
**`PyramidVisionBackbone`** — multi-scale vision backbone (PVTv2-style hybrid). Stages of `spatial/` attention joined by `PatchMerging` downsamples; consumes a `SpatialInput` from your own patch-embed stem and returns one `(b, d, H, W)` feature map per stage. `pyramid_vision_config` defaults to 2-D window attention in the high-resolution early stages and spatially-reduced global attention later, tuned for 1024² images with an 8×8 patch stem. Unlike the other presets it is grid-based, so it takes `SpatialInput` rather than `PaddedInput`/`PackedInput`.
|
|
23
|
+
|
|
24
|
+
The other presets accept `PaddedInput` or `PackedInput` interchangeably.
|
|
23
25
|
|
|
24
26
|
## Customising a collaborator (subclass the concrete preset)
|
|
25
27
|
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import torch.nn as nn
|
|
4
|
+
from einops import rearrange
|
|
5
|
+
from jaxtyping import Float
|
|
6
|
+
from pydantic import BaseModel, Field, model_validator
|
|
7
|
+
from torch import Tensor
|
|
8
|
+
|
|
9
|
+
from stackformers.feedforward.config import FeedForwardConfig, SwiGLUConfig
|
|
10
|
+
from stackformers.feedforward.factory import build_ff
|
|
11
|
+
from stackformers.norm.config import NormConfig, RMSNormConfig
|
|
12
|
+
from stackformers.norm.factory import build_norm
|
|
13
|
+
from stackformers.positional.config import PosEncodingConfig, RoPE2DConfig
|
|
14
|
+
from stackformers.positional.factory import build_pos_encoding
|
|
15
|
+
from stackformers.spatial.config import (
|
|
16
|
+
PatchMergingConfig,
|
|
17
|
+
SpatialAttnConfig,
|
|
18
|
+
SpatialReductionAttentionConfig,
|
|
19
|
+
WindowAttention2DConfig,
|
|
20
|
+
)
|
|
21
|
+
from stackformers.spatial.factory import build_patch_merging, build_spatial_attn
|
|
22
|
+
from stackformers.spatial.input import SpatialInput
|
|
23
|
+
from stackformers.spatial.layer import SpatialTransformerLayer
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PyramidStageConfig(BaseModel):
|
|
27
|
+
"""One pyramid stage: `depth` spatial-attention layers at a fixed grid and width."""
|
|
28
|
+
|
|
29
|
+
attn: SpatialAttnConfig
|
|
30
|
+
ff: FeedForwardConfig
|
|
31
|
+
norm: NormConfig
|
|
32
|
+
pos_encoding: PosEncodingConfig
|
|
33
|
+
depth: int = Field(gt=0)
|
|
34
|
+
|
|
35
|
+
@model_validator(mode="after")
|
|
36
|
+
def _dims_agree(self) -> PyramidStageConfig:
|
|
37
|
+
if not self.attn.dim == self.ff.dim == self.norm.dim:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"stage dims disagree: attn={self.attn.dim}, ff={self.ff.dim}, norm={self.norm.dim}"
|
|
40
|
+
)
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class PyramidVisionConfig(BaseModel):
|
|
45
|
+
"""A multi-scale vision backbone: stages joined by patch-merging downsamples.
|
|
46
|
+
|
|
47
|
+
inter_stage[i] merges the output of stage i into the input of stage i+1, so there is
|
|
48
|
+
exactly one fewer merge than there are stages.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
stages: list[PyramidStageConfig] = Field(min_length=1)
|
|
52
|
+
inter_stage: list[PatchMergingConfig]
|
|
53
|
+
|
|
54
|
+
@model_validator(mode="after")
|
|
55
|
+
def _stages_and_merges_chain(self) -> PyramidVisionConfig:
|
|
56
|
+
if len(self.inter_stage) != len(self.stages) - 1:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"need {len(self.stages) - 1} inter-stage merges, got {len(self.inter_stage)}"
|
|
59
|
+
)
|
|
60
|
+
for i, merge in enumerate(self.inter_stage):
|
|
61
|
+
if merge.in_dim != self.stages[i].attn.dim:
|
|
62
|
+
raise ValueError(f"merge {i} in_dim {merge.in_dim} != stage {i} dim")
|
|
63
|
+
if merge.out_dim != self.stages[i + 1].attn.dim:
|
|
64
|
+
raise ValueError(f"merge {i} out_dim {merge.out_dim} != stage {i + 1} dim")
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pyramid_vision_config(
|
|
69
|
+
*,
|
|
70
|
+
dims: tuple[int, ...] = (64, 128, 320, 512),
|
|
71
|
+
depths: tuple[int, ...] = (3, 4, 6, 3),
|
|
72
|
+
heads: tuple[int, ...] = (1, 2, 5, 8),
|
|
73
|
+
sr_ratios: tuple[int, ...] = (8, 4, 2, 1),
|
|
74
|
+
window: int = 8,
|
|
75
|
+
window_stages: int = 2,
|
|
76
|
+
ff_mult: float = 4.0,
|
|
77
|
+
dropout: float = 0.0,
|
|
78
|
+
) -> PyramidVisionConfig:
|
|
79
|
+
"""PVTv2-style hybrid backbone: 2-D window attention in the first `window_stages`
|
|
80
|
+
high-resolution stages, spatially-reduced global attention in the rest.
|
|
81
|
+
|
|
82
|
+
Defaults are tuned for 1024² images patch-embedded with an 8×8 stem (stage-1 grid
|
|
83
|
+
128×128); `window=8` and the sr_ratios divide that grid cleanly. RoPE 2-D positions
|
|
84
|
+
every stage; norm defaults to RMSNorm.
|
|
85
|
+
|
|
86
|
+
Papers: PVT/PVTv2 (Wang et al., 2021/2022, https://arxiv.org/abs/2102.12122,
|
|
87
|
+
https://arxiv.org/abs/2106.13797) for the SRA pyramid; Swin (Liu et al., 2021,
|
|
88
|
+
https://arxiv.org/abs/2103.14030) for the window-attention stages.
|
|
89
|
+
"""
|
|
90
|
+
n = len(dims)
|
|
91
|
+
stages: list[PyramidStageConfig] = []
|
|
92
|
+
for i in range(n):
|
|
93
|
+
dim, dim_head = dims[i], dims[i] // heads[i]
|
|
94
|
+
attn: SpatialAttnConfig = (
|
|
95
|
+
WindowAttention2DConfig(
|
|
96
|
+
dim=dim, heads=heads[i], dim_head=dim_head, window=window, dropout=dropout
|
|
97
|
+
)
|
|
98
|
+
if i < window_stages
|
|
99
|
+
else SpatialReductionAttentionConfig(
|
|
100
|
+
dim=dim, heads=heads[i], dim_head=dim_head, reduction=sr_ratios[i], dropout=dropout
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
stages.append(
|
|
104
|
+
PyramidStageConfig(
|
|
105
|
+
attn=attn,
|
|
106
|
+
ff=SwiGLUConfig(dim=dim, mult=ff_mult, dropout=dropout),
|
|
107
|
+
norm=RMSNormConfig(dim=dim),
|
|
108
|
+
pos_encoding=RoPE2DConfig(dim_head=dim_head),
|
|
109
|
+
depth=depths[i],
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
merges = [PatchMergingConfig(in_dim=dims[i], out_dim=dims[i + 1]) for i in range(n - 1)]
|
|
113
|
+
return PyramidVisionConfig(stages=stages, inter_stage=merges)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class _PyramidStage(nn.Module):
|
|
117
|
+
"""A stack of SpatialTransformerLayers at a fixed grid; SpatialInput → SpatialInput."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, config: PyramidStageConfig) -> None:
|
|
120
|
+
super().__init__()
|
|
121
|
+
pos = build_pos_encoding(config.pos_encoding)
|
|
122
|
+
self.layers = nn.ModuleList(
|
|
123
|
+
SpatialTransformerLayer(
|
|
124
|
+
attn=build_spatial_attn(config.attn, pos),
|
|
125
|
+
ff=build_ff(config.ff),
|
|
126
|
+
norm_attn=build_norm(config.norm),
|
|
127
|
+
norm_ff=build_norm(config.norm),
|
|
128
|
+
)
|
|
129
|
+
for _ in range(config.depth)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def forward(self, input: SpatialInput) -> SpatialInput:
|
|
133
|
+
for layer in self.layers:
|
|
134
|
+
input = layer(input)
|
|
135
|
+
return input
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _to_feature_map(input: SpatialInput) -> Float[Tensor, "b d h w"]:
|
|
139
|
+
h, w = input.grid
|
|
140
|
+
return rearrange(input.x, "b (h w) d -> b d h w", h=h, w=w)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class PyramidVisionBackbone(nn.Module):
|
|
144
|
+
"""Multi-scale vision backbone (PVTv2-style hybrid).
|
|
145
|
+
|
|
146
|
+
Consumes a SpatialInput from your own patch-embed stem and returns one feature map per
|
|
147
|
+
stage as (b, d, H, W) — the multi-scale pyramid a detection/segmentation head expects.
|
|
148
|
+
|
|
149
|
+
Paper: "PVTv2: Improved Baselines with Pyramid Vision Transformer", Wang et al., 2022 —
|
|
150
|
+
https://arxiv.org/abs/2106.13797
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, config: PyramidVisionConfig) -> None:
|
|
154
|
+
super().__init__()
|
|
155
|
+
self.stages = nn.ModuleList(_PyramidStage(stage) for stage in config.stages)
|
|
156
|
+
self.merges = nn.ModuleList(build_patch_merging(m) for m in config.inter_stage)
|
|
157
|
+
|
|
158
|
+
def forward(self, input: SpatialInput) -> list[Float[Tensor, "b d h w"]]:
|
|
159
|
+
"""Shape flow (4-stage example, stem grid H₀×W₀, dims d₀..d₃):
|
|
160
|
+
|
|
161
|
+
stem in (b, H₀·W₀, d₀)
|
|
162
|
+
stage i (b, Hᵢ·Wᵢ, dᵢ) emit feature map (b, dᵢ, Hᵢ, Wᵢ)
|
|
163
|
+
merge i (b, Hᵢ₊₁·Wᵢ₊₁, dᵢ₊₁) grid halved, width widened
|
|
164
|
+
returns [(b, dᵢ, Hᵢ, Wᵢ) for each stage] — the feature pyramid
|
|
165
|
+
"""
|
|
166
|
+
features: list[Tensor] = []
|
|
167
|
+
x = input
|
|
168
|
+
for i, stage in enumerate(self.stages):
|
|
169
|
+
x = stage(x)
|
|
170
|
+
features.append(_to_feature_map(x))
|
|
171
|
+
if i < len(self.merges):
|
|
172
|
+
x = self.merges[i](x)
|
|
173
|
+
return features
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# spatial
|
|
2
|
+
|
|
3
|
+
Attention over a 2-D grid of image tokens, for efficient vision backbones. Self-contained:
|
|
4
|
+
nothing here touches the 1-D `SequenceInput` / `attention/` stack — it only reuses
|
|
5
|
+
`padded_sdpa`, position encodings, norms, and feed-forwards.
|
|
6
|
+
|
|
7
|
+
## Design
|
|
8
|
+
|
|
9
|
+
**A separate input type, not a widened `SequenceInput`.** `SpatialInput` carries the grid
|
|
10
|
+
shape `(H, W)` so attention can reshape the flat token sequence back to `(b, d, H, W)`. It is
|
|
11
|
+
deliberately kept out of the `SequenceInput` union: that union stays a sealed 1-D abstraction,
|
|
12
|
+
and spatial attention gets its own `SpatialAttn` protocol. Padded-only — fixed-resolution
|
|
13
|
+
images do not vary in length, so there is no packed spatial variant.
|
|
14
|
+
|
|
15
|
+
**Two attention variants, one protocol.** Both satisfy `SpatialAttn` and stay dot-product
|
|
16
|
+
attention (so RoPE 2-D, GQA, and `qk_norm` all apply):
|
|
17
|
+
- `WindowAttention2D` — non-overlapping `w×w` tiles, O(n·w²). Local; cheapest at the
|
|
18
|
+
high-resolution early stages. Distinct from `SelfAttentionConfig.window_size`, which is a
|
|
19
|
+
*1-D sliding* window over a flat sequence; this partitions the *2-D grid* into square tiles.
|
|
20
|
+
- `SpatialReductionAttention` — full-grid queries attend to a spatially-reduced K/V context
|
|
21
|
+
(PVTv2 SRA), O(n·s) with s ≪ n. Global; affordable once the grid is small.
|
|
22
|
+
|
|
23
|
+
**Reduction is an injected `KVReduction`.** `SpatialReductionAttention` delegates K/V
|
|
24
|
+
downsampling to a collaborator (`NoKVReduction` for r=1 full attention, `ConvKVReduction` for a
|
|
25
|
+
strided conv). The reduction's norm is itself an injected `Norm` — pluggable like everywhere
|
|
26
|
+
else, defaulting to RMSNorm.
|
|
27
|
+
|
|
28
|
+
**Window inverse is a closure.** `partition_windows` returns `(windows, merge)`; the `merge`
|
|
29
|
+
inverse is bound to the exact `(b, H, W, window)` used to partition, so the un-partition step
|
|
30
|
+
can never be called with mismatched parameters.
|
|
31
|
+
|
|
32
|
+
**Export-friendly.** Grid shapes are Python ints throughout (positions, window counts, merged
|
|
33
|
+
grid sizes), so reshapes stay static under `torch.export` / `torch.compile`.
|
|
34
|
+
|
|
35
|
+
## Extending
|
|
36
|
+
|
|
37
|
+
Add a new variant as a new `nn.Module` satisfying `SpatialAttn`, plus a config with a `kind`
|
|
38
|
+
discriminator and a `match` arm in `factory.py`. To add a new K/V reduction, implement
|
|
39
|
+
`KVReduction` and extend `build_kv_reduction`. Shifted windows would be a new variant here, not
|
|
40
|
+
a flag on `WindowAttention2D`.
|
|
41
|
+
|
|
42
|
+
The opinionated multi-scale assembly (stages + patch-merging → feature pyramid) lives in
|
|
43
|
+
`presets/pyramid_vision.py`, not here — this module only provides the reusable primitives.
|
|
44
|
+
|
|
45
|
+
## References
|
|
46
|
+
|
|
47
|
+
- **Swin Transformer** — window partitioning. Liu et al., 2021. https://arxiv.org/abs/2103.14030
|
|
48
|
+
- **Pyramid Vision Transformer (PVT)** — spatial-reduction attention. Wang et al., 2021. https://arxiv.org/abs/2102.12122
|
|
49
|
+
- **PVTv2** — overlapping patch embedding / improved pyramid. Wang et al., 2022. https://arxiv.org/abs/2106.13797
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from stackformers.spatial.config import (
|
|
2
|
+
ConvKVReductionConfig,
|
|
3
|
+
KVReductionConfig,
|
|
4
|
+
NoKVReductionConfig,
|
|
5
|
+
PatchMergingConfig,
|
|
6
|
+
SpatialAttnConfig,
|
|
7
|
+
SpatialReductionAttentionConfig,
|
|
8
|
+
WindowAttention2DConfig,
|
|
9
|
+
)
|
|
10
|
+
from stackformers.spatial.factory import (
|
|
11
|
+
build_kv_reduction,
|
|
12
|
+
build_patch_merging,
|
|
13
|
+
build_spatial_attn,
|
|
14
|
+
)
|
|
15
|
+
from stackformers.spatial.input import SpatialInput, grid_positions, make_spatial_input
|
|
16
|
+
from stackformers.spatial.kv_reduction import ConvKVReduction, NoKVReduction
|
|
17
|
+
from stackformers.spatial.layer import SpatialTransformerLayer
|
|
18
|
+
from stackformers.spatial.patch_merging import PatchMerging
|
|
19
|
+
from stackformers.spatial.protocols import KVReduction, SpatialAttn
|
|
20
|
+
from stackformers.spatial.spatial_reduction import SpatialReductionAttention
|
|
21
|
+
from stackformers.spatial.window import WindowAttention2D
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"SpatialInput",
|
|
25
|
+
"make_spatial_input",
|
|
26
|
+
"grid_positions",
|
|
27
|
+
"SpatialAttn",
|
|
28
|
+
"KVReduction",
|
|
29
|
+
"NoKVReduction",
|
|
30
|
+
"ConvKVReduction",
|
|
31
|
+
"WindowAttention2D",
|
|
32
|
+
"SpatialReductionAttention",
|
|
33
|
+
"SpatialTransformerLayer",
|
|
34
|
+
"PatchMerging",
|
|
35
|
+
"NoKVReductionConfig",
|
|
36
|
+
"ConvKVReductionConfig",
|
|
37
|
+
"KVReductionConfig",
|
|
38
|
+
"WindowAttention2DConfig",
|
|
39
|
+
"SpatialReductionAttentionConfig",
|
|
40
|
+
"SpatialAttnConfig",
|
|
41
|
+
"PatchMergingConfig",
|
|
42
|
+
"build_kv_reduction",
|
|
43
|
+
"build_spatial_attn",
|
|
44
|
+
"build_patch_merging",
|
|
45
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, model_validator
|
|
6
|
+
|
|
7
|
+
from stackformers.attention.config import _validate_attn_dims
|
|
8
|
+
from stackformers.norm.config import NormConfig, RMSNormConfig
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NoKVReductionConfig(BaseModel):
|
|
12
|
+
kind: Literal["none"] = "none"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConvKVReductionConfig(BaseModel):
|
|
16
|
+
"""Strided-conv spatial reduction of the key/value context.
|
|
17
|
+
|
|
18
|
+
Paper: "Pyramid Vision Transformer" (SRA), Wang et al., 2021 —
|
|
19
|
+
https://arxiv.org/abs/2102.12122
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
kind: Literal["conv"] = "conv"
|
|
23
|
+
dim: int = Field(gt=0, description="Channel width of the tokens being reduced.")
|
|
24
|
+
reduction: int = Field(
|
|
25
|
+
gt=1,
|
|
26
|
+
description="Spatial stride r: an H×W grid is pooled to (H/r)×(W/r). Must divide H and W.",
|
|
27
|
+
)
|
|
28
|
+
norm: NormConfig = Field(
|
|
29
|
+
default_factory=lambda: RMSNormConfig(dim=1),
|
|
30
|
+
description="Norm applied to the reduced tokens. Defaults to RMSNorm, matching the "
|
|
31
|
+
"rest of the library; set to LayerNormConfig for the original PVTv2 behaviour.",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
@model_validator(mode="after")
|
|
35
|
+
def _sync_norm_dim(self) -> ConvKVReductionConfig:
|
|
36
|
+
# The norm always acts on `dim` channels, so derive its width rather than making
|
|
37
|
+
# the caller repeat it (and risk a mismatch).
|
|
38
|
+
self.norm = self.norm.model_copy(update={"dim": self.dim})
|
|
39
|
+
return self
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
KVReductionConfig = Annotated[
|
|
43
|
+
NoKVReductionConfig | ConvKVReductionConfig,
|
|
44
|
+
Field(discriminator="kind"),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class _SpatialAttnBase(BaseModel):
|
|
49
|
+
"""Shared geometry for 2-D attention variants (mirrors SelfAttentionConfig)."""
|
|
50
|
+
|
|
51
|
+
dim: int = Field(gt=0, description="Model dimension — input/output width of the sublayer.")
|
|
52
|
+
heads: int = Field(default=8, gt=0, description="Number of query heads.")
|
|
53
|
+
dim_head: int = Field(default=64, gt=0, description="Dimension per head.")
|
|
54
|
+
kv_heads: int | None = Field(
|
|
55
|
+
default=None, description="Key/value heads. None = heads (MHA); a divisor of heads = GQA."
|
|
56
|
+
)
|
|
57
|
+
dropout: float = Field(default=0.0, ge=0.0, le=1.0, description="Output-projection dropout.")
|
|
58
|
+
qk_norm: bool = Field(default=False, description="RMSNorm queries and keys before the product.")
|
|
59
|
+
|
|
60
|
+
@model_validator(mode="after")
|
|
61
|
+
def _validate_dims(self) -> _SpatialAttnBase:
|
|
62
|
+
_validate_attn_dims(self.dim, self.heads, self.dim_head, self.kv_heads)
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def effective_kv_heads(self) -> int:
|
|
67
|
+
return self.kv_heads if self.kv_heads is not None else self.heads
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def groups(self) -> int:
|
|
71
|
+
return self.heads // self.effective_kv_heads
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class WindowAttention2DConfig(_SpatialAttnBase):
|
|
75
|
+
"""Non-overlapping w×w window attention over the grid — O(n·w²), local.
|
|
76
|
+
|
|
77
|
+
Distinct from SelfAttentionConfig.window_size, which is a 1-D sliding window over a
|
|
78
|
+
flat sequence; this partitions the 2-D grid into square tiles.
|
|
79
|
+
|
|
80
|
+
Paper: "Swin Transformer" (window partitioning), Liu et al., 2021 —
|
|
81
|
+
https://arxiv.org/abs/2103.14030
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
kind: Literal["window2d"] = "window2d"
|
|
85
|
+
window: int = Field(gt=0, description="Window side length w; must divide grid H and W.")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SpatialReductionAttentionConfig(_SpatialAttnBase):
|
|
89
|
+
"""Global attention with spatially-reduced K/V — O(n·s).
|
|
90
|
+
|
|
91
|
+
Paper: "Pyramid Vision Transformer" (SRA), Wang et al., 2021 —
|
|
92
|
+
https://arxiv.org/abs/2102.12122
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
kind: Literal["spatial_reduction"] = "spatial_reduction"
|
|
96
|
+
reduction: int = Field(
|
|
97
|
+
default=1,
|
|
98
|
+
ge=1,
|
|
99
|
+
description="K/V spatial stride r. 1 = full global attention (no reduction).",
|
|
100
|
+
)
|
|
101
|
+
kv_norm: NormConfig = Field(
|
|
102
|
+
default_factory=lambda: RMSNormConfig(dim=1),
|
|
103
|
+
description="Norm applied to reduced K/V tokens (ignored when reduction == 1).",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@model_validator(mode="after")
|
|
107
|
+
def _sync_kv_norm_dim(self) -> SpatialReductionAttentionConfig:
|
|
108
|
+
self.kv_norm = self.kv_norm.model_copy(update={"dim": self.dim})
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
SpatialAttnConfig = Annotated[
|
|
113
|
+
WindowAttention2DConfig | SpatialReductionAttentionConfig,
|
|
114
|
+
Field(discriminator="kind"),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class PatchMergingConfig(BaseModel):
|
|
119
|
+
"""Overlapping strided-conv downsample between pyramid stages (defaults k3 s2 p1).
|
|
120
|
+
|
|
121
|
+
Paper: "PVTv2: Improved Baselines with Pyramid Vision Transformer" (overlapping patch
|
|
122
|
+
embedding), Wang et al., 2022 — https://arxiv.org/abs/2106.13797
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
in_dim: int = Field(gt=0, description="Input channel width.")
|
|
126
|
+
out_dim: int = Field(gt=0, description="Output channel width after merging.")
|
|
127
|
+
kernel: int = Field(default=3, gt=0, description="Conv kernel size.")
|
|
128
|
+
stride: int = Field(default=2, gt=0, description="Conv stride — the downsample factor.")
|
|
129
|
+
padding: int = Field(default=1, ge=0, description="Conv padding.")
|
|
130
|
+
norm: NormConfig = Field(
|
|
131
|
+
default_factory=lambda: RMSNormConfig(dim=1),
|
|
132
|
+
description="Norm applied to merged tokens; dim is synced to out_dim.",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@model_validator(mode="after")
|
|
136
|
+
def _sync_norm_dim(self) -> PatchMergingConfig:
|
|
137
|
+
self.norm = self.norm.model_copy(update={"dim": self.out_dim})
|
|
138
|
+
return self
|