stackformers 4.3.0__tar.gz → 4.3.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-4.3.0 → stackformers-4.3.1}/CHANGELOG.md +21 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/PKG-INFO +1 -1
- {stackformers-4.3.0 → stackformers-4.3.1}/pyproject.toml +1 -1
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/mlm/README.md +1 -1
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/mlm/wrapper.py +13 -4
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/mlm/test_wrapper.py +29 -8
- {stackformers-4.3.0 → stackformers-4.3.1}/uv.lock +1 -1
- {stackformers-4.3.0 → stackformers-4.3.1}/.claudeignore +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/.gitignore +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/.python-version +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/.vscode/settings.json +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/CLAUDE.md +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/Justfile +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/LICENSE +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/PLAN.md +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/README.md +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/run-remote-tests.sh +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/README.md +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/bias.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/config.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/cross_attn.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/distance_bias.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/factory.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/ops.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/protocols.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/self_attn.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/attention/varlen_backend.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/config.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/cross_attender.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/decoder.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/encoder.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/feedforward/README.md +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/feedforward/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/feedforward/config.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/feedforward/factory.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/feedforward/geglu.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/feedforward/protocols.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/feedforward/relu_squared.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/feedforward/swiglu.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/layers.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/mlm/config.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/mlm/head.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/mlm/masking.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/mlm/protocols.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/norm/README.md +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/norm/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/norm/config.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/norm/factory.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/norm/protocols.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/positional/README.md +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/positional/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/positional/config.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/positional/factory.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/positional/learned.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/positional/none.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/positional/protocols.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/positional/rope1d.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/positional/rope2d.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/positional/rope_nd.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/presets/README.md +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/presets/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/presets/cross_attender.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/presets/decoder.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/presets/encoder.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/stackformers/sequence.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/attention/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/attention/test_cross_attn.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/attention/test_distance_bias.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/attention/test_kernels.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/attention/test_ops.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/attention/test_self_attn.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/attention/test_varlen_backend.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/conftest.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/feedforward/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/feedforward/test_geglu.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/feedforward/test_relu_squared.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/feedforward/test_swiglu.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/mlm/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/mlm/test_head.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/mlm/test_masking.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/positional/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/positional/test_learned_pos.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/positional/test_rope.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/positional/test_rope_nd.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/presets/__init__.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/presets/test_cross_attender.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/presets/test_decoder.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/presets/test_encoder.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/test_cross_attender.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/test_decoder.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/test_encoder.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/test_layers.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/test_norm.py +0 -0
- {stackformers-4.3.0 → stackformers-4.3.1}/tests/test_sequence.py +0 -0
|
@@ -6,6 +6,27 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
6
6
|
adheres to [Semantic Versioning](https://semver.org/): MAJOR for breaking public API changes,
|
|
7
7
|
MINOR for backwards-compatible features, PATCH for bug fixes and internal changes.
|
|
8
8
|
|
|
9
|
+
## [4.3.1] — 2026-07-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **`MLMWrapper`'s masked pass leaked gradient to whatever produced `input.x`, despite
|
|
14
|
+
the target being detached.** `corrupted_x` was built from the live (undetached)
|
|
15
|
+
`input.x` at unmasked positions; self-attention mixes those into the masked
|
|
16
|
+
positions' predictions, so `mlm_loss` still reached back through every unmasked
|
|
17
|
+
position — confirmed empirically at 432/512 nonzero gradient entries under a
|
|
18
|
+
realistic 15% mask ratio. The existing test only covered 100% masking, which zeroes
|
|
19
|
+
that path as a side effect of `torch.where`'s backward and never exercised the
|
|
20
|
+
realistic partial-masking case.
|
|
21
|
+
|
|
22
|
+
Fixed by detaching `input.x` once, up front
|
|
23
|
+
(`input = input._replace(x=input.x.detach())`), so both `corrupted_x` and `target`
|
|
24
|
+
are built from the same severed copy. `mlm_loss` now trains only `mask_token`,
|
|
25
|
+
`encoder`, and `head` — never whatever produced `input.x` — regardless of mask
|
|
26
|
+
ratio. Added a regression test at the realistic mask ratio
|
|
27
|
+
(`test_wrapper_severs_gradient_to_upstream_input_under_partial_masking`) alongside
|
|
28
|
+
the existing full-masking one.
|
|
29
|
+
|
|
9
30
|
## [4.3.0] — 2026-07-17
|
|
10
31
|
|
|
11
32
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackformers
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.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
|
|
@@ -41,7 +41,7 @@ One call site covers both modes: in training, `mlm_wrapper` runs the encoder twi
|
|
|
41
41
|
|
|
42
42
|
**The mask token is owned state, not an injected collaborator.** It's the wrapper's own learned parameter, analogous to a `nn.Linear`'s weight — not swappable behavior, so it isn't a `Protocol`.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
**`input.x` is detached once, before it is used for anything — not only as the target.** The masked pass replaces `input` with `input._replace(x=input.x.detach())` up front, so both the unmasked context fed to the encoder and the reconstruction target come from that same detached copy. Detaching only the target is not sufficient: self-attention still mixes the live, undetached unmasked positions into the masked positions' predictions, so a gradient path back to whatever produced `input.x` would otherwise survive at every unmasked position — worse, it grows with the fraction of tokens left unmasked. Severing the whole input removes the standard collapse shortcut for self-supervised regression targets (drive every token toward one constant vector, and reconstruction becomes trivial) — not just the degenerate all-masked case, but the realistic partial-masking case a target-only detach would silently miss. See `tests/mlm/test_wrapper.py::test_wrapper_severs_gradient_to_upstream_input_under_partial_masking` for the regression test.
|
|
45
45
|
|
|
46
46
|
**No layout dispatch in the wrapper.** `MaskingStrategy` returns a boolean tensor shaped like `input.x`'s leading dims — `(b, n)` for `PaddedInput`, `(nt,)` for `PackedInput`. `torch.where` and boolean advanced indexing both broadcast/gather correctly against either shape, so `MLMWrapper.forward` never matches on the sequence variant; only `RandomMasking` does, internally.
|
|
47
47
|
|
|
@@ -38,9 +38,13 @@ class MLMWrapper(nn.Module):
|
|
|
38
38
|
invoke this unconditionally in both modes without an if-training branch of their
|
|
39
39
|
own — the same role self.training already plays in nn.Dropout or nn.BatchNorm.
|
|
40
40
|
|
|
41
|
-
The
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
The masked pass detaches input.x once, up front — both the unmasked context fed to
|
|
42
|
+
the encoder and the reconstruction target come from that same detached copy — so
|
|
43
|
+
mlm_loss trains the encoder, mask_token, and head, never whatever produced input.x.
|
|
44
|
+
Detaching only the target is not sufficient: self-attention still mixes the
|
|
45
|
+
unmasked (undetached) positions into the masked positions' predictions, so a live
|
|
46
|
+
path back to input.x would otherwise survive at every unmasked position. Severing
|
|
47
|
+
it entirely removes the representation-collapse shortcut a trainable tokenizer
|
|
44
48
|
would otherwise have available (drive every token toward a constant vector to make
|
|
45
49
|
reconstruction trivial).
|
|
46
50
|
"""
|
|
@@ -60,8 +64,13 @@ class MLMWrapper(nn.Module):
|
|
|
60
64
|
self.head = head if head is not None else RegressionHead(config.dim)
|
|
61
65
|
|
|
62
66
|
def _masked_loss(self, input: SequenceInput, encoder: EncoderLike) -> Tensor:
|
|
67
|
+
# Detached once, up front, and reused for both corrupted_x and target below —
|
|
68
|
+
# unmasked positions must not carry a live path back to whatever produced
|
|
69
|
+
# input.x either, or self-attention mixes them into the masked positions'
|
|
70
|
+
# predictions and mlm_loss reaches upstream anyway despite target being detached.
|
|
71
|
+
input = input._replace(x=input.x.detach())
|
|
72
|
+
target = input.x
|
|
63
73
|
should_mask = self.masking_strategy(input)
|
|
64
|
-
target = input.x.detach()
|
|
65
74
|
# should_mask's shape always matches input.x's leading dims, so this select and
|
|
66
75
|
# the boolean indexing below both work for PaddedInput and PackedInput alike.
|
|
67
76
|
corrupted_x = torch.where(should_mask.unsqueeze(-1), self.mask_token, input.x)
|
|
@@ -129,16 +129,17 @@ def test_wrapper_does_not_own_encoder(device: torch.device) -> None:
|
|
|
129
129
|
assert encoder_param_ids.isdisjoint(wrapper_param_ids)
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
def
|
|
132
|
+
def test_wrapper_severs_gradient_to_upstream_input_under_full_masking(
|
|
133
|
+
device: torch.device,
|
|
134
|
+
) -> None:
|
|
133
135
|
"""Collapse guard (design doc §5): a trainable tokenizer upstream of x must get no
|
|
134
136
|
gradient from this loss, or the optimizer can collapse every token to one constant
|
|
135
137
|
vector to make reconstruction trivial.
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
must be exactly zero.
|
|
139
|
+
With every position masked, x is detached before corrupted_x is built from it, so
|
|
140
|
+
corrupted_x is pure mask_token — independent of x entirely. x never appears in the
|
|
141
|
+
masked pass's graph at all, so x.grad must be exactly None after backward, not just
|
|
142
|
+
zero-valued.
|
|
142
143
|
"""
|
|
143
144
|
config = MLMWrapperConfig(dim=D, mask_ratio=0.5)
|
|
144
145
|
encoder = _build_encoder(device, torch.float32)
|
|
@@ -147,8 +148,28 @@ def test_wrapper_detaches_target_from_upstream_input(device: torch.device) -> No
|
|
|
147
148
|
mask = torch.ones(B, N, dtype=torch.bool, device=device)
|
|
148
149
|
res = wrapper(make_padded_input(x, mask), encoder)
|
|
149
150
|
res.mlm_loss.backward()
|
|
150
|
-
assert x.grad is
|
|
151
|
-
|
|
151
|
+
assert x.grad is None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_wrapper_severs_gradient_to_upstream_input_under_partial_masking(
|
|
155
|
+
device: torch.device,
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Regression test: detaching only the target is not enough to close the collapse
|
|
158
|
+
shortcut. With the default (partial) mask ratio, corrupted_x still holds the live x
|
|
159
|
+
at unmasked positions, and self-attention mixes those into the masked positions'
|
|
160
|
+
predictions — so mlm_loss could still reach x through that path unless x is
|
|
161
|
+
detached before it is used for anything, not only before it is used as the target.
|
|
162
|
+
This is the realistic training case; test_..._full_masking above is the degenerate
|
|
163
|
+
edge case that a target-only detach could still pass.
|
|
164
|
+
"""
|
|
165
|
+
config = MLMWrapperConfig(dim=D, mask_ratio=0.15)
|
|
166
|
+
encoder = _build_encoder(device, torch.float32)
|
|
167
|
+
wrapper = MLMWrapper(config).to(device)
|
|
168
|
+
x = torch.randn(B, N, D, device=device, requires_grad=True)
|
|
169
|
+
mask = torch.ones(B, N, dtype=torch.bool, device=device)
|
|
170
|
+
res = wrapper(make_padded_input(x, mask), encoder)
|
|
171
|
+
res.mlm_loss.backward()
|
|
172
|
+
assert x.grad is None
|
|
152
173
|
|
|
153
174
|
|
|
154
175
|
def test_wrapper_accepts_custom_masking_strategy(device: torch.device) -> None:
|
|
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
|