stackformers 3.8.2__tar.gz → 3.9.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.
Files changed (99) hide show
  1. {stackformers-3.8.2 → stackformers-3.9.0}/PKG-INFO +1 -1
  2. {stackformers-3.8.2 → stackformers-3.9.0}/pyproject.toml +1 -1
  3. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/presets/README.md +3 -1
  4. stackformers-3.9.0/stackformers/presets/pyramid_vision.py +173 -0
  5. stackformers-3.9.0/stackformers/spatial/README.md +49 -0
  6. stackformers-3.9.0/stackformers/spatial/__init__.py +45 -0
  7. stackformers-3.9.0/stackformers/spatial/config.py +138 -0
  8. stackformers-3.9.0/stackformers/spatial/factory.py +49 -0
  9. stackformers-3.9.0/stackformers/spatial/input.py +53 -0
  10. stackformers-3.9.0/stackformers/spatial/kv_reduction.py +69 -0
  11. stackformers-3.9.0/stackformers/spatial/layer.py +42 -0
  12. stackformers-3.9.0/stackformers/spatial/patch_merging.py +54 -0
  13. stackformers-3.9.0/stackformers/spatial/protocols.py +33 -0
  14. stackformers-3.9.0/stackformers/spatial/spatial_reduction.py +70 -0
  15. stackformers-3.9.0/stackformers/spatial/window.py +101 -0
  16. stackformers-3.9.0/tests/presets/test_pyramid_vision.py +71 -0
  17. stackformers-3.9.0/tests/spatial/__init__.py +0 -0
  18. stackformers-3.9.0/tests/spatial/test_input.py +41 -0
  19. stackformers-3.9.0/tests/spatial/test_kv_reduction.py +66 -0
  20. stackformers-3.9.0/tests/spatial/test_layer.py +45 -0
  21. stackformers-3.9.0/tests/spatial/test_patch_merging.py +37 -0
  22. stackformers-3.9.0/tests/spatial/test_spatial_reduction.py +81 -0
  23. stackformers-3.9.0/tests/spatial/test_window.py +75 -0
  24. {stackformers-3.8.2 → stackformers-3.9.0}/uv.lock +1 -1
  25. stackformers-3.8.2/.claude/settings.json +0 -7
  26. {stackformers-3.8.2 → stackformers-3.9.0}/.claudeignore +0 -0
  27. {stackformers-3.8.2 → stackformers-3.9.0}/.gitignore +0 -0
  28. {stackformers-3.8.2 → stackformers-3.9.0}/.python-version +0 -0
  29. {stackformers-3.8.2 → stackformers-3.9.0}/.vscode/settings.json +0 -0
  30. {stackformers-3.8.2 → stackformers-3.9.0}/CLAUDE.md +0 -0
  31. {stackformers-3.8.2 → stackformers-3.9.0}/Justfile +0 -0
  32. {stackformers-3.8.2 → stackformers-3.9.0}/LICENSE +0 -0
  33. {stackformers-3.8.2 → stackformers-3.9.0}/PLAN.md +0 -0
  34. {stackformers-3.8.2 → stackformers-3.9.0}/README.md +0 -0
  35. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/__init__.py +0 -0
  36. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/attention/README.md +0 -0
  37. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/attention/__init__.py +0 -0
  38. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/attention/bias.py +0 -0
  39. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/attention/config.py +0 -0
  40. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/attention/cross_attn.py +0 -0
  41. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/attention/ops.py +0 -0
  42. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/attention/protocols.py +0 -0
  43. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/attention/self_attn.py +0 -0
  44. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/config.py +0 -0
  45. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/cross_attender.py +0 -0
  46. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/decoder.py +0 -0
  47. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/encoder.py +0 -0
  48. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/feedforward/README.md +0 -0
  49. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/feedforward/__init__.py +0 -0
  50. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/feedforward/config.py +0 -0
  51. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/feedforward/factory.py +0 -0
  52. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/feedforward/geglu.py +0 -0
  53. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/feedforward/protocols.py +0 -0
  54. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/feedforward/relu_squared.py +0 -0
  55. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/feedforward/swiglu.py +0 -0
  56. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/layers.py +0 -0
  57. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/norm/README.md +0 -0
  58. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/norm/__init__.py +0 -0
  59. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/norm/config.py +0 -0
  60. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/norm/factory.py +0 -0
  61. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/norm/protocols.py +0 -0
  62. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/positional/README.md +0 -0
  63. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/positional/__init__.py +0 -0
  64. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/positional/config.py +0 -0
  65. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/positional/factory.py +0 -0
  66. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/positional/learned.py +0 -0
  67. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/positional/none.py +0 -0
  68. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/positional/protocols.py +0 -0
  69. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/positional/rope1d.py +0 -0
  70. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/positional/rope2d.py +0 -0
  71. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/presets/__init__.py +0 -0
  72. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/presets/cross_attender.py +0 -0
  73. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/presets/decoder.py +0 -0
  74. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/presets/encoder.py +0 -0
  75. {stackformers-3.8.2 → stackformers-3.9.0}/stackformers/sequence.py +0 -0
  76. {stackformers-3.8.2 → stackformers-3.9.0}/tests/__init__.py +0 -0
  77. {stackformers-3.8.2 → stackformers-3.9.0}/tests/attention/__init__.py +0 -0
  78. {stackformers-3.8.2 → stackformers-3.9.0}/tests/attention/test_cross_attn.py +0 -0
  79. {stackformers-3.8.2 → stackformers-3.9.0}/tests/attention/test_kernels.py +0 -0
  80. {stackformers-3.8.2 → stackformers-3.9.0}/tests/attention/test_ops.py +0 -0
  81. {stackformers-3.8.2 → stackformers-3.9.0}/tests/attention/test_self_attn.py +0 -0
  82. {stackformers-3.8.2 → stackformers-3.9.0}/tests/conftest.py +0 -0
  83. {stackformers-3.8.2 → stackformers-3.9.0}/tests/feedforward/__init__.py +0 -0
  84. {stackformers-3.8.2 → stackformers-3.9.0}/tests/feedforward/test_geglu.py +0 -0
  85. {stackformers-3.8.2 → stackformers-3.9.0}/tests/feedforward/test_relu_squared.py +0 -0
  86. {stackformers-3.8.2 → stackformers-3.9.0}/tests/feedforward/test_swiglu.py +0 -0
  87. {stackformers-3.8.2 → stackformers-3.9.0}/tests/positional/__init__.py +0 -0
  88. {stackformers-3.8.2 → stackformers-3.9.0}/tests/positional/test_learned_pos.py +0 -0
  89. {stackformers-3.8.2 → stackformers-3.9.0}/tests/positional/test_rope.py +0 -0
  90. {stackformers-3.8.2 → stackformers-3.9.0}/tests/presets/__init__.py +0 -0
  91. {stackformers-3.8.2 → stackformers-3.9.0}/tests/presets/test_cross_attender.py +0 -0
  92. {stackformers-3.8.2 → stackformers-3.9.0}/tests/presets/test_decoder.py +0 -0
  93. {stackformers-3.8.2 → stackformers-3.9.0}/tests/presets/test_encoder.py +0 -0
  94. {stackformers-3.8.2 → stackformers-3.9.0}/tests/test_cross_attender.py +0 -0
  95. {stackformers-3.8.2 → stackformers-3.9.0}/tests/test_decoder.py +0 -0
  96. {stackformers-3.8.2 → stackformers-3.9.0}/tests/test_encoder.py +0 -0
  97. {stackformers-3.8.2 → stackformers-3.9.0}/tests/test_layers.py +0 -0
  98. {stackformers-3.8.2 → stackformers-3.9.0}/tests/test_norm.py +0 -0
  99. {stackformers-3.8.2 → stackformers-3.9.0}/tests/test_sequence.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackformers
3
- Version: 3.8.2
3
+ Version: 3.9.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "stackformers"
7
- version = "3.8.2"
7
+ version = "3.9.0"
8
8
  description = "Typed, composable, SOLID transformer library for PyTorch"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -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
- All presets accept `PaddedInput` or `PackedInput` interchangeably.
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
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from stackformers.norm.factory import build_norm
4
+ from stackformers.positional.protocols import PosEncoding
5
+ from stackformers.spatial.config import (
6
+ ConvKVReductionConfig,
7
+ KVReductionConfig,
8
+ NoKVReductionConfig,
9
+ PatchMergingConfig,
10
+ SpatialAttnConfig,
11
+ SpatialReductionAttentionConfig,
12
+ WindowAttention2DConfig,
13
+ )
14
+ from stackformers.spatial.kv_reduction import ConvKVReduction, NoKVReduction
15
+ from stackformers.spatial.patch_merging import PatchMerging
16
+ from stackformers.spatial.protocols import KVReduction, SpatialAttn
17
+ from stackformers.spatial.spatial_reduction import SpatialReductionAttention
18
+ from stackformers.spatial.window import WindowAttention2D
19
+
20
+
21
+ def build_kv_reduction(config: KVReductionConfig) -> KVReduction:
22
+ match config:
23
+ case NoKVReductionConfig():
24
+ return NoKVReduction()
25
+ case ConvKVReductionConfig():
26
+ return ConvKVReduction(config, build_norm(config.norm))
27
+ case _:
28
+ raise AssertionError(f"Unhandled KV reduction config: {type(config)}")
29
+
30
+
31
+ def _kv_reduction_for(config: SpatialReductionAttentionConfig) -> KVReductionConfig:
32
+ if config.reduction == 1:
33
+ return NoKVReductionConfig()
34
+ return ConvKVReductionConfig(dim=config.dim, reduction=config.reduction, norm=config.kv_norm)
35
+
36
+
37
+ def build_spatial_attn(config: SpatialAttnConfig, pos_encoding: PosEncoding) -> SpatialAttn:
38
+ match config:
39
+ case WindowAttention2DConfig():
40
+ return WindowAttention2D(config, pos_encoding)
41
+ case SpatialReductionAttentionConfig():
42
+ reducer = build_kv_reduction(_kv_reduction_for(config))
43
+ return SpatialReductionAttention(config, pos_encoding, reducer)
44
+ case _:
45
+ raise AssertionError(f"Unhandled spatial attention config: {type(config)}")
46
+
47
+
48
+ def build_patch_merging(config: PatchMergingConfig) -> PatchMerging:
49
+ return PatchMerging(config, build_norm(config.norm))
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import NamedTuple
4
+
5
+ import torch
6
+ from jaxtyping import Bool, Float
7
+ from torch import Tensor
8
+
9
+
10
+ def grid_positions(h: int, w: int, device: torch.device) -> Float[Tensor, "n c"]:
11
+ """Row-major (row, col) coordinates for an h×w grid, shape (h*w, 2).
12
+
13
+ Built with static h, w so it stays torch.export / torch.compile friendly.
14
+ """
15
+ rows = torch.arange(h, device=device).repeat_interleave(w)
16
+ cols = torch.arange(w, device=device).repeat(h)
17
+ return torch.stack([rows, cols], dim=-1).to(torch.float32)
18
+
19
+
20
+ class SpatialInput(NamedTuple):
21
+ """Padded batch of tokens laid out on a 2-D grid.
22
+
23
+ Distinct from SequenceInput: it carries the grid shape so spatial attention can
24
+ reshape the flat token sequence back to (b, d, H, W). Padded-only — there is no
25
+ packed spatial variant (fixed-resolution images do not vary in length).
26
+
27
+ NamedTuple (not dataclass) so torch.export / torch.compile see the tensor fields
28
+ natively, matching PaddedInput / PackedInput.
29
+ """
30
+
31
+ x: Float[Tensor, "b n d"]
32
+ mask: Bool[Tensor, "b n"] # True = valid token
33
+ abs_positions: Float[Tensor, "b n c"] # c=2, (row, col) per token
34
+ grid: tuple[int, int] # (H, W); H * W == n
35
+
36
+
37
+ def make_spatial_input(
38
+ x: Float[Tensor, "b n d"],
39
+ grid: tuple[int, int],
40
+ mask: Bool[Tensor, "b n"] | None = None,
41
+ ) -> SpatialInput:
42
+ """Build a SpatialInput with row-major (row, col) positions from the grid shape.
43
+
44
+ mask defaults to all-valid; pass an explicit mask only when the batch is padded.
45
+ """
46
+ h, w = grid
47
+ n = h * w
48
+ if x.shape[1] != n:
49
+ raise ValueError(f"x has {x.shape[1]} tokens but grid {grid} implies {n}")
50
+ positions = grid_positions(h, w, x.device).unsqueeze(0).expand(x.shape[0], -1, -1)
51
+ if mask is None:
52
+ mask = torch.ones(x.shape[0], n, dtype=torch.bool, device=x.device)
53
+ return SpatialInput(x=x, mask=mask, abs_positions=positions, grid=grid)
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import torch.nn as nn
4
+ from einops import rearrange
5
+ from jaxtyping import Float
6
+ from torch import Tensor
7
+
8
+ from stackformers.norm.protocols import Norm
9
+ from stackformers.spatial.config import ConvKVReductionConfig
10
+ from stackformers.spatial.input import grid_positions
11
+
12
+
13
+ class NoKVReduction(nn.Module):
14
+ """Null object for KVReduction — returns the full grid unchanged.
15
+
16
+ Keeps the attention forward() free of `if reduction is not None` branches.
17
+ """
18
+
19
+ def __call__(
20
+ self,
21
+ x: Float[Tensor, "b n d"],
22
+ grid: tuple[int, int],
23
+ ) -> tuple[Float[Tensor, "b s d"], Float[Tensor, "b s c"]]:
24
+ h, w = grid
25
+ positions = grid_positions(h, w, x.device).unsqueeze(0).expand(x.shape[0], -1, -1)
26
+ return x, positions
27
+
28
+
29
+ class ConvKVReduction(nn.Module):
30
+ """Downsample K/V context with a strided conv, then norm.
31
+
32
+ Reshapes the flat token grid to (b, d, H, W), applies a stride-r conv that pools each
33
+ r×r block to one token, flattens back, and normalises with the injected Norm. Emits the
34
+ coarse grid's (row, col) positions so the caller can position-encode the reduced keys.
35
+
36
+ Paper: "Pyramid Vision Transformer" (spatial-reduction attention), Wang et al., 2021 —
37
+ https://arxiv.org/abs/2102.12122
38
+ """
39
+
40
+ def __init__(self, config: ConvKVReductionConfig, norm: Norm) -> None:
41
+ super().__init__()
42
+ self.reduction = config.reduction
43
+ self.conv = nn.Conv2d(
44
+ config.dim, config.dim, kernel_size=config.reduction, stride=config.reduction
45
+ )
46
+ self.norm = norm
47
+
48
+ def __call__(
49
+ self,
50
+ x: Float[Tensor, "b n d"],
51
+ grid: tuple[int, int],
52
+ ) -> tuple[Float[Tensor, "b s d"], Float[Tensor, "b s c"]]:
53
+ """Shape flow (r = reduction, s = (H/r)·(W/r)):
54
+
55
+ x (b, n, d) n = H·W
56
+ to grid (b, d, H, W)
57
+ conv (b, d, H/r, W/r)
58
+ ctx (b, s, d) flattened + normed reduced tokens
59
+ pos (b, s, 2) coarse-grid (row, col)
60
+ """
61
+ h, w = grid
62
+ r = self.reduction
63
+ if h % r != 0 or w % r != 0:
64
+ raise ValueError(f"grid {grid} not divisible by reduction {r}")
65
+ feat = rearrange(x, "b (h w) d -> b d h w", h=h, w=w)
66
+ feat = self.conv(feat)
67
+ ctx = self.norm(rearrange(feat, "b d h w -> b (h w) d"))
68
+ positions = grid_positions(h // r, w // r, x.device).unsqueeze(0).expand(x.shape[0], -1, -1)
69
+ return ctx, positions
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import torch.nn as nn
4
+
5
+ from stackformers.feedforward.protocols import FeedForward
6
+ from stackformers.norm.protocols import Norm
7
+ from stackformers.spatial.input import SpatialInput
8
+ from stackformers.spatial.protocols import SpatialAttn
9
+
10
+
11
+ class SpatialTransformerLayer(nn.Module):
12
+ """Pre-norm transformer layer over a 2-D grid: norm → spatial-attn → residual,
13
+ norm → ff → residual.
14
+
15
+ Mirrors TransformerLayer but is typed to SpatialAttn / SpatialInput so the grid shape
16
+ rides through unchanged via NamedTuple._replace. The feed-forward and norms are the same
17
+ injected protocols used everywhere else.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ attn: SpatialAttn,
23
+ ff: FeedForward,
24
+ norm_attn: Norm,
25
+ norm_ff: Norm,
26
+ ) -> None:
27
+ super().__init__()
28
+ self.attn = attn
29
+ self.ff = ff
30
+ self.norm_attn = norm_attn
31
+ self.norm_ff = norm_ff
32
+
33
+ def forward(self, input: SpatialInput) -> SpatialInput:
34
+ """Shape-invariant: x stays (b, n, d) throughout; grid rides along via _replace.
35
+
36
+ x = x + attn(norm(x)) # (b, n, d) → (b, n, d)
37
+ x = x + ff(norm(x)) # (b, n, d) → (b, n, d)
38
+ """
39
+ normed = input._replace(x=self.norm_attn(input.x))
40
+ x = input.x + self.attn(normed)
41
+ x = x + self.ff(self.norm_ff(x))
42
+ return input._replace(x=x)