shadax 0.2.0__py3-none-any.whl

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.
shadax/__init__.py ADDED
@@ -0,0 +1,50 @@
1
+ """
2
+ SHADAX - Self-supervised Hierarchical Adaptive Hybrid Algorithm.
3
+
4
+ A production-ready Python implementation of the SHADA algorithm: a four-stage
5
+ hierarchical encoder shared across the image and text modalities, four task
6
+ heads (classification, detection, segmentation, language modeling), two
7
+ self-supervised objectives and a four-phase training pipeline, all behind a
8
+ familiar sklearn-style estimator.
9
+
10
+ Public API:
11
+ SHADA: High-level sklearn-style estimator (fit/predict/score).
12
+ SHADAConfig: Configuration dataclass (the model-shape contract).
13
+ create_config: Tier-aware configuration factory.
14
+ SHADANet: The unified encoder + head + SSL ``nn.Module``.
15
+ HierarchicalEncoder: The shared multi-modal backbone.
16
+ ModelTier / TaskType / TrainingPhase / Modality: Controlled vocabularies.
17
+
18
+ Example:
19
+ >>> from shadax import SHADA
20
+ >>> model = SHADA(tier="base", num_classes=10)
21
+ >>> model.fit(X_train, y_train) # doctest: +SKIP
22
+ >>> predictions = model.predict(X_test) # doctest: +SKIP
23
+ """
24
+
25
+ __version__ = "0.2.0"
26
+ __author__ = "Omar"
27
+
28
+ from .config import (
29
+ Modality,
30
+ ModelTier,
31
+ SHADAConfig,
32
+ TaskType,
33
+ TrainingPhase,
34
+ create_config,
35
+ )
36
+ from .encoder import HierarchicalEncoder
37
+ from .model import SHADA
38
+ from .network import SHADANet
39
+
40
+ __all__ = [
41
+ "SHADA",
42
+ "SHADAConfig",
43
+ "create_config",
44
+ "ModelTier",
45
+ "TaskType",
46
+ "TrainingPhase",
47
+ "Modality",
48
+ "HierarchicalEncoder",
49
+ "SHADANet",
50
+ ]
shadax/config.py ADDED
@@ -0,0 +1,294 @@
1
+ """
2
+ SHADA configuration layer.
3
+
4
+ This module is the *contract* that every other module in :mod:`shadax`
5
+ depends on. It is intentionally free of any heavy dependency (no PyTorch),
6
+ so it can be imported cheaply and reasoned about in isolation.
7
+
8
+ It defines:
9
+
10
+ * :class:`ModelTier`, :class:`TaskType`, :class:`TrainingPhase`,
11
+ :class:`Modality` -- the controlled vocabularies used across the library.
12
+ * :class:`SHADAConfig` -- the single source of truth for the model shape.
13
+ * :data:`_TIER_CONFIGS` -- the per-tier architectural presets.
14
+ * :func:`create_config` -- the factory used by the high level API.
15
+
16
+ Spatial contract (images)
17
+ --------------------------
18
+ The hierarchical encoder reduces the spatial resolution by a fixed total
19
+ factor of ``32`` (a ``/4`` convolutional stem followed by three ``/2`` stage
20
+ downsamples). Input height and width must therefore be divisible by ``32``;
21
+ :func:`SHADAConfig.validate` and the encoder both enforce this.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from dataclasses import dataclass, field
27
+ from enum import Enum
28
+ from typing import Dict, List
29
+
30
+ __all__ = [
31
+ "ModelTier",
32
+ "TaskType",
33
+ "TrainingPhase",
34
+ "Modality",
35
+ "SHADAConfig",
36
+ "create_config",
37
+ "STEM_REDUCTION",
38
+ "STAGE_REDUCTION",
39
+ "TOTAL_REDUCTION",
40
+ "NUM_STAGES",
41
+ ]
42
+
43
+ # --------------------------------------------------------------------------- #
44
+ # Architectural constants (the spatial contract).
45
+ # --------------------------------------------------------------------------- #
46
+ NUM_STAGES: int = 4
47
+ STEM_REDUCTION: int = 4 # convolutional stem downsamples H, W by 4.
48
+ STAGE_REDUCTION: int = 2 # each inter-stage downsample halves H, W.
49
+ # stem (/4) + 3 downsamples (/2 each) -> /32 total.
50
+ TOTAL_REDUCTION: int = STEM_REDUCTION * (STAGE_REDUCTION ** (NUM_STAGES - 1))
51
+
52
+
53
+ class ModelTier(str, Enum):
54
+ """Model size tiers, ordered from smallest to largest."""
55
+
56
+ NANO = "nano"
57
+ BASE = "base"
58
+ LARGE = "large"
59
+ XL = "xl"
60
+
61
+
62
+ class TaskType(str, Enum):
63
+ """Supported downstream tasks.
64
+
65
+ Each value maps to a concrete task head in :mod:`shadax.heads` and to a
66
+ loss in :mod:`shadax.training`.
67
+ """
68
+
69
+ CLASSIFICATION = "classification"
70
+ DETECTION = "detection"
71
+ SEGMENTATION = "segmentation"
72
+ LANGUAGE_MODEL = "lm"
73
+
74
+
75
+ class TrainingPhase(str, Enum):
76
+ """The four phases of the SHADA training pipeline.
77
+
78
+ * ``PRETRAIN`` -- self-supervised pretraining (no labels required).
79
+ * ``MULTITASK`` -- joint optimisation of the supervised objective and the
80
+ self-supervised objective.
81
+ * ``FINETUNE`` -- purely supervised optimisation of the target task.
82
+ * ``DEPLOY`` -- inference / frozen-weights mode (no optimisation).
83
+ """
84
+
85
+ PRETRAIN = "pretrain"
86
+ MULTITASK = "multitask"
87
+ FINETUNE = "finetune"
88
+ DEPLOY = "deploy"
89
+
90
+
91
+ class Modality(str, Enum):
92
+ """Input modalities understood by the encoder."""
93
+
94
+ IMAGE = "image"
95
+ TEXT = "text"
96
+
97
+
98
+ @dataclass
99
+ class SHADAConfig:
100
+ """Single source of truth for the SHADA architecture.
101
+
102
+ Attributes:
103
+ tier: Model size tier name (``nano``/``base``/``large``/``xl``).
104
+ encoder_dims: Channel width of each of the four encoder stages.
105
+ encoder_depths: Number of transformer blocks in each stage.
106
+ num_heads: Number of attention heads in each stage.
107
+ mlp_ratio: Hidden expansion ratio of the feed-forward sub-layer.
108
+ dropout: Dropout probability used throughout the network.
109
+ in_channels: Number of input image channels (e.g. 3 for RGB).
110
+ image_size: Reference image size used for defaults; the encoder is
111
+ resolution-adaptive, so this is only a hint (any H, W divisible by
112
+ :data:`TOTAL_REDUCTION` works).
113
+ max_seq_len: Maximum text sequence length supported.
114
+ vocab_size: Vocabulary size for the text modality / language model.
115
+ task: Primary :class:`TaskType` value (stored as ``str``).
116
+ num_classes: Number of output classes. Interpreted per task:
117
+ classification -> number of labels, detection -> object
118
+ categories, segmentation -> per-pixel classes. Ignored for ``lm``
119
+ (which predicts over ``vocab_size``).
120
+ mask_ratio: Fraction of image patches masked during self-supervised
121
+ pretraining (masked image modeling).
122
+ text_mask_ratio: Fraction of text tokens masked during masked language
123
+ modeling.
124
+ decoder_dim: Width of the lightweight self-supervised decoder.
125
+ decoder_depth: Number of blocks in the self-supervised decoder.
126
+ pad_token_id: Token id treated as padding / ignored by the LM loss.
127
+ """
128
+
129
+ tier: str = "base"
130
+ encoder_dims: List[int] = field(default_factory=lambda: [128, 256, 512, 1024])
131
+ encoder_depths: List[int] = field(default_factory=lambda: [3, 4, 6, 3])
132
+ num_heads: List[int] = field(default_factory=lambda: [4, 8, 16, 32])
133
+ mlp_ratio: float = 4.0
134
+ dropout: float = 0.1
135
+ in_channels: int = 3
136
+ image_size: int = 224
137
+ max_seq_len: int = 1024
138
+ vocab_size: int = 50257
139
+ task: str = "classification"
140
+ num_classes: int = 1000
141
+ mask_ratio: float = 0.75
142
+ text_mask_ratio: float = 0.15
143
+ decoder_dim: int = 256
144
+ decoder_depth: int = 2
145
+ pad_token_id: int = 0
146
+
147
+ def __post_init__(self) -> None:
148
+ self.validate()
149
+
150
+ # ----------------------------------------------------------------- #
151
+ # Convenience accessors used by the rest of the library.
152
+ # ----------------------------------------------------------------- #
153
+ @property
154
+ def embed_dim(self) -> int:
155
+ """Width of the first (stem) stage."""
156
+ return self.encoder_dims[0]
157
+
158
+ @property
159
+ def final_dim(self) -> int:
160
+ """Width of the last encoder stage (the global feature dimension)."""
161
+ return self.encoder_dims[-1]
162
+
163
+ @property
164
+ def num_stages(self) -> int:
165
+ return len(self.encoder_dims)
166
+
167
+ @property
168
+ def task_type(self) -> TaskType:
169
+ """The :class:`TaskType` enum corresponding to ``self.task``."""
170
+ return TaskType(self.task)
171
+
172
+ def stage_reduction(self, stage: int) -> int:
173
+ """Total spatial reduction factor at the output of ``stage`` (0-based)."""
174
+ return STEM_REDUCTION * (STAGE_REDUCTION ** stage)
175
+
176
+ def validate(self) -> "SHADAConfig":
177
+ """Validate internal consistency. Returns ``self`` for chaining.
178
+
179
+ Raises:
180
+ ValueError: if the per-stage lists are inconsistent or any value
181
+ is out of range.
182
+ """
183
+ n = NUM_STAGES
184
+ for name, seq in (
185
+ ("encoder_dims", self.encoder_dims),
186
+ ("encoder_depths", self.encoder_depths),
187
+ ("num_heads", self.num_heads),
188
+ ):
189
+ if len(seq) != n:
190
+ raise ValueError(
191
+ f"{name} must have exactly {n} entries, got {len(seq)}: {seq!r}"
192
+ )
193
+ for dim, heads in zip(self.encoder_dims, self.num_heads):
194
+ if dim % heads != 0:
195
+ raise ValueError(
196
+ f"each encoder dim must be divisible by its head count; "
197
+ f"dim={dim} is not divisible by num_heads={heads}"
198
+ )
199
+ if self.task not in {t.value for t in TaskType}:
200
+ raise ValueError(
201
+ f"task must be one of {[t.value for t in TaskType]}, got {self.task!r}"
202
+ )
203
+ if not (0.0 <= self.mask_ratio < 1.0):
204
+ raise ValueError(f"mask_ratio must be in [0, 1), got {self.mask_ratio}")
205
+ if not (0.0 <= self.text_mask_ratio < 1.0):
206
+ raise ValueError(
207
+ f"text_mask_ratio must be in [0, 1), got {self.text_mask_ratio}"
208
+ )
209
+ if self.num_classes < 1:
210
+ raise ValueError(f"num_classes must be >= 1, got {self.num_classes}")
211
+ return self
212
+
213
+
214
+ # --------------------------------------------------------------------------- #
215
+ # Per-tier presets. Only architectural fields are stored here; task-specific
216
+ # fields (task, num_classes, ...) are supplied by create_config / the user.
217
+ # --------------------------------------------------------------------------- #
218
+ _TIER_CONFIGS: Dict[str, Dict] = {
219
+ "nano": {
220
+ "encoder_dims": [64, 128, 256, 512],
221
+ "encoder_depths": [2, 2, 4, 2],
222
+ "num_heads": [2, 4, 8, 16],
223
+ "max_seq_len": 512,
224
+ "decoder_dim": 128,
225
+ "decoder_depth": 1,
226
+ },
227
+ "base": {
228
+ "encoder_dims": [128, 256, 512, 1024],
229
+ "encoder_depths": [3, 4, 6, 3],
230
+ "num_heads": [4, 8, 16, 32],
231
+ "max_seq_len": 1024,
232
+ "decoder_dim": 256,
233
+ "decoder_depth": 2,
234
+ },
235
+ "large": {
236
+ "encoder_dims": [192, 384, 768, 1536],
237
+ "encoder_depths": [3, 4, 18, 3],
238
+ "num_heads": [6, 12, 24, 48],
239
+ "max_seq_len": 2048,
240
+ "decoder_dim": 384,
241
+ "decoder_depth": 2,
242
+ },
243
+ "xl": {
244
+ "encoder_dims": [256, 512, 1024, 2048],
245
+ "encoder_depths": [3, 4, 24, 3],
246
+ "num_heads": [8, 16, 32, 64],
247
+ "max_seq_len": 4096,
248
+ "decoder_dim": 512,
249
+ "decoder_depth": 4,
250
+ },
251
+ }
252
+
253
+
254
+ def create_config(
255
+ tier: str = "base",
256
+ task: str = "classification",
257
+ num_classes: int = 1000,
258
+ **overrides,
259
+ ) -> SHADAConfig:
260
+ """Create a :class:`SHADAConfig` with tier-specific defaults.
261
+
262
+ Args:
263
+ tier: Model tier (``nano``/``base``/``large``/``xl``).
264
+ task: Task type (``classification``/``detection``/``segmentation``/``lm``).
265
+ num_classes: Number of output classes (see :class:`SHADAConfig`).
266
+ **overrides: Any :class:`SHADAConfig` field to override.
267
+
268
+ Returns:
269
+ A validated :class:`SHADAConfig` instance.
270
+
271
+ Raises:
272
+ ValueError: if ``tier`` is unknown or the resulting config is invalid.
273
+
274
+ Example:
275
+ >>> config = create_config("base", task="classification", num_classes=10)
276
+ >>> config.final_dim
277
+ 1024
278
+ """
279
+ if isinstance(tier, ModelTier):
280
+ tier = tier.value
281
+ if tier not in _TIER_CONFIGS:
282
+ raise ValueError(
283
+ f"tier must be one of {list(_TIER_CONFIGS)}, got {tier!r}"
284
+ )
285
+ if isinstance(task, TaskType):
286
+ task = task.value
287
+ kwargs = {
288
+ **_TIER_CONFIGS[tier],
289
+ "tier": tier,
290
+ "task": task,
291
+ "num_classes": num_classes,
292
+ **overrides,
293
+ }
294
+ return SHADAConfig(**kwargs)
shadax/core.py ADDED
@@ -0,0 +1,34 @@
1
+ """
2
+ Backward-compatibility shim for :mod:`shadax.core`.
3
+
4
+ The original monolithic ``core.py`` has been split into focused modules
5
+ (:mod:`shadax.config`, :mod:`shadax.network`, :mod:`shadax.model`, ...). This
6
+ module preserves the old import surface so that existing code such as::
7
+
8
+ from shadax.core import SHADA, SHADAConfig, create_config
9
+ from shadax.core import ModelTier, TaskType, TrainingPhase
10
+
11
+ keeps working unchanged. Everything is re-exported from its new home.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from shadax.config import (
17
+ Modality,
18
+ ModelTier,
19
+ SHADAConfig,
20
+ TaskType,
21
+ TrainingPhase,
22
+ create_config,
23
+ )
24
+ from shadax.model import SHADA
25
+
26
+ __all__ = [
27
+ "SHADA",
28
+ "SHADAConfig",
29
+ "create_config",
30
+ "ModelTier",
31
+ "TaskType",
32
+ "TrainingPhase",
33
+ "Modality",
34
+ ]
shadax/encoder.py ADDED
@@ -0,0 +1,301 @@
1
+ """
2
+ SHADA hierarchical multi-modal encoder.
3
+
4
+ This module defines :class:`HierarchicalEncoder`, the four-stage backbone that
5
+ turns either an image ``(B, C, H, W)`` or a text token sequence ``(B, L)`` into
6
+ a hierarchy of feature maps plus pooled global features.
7
+
8
+ The encoder is built entirely from the primitives in :mod:`shadax.modules` and
9
+ driven by a :class:`shadax.config.SHADAConfig`. It has two input paths that
10
+ share their per-stage Transformer blocks:
11
+
12
+ * **Image path** -- a :class:`~shadax.modules.ConvStem` (``/4``), then four
13
+ stages each consisting of a :class:`~shadax.modules.ConditionalPositionalEncoding`
14
+ and a list of :class:`~shadax.modules.TransformerBlock` s operating on the
15
+ flattened tokens, with a :class:`~shadax.modules.StageDownsample` (``/2``)
16
+ between consecutive stages. Total spatial reduction is
17
+ :data:`~shadax.config.TOTAL_REDUCTION` (= 32).
18
+ * **Text path** -- a :class:`~shadax.modules.TextEmbedding`, then the same four
19
+ stages of Transformer blocks (text tokens are already a sequence), with a
20
+ length-preserving :class:`~shadax.modules.TextStageProject` between stages.
21
+
22
+ The Transformer blocks are shared across modalities: both paths ultimately call
23
+ them on token sequences ``(B, N, dim)``.
24
+
25
+ The dictionary returned by :meth:`HierarchicalEncoder.forward` is a hard
26
+ contract consumed by :mod:`shadax.heads` and :mod:`shadax.ssl`; its keys and
27
+ shapes must not change.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ from typing import Any, Dict, List, Optional, Tuple, Union
33
+
34
+ import torch
35
+ import torch.nn as nn
36
+
37
+ from shadax.config import (
38
+ NUM_STAGES,
39
+ STEM_REDUCTION,
40
+ TOTAL_REDUCTION,
41
+ Modality,
42
+ SHADAConfig,
43
+ )
44
+ from shadax.modules import (
45
+ ConditionalPositionalEncoding,
46
+ ConvStem,
47
+ StageDownsample,
48
+ TextEmbedding,
49
+ TextStageProject,
50
+ TransformerBlock,
51
+ )
52
+
53
+
54
+ __all__ = ["HierarchicalEncoder"]
55
+
56
+
57
+ class _Stage(nn.Module):
58
+ """A single encoder stage: positional encoding plus Transformer blocks.
59
+
60
+ The conditional positional encoding is only used by the image path (it
61
+ operates on spatial maps); the Transformer block list is shared between the
62
+ image and text paths, both of which feed it token sequences.
63
+
64
+ Args:
65
+ dim: Channel / token width of this stage.
66
+ depth: Number of Transformer blocks in this stage.
67
+ num_heads: Number of attention heads per block.
68
+ mlp_ratio: Hidden expansion ratio of each block's feed-forward layer.
69
+ dropout: Dropout probability used throughout the stage.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ dim: int,
75
+ depth: int,
76
+ num_heads: int,
77
+ mlp_ratio: float,
78
+ dropout: float,
79
+ ) -> None:
80
+ super().__init__()
81
+ self.cpe = ConditionalPositionalEncoding(dim)
82
+ self.blocks = nn.ModuleList(
83
+ [
84
+ TransformerBlock(dim, num_heads, mlp_ratio=mlp_ratio, dropout=dropout)
85
+ for _ in range(depth)
86
+ ]
87
+ )
88
+
89
+ def run_blocks(
90
+ self, tokens: torch.Tensor, attn_mask: Optional[torch.Tensor] = None
91
+ ) -> torch.Tensor:
92
+ """Run every Transformer block in this stage over a token sequence.
93
+
94
+ Args:
95
+ tokens: Token sequence of shape ``(B, N, dim)``.
96
+ attn_mask: Optional additive float attention mask ``(N, N)``.
97
+
98
+ Returns:
99
+ Token sequence of shape ``(B, N, dim)``.
100
+ """
101
+ for block in self.blocks:
102
+ tokens = block(tokens, attn_mask=attn_mask)
103
+ return tokens
104
+
105
+
106
+ class HierarchicalEncoder(nn.Module):
107
+ """Four-stage hierarchical encoder shared across image and text modalities.
108
+
109
+ Built from a :class:`~shadax.config.SHADAConfig`, the encoder exposes a
110
+ single :meth:`forward` that branches on ``modality``. Both branches produce
111
+ four feature maps of increasing channel width (``config.encoder_dims``),
112
+ plus a final token sequence and pooled global features.
113
+
114
+ For the image modality, ``H`` and ``W`` must each be divisible by
115
+ :data:`~shadax.config.TOTAL_REDUCTION` (= 32).
116
+
117
+ Args:
118
+ config: The validated SHADA configuration describing the model shape.
119
+ """
120
+
121
+ def __init__(self, config: SHADAConfig) -> None:
122
+ super().__init__()
123
+ self.config = config
124
+ dims: List[int] = config.encoder_dims
125
+ depths: List[int] = config.encoder_depths
126
+ heads: List[int] = config.num_heads
127
+ mlp_ratio: float = config.mlp_ratio
128
+ dropout: float = config.dropout
129
+
130
+ # Image entry point.
131
+ self.stem = ConvStem(config.in_channels, dims[0], dropout=dropout)
132
+
133
+ # Text entry point.
134
+ self.text_embed = TextEmbedding(
135
+ vocab_size=config.vocab_size,
136
+ embed_dim=dims[0],
137
+ max_seq_len=config.max_seq_len,
138
+ dropout=dropout,
139
+ pad_token_id=config.pad_token_id,
140
+ )
141
+
142
+ # Per-stage modules (Transformer blocks shared across modalities).
143
+ self.stages = nn.ModuleList(
144
+ [
145
+ _Stage(dims[i], depths[i], heads[i], mlp_ratio, dropout)
146
+ for i in range(NUM_STAGES)
147
+ ]
148
+ )
149
+
150
+ # Inter-stage reductions: image spatial downsamples and text channel
151
+ # projections (3 each, one between every pair of consecutive stages).
152
+ self.downsamples = nn.ModuleList(
153
+ [StageDownsample(dims[i], dims[i + 1]) for i in range(NUM_STAGES - 1)]
154
+ )
155
+ self.text_projects = nn.ModuleList(
156
+ [TextStageProject(dims[i], dims[i + 1]) for i in range(NUM_STAGES - 1)]
157
+ )
158
+
159
+ # ------------------------------------------------------------------ #
160
+ # Forward dispatch.
161
+ # ------------------------------------------------------------------ #
162
+ def forward(
163
+ self,
164
+ x: torch.Tensor,
165
+ modality: Union[str, Modality] = "image",
166
+ causal: bool = False,
167
+ ) -> Dict[str, Any]:
168
+ """Encode an input into a hierarchy of features.
169
+
170
+ Args:
171
+ x: Either an image ``(B, in_channels, H, W)`` (image modality) or a
172
+ long tensor of token ids ``(B, L)`` (text modality).
173
+ modality: ``"image"``/``"text"`` (or the corresponding
174
+ :class:`~shadax.config.Modality` enum value).
175
+ causal: Text only. If ``True``, a causal attention mask is applied
176
+ so each position may attend only to itself and earlier
177
+ positions.
178
+
179
+ Returns:
180
+ A dict with exactly the keys ``"feature_maps"`` (list of 4
181
+ tensors), ``"tokens"`` ``(B, N_last, dims[-1])``,
182
+ ``"global_features"`` ``(B, dims[-1])``, ``"hw"`` (an ``(Hs, Ws)``
183
+ int tuple for images or ``None`` for text) and ``"modality"`` (the
184
+ resolved modality string).
185
+
186
+ Raises:
187
+ ValueError: for the image modality if ``H`` or ``W`` is not
188
+ divisible by :data:`~shadax.config.TOTAL_REDUCTION`, or if
189
+ ``modality`` is not a recognised value.
190
+ """
191
+ modality = Modality(modality)
192
+ if modality is Modality.IMAGE:
193
+ return self._forward_image(x)
194
+ if modality is Modality.TEXT:
195
+ return self._forward_text(x, causal=causal)
196
+ raise ValueError(f"unsupported modality: {modality!r}")
197
+
198
+ def forward_features(
199
+ self,
200
+ x: torch.Tensor,
201
+ modality: Union[str, Modality] = "image",
202
+ causal: bool = False,
203
+ ) -> List[torch.Tensor]:
204
+ """Return only the list of four hierarchical feature maps.
205
+
206
+ Convenience wrapper around :meth:`forward` that discards everything
207
+ except the ``"feature_maps"`` entry.
208
+
209
+ Args:
210
+ x: Input image or token ids (see :meth:`forward`).
211
+ modality: Input modality (see :meth:`forward`).
212
+ causal: Whether to apply a causal mask (text only).
213
+
214
+ Returns:
215
+ The list of four feature-map tensors.
216
+ """
217
+ return self.forward(x, modality=modality, causal=causal)["feature_maps"]
218
+
219
+ # ------------------------------------------------------------------ #
220
+ # Modality-specific paths.
221
+ # ------------------------------------------------------------------ #
222
+ def _forward_image(self, x: torch.Tensor) -> Dict[str, Any]:
223
+ """Image path of :meth:`forward` (see its docstring)."""
224
+ _, _, height, width = x.shape
225
+ if height % TOTAL_REDUCTION != 0 or width % TOTAL_REDUCTION != 0:
226
+ raise ValueError(
227
+ f"image height and width must each be divisible by "
228
+ f"{TOTAL_REDUCTION}, got H={height}, W={width}"
229
+ )
230
+
231
+ m = self.stem(x) # (B, dims[0], H/4, W/4)
232
+ feature_maps: List[torch.Tensor] = []
233
+
234
+ for i, stage in enumerate(self.stages):
235
+ m = stage.cpe(m)
236
+ b, c, h, w = m.shape
237
+ tokens = m.flatten(2).transpose(1, 2) # (B, H*W, C)
238
+ tokens = stage.run_blocks(tokens)
239
+ m = tokens.transpose(1, 2).reshape(b, c, h, w) # (B, C, H, W)
240
+ feature_maps.append(m)
241
+ if i < NUM_STAGES - 1:
242
+ m = self.downsamples[i](m)
243
+
244
+ last = feature_maps[-1]
245
+ b, c, h, w = last.shape
246
+ tokens = last.flatten(2).transpose(1, 2) # (B, N_last, dims[-1])
247
+ global_features = tokens.mean(dim=1) # (B, dims[-1])
248
+
249
+ return {
250
+ "feature_maps": feature_maps,
251
+ "tokens": tokens,
252
+ "global_features": global_features,
253
+ "hw": (h, w),
254
+ "modality": Modality.IMAGE.value,
255
+ }
256
+
257
+ def _forward_text(self, x: torch.Tensor, causal: bool = False) -> Dict[str, Any]:
258
+ """Text path of :meth:`forward` (see its docstring)."""
259
+ t = self.text_embed(x) # (B, L, dims[0])
260
+ seq_len = t.shape[1]
261
+
262
+ attn_mask: Optional[torch.Tensor] = None
263
+ if causal:
264
+ attn_mask = self._causal_mask(seq_len, device=t.device, dtype=t.dtype)
265
+
266
+ feature_maps: List[torch.Tensor] = []
267
+ for i, stage in enumerate(self.stages):
268
+ t = stage.run_blocks(t, attn_mask=attn_mask) # (B, L, dims[i])
269
+ feature_maps.append(t)
270
+ if i < NUM_STAGES - 1:
271
+ t = self.text_projects[i](t) # (B, L, dims[i+1])
272
+
273
+ tokens = t # (B, L, dims[-1])
274
+ global_features = tokens.mean(dim=1) # (B, dims[-1])
275
+
276
+ return {
277
+ "feature_maps": feature_maps,
278
+ "tokens": tokens,
279
+ "global_features": global_features,
280
+ "hw": None,
281
+ "modality": Modality.TEXT.value,
282
+ }
283
+
284
+ @staticmethod
285
+ def _causal_mask(
286
+ seq_len: int, device: torch.device, dtype: torch.dtype
287
+ ) -> torch.Tensor:
288
+ """Build an additive causal attention mask.
289
+
290
+ Args:
291
+ seq_len: Sequence length ``L``.
292
+ device: Device of the produced mask.
293
+ dtype: Floating dtype of the produced mask.
294
+
295
+ Returns:
296
+ A ``(L, L)`` float tensor that is ``0`` on and below the diagonal
297
+ and ``-inf`` above it, suitable for passing as ``attn_mask`` to
298
+ :class:`~shadax.modules.TransformerBlock`.
299
+ """
300
+ mask = torch.full((seq_len, seq_len), float("-inf"), device=device, dtype=dtype)
301
+ return torch.triu(mask, diagonal=1)