stackformers 3.9.1__tar.gz → 4.2.0__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-4.2.0/CHANGELOG.md +320 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/PKG-INFO +1 -1
- {stackformers-3.9.1 → stackformers-4.2.0}/pyproject.toml +1 -1
- stackformers-4.2.0/run-remote-tests.sh +24 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/attention/README.md +8 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/attention/__init__.py +14 -1
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/attention/config.py +43 -0
- stackformers-4.2.0/stackformers/attention/distance_bias.py +49 -0
- stackformers-4.2.0/stackformers/attention/factory.py +20 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/attention/ops.py +13 -5
- stackformers-4.2.0/stackformers/positional/README.md +28 -0
- stackformers-4.2.0/stackformers/positional/config.py +126 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/positional/factory.py +4 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/positional/rope2d.py +15 -5
- stackformers-4.2.0/stackformers/positional/rope_nd.py +99 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/presets/README.md +1 -3
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/presets/encoder.py +63 -6
- stackformers-4.2.0/tests/attention/test_distance_bias.py +123 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/conftest.py +16 -2
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/positional/test_rope.py +163 -8
- stackformers-4.2.0/tests/positional/test_rope_nd.py +214 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/uv.lock +1 -1
- stackformers-3.9.1/stackformers/positional/README.md +0 -13
- stackformers-3.9.1/stackformers/positional/config.py +0 -65
- stackformers-3.9.1/stackformers/presets/pyramid_vision.py +0 -173
- stackformers-3.9.1/stackformers/spatial/README.md +0 -49
- stackformers-3.9.1/stackformers/spatial/__init__.py +0 -45
- stackformers-3.9.1/stackformers/spatial/config.py +0 -138
- stackformers-3.9.1/stackformers/spatial/factory.py +0 -49
- stackformers-3.9.1/stackformers/spatial/input.py +0 -53
- stackformers-3.9.1/stackformers/spatial/kv_reduction.py +0 -69
- stackformers-3.9.1/stackformers/spatial/layer.py +0 -42
- stackformers-3.9.1/stackformers/spatial/patch_merging.py +0 -54
- stackformers-3.9.1/stackformers/spatial/protocols.py +0 -33
- stackformers-3.9.1/stackformers/spatial/spatial_reduction.py +0 -70
- stackformers-3.9.1/stackformers/spatial/window.py +0 -101
- stackformers-3.9.1/tests/presets/test_pyramid_vision.py +0 -71
- stackformers-3.9.1/tests/spatial/__init__.py +0 -0
- stackformers-3.9.1/tests/spatial/test_input.py +0 -41
- stackformers-3.9.1/tests/spatial/test_kv_reduction.py +0 -66
- stackformers-3.9.1/tests/spatial/test_layer.py +0 -45
- stackformers-3.9.1/tests/spatial/test_patch_merging.py +0 -37
- stackformers-3.9.1/tests/spatial/test_spatial_reduction.py +0 -81
- stackformers-3.9.1/tests/spatial/test_window.py +0 -75
- {stackformers-3.9.1 → stackformers-4.2.0}/.claudeignore +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/.gitignore +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/.python-version +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/.vscode/settings.json +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/CLAUDE.md +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/Justfile +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/LICENSE +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/PLAN.md +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/README.md +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/attention/bias.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/attention/cross_attn.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/attention/protocols.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/attention/self_attn.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/attention/varlen_backend.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/config.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/cross_attender.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/decoder.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/encoder.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/feedforward/README.md +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/feedforward/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/feedforward/config.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/feedforward/factory.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/feedforward/geglu.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/feedforward/protocols.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/feedforward/relu_squared.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/feedforward/swiglu.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/layers.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/norm/README.md +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/norm/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/norm/config.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/norm/factory.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/norm/protocols.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/positional/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/positional/learned.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/positional/none.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/positional/protocols.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/positional/rope1d.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/presets/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/presets/cross_attender.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/presets/decoder.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/stackformers/sequence.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/attention/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/attention/test_cross_attn.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/attention/test_kernels.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/attention/test_ops.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/attention/test_self_attn.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/attention/test_varlen_backend.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/feedforward/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/feedforward/test_geglu.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/feedforward/test_relu_squared.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/feedforward/test_swiglu.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/positional/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/positional/test_learned_pos.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/presets/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/presets/test_cross_attender.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/presets/test_decoder.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/presets/test_encoder.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/test_cross_attender.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/test_decoder.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/test_encoder.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/test_layers.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/test_norm.py +0 -0
- {stackformers-3.9.1 → stackformers-4.2.0}/tests/test_sequence.py +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
6
|
+
adheres to [Semantic Versioning](https://semver.org/): MAJOR for breaking public API changes,
|
|
7
|
+
MINOR for backwards-compatible features, PATCH for bug fixes and internal changes.
|
|
8
|
+
|
|
9
|
+
## [4.2.0] — 2026-07-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`RotaryEmbeddingND`** (`positional/rope_nd.py`) — rotary position encoding over `c` spatial
|
|
14
|
+
dimensions, splitting `dim_head` into `c` per-axis blocks. `c=1` and `c=2` reduce to the
|
|
15
|
+
existing 1-D and 2-D encodings; 3-D and beyond now work with the same class.
|
|
16
|
+
- `RoPENDConfig`, parameterised by **`r_min` / `r_max` instead of `base`**.
|
|
17
|
+
|
|
18
|
+
### Notes
|
|
19
|
+
|
|
20
|
+
- **`base` is wrong for continuous coordinates**, and `RoPENDConfig` therefore does not have
|
|
21
|
+
it. RoPE's fastest band is pinned at `ω = 1` — a wavelength of exactly 2π *coordinate units* —
|
|
22
|
+
whatever the base; `base` can only stretch the slow end. So the ladder lands correctly only
|
|
23
|
+
when tokens happen to sit one unit apart. That is true of text and of a patch grid, and of
|
|
24
|
+
nothing else: `base=10000` on a 14×14 patch grid leaves 14 of its 16 bands frozen, and
|
|
25
|
+
coordinates normalised to `[0, 1]` leave every band longer than the whole domain.
|
|
26
|
+
|
|
27
|
+
The ladder is built from the data instead — `ω_hi = π / r_min` (Nyquist on the finest
|
|
28
|
+
separation the model must resolve) down to `ω_lo = 2π / (headroom · r_max)` (the longest
|
|
29
|
+
wavelength spans the domain). It then depends only on the *dynamic range* `r_max / r_min`, so
|
|
30
|
+
metres, pixels and millimetres give the identical encoding — the property `base` never had,
|
|
31
|
+
and the one that makes the setting measurable from the data rather than tuned.
|
|
32
|
+
|
|
33
|
+
- `RoPE1DConfig` and `RoPE2DConfig` keep `base` and are unchanged. Use them for text and grids.
|
|
34
|
+
|
|
35
|
+
- **Centre your coordinates.** Not for invariance — RoPE is translation-invariant exactly, since
|
|
36
|
+
the rotations cancel into `ω·(pᵢ − pⱼ)`. For float32: the angle is `ω·p`, and coordinates far
|
|
37
|
+
from the origin push it into the hundreds of radians and spend the mantissa before the cosine
|
|
38
|
+
is taken. A 1e5 offset costs three orders of magnitude of accuracy.
|
|
39
|
+
|
|
40
|
+
- **It encodes direction, not just distance**, so it is not rotation invariant — by design, as
|
|
41
|
+
the relative offset is strictly more information than the distance. Where the global frame is
|
|
42
|
+
arbitrary, train with rotation augmentation. Exact rotation invariance in the architecture
|
|
43
|
+
costs either an O(n²) bias (`RelativeDistanceBias`, above) or a canonicalisation of the input
|
|
44
|
+
frame, which is inherently discontinuous: a near-isotropic point set flips frames under a 1%
|
|
45
|
+
perturbation, measured on 1–2% of samples.
|
|
46
|
+
|
|
47
|
+
## [4.1.0] — 2026-07-14
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **`RelativeDistanceBias`** (`attention/distance_bias.py`) — an additive attention bias read
|
|
52
|
+
off the Euclidean distance between node positions: pairwise distance → Gaussian radial
|
|
53
|
+
shells → a learned per-head profile. Only `‖pᵢ − pⱼ‖` enters the logit, so attention is
|
|
54
|
+
invariant to any global translation *or rotation* of the node set. For 2-D/N-D node sets
|
|
55
|
+
whose coordinate frame is arbitrary.
|
|
56
|
+
- `DistanceBiasConfig`, `NoAttnBiasConfig`, and the `AttnBiasConfig` discriminated union;
|
|
57
|
+
`attention/factory.py` with `build_attn_bias`; a `node_encoder_config` preset pairing the
|
|
58
|
+
bias with `NoPosEncoding`.
|
|
59
|
+
|
|
60
|
+
### Notes
|
|
61
|
+
|
|
62
|
+
- **A rotary encoding cannot express this.** RoPE rotates by `ω·p`, which is linear in position
|
|
63
|
+
by construction — and that linearity is exactly what makes the query and key rotations cancel
|
|
64
|
+
into a relative offset. Distance is not linear in position, so it can only enter as a bias.
|
|
65
|
+
Concretely, RoPE-2D scores two neighbours *the same distance away* 2.7× differently depending
|
|
66
|
+
on whether the offset is axis-aligned or diagonal, and rotating a node set by 30° moves its
|
|
67
|
+
attention logits by 83%.
|
|
68
|
+
- **Opt-in, and it has a ceiling.** `NoAttnBias` remains the default and returns `None`, which
|
|
69
|
+
keeps `varlen_attn` available, so the bias costs nothing when unused. When used it costs a
|
|
70
|
+
great deal: a bias has no varlen slot, so it forces padded SDPA and an O(n²) tensor, and the
|
|
71
|
+
`(b, n, s, num_rbf)` shell intermediate — retained for backward, and `num_rbf/h` times larger
|
|
72
|
+
than the bias itself — dominates activation memory. Measured OOM past roughly 2 000 nodes on
|
|
73
|
+
16 GiB with 4 layers. Where that does not fit, `RotaryEmbeddingND` plus rotation augmentation
|
|
74
|
+
is the cheaper trade.
|
|
75
|
+
|
|
76
|
+
## [4.0.1] — 2026-07-14
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- **Causal and windowed attention crashed on CUDA in half precision.** `window_mask` built its
|
|
81
|
+
mask at `torch.float` while `padding_mask` used the query's dtype; adding them promoted the
|
|
82
|
+
sum to float32, and CUDA's SDPA rejects a mask whose dtype differs from the query
|
|
83
|
+
(`RuntimeError: invalid dtype for bias`). Every causal or sliding-window call in
|
|
84
|
+
`float16`/`bfloat16` was affected — that is, the configuration a model actually trains in.
|
|
85
|
+
`window_mask` now takes an explicit `dtype` and `padded_sdpa` passes `q.dtype`.
|
|
86
|
+
|
|
87
|
+
It went unnoticed because CPU's SDPA math backend tolerates the mismatch, and the suite had
|
|
88
|
+
only ever run on CPU — half precision exists solely in the CUDA half of the device matrix.
|
|
89
|
+
Running the untouched tree on a GPU fails 30 tests that pass locally.
|
|
90
|
+
|
|
91
|
+
## [4.0.0] — 2026-07-14
|
|
92
|
+
|
|
93
|
+
### Removed
|
|
94
|
+
|
|
95
|
+
- **Breaking: the 2-D vision modules.** The `spatial` package (`WindowAttention2D`,
|
|
96
|
+
`SpatialReductionAttention`, `ConvKVReduction`/`NoKVReduction`, `PatchMerging`,
|
|
97
|
+
`SpatialInput`, `SpatialTransformerLayer`) and the `PyramidVisionBackbone` preset with
|
|
98
|
+
`pyramid_vision_config` are gone. The library returns to sequence models only.
|
|
99
|
+
- `RotaryEmbedding2D` and `RoPE2DConfig` are **kept** — they are independent of `spatial` and
|
|
100
|
+
encode any two-coordinate position (grid row/col, or point x/y).
|
|
101
|
+
|
|
102
|
+
### Notes
|
|
103
|
+
|
|
104
|
+
- A coordinate-frame bug in `ConvKVReduction` (pooled keys were positioned in the reduced
|
|
105
|
+
grid's index space while queries used full-grid coordinates, so a relative encoding saw an
|
|
106
|
+
offset that grew with absolute position) was found but not fixed, as the module is removed
|
|
107
|
+
here. Anyone reviving this code from history must fix it first: pooled tokens belong at their
|
|
108
|
+
block centre, `i·r + (r − 1)/2`, in the original grid's frame.
|
|
109
|
+
|
|
110
|
+
## [3.9.2] — 2026-07-14
|
|
111
|
+
|
|
112
|
+
### Fixed
|
|
113
|
+
|
|
114
|
+
- **`RotaryEmbedding2D` was not computing a rotation.** The frequency vector was laid out as
|
|
115
|
+
`[row | row | col | col]`, but `_rotate_half` pairs channel `i` with channel `i + dh/2`, so a
|
|
116
|
+
row angle was paired against a column angle. The resulting 2×2 map had
|
|
117
|
+
`det = cos((row − col) · ωᵢ)` and singular values `√(1 ± |sin D|)` — a squeeze, not a rotation.
|
|
118
|
+
It was therefore non-orthogonal, annihilated high-frequency channels wherever the determinant
|
|
119
|
+
reached zero, and produced attention scores that leaked absolute position instead of depending
|
|
120
|
+
only on relative offset. Frequencies are now built as `[row | col | row | col]`.
|
|
121
|
+
- `RotaryEmbedding2D` now casts positions and `inv_freq` to float32 for the outer product,
|
|
122
|
+
matching `RotaryEmbedding1D`. A `.half()`-ed module previously lost position precision.
|
|
123
|
+
|
|
124
|
+
### Changed
|
|
125
|
+
|
|
126
|
+
- **Numerics of `RotaryEmbedding2D` change.** Any checkpoint trained against the previous
|
|
127
|
+
encoding is tuned to the broken position code and requires retraining or finetuning.
|
|
128
|
+
|
|
129
|
+
### Added
|
|
130
|
+
|
|
131
|
+
- Math tests for `RotaryEmbedding2D` on a genuine row-major grid: a definition-first reference
|
|
132
|
+
oracle, orthogonality (norms and inner products), relative-offset invariance, per-axis
|
|
133
|
+
sensitivity, distinct-offset separation, and packed/padded agreement. The previous test used a
|
|
134
|
+
`row == col` position grid — the main diagonal, the one locus where the row and column angles
|
|
135
|
+
coincide and the broken layout is indistinguishable from a correct one.
|
|
136
|
+
|
|
137
|
+
## [3.9.1] — 2026-06-30
|
|
138
|
+
|
|
139
|
+
### Fixed
|
|
140
|
+
|
|
141
|
+
- Make the experimental `varlen_attn` import and call robust across PyTorch versions.
|
|
142
|
+
|
|
143
|
+
## [3.9.0] — 2026-06-17
|
|
144
|
+
|
|
145
|
+
### Added
|
|
146
|
+
|
|
147
|
+
- 2-D grid attention (`spatial`) and the pyramid vision backbone preset.
|
|
148
|
+
|
|
149
|
+
## [3.8.2] — 2026-05-29
|
|
150
|
+
|
|
151
|
+
### Fixed
|
|
152
|
+
|
|
153
|
+
- Force absolute positions to float32 in `sequence`.
|
|
154
|
+
|
|
155
|
+
## [3.8.1] — 2026-05-29
|
|
156
|
+
|
|
157
|
+
### Fixed
|
|
158
|
+
|
|
159
|
+
- Keep batch size as a `SymInt` in the packed attention fallback, preserving `torch.export`
|
|
160
|
+
compatibility.
|
|
161
|
+
|
|
162
|
+
## [3.8.0] — 2026-05-27
|
|
163
|
+
|
|
164
|
+
### Fixed
|
|
165
|
+
|
|
166
|
+
- Replace `.tolist()` loops in `sequence` and `ops` with tensor operations, making them
|
|
167
|
+
`torch.export`-compatible.
|
|
168
|
+
|
|
169
|
+
### Added
|
|
170
|
+
|
|
171
|
+
- PyPI publish metadata in `pyproject.toml`.
|
|
172
|
+
|
|
173
|
+
## [3.7.0] — 2026-05-21
|
|
174
|
+
|
|
175
|
+
### Changed
|
|
176
|
+
|
|
177
|
+
- Introduce abstract generic base classes for all presets.
|
|
178
|
+
|
|
179
|
+
## [3.6.0] — 2026-05-21
|
|
180
|
+
|
|
181
|
+
### Added
|
|
182
|
+
|
|
183
|
+
- `AttnBias` protocol and the `NoAttnBias` null object on `SelfAttention`.
|
|
184
|
+
|
|
185
|
+
## [3.5.2] — 2026-05-21
|
|
186
|
+
|
|
187
|
+
### Fixed
|
|
188
|
+
|
|
189
|
+
- Warn and fall back to SDPA when `varlen_attn` raises.
|
|
190
|
+
|
|
191
|
+
## [3.5.1] — 2026-05-18
|
|
192
|
+
|
|
193
|
+
### Changed
|
|
194
|
+
|
|
195
|
+
- Move attention dropout to the output projection.
|
|
196
|
+
|
|
197
|
+
## [3.5.0] — 2026-05-18
|
|
198
|
+
|
|
199
|
+
### Added
|
|
200
|
+
|
|
201
|
+
- Packed→padded SDPA fallback; extract `ops.py`.
|
|
202
|
+
|
|
203
|
+
## [3.4.2] — 2026-05-18
|
|
204
|
+
|
|
205
|
+
### Added
|
|
206
|
+
|
|
207
|
+
- `padded_to_packed` and `packed_to_padded` helpers.
|
|
208
|
+
|
|
209
|
+
## [3.4.1] — 2026-05-18
|
|
210
|
+
|
|
211
|
+
### Removed
|
|
212
|
+
|
|
213
|
+
- Packed-attention CPU fallback; warn on dropout instead.
|
|
214
|
+
|
|
215
|
+
## [3.4.0] — 2026-05-17
|
|
216
|
+
|
|
217
|
+
### Added
|
|
218
|
+
|
|
219
|
+
- ReLU² feed-forward network.
|
|
220
|
+
|
|
221
|
+
## [3.3.0] — 2026-05-17
|
|
222
|
+
|
|
223
|
+
### Added
|
|
224
|
+
|
|
225
|
+
- QK-norm in attention; all config fields documented.
|
|
226
|
+
|
|
227
|
+
## [3.2.0] — 2026-05-17
|
|
228
|
+
|
|
229
|
+
### Changed
|
|
230
|
+
|
|
231
|
+
- Eliminate layout-specific duplicates in positional encoding implementations.
|
|
232
|
+
|
|
233
|
+
## [3.1.0] — 2026-05-17
|
|
234
|
+
|
|
235
|
+
### Changed
|
|
236
|
+
|
|
237
|
+
- Replace `forward()` on positional encodings with `forward_padded` / `forward_packed`, so
|
|
238
|
+
callers pick the path for the layout they already know.
|
|
239
|
+
|
|
240
|
+
## [3.0.1] — 2026-05-17
|
|
241
|
+
|
|
242
|
+
### Fixed
|
|
243
|
+
|
|
244
|
+
- Use the GELU tanh approximation in GEGLU.
|
|
245
|
+
|
|
246
|
+
## [3.0.0] — 2026-05-16
|
|
247
|
+
|
|
248
|
+
### Changed
|
|
249
|
+
|
|
250
|
+
- **Breaking:** remove the kernel abstraction; padded and packed dispatch are unified.
|
|
251
|
+
|
|
252
|
+
## [2.0.2] — 2026-05-16
|
|
253
|
+
|
|
254
|
+
### Removed
|
|
255
|
+
|
|
256
|
+
- The unused local-attention package.
|
|
257
|
+
|
|
258
|
+
## [2.0.1] — 2026-05-16
|
|
259
|
+
|
|
260
|
+
### Changed
|
|
261
|
+
|
|
262
|
+
- Store feed-forward activations as `nn.Module` attributes.
|
|
263
|
+
|
|
264
|
+
## [2.0.0] — 2026-05-16
|
|
265
|
+
|
|
266
|
+
### Removed
|
|
267
|
+
|
|
268
|
+
- **Breaking:** the `attn_bias` / ALiBi system.
|
|
269
|
+
|
|
270
|
+
## [1.1.0] — 2026-05-16
|
|
271
|
+
|
|
272
|
+
### Added
|
|
273
|
+
|
|
274
|
+
- `LearnedPosEncoding` with absolute position embeddings.
|
|
275
|
+
|
|
276
|
+
## [1.0.1] — 2026-05-16
|
|
277
|
+
|
|
278
|
+
### Added
|
|
279
|
+
|
|
280
|
+
- `LayerNorm` wrapper.
|
|
281
|
+
|
|
282
|
+
## [1.0.0] — 2026-05-16
|
|
283
|
+
|
|
284
|
+
### Added
|
|
285
|
+
|
|
286
|
+
- GEGLU feed-forward variant.
|
|
287
|
+
|
|
288
|
+
### Changed
|
|
289
|
+
|
|
290
|
+
- **Breaking:** feed-forward config becomes a discriminated union.
|
|
291
|
+
|
|
292
|
+
## [0.5.0] — 2026-05-16
|
|
293
|
+
|
|
294
|
+
### Added
|
|
295
|
+
|
|
296
|
+
- Unfold mode for the windowed SDPA kernel.
|
|
297
|
+
|
|
298
|
+
## [0.4.0] — 2026-05-15
|
|
299
|
+
|
|
300
|
+
### Added
|
|
301
|
+
|
|
302
|
+
- `RotaryEmbedding2D` and the packed cross-attender.
|
|
303
|
+
|
|
304
|
+
## [0.3.0] — 2026-05-14
|
|
305
|
+
|
|
306
|
+
### Changed
|
|
307
|
+
|
|
308
|
+
- Establish the semantic versioning convention.
|
|
309
|
+
|
|
310
|
+
## [0.2.0] — 2026-05-13
|
|
311
|
+
|
|
312
|
+
### Changed
|
|
313
|
+
|
|
314
|
+
- Remove the `v1` namespace; add the `presets` package.
|
|
315
|
+
|
|
316
|
+
## [0.1.0] — 2026-05-13
|
|
317
|
+
|
|
318
|
+
### Added
|
|
319
|
+
|
|
320
|
+
- Initial library scaffold.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackformers
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.2.0
|
|
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
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Run the test suite on a remote CUDA box.
|
|
3
|
+
#
|
|
4
|
+
# flex_attention's backward is CUDA-only, so the tests that cover it are skipped on this
|
|
5
|
+
# Mac and only ever execute here.
|
|
6
|
+
#
|
|
7
|
+
# ./run-remote-tests.sh # whole suite
|
|
8
|
+
# ./run-remote-tests.sh tests/attention -k flex # anything after the script is passed to pytest
|
|
9
|
+
# REMOTE=other-box ./run-remote-tests.sh
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
REMOTE="${REMOTE:-srukr-redeyed-pc}"
|
|
13
|
+
REMOTE_DIR="${REMOTE_DIR:-/tmp/sf-cuda/repo}"
|
|
14
|
+
UV="${UV:-\$HOME/.local/bin/uv}" # resolved on the remote, not here
|
|
15
|
+
LOCAL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
|
|
17
|
+
echo "==> syncing $LOCAL_DIR -> $REMOTE:$REMOTE_DIR"
|
|
18
|
+
rsync -az --delete \
|
|
19
|
+
--exclude '.venv' --exclude '.git' --exclude 'x-transformers' \
|
|
20
|
+
--exclude '__pycache__' --exclude '.pytest_cache' --exclude '.ruff_cache' \
|
|
21
|
+
"$LOCAL_DIR/" "$REMOTE:$REMOTE_DIR/"
|
|
22
|
+
|
|
23
|
+
echo "==> uv sync + pytest on $REMOTE"
|
|
24
|
+
ssh "$REMOTE" "cd '$REMOTE_DIR' && $UV sync --quiet && $UV run pytest ${*:-tests/} -q"
|
|
@@ -18,6 +18,14 @@ Multi-head self-attention and cross-attention.
|
|
|
18
18
|
|
|
19
19
|
**GQA / MQA.** Set `kv_heads < heads` in either config. Key and value heads are repeated via `einops.repeat` before the dot-product — no attention-specific changes needed.
|
|
20
20
|
|
|
21
|
+
**Geometry a rotary encoding cannot carry.** `RelativeDistanceBias` adds a learned function of the Euclidean distance between node positions to the logits, making attention invariant to any global translation or rotation of the input. It lives here rather than in `positional/` because it *cannot* live there: RoPE rotates by `ω·p`, which is linear in position by construction — and that linearity is exactly what makes the query and key rotations cancel into a relative offset. Distance is not linear in position, so no rotary encoding, axial or otherwise, can express it; a bias is the only route. Pair it with `NoPosEncoding` — a rotary encoding alongside it would reintroduce the preferred frame it exists to remove. Use it when node coordinates have no meaningful axes; when up and right *do* mean something, RoPE-2D keeps direction and stays varlen-compatible.
|
|
22
|
+
|
|
23
|
+
**The bias costs the varlen path, by construction.** `varlen_attn` has no bias slot, so any non-`None` `AttnBias` forces the padded SDPA path and materialises a `(b, h, n, s)` tensor. The `(b, n, s, num_rbf)` shell intermediate is retained for backward and is `num_rbf/h` times larger still — it, not the bias, dominates activation memory. `NoAttnBias` returns `None` precisely so the common case keeps the kernel. `TransformerEncoder` shares one bias *module* across layers, but each layer still *calls* it, so the tensor is recomputed and retained per layer; at large node counts the levers are a smaller `num_rbf`, checkpointing the bias, or hoisting the call out of the layer loop.
|
|
24
|
+
|
|
25
|
+
**Why not `flex_attention`.** It fuses the bias into the kernel and would make it O(n) instead of O(n²) — attempted, and parked on the `flex-attention` branch. It needs `dim_head >= 16`; its `score_mod` must lower as a *pointwise* subgraph, which forbids both a `vector_norm` over coordinates and a sum over shells; a captured tensor requiring grad may be indexed exactly once, which forbids unrolling that sum instead; called eagerly it is unfused and catastrophic (measured 64 s/step at n=2048, OOM at n=4096, against ~40 ms for SDPA), so it is viable only inside `torch.compile`; and under `torch.compile` with *dynamic* shapes it bails back to SDPA anyway. With node counts that vary per batch, the fused kernel would rarely be the one that runs.
|
|
26
|
+
|
|
21
27
|
## Extending
|
|
22
28
|
|
|
23
29
|
To add a new attention variant, create a new `nn.Module` that satisfies the `SelfAttn` or `CrossAttn` protocol. No kernel registration or factory changes required.
|
|
30
|
+
|
|
31
|
+
To add a new attention bias, write an `nn.Module` satisfying `AttnBias` (take `PaddedInput`, return a broadcastable `(b, h, n, s)` tensor or `None`), give it a config with a `kind` discriminator, join it to the `AttnBiasConfig` union, and add one `match` arm to `build_attn_bias`.
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
from stackformers.attention.bias import NoAttnBias
|
|
2
|
-
from stackformers.attention.config import
|
|
2
|
+
from stackformers.attention.config import (
|
|
3
|
+
AttnBiasConfig,
|
|
4
|
+
CrossAttentionConfig,
|
|
5
|
+
DistanceBiasConfig,
|
|
6
|
+
NoAttnBiasConfig,
|
|
7
|
+
SelfAttentionConfig,
|
|
8
|
+
)
|
|
3
9
|
from stackformers.attention.cross_attn import CrossAttention
|
|
10
|
+
from stackformers.attention.distance_bias import RelativeDistanceBias
|
|
11
|
+
from stackformers.attention.factory import build_attn_bias
|
|
4
12
|
from stackformers.attention.protocols import AttnBias, CrossAttn, SelfAttn
|
|
5
13
|
from stackformers.attention.self_attn import SelfAttention
|
|
6
14
|
|
|
7
15
|
__all__ = [
|
|
8
16
|
"SelfAttentionConfig",
|
|
9
17
|
"CrossAttentionConfig",
|
|
18
|
+
"AttnBiasConfig",
|
|
19
|
+
"NoAttnBiasConfig",
|
|
20
|
+
"DistanceBiasConfig",
|
|
10
21
|
"SelfAttn",
|
|
11
22
|
"CrossAttn",
|
|
12
23
|
"AttnBias",
|
|
13
24
|
"SelfAttention",
|
|
14
25
|
"CrossAttention",
|
|
15
26
|
"NoAttnBias",
|
|
27
|
+
"RelativeDistanceBias",
|
|
28
|
+
"build_attn_bias",
|
|
16
29
|
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import warnings
|
|
4
|
+
from typing import Annotated, Literal
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, Field, model_validator
|
|
6
7
|
|
|
@@ -96,6 +97,48 @@ class SelfAttentionConfig(BaseModel):
|
|
|
96
97
|
return self.heads // self.effective_kv_heads
|
|
97
98
|
|
|
98
99
|
|
|
100
|
+
class NoAttnBiasConfig(BaseModel):
|
|
101
|
+
"""No attention bias — leaves varlen_attn available on the packed path."""
|
|
102
|
+
|
|
103
|
+
kind: Literal["none"] = "none"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class DistanceBiasConfig(BaseModel):
|
|
107
|
+
"""Config for RelativeDistanceBias.
|
|
108
|
+
|
|
109
|
+
Attention logits gain a learned function of the Euclidean distance between node
|
|
110
|
+
positions, making them invariant to global translation and rotation of the node set.
|
|
111
|
+
Use with NoPosEncodingConfig: a rotary encoding would reintroduce a preferred frame.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
kind: Literal["distance"] = "distance"
|
|
115
|
+
heads: int = Field(gt=0, description="Number of query heads. Must match the attention config.")
|
|
116
|
+
r_max: float = Field(
|
|
117
|
+
gt=0.0,
|
|
118
|
+
description=(
|
|
119
|
+
"Largest pairwise node distance to resolve. Radial-basis shells are spread over"
|
|
120
|
+
" [0, r_max]; pairs further apart fall in the tail of the outermost shell and become"
|
|
121
|
+
" indistinguishable. Measure it from the data — a high percentile of the pairwise"
|
|
122
|
+
" distance distribution, not the maximum, which is an outlier."
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
num_rbf: int = Field(
|
|
126
|
+
default=32,
|
|
127
|
+
gt=1,
|
|
128
|
+
description=(
|
|
129
|
+
"Number of Gaussian radial-basis shells. Distance resolution is r_max / (num_rbf - 1);"
|
|
130
|
+
" raising it sharpens the profile a head can learn but costs a (b, n, s, num_rbf)"
|
|
131
|
+
" intermediate, which dominates activation memory at large node counts."
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
AttnBiasConfig = Annotated[
|
|
137
|
+
NoAttnBiasConfig | DistanceBiasConfig,
|
|
138
|
+
Field(discriminator="kind"),
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
|
|
99
142
|
class CrossAttentionConfig(BaseModel):
|
|
100
143
|
dim: int = Field(
|
|
101
144
|
gt=0,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
import torch.nn as nn
|
|
5
|
+
from einops import rearrange
|
|
6
|
+
from jaxtyping import Float
|
|
7
|
+
from torch import Tensor
|
|
8
|
+
|
|
9
|
+
from stackformers.attention.config import DistanceBiasConfig
|
|
10
|
+
from stackformers.sequence import PaddedInput
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RelativeDistanceBias(nn.Module):
|
|
14
|
+
"""Additive attention bias read off the Euclidean distance between node positions.
|
|
15
|
+
|
|
16
|
+
Only ‖p_i − p_j‖ enters the logit, so attention is invariant to any global translation
|
|
17
|
+
or rotation of the node set: the encoding privileges no axis and no origin. Each head
|
|
18
|
+
mixes a shared bank of Gaussian radial-basis shells into its own profile over distance,
|
|
19
|
+
so some heads can go local and others long-range.
|
|
20
|
+
|
|
21
|
+
A rotary encoding cannot express this. RoPE rotates by ω·p, which is linear in position
|
|
22
|
+
by construction — that linearity is exactly what makes the query and key rotations cancel
|
|
23
|
+
into a relative offset. Distance is not linear in position, so it can only enter as a bias.
|
|
24
|
+
|
|
25
|
+
Costs one (b, h, n, s) tensor, which rules out varlen_attn (it has no bias slot) and forces
|
|
26
|
+
the padded SDPA path. Affordable for node counts in the low thousands; beyond that the
|
|
27
|
+
(b, n, s, num_rbf) shell intermediate, not the bias, is what dominates activation memory.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: DistanceBiasConfig) -> None:
|
|
31
|
+
super().__init__()
|
|
32
|
+
centres = torch.linspace(0.0, config.r_max, config.num_rbf)
|
|
33
|
+
self.register_buffer("centres", centres, persistent=False)
|
|
34
|
+
self.width = config.r_max / (config.num_rbf - 1) # neighbouring shells cross at 1/e
|
|
35
|
+
self.to_bias = nn.Linear(config.num_rbf, config.heads, bias=False)
|
|
36
|
+
nn.init.zeros_(self.to_bias.weight) # start at content-only attention, learn the profile
|
|
37
|
+
|
|
38
|
+
def _shell_weights(self, dist: Float[Tensor, "b n s"]) -> Float[Tensor, "b n s k"]:
|
|
39
|
+
centres: Tensor = self.centres # type: ignore[assignment]
|
|
40
|
+
return torch.exp(-(((dist.unsqueeze(-1) - centres) / self.width) ** 2))
|
|
41
|
+
|
|
42
|
+
def forward(self, input: PaddedInput) -> Float[Tensor, "b h n s"]:
|
|
43
|
+
"""float32 distances keep half-precision coordinates from collapsing nearby nodes."""
|
|
44
|
+
pos = input.abs_positions.float()
|
|
45
|
+
delta = pos.unsqueeze(2) - pos.unsqueeze(1) # b n s c
|
|
46
|
+
dist = torch.linalg.vector_norm(delta, dim=-1) # b n s
|
|
47
|
+
shells = self._shell_weights(dist).to(self.to_bias.weight.dtype)
|
|
48
|
+
bias = rearrange(self.to_bias(shells), "b n s h -> b h n s")
|
|
49
|
+
return bias.to(input.x.dtype)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from stackformers.attention.bias import NoAttnBias
|
|
4
|
+
from stackformers.attention.config import (
|
|
5
|
+
AttnBiasConfig,
|
|
6
|
+
DistanceBiasConfig,
|
|
7
|
+
NoAttnBiasConfig,
|
|
8
|
+
)
|
|
9
|
+
from stackformers.attention.distance_bias import RelativeDistanceBias
|
|
10
|
+
from stackformers.attention.protocols import AttnBias
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_attn_bias(config: AttnBiasConfig) -> AttnBias:
|
|
14
|
+
match config:
|
|
15
|
+
case NoAttnBiasConfig():
|
|
16
|
+
return NoAttnBias()
|
|
17
|
+
case DistanceBiasConfig():
|
|
18
|
+
return RelativeDistanceBias(config)
|
|
19
|
+
case _:
|
|
20
|
+
raise AssertionError(f"Unhandled attn bias config: {type(config)}")
|
|
@@ -14,8 +14,16 @@ def padding_mask(mask: Tensor, dtype: torch.dtype) -> Tensor:
|
|
|
14
14
|
return bias.view(mask.shape[0], 1, 1, mask.shape[1])
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def window_mask(
|
|
18
|
-
|
|
17
|
+
def window_mask(
|
|
18
|
+
n: int, s: int, window_size: int, causal: bool, device: torch.device, dtype: torch.dtype
|
|
19
|
+
) -> Tensor:
|
|
20
|
+
"""Additive sliding-window mask (0 = attend, -inf = ignore): (1, 1, n, s).
|
|
21
|
+
|
|
22
|
+
``dtype`` must be the query's. CUDA's SDPA rejects a mask whose dtype differs from the
|
|
23
|
+
query ("invalid dtype for bias"), and a float32 mask added to a float16 padding mask
|
|
24
|
+
promotes the sum to float32 — which broke every causal and windowed call in half
|
|
25
|
+
precision on GPU, while passing on CPU, whose math backend tolerates the mismatch.
|
|
26
|
+
"""
|
|
19
27
|
q_pos = torch.arange(n, device=device).unsqueeze(1)
|
|
20
28
|
k_pos = torch.arange(s, device=device).unsqueeze(0)
|
|
21
29
|
if causal:
|
|
@@ -23,7 +31,7 @@ def window_mask(n: int, s: int, window_size: int, causal: bool, device: torch.de
|
|
|
23
31
|
else:
|
|
24
32
|
half = window_size // 2
|
|
25
33
|
allowed = (k_pos >= q_pos - half) & (k_pos <= q_pos + half)
|
|
26
|
-
mask = torch.zeros(1, 1, n, s, dtype=
|
|
34
|
+
mask = torch.zeros(1, 1, n, s, dtype=dtype, device=device)
|
|
27
35
|
mask.masked_fill_(~allowed.unsqueeze(0).unsqueeze(0), float("-inf"))
|
|
28
36
|
return mask
|
|
29
37
|
|
|
@@ -44,11 +52,11 @@ def padded_sdpa(
|
|
|
44
52
|
attn_mask = attn_mask + bias
|
|
45
53
|
if window_size is None:
|
|
46
54
|
if causal:
|
|
47
|
-
attn_mask = attn_mask + window_mask(n, s, s,
|
|
55
|
+
attn_mask = attn_mask + window_mask(n, s, s, True, q.device, q.dtype)
|
|
48
56
|
# is_causal=False: causal constraint already encoded in attn_mask above
|
|
49
57
|
return F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, is_causal=False)
|
|
50
58
|
else:
|
|
51
|
-
win_mask = window_mask(n, s, window_size, causal, q.device)
|
|
59
|
+
win_mask = window_mask(n, s, window_size, causal, q.device, q.dtype)
|
|
52
60
|
return F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask + win_mask)
|
|
53
61
|
|
|
54
62
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# positional
|
|
2
|
+
|
|
3
|
+
Positional encodings applied to Q and K tensors inside attention — not to the residual stream.
|
|
4
|
+
|
|
5
|
+
`RotaryEmbedding1D` is the standard choice. It supports YaRN context extension via `RoPE1DConfig(yarn=YaRNConfig(...))`. `RotaryEmbedding2D` takes two coordinates per token (a grid's row/col, or a point's x/y — the two axes are treated symmetrically, so the order is yours to pick as long as it is consistent) and splits the head dimension between them. `NoPosEncoding` is a null object for cross-attention paths that need no positional information.
|
|
6
|
+
|
|
7
|
+
## `base` is for grids. `RotaryEmbeddingND` is for continuous coordinates.
|
|
8
|
+
|
|
9
|
+
RoPE's shortest wavelength is fixed at 2π coordinate units, and `base` only stretches the long end of the band range. So `base` lands the ladder correctly only when tokens sit *one unit apart* — true for text, true for a patch grid, meaningless for scattered continuous coordinates. Positions normalised to `[0, 1]` leave every band longer than the whole domain, so the encoding barely rotates and carries almost no signal; and `base=10000` on a 14×14 patch grid leaves 14 of 16 bands frozen.
|
|
10
|
+
|
|
11
|
+
`RotaryEmbeddingND` (`RoPENDConfig`) drops `base` entirely and builds the ladder from the two numbers that actually mean something, in any number of dimensions:
|
|
12
|
+
|
|
13
|
+
- `r_min` — the finest separation the model must resolve. Sets the shortest wavelength to `2·r_min`, the Nyquist limit. Measure it as a low percentile of the nearest-neighbour distance.
|
|
14
|
+
- `r_max` — the domain diameter. Sets the longest wavelength (times `headroom`, so the slowest band stays monotone rather than wrapping). Measure it as a high percentile of the pairwise distance distribution.
|
|
15
|
+
|
|
16
|
+
The resulting ladder depends only on the *dynamic range* `r_max / r_min`, so metres, pixels and millimetres all give the identical encoding — the property `base` never had. `RoPE1DConfig` and `RoPE2DConfig` keep `base` and are unchanged; use them for text and grids.
|
|
17
|
+
|
|
18
|
+
**Centre your coordinates.** Not for invariance — RoPE is translation-invariant exactly, since the query and key rotations cancel into `ω·(pᵢ − pⱼ)`. It is for float32: the angle is `ω·p`, and coordinates far from the origin push it into the hundreds of radians and spend the mantissa before the cosine is taken. A 1e5 offset costs three orders of magnitude of accuracy. This cannot be done inside the module — cross-attention must subtract the *same* constant from query and key positions, and the module sees them separately — so subtract the centroid at the input boundary.
|
|
19
|
+
|
|
20
|
+
**RoPE encodes direction, not just distance.** It is not rotation invariant, by design: it gives the model the relative *offset*, which is strictly more information than the distance. Where the global frame is arbitrary, train with rotation augmentation. Buying exact rotation invariance in the architecture costs either an O(n²) attention bias (see `attention/RelativeDistanceBias`, which OOMs past a few thousand nodes) or a canonicalisation of the input frame, which is inherently discontinuous — a near-isotropic point set flips frames under a 1% perturbation.
|
|
21
|
+
|
|
22
|
+
The `PosEncoding` protocol exposes two methods — `forward_padded` and `forward_packed` — so callers (which already know their layout) pick the right path directly without internal dispatch. All implementations share the same computation via layout-agnostic helpers, since positional encoding is a per-token operation that does not depend on sequence boundaries.
|
|
23
|
+
|
|
24
|
+
## Adding a new encoding
|
|
25
|
+
|
|
26
|
+
1. Add a config class with a `kind: Literal[...]` discriminator to `config.py` and include it in the `PosEncodingConfig` union.
|
|
27
|
+
2. Implement the class satisfying `PosEncoding` (and optionally `PackedPosEncoding`) structurally.
|
|
28
|
+
3. Add a `case` branch in `factory.py::build_pos_encoding`.
|