stackformers 3.9.1__tar.gz → 3.9.2__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.2/CHANGELOG.md +219 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/PKG-INFO +1 -1
- {stackformers-3.9.1 → stackformers-3.9.2}/pyproject.toml +1 -1
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/positional/rope2d.py +15 -5
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/positional/test_rope.py +163 -8
- {stackformers-3.9.1 → stackformers-3.9.2}/uv.lock +1 -1
- {stackformers-3.9.1 → stackformers-3.9.2}/.claudeignore +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/.gitignore +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/.python-version +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/.vscode/settings.json +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/CLAUDE.md +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/Justfile +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/LICENSE +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/PLAN.md +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/README.md +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/attention/README.md +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/attention/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/attention/bias.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/attention/config.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/attention/cross_attn.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/attention/ops.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/attention/protocols.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/attention/self_attn.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/attention/varlen_backend.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/config.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/cross_attender.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/decoder.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/encoder.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/feedforward/README.md +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/feedforward/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/feedforward/config.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/feedforward/factory.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/feedforward/geglu.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/feedforward/protocols.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/feedforward/relu_squared.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/feedforward/swiglu.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/layers.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/norm/README.md +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/norm/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/norm/config.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/norm/factory.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/norm/protocols.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/positional/README.md +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/positional/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/positional/config.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/positional/factory.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/positional/learned.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/positional/none.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/positional/protocols.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/positional/rope1d.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/presets/README.md +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/presets/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/presets/cross_attender.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/presets/decoder.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/presets/encoder.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/presets/pyramid_vision.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/sequence.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/README.md +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/config.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/factory.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/input.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/kv_reduction.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/layer.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/patch_merging.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/protocols.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/spatial_reduction.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/stackformers/spatial/window.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/attention/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/attention/test_cross_attn.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/attention/test_kernels.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/attention/test_ops.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/attention/test_self_attn.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/attention/test_varlen_backend.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/conftest.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/feedforward/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/feedforward/test_geglu.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/feedforward/test_relu_squared.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/feedforward/test_swiglu.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/positional/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/positional/test_learned_pos.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/presets/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/presets/test_cross_attender.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/presets/test_decoder.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/presets/test_encoder.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/presets/test_pyramid_vision.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/spatial/__init__.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/spatial/test_input.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/spatial/test_kv_reduction.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/spatial/test_layer.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/spatial/test_patch_merging.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/spatial/test_spatial_reduction.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/spatial/test_window.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/test_cross_attender.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/test_decoder.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/test_encoder.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/test_layers.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/test_norm.py +0 -0
- {stackformers-3.9.1 → stackformers-3.9.2}/tests/test_sequence.py +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
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
|
+
## [3.9.2] — 2026-07-14
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`RotaryEmbedding2D` was not computing a rotation.** The frequency vector was laid out as
|
|
14
|
+
`[row | row | col | col]`, but `_rotate_half` pairs channel `i` with channel `i + dh/2`, so a
|
|
15
|
+
row angle was paired against a column angle. The resulting 2×2 map had
|
|
16
|
+
`det = cos((row − col) · ωᵢ)` and singular values `√(1 ± |sin D|)` — a squeeze, not a rotation.
|
|
17
|
+
It was therefore non-orthogonal, annihilated high-frequency channels wherever the determinant
|
|
18
|
+
reached zero, and produced attention scores that leaked absolute position instead of depending
|
|
19
|
+
only on relative offset. Frequencies are now built as `[row | col | row | col]`.
|
|
20
|
+
- `RotaryEmbedding2D` now casts positions and `inv_freq` to float32 for the outer product,
|
|
21
|
+
matching `RotaryEmbedding1D`. A `.half()`-ed module previously lost position precision.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- **Numerics of `RotaryEmbedding2D` change.** Any checkpoint trained against the previous
|
|
26
|
+
encoding is tuned to the broken position code and requires retraining or finetuning.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Math tests for `RotaryEmbedding2D` on a genuine row-major grid: a definition-first reference
|
|
31
|
+
oracle, orthogonality (norms and inner products), relative-offset invariance, per-axis
|
|
32
|
+
sensitivity, distinct-offset separation, and packed/padded agreement. The previous test used a
|
|
33
|
+
`row == col` position grid — the main diagonal, the one locus where the row and column angles
|
|
34
|
+
coincide and the broken layout is indistinguishable from a correct one.
|
|
35
|
+
|
|
36
|
+
## [3.9.1] — 2026-06-30
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- Make the experimental `varlen_attn` import and call robust across PyTorch versions.
|
|
41
|
+
|
|
42
|
+
## [3.9.0] — 2026-06-17
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- 2-D grid attention (`spatial`) and the pyramid vision backbone preset.
|
|
47
|
+
|
|
48
|
+
## [3.8.2] — 2026-05-29
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- Force absolute positions to float32 in `sequence`.
|
|
53
|
+
|
|
54
|
+
## [3.8.1] — 2026-05-29
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- Keep batch size as a `SymInt` in the packed attention fallback, preserving `torch.export`
|
|
59
|
+
compatibility.
|
|
60
|
+
|
|
61
|
+
## [3.8.0] — 2026-05-27
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- Replace `.tolist()` loops in `sequence` and `ops` with tensor operations, making them
|
|
66
|
+
`torch.export`-compatible.
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- PyPI publish metadata in `pyproject.toml`.
|
|
71
|
+
|
|
72
|
+
## [3.7.0] — 2026-05-21
|
|
73
|
+
|
|
74
|
+
### Changed
|
|
75
|
+
|
|
76
|
+
- Introduce abstract generic base classes for all presets.
|
|
77
|
+
|
|
78
|
+
## [3.6.0] — 2026-05-21
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
|
|
82
|
+
- `AttnBias` protocol and the `NoAttnBias` null object on `SelfAttention`.
|
|
83
|
+
|
|
84
|
+
## [3.5.2] — 2026-05-21
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- Warn and fall back to SDPA when `varlen_attn` raises.
|
|
89
|
+
|
|
90
|
+
## [3.5.1] — 2026-05-18
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- Move attention dropout to the output projection.
|
|
95
|
+
|
|
96
|
+
## [3.5.0] — 2026-05-18
|
|
97
|
+
|
|
98
|
+
### Added
|
|
99
|
+
|
|
100
|
+
- Packed→padded SDPA fallback; extract `ops.py`.
|
|
101
|
+
|
|
102
|
+
## [3.4.2] — 2026-05-18
|
|
103
|
+
|
|
104
|
+
### Added
|
|
105
|
+
|
|
106
|
+
- `padded_to_packed` and `packed_to_padded` helpers.
|
|
107
|
+
|
|
108
|
+
## [3.4.1] — 2026-05-18
|
|
109
|
+
|
|
110
|
+
### Removed
|
|
111
|
+
|
|
112
|
+
- Packed-attention CPU fallback; warn on dropout instead.
|
|
113
|
+
|
|
114
|
+
## [3.4.0] — 2026-05-17
|
|
115
|
+
|
|
116
|
+
### Added
|
|
117
|
+
|
|
118
|
+
- ReLU² feed-forward network.
|
|
119
|
+
|
|
120
|
+
## [3.3.0] — 2026-05-17
|
|
121
|
+
|
|
122
|
+
### Added
|
|
123
|
+
|
|
124
|
+
- QK-norm in attention; all config fields documented.
|
|
125
|
+
|
|
126
|
+
## [3.2.0] — 2026-05-17
|
|
127
|
+
|
|
128
|
+
### Changed
|
|
129
|
+
|
|
130
|
+
- Eliminate layout-specific duplicates in positional encoding implementations.
|
|
131
|
+
|
|
132
|
+
## [3.1.0] — 2026-05-17
|
|
133
|
+
|
|
134
|
+
### Changed
|
|
135
|
+
|
|
136
|
+
- Replace `forward()` on positional encodings with `forward_padded` / `forward_packed`, so
|
|
137
|
+
callers pick the path for the layout they already know.
|
|
138
|
+
|
|
139
|
+
## [3.0.1] — 2026-05-17
|
|
140
|
+
|
|
141
|
+
### Fixed
|
|
142
|
+
|
|
143
|
+
- Use the GELU tanh approximation in GEGLU.
|
|
144
|
+
|
|
145
|
+
## [3.0.0] — 2026-05-16
|
|
146
|
+
|
|
147
|
+
### Changed
|
|
148
|
+
|
|
149
|
+
- **Breaking:** remove the kernel abstraction; padded and packed dispatch are unified.
|
|
150
|
+
|
|
151
|
+
## [2.0.2] — 2026-05-16
|
|
152
|
+
|
|
153
|
+
### Removed
|
|
154
|
+
|
|
155
|
+
- The unused local-attention package.
|
|
156
|
+
|
|
157
|
+
## [2.0.1] — 2026-05-16
|
|
158
|
+
|
|
159
|
+
### Changed
|
|
160
|
+
|
|
161
|
+
- Store feed-forward activations as `nn.Module` attributes.
|
|
162
|
+
|
|
163
|
+
## [2.0.0] — 2026-05-16
|
|
164
|
+
|
|
165
|
+
### Removed
|
|
166
|
+
|
|
167
|
+
- **Breaking:** the `attn_bias` / ALiBi system.
|
|
168
|
+
|
|
169
|
+
## [1.1.0] — 2026-05-16
|
|
170
|
+
|
|
171
|
+
### Added
|
|
172
|
+
|
|
173
|
+
- `LearnedPosEncoding` with absolute position embeddings.
|
|
174
|
+
|
|
175
|
+
## [1.0.1] — 2026-05-16
|
|
176
|
+
|
|
177
|
+
### Added
|
|
178
|
+
|
|
179
|
+
- `LayerNorm` wrapper.
|
|
180
|
+
|
|
181
|
+
## [1.0.0] — 2026-05-16
|
|
182
|
+
|
|
183
|
+
### Added
|
|
184
|
+
|
|
185
|
+
- GEGLU feed-forward variant.
|
|
186
|
+
|
|
187
|
+
### Changed
|
|
188
|
+
|
|
189
|
+
- **Breaking:** feed-forward config becomes a discriminated union.
|
|
190
|
+
|
|
191
|
+
## [0.5.0] — 2026-05-16
|
|
192
|
+
|
|
193
|
+
### Added
|
|
194
|
+
|
|
195
|
+
- Unfold mode for the windowed SDPA kernel.
|
|
196
|
+
|
|
197
|
+
## [0.4.0] — 2026-05-15
|
|
198
|
+
|
|
199
|
+
### Added
|
|
200
|
+
|
|
201
|
+
- `RotaryEmbedding2D` and the packed cross-attender.
|
|
202
|
+
|
|
203
|
+
## [0.3.0] — 2026-05-14
|
|
204
|
+
|
|
205
|
+
### Changed
|
|
206
|
+
|
|
207
|
+
- Establish the semantic versioning convention.
|
|
208
|
+
|
|
209
|
+
## [0.2.0] — 2026-05-13
|
|
210
|
+
|
|
211
|
+
### Changed
|
|
212
|
+
|
|
213
|
+
- Remove the `v1` namespace; add the `presets` package.
|
|
214
|
+
|
|
215
|
+
## [0.1.0] — 2026-05-13
|
|
216
|
+
|
|
217
|
+
### Added
|
|
218
|
+
|
|
219
|
+
- Initial library scaffold.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackformers
|
|
3
|
-
Version: 3.9.
|
|
3
|
+
Version: 3.9.2
|
|
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
|
|
@@ -25,12 +25,22 @@ class RotaryEmbedding2D(nn.Module):
|
|
|
25
25
|
|
|
26
26
|
@torch.no_grad()
|
|
27
27
|
def _freqs_from_positions(self, positions: Tensor) -> Tensor:
|
|
28
|
-
"""positions: (..., 2) → freqs: (..., dh)
|
|
28
|
+
"""positions: (..., 2) → freqs: (..., dh), laid out as [row | col | row | col].
|
|
29
|
+
|
|
30
|
+
_rotate_half pairs channel i with channel i + dh/2, so both members of a pair must
|
|
31
|
+
carry the same angle for the map to be a rotation. Duplicating the *concatenated*
|
|
32
|
+
half-vector satisfies that; concatenating the *duplicated* row and col blocks
|
|
33
|
+
([row | row | col | col]) would pair a row angle against a col angle and yield a
|
|
34
|
+
squeeze instead — non-orthogonal, and not a function of relative position.
|
|
35
|
+
|
|
36
|
+
float32 cast ensures half-precision inputs don't lose precision in the outer product.
|
|
37
|
+
"""
|
|
29
38
|
inv: Tensor = self.inv_freq # type: ignore[assignment]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
pos = positions.to(dtype=torch.float32)
|
|
40
|
+
row = pos[..., 0].unsqueeze(-1) * inv.float() # (..., dh//4)
|
|
41
|
+
col = pos[..., 1].unsqueeze(-1) * inv.float() # (..., dh//4)
|
|
42
|
+
half = torch.cat([row, col], dim=-1) # (..., dh//2)
|
|
43
|
+
return torch.cat([half, half], dim=-1) # (..., dh)
|
|
34
44
|
|
|
35
45
|
def _encode(
|
|
36
46
|
self, q: Tensor, k: Tensor, q_positions: Tensor, k_positions: Tensor
|
|
@@ -125,21 +125,176 @@ def test_rope1d_preserves_norms(
|
|
|
125
125
|
assert torch.allclose(k.norm(dim=-1), k_out.norm(dim=-1), atol=tol)
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
# --- RoPE-2D ---
|
|
129
|
+
#
|
|
130
|
+
# A row-major GH x GW grid, never row == col. On the diagonal (row == col) the row and column
|
|
131
|
+
# angles coincide, which makes a broken frequency layout indistinguishable from a correct one.
|
|
132
|
+
|
|
133
|
+
GH, GW = 3, 5
|
|
134
|
+
GN = GH * GW
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _grid_positions(b: int, device: torch.device, dtype: torch.dtype) -> torch.Tensor:
|
|
138
|
+
"""Row-major (row, col) positions over a GH x GW grid, as (b, GN, 2)."""
|
|
139
|
+
rows = torch.arange(GH, device=device, dtype=dtype).repeat_interleave(GW)
|
|
140
|
+
cols = torch.arange(GW, device=device, dtype=dtype).repeat(GH)
|
|
141
|
+
return torch.stack([rows, cols], dim=-1).unsqueeze(0).expand(b, -1, -1).clone()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _reference_rope2d(t: torch.Tensor, positions: torch.Tensor, base: int = 10_000) -> torch.Tensor:
|
|
145
|
+
"""Axial 2-D RoPE written directly from the definition, independent of the implementation.
|
|
146
|
+
|
|
147
|
+
Rotates channel pair (i, i + dh/2) by row * w_i for the first dh/4 pairs and by col * w_i
|
|
148
|
+
for the rest. t: (b, h, n, dh), positions: (b, n, 2).
|
|
149
|
+
"""
|
|
150
|
+
dh = t.shape[-1]
|
|
151
|
+
half, quarter = dh // 2, dh // 4
|
|
152
|
+
inv = 1.0 / (base ** (torch.arange(0, half, 2, dtype=torch.float32) / half))
|
|
153
|
+
inv = inv.to(device=t.device)
|
|
154
|
+
|
|
155
|
+
out = torch.zeros_like(t, dtype=torch.float32)
|
|
156
|
+
src = t.float()
|
|
157
|
+
for i in range(half):
|
|
158
|
+
axis = 0 if i < quarter else 1
|
|
159
|
+
freq = inv[i if i < quarter else i - quarter]
|
|
160
|
+
angle = positions[..., axis].float() * freq # (b, n)
|
|
161
|
+
cos, sin = angle.cos().unsqueeze(1), angle.sin().unsqueeze(1) # (b, 1, n)
|
|
162
|
+
x, y = src[..., i], src[..., i + half]
|
|
163
|
+
out[..., i] = x * cos - y * sin
|
|
164
|
+
out[..., i + half] = x * sin + y * cos
|
|
165
|
+
return out.to(t.dtype)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@pytest.fixture
|
|
169
|
+
def grid_qk(device_dtype: tuple[torch.device, torch.dtype]) -> tuple[torch.Tensor, torch.Tensor]:
|
|
170
|
+
device, dtype = device_dtype
|
|
171
|
+
return (
|
|
172
|
+
torch.randn(B, H, GN, DH, device=device, dtype=dtype),
|
|
173
|
+
torch.randn(B, H, GN, DH, device=device, dtype=dtype),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.fixture
|
|
178
|
+
def grid_pos(device_dtype: tuple[torch.device, torch.dtype]) -> torch.Tensor:
|
|
179
|
+
device, dtype = device_dtype
|
|
180
|
+
return _grid_positions(B, device, dtype)
|
|
181
|
+
|
|
182
|
+
|
|
128
183
|
def test_rope2d_output_shape(
|
|
129
184
|
rope2d: RotaryEmbedding2D,
|
|
130
|
-
|
|
185
|
+
grid_qk: tuple[torch.Tensor, torch.Tensor],
|
|
186
|
+
grid_pos: torch.Tensor,
|
|
131
187
|
) -> None:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
k = torch.randn(B, H, N, DH, device=device, dtype=dtype)
|
|
135
|
-
row_ids = torch.arange(N, dtype=dtype, device=device)
|
|
136
|
-
col_ids = torch.arange(N, dtype=dtype, device=device)
|
|
137
|
-
pos = torch.stack([row_ids, col_ids], dim=-1).unsqueeze(0).expand(B, -1, -1) # b n 2
|
|
138
|
-
q_out, k_out = rope2d.forward_padded(q, k, pos, pos)
|
|
188
|
+
q, k = grid_qk
|
|
189
|
+
q_out, k_out = rope2d.forward_padded(q, k, grid_pos, grid_pos)
|
|
139
190
|
assert q_out.shape == q.shape
|
|
140
191
|
assert k_out.shape == k.shape
|
|
141
192
|
|
|
142
193
|
|
|
194
|
+
def test_rope2d_matches_reference_rotation(
|
|
195
|
+
rope2d: RotaryEmbedding2D,
|
|
196
|
+
grid_qk: tuple[torch.Tensor, torch.Tensor],
|
|
197
|
+
grid_pos: torch.Tensor,
|
|
198
|
+
device_dtype: tuple[torch.device, torch.dtype],
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Pins the exact rotation against a definition-first oracle, not just its properties."""
|
|
201
|
+
_, dtype = device_dtype
|
|
202
|
+
q, k = grid_qk
|
|
203
|
+
q_out, k_out = rope2d.forward_padded(q, k, grid_pos, grid_pos)
|
|
204
|
+
assert torch.allclose(q_out, _reference_rope2d(q, grid_pos), atol=atol(dtype))
|
|
205
|
+
assert torch.allclose(k_out, _reference_rope2d(k, grid_pos), atol=atol(dtype))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_rope2d_is_orthogonal(
|
|
209
|
+
rope2d: RotaryEmbedding2D,
|
|
210
|
+
grid_qk: tuple[torch.Tensor, torch.Tensor],
|
|
211
|
+
grid_pos: torch.Tensor,
|
|
212
|
+
device_dtype: tuple[torch.device, torch.dtype],
|
|
213
|
+
) -> None:
|
|
214
|
+
"""A rotation preserves norms and inner products; a squeeze or reflection does not."""
|
|
215
|
+
_, dtype = device_dtype
|
|
216
|
+
q, k = grid_qk
|
|
217
|
+
q_out, k_out = rope2d.forward_padded(q, k, grid_pos, grid_pos)
|
|
218
|
+
tol = atol(dtype)
|
|
219
|
+
assert torch.allclose(q.norm(dim=-1), q_out.norm(dim=-1), atol=tol)
|
|
220
|
+
assert torch.allclose(k.norm(dim=-1), k_out.norm(dim=-1), atol=tol)
|
|
221
|
+
# Same position for q and k, so the shared rotation must cancel in the inner product.
|
|
222
|
+
before = (q.float() * k.float()).sum(dim=-1)
|
|
223
|
+
after = (q_out.float() * k_out.float()).sum(dim=-1)
|
|
224
|
+
assert torch.allclose(before, after, atol=tol * DH)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@pytest.mark.parametrize("offset", [(0, 1), (1, 0), (2, 3), (-1, 2)], ids=str)
|
|
228
|
+
def test_rope2d_score_depends_only_on_relative_offset(
|
|
229
|
+
device: torch.device,
|
|
230
|
+
offset: tuple[int, int],
|
|
231
|
+
) -> None:
|
|
232
|
+
"""The defining property: sliding a query/key pair across the grid must not move the score."""
|
|
233
|
+
rope = RotaryEmbedding2D(RoPE2DConfig(dim_head=DH)).to(device=device)
|
|
234
|
+
q = torch.randn(1, H, 1, DH, device=device)
|
|
235
|
+
k = torch.randn(1, H, 1, DH, device=device)
|
|
236
|
+
d_row, d_col = offset
|
|
237
|
+
|
|
238
|
+
def score(row: int, col: int) -> torch.Tensor:
|
|
239
|
+
q_pos = torch.tensor([[[float(row), float(col)]]], device=device)
|
|
240
|
+
k_pos = torch.tensor([[[float(row + d_row), float(col + d_col)]]], device=device)
|
|
241
|
+
q_out, k_out = rope.forward_padded(q, k, q_pos, k_pos)
|
|
242
|
+
return (q_out @ k_out.transpose(-1, -2))[0, :, 0, 0]
|
|
243
|
+
|
|
244
|
+
anchor = score(0, 0)
|
|
245
|
+
for row, col in [(2, 3), (5, 9), (11, 4), (7, 7)]:
|
|
246
|
+
assert torch.allclose(anchor, score(row, col), atol=atol(torch.float32) * 10)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def test_rope2d_distinct_offsets_give_distinct_scores(device: torch.device) -> None:
|
|
250
|
+
"""Guards the degenerate fix where every offset collapses to the same score."""
|
|
251
|
+
rope = RotaryEmbedding2D(RoPE2DConfig(dim_head=DH)).to(device=device)
|
|
252
|
+
q = torch.randn(1, H, 1, DH, device=device)
|
|
253
|
+
k = torch.randn(1, H, 1, DH, device=device)
|
|
254
|
+
|
|
255
|
+
def score(k_row: float, k_col: float) -> torch.Tensor:
|
|
256
|
+
q_pos = torch.zeros(1, 1, 2, device=device)
|
|
257
|
+
k_pos = torch.tensor([[[k_row, k_col]]], device=device)
|
|
258
|
+
q_out, k_out = rope.forward_padded(q, k, q_pos, k_pos)
|
|
259
|
+
return (q_out @ k_out.transpose(-1, -2))[0, :, 0, 0]
|
|
260
|
+
|
|
261
|
+
# A row-offset and the matching col-offset must not be conflated: the axes are separate.
|
|
262
|
+
assert not torch.allclose(score(2.0, 0.0), score(0.0, 2.0))
|
|
263
|
+
assert not torch.allclose(score(0.0, 0.0), score(1.0, 1.0))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@pytest.mark.parametrize("axis", [0, 1], ids=["row", "col"])
|
|
267
|
+
def test_rope2d_each_axis_changes_the_encoding(
|
|
268
|
+
rope2d: RotaryEmbedding2D,
|
|
269
|
+
device_dtype: tuple[torch.device, torch.dtype],
|
|
270
|
+
axis: int,
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Moving along either axis alone must change the output — neither axis may be ignored."""
|
|
273
|
+
device, dtype = device_dtype
|
|
274
|
+
q = torch.randn(1, H, 1, DH, device=device, dtype=dtype)
|
|
275
|
+
base_pos = torch.full((1, 1, 2), 2.0, device=device, dtype=dtype)
|
|
276
|
+
moved_pos = base_pos.clone()
|
|
277
|
+
moved_pos[..., axis] += 3.0
|
|
278
|
+
base_out, _ = rope2d.forward_padded(q, q, base_pos, base_pos)
|
|
279
|
+
moved_out, _ = rope2d.forward_padded(q, q, moved_pos, moved_pos)
|
|
280
|
+
assert not torch.allclose(base_out, moved_out, atol=atol(dtype))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_rope2d_packed_matches_padded(
|
|
284
|
+
rope2d: RotaryEmbedding2D,
|
|
285
|
+
device_dtype: tuple[torch.device, torch.dtype],
|
|
286
|
+
) -> None:
|
|
287
|
+
"""Both layouts are the same per-token operation and must agree token for token."""
|
|
288
|
+
device, dtype = device_dtype
|
|
289
|
+
q = torch.randn(1, H, GN, DH, device=device, dtype=dtype)
|
|
290
|
+
pos = _grid_positions(1, device, dtype)
|
|
291
|
+
padded, _ = rope2d.forward_padded(q, q, pos, pos)
|
|
292
|
+
|
|
293
|
+
packed_q = q[0].transpose(0, 1).contiguous() # (nt, h, dh)
|
|
294
|
+
packed_out, _ = rope2d.forward_packed(packed_q, packed_q, pos[0], pos[0])
|
|
295
|
+
assert torch.allclose(packed_out, padded[0].transpose(0, 1), atol=atol(dtype))
|
|
296
|
+
|
|
297
|
+
|
|
143
298
|
# --- YaRN scaling ---
|
|
144
299
|
|
|
145
300
|
_YARN = YaRNConfig(scale=4.0, original_max_seq_len=512)
|
|
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
|
|
File without changes
|
|
File without changes
|