kurogane commited on
Commit
61e43e6
·
verified ·
1 Parent(s): c8aa1bf

Upload 5 files

Browse files
Files changed (5) hide show
  1. __init__.py +59 -0
  2. compile_utils.py +93 -0
  3. configuration_multiscreen.py +336 -0
  4. data.py +136 -0
  5. modeling_multiscreen.py +1084 -0
__init__.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Transformers-compatible Multiscreen implementation.
2
+
3
+ This package ports the core architecture from ``dieOD/multiscreen-pytorch`` to
4
+ Hugging Face Transformers-style ``PreTrainedConfig`` / ``PreTrainedModel``
5
+ classes.
6
+ """
7
+
8
+ from .configuration_multiscreen import MultiscreenConfig
9
+ from .compile_utils import find_msvc_cl, load_vcvars_env, setup_compile_env
10
+ from .data import PackedTextDataset
11
+ from .modeling_multiscreen import (
12
+ GatedScreeningBlock,
13
+ MultiscreenForCausalLM,
14
+ MultiscreenLayer,
15
+ MultiscreenModel,
16
+ MultiscreenPreTrainedModel,
17
+ ScreeningCache,
18
+ convert_original_state_dict_for_causal_lm,
19
+ convert_original_state_dict_for_model,
20
+ )
21
+
22
+ __version__ = "0.1.2"
23
+
24
+ __all__ = [
25
+ "MultiscreenConfig",
26
+ "MultiscreenPreTrainedModel",
27
+ "MultiscreenModel",
28
+ "MultiscreenForCausalLM",
29
+ "MultiscreenLayer",
30
+ "GatedScreeningBlock",
31
+ "ScreeningCache",
32
+ "convert_original_state_dict_for_causal_lm",
33
+ "convert_original_state_dict_for_model",
34
+ "PackedTextDataset",
35
+ "find_msvc_cl",
36
+ "load_vcvars_env",
37
+ "setup_compile_env",
38
+ "register_multiscreen_auto_classes",
39
+ ]
40
+
41
+
42
+ def register_multiscreen_auto_classes() -> None:
43
+ """Register Multiscreen with Transformers auto classes in this process.
44
+
45
+ Use this when loading local checkpoints without ``trust_remote_code`` and
46
+ without installing the model into a Transformers source tree::
47
+
48
+ from multiscreen_transformers import register_multiscreen_auto_classes
49
+ register_multiscreen_auto_classes()
50
+
51
+ from transformers import AutoModelForCausalLM
52
+ model = AutoModelForCausalLM.from_pretrained("./checkpoint")
53
+ """
54
+
55
+ from transformers import AutoConfig, AutoModel, AutoModelForCausalLM
56
+
57
+ AutoConfig.register(MultiscreenConfig.model_type, MultiscreenConfig)
58
+ AutoModel.register(MultiscreenConfig, MultiscreenModel)
59
+ AutoModelForCausalLM.register(MultiscreenConfig, MultiscreenForCausalLM)
compile_utils.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helpers for torch.compile setup, especially on Windows/MSVC."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import glob
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+
11
+
12
+ def find_msvc_cl() -> str | None:
13
+ """Find MSVC ``cl.exe`` for Triton/torch.compile on Windows."""
14
+
15
+ if os.environ.get("CC"):
16
+ return os.environ["CC"]
17
+
18
+ bases = [
19
+ Path(r"C:\Program Files (x86)\Microsoft Visual Studio"),
20
+ Path(r"C:\Program Files\Microsoft Visual Studio"),
21
+ ]
22
+ for base in bases:
23
+ if not base.exists():
24
+ continue
25
+ for vs in sorted(base.iterdir(), reverse=True):
26
+ pattern = str(
27
+ vs / "BuildTools" / "VC" / "Tools" / "MSVC" / "*" / "bin" / "Hostx64" / "x64" / "cl.exe"
28
+ )
29
+ matches = sorted(glob.glob(pattern))
30
+ if matches:
31
+ return matches[-1]
32
+ return None
33
+
34
+
35
+ def _find_vcvarsall() -> Path | None:
36
+ bases = [
37
+ Path(r"C:\Program Files (x86)\Microsoft Visual Studio"),
38
+ Path(r"C:\Program Files\Microsoft Visual Studio"),
39
+ ]
40
+ for base in bases:
41
+ if not base.exists():
42
+ continue
43
+ for vs in sorted(base.iterdir(), reverse=True):
44
+ candidate = vs / "BuildTools" / "VC" / "Auxiliary" / "Build" / "vcvarsall.bat"
45
+ if candidate.exists():
46
+ return candidate
47
+ return None
48
+
49
+
50
+ def load_vcvars_env() -> bool:
51
+ """Load the full MSVC build environment into ``os.environ`` on Windows."""
52
+
53
+ if sys.platform != "win32":
54
+ return False
55
+ if os.environ.get("VSCMD_VER"):
56
+ return True
57
+
58
+ vcvarsall = _find_vcvarsall()
59
+ if vcvarsall is None:
60
+ return False
61
+
62
+ try:
63
+ result = subprocess.run(
64
+ f'"{vcvarsall}" x64 >nul && set',
65
+ shell=True,
66
+ capture_output=True,
67
+ text=True,
68
+ check=False,
69
+ )
70
+ except OSError:
71
+ return False
72
+ if result.returncode != 0:
73
+ return False
74
+
75
+ for line in result.stdout.splitlines():
76
+ if "=" in line:
77
+ key, value = line.split("=", 1)
78
+ os.environ[key] = value
79
+ return True
80
+
81
+
82
+ def setup_compile_env() -> str | None:
83
+ """Auto-detect MSVC and set ``CC`` for ``torch.compile`` when needed."""
84
+
85
+ if sys.platform == "win32":
86
+ load_vcvars_env()
87
+ if os.environ.get("CC"):
88
+ return os.environ["CC"]
89
+ cl_path = find_msvc_cl()
90
+ if cl_path:
91
+ os.environ["CC"] = cl_path
92
+ return cl_path
93
+ return os.environ.get("CC")
configuration_multiscreen.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration for the Transformers-compatible Multiscreen model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ try: # Transformers has historically used both spellings in examples.
9
+ from transformers import PreTrainedConfig
10
+ except ImportError: # pragma: no cover - compatibility fallback for old releases.
11
+ from transformers import PretrainedConfig as PreTrainedConfig # type: ignore
12
+
13
+
14
+ class MultiscreenConfig(PreTrainedConfig):
15
+ """Configuration for Multiscreen causal language models.
16
+
17
+ This mirrors the architecture knobs from ``dieOD/multiscreen-pytorch`` while
18
+ exposing the conventional Transformers names where possible.
19
+
20
+ Important aliases
21
+ -----------------
22
+ ``hidden_size`` <-> original ``hidden_dim``
23
+ ``num_hidden_layers`` <-> original ``num_layers``
24
+ ``num_attention_heads`` <-> original ``num_heads``
25
+ ``max_position_embeddings`` <-> original ``max_seq_len``
26
+
27
+ Reproducibility controls
28
+ ------------------------
29
+ ``mipe_compute_dtype`` and ``softmask_compute_dtype`` can be ``"fp32"``
30
+ for the numerically safer Transformers port behavior, or ``"reference"``
31
+ to use the incoming tensor dtype like the standalone PyTorch reference.
32
+ ``strict_position_ids`` rejects batch-specific or non-contiguous
33
+ ``position_ids`` because the reference cache API is based on a scalar
34
+ ``start_pos``. ``zero_pad_hidden_states`` can additionally zero padded
35
+ query states after each residual layer; it defaults to ``False`` to keep
36
+ original residual behavior.
37
+ """
38
+
39
+ model_type = "multiscreen"
40
+ keys_to_ignore_at_inference = ["past_key_values"]
41
+ _alias_to_primary = {
42
+ "hidden_dim": "hidden_size",
43
+ "num_layers": "num_hidden_layers",
44
+ "num_heads": "num_attention_heads",
45
+ "max_seq_len": "max_position_embeddings",
46
+ }
47
+
48
+ def __init__(
49
+ self,
50
+ vocab_size: int = 50_257,
51
+ hidden_size: int | None = None,
52
+ hidden_dim: int | None = None,
53
+ num_hidden_layers: int | None = None,
54
+ num_layers: int | None = None,
55
+ num_attention_heads: int | None = None,
56
+ num_heads: int | None = None,
57
+ key_dim: int = 16,
58
+ value_dim: int = 64,
59
+ max_position_embeddings: int | None = None,
60
+ max_seq_len: int | None = None,
61
+ mipe_threshold: float = 256.0,
62
+ gradient_checkpointing: bool = False,
63
+ use_cache: bool = True,
64
+ labels_are_shifted: bool = False,
65
+ mipe_compute_dtype: str = "fp32",
66
+ softmask_compute_dtype: str = "fp32",
67
+ strict_position_ids: bool = True,
68
+ zero_pad_hidden_states: bool = False,
69
+ initializer_range: float = 0.1,
70
+ bos_token_id: int | None = None,
71
+ eos_token_id: int | None = None,
72
+ pad_token_id: int | None = None,
73
+ tie_word_embeddings: bool = True,
74
+ **kwargs: Any,
75
+ ) -> None:
76
+ # Saved Transformers configs may contain these superclass fields in
77
+ # kwargs. Multiscreen is always a decoder-only model, and passing them
78
+ # through while also setting them below would duplicate keyword args.
79
+ is_decoder = kwargs.pop("is_decoder", True)
80
+ is_encoder_decoder = kwargs.pop("is_encoder_decoder", False)
81
+ kwargs.pop("model_type", None)
82
+ if is_decoder is not True:
83
+ raise ValueError("MultiscreenConfig requires is_decoder=True")
84
+ if is_encoder_decoder is not False:
85
+ raise ValueError("MultiscreenConfig requires is_encoder_decoder=False")
86
+
87
+ hidden_size = self._resolve_alias(
88
+ primary=hidden_size,
89
+ alias=hidden_dim,
90
+ default=256,
91
+ primary_name="hidden_size",
92
+ alias_name="hidden_dim",
93
+ )
94
+ num_hidden_layers = self._resolve_alias(
95
+ primary=num_hidden_layers,
96
+ alias=num_layers,
97
+ default=8,
98
+ primary_name="num_hidden_layers",
99
+ alias_name="num_layers",
100
+ )
101
+ num_attention_heads = self._resolve_alias(
102
+ primary=num_attention_heads,
103
+ alias=num_heads,
104
+ default=8,
105
+ primary_name="num_attention_heads",
106
+ alias_name="num_heads",
107
+ )
108
+ max_position_embeddings = self._resolve_alias(
109
+ primary=max_position_embeddings,
110
+ alias=max_seq_len,
111
+ default=256,
112
+ primary_name="max_position_embeddings",
113
+ alias_name="max_seq_len",
114
+ )
115
+
116
+ if not bool(tie_word_embeddings):
117
+ raise ValueError(
118
+ "Multiscreen uses normalized tied input/output embeddings; "
119
+ "tie_word_embeddings must be True."
120
+ )
121
+
122
+ self.vocab_size = int(vocab_size)
123
+ self.hidden_size = int(hidden_size)
124
+ self.hidden_dim = int(hidden_size) # original repo alias
125
+ self.num_hidden_layers = int(num_hidden_layers)
126
+ self.num_layers = int(num_hidden_layers) # original repo alias
127
+ self.num_attention_heads = int(num_attention_heads)
128
+ self.num_heads = int(num_attention_heads) # original repo alias
129
+ self.key_dim = int(key_dim)
130
+ self.value_dim = int(value_dim)
131
+ self.max_position_embeddings = int(max_position_embeddings)
132
+ self.max_seq_len = int(max_position_embeddings) # original repo alias
133
+ self.mipe_threshold = float(mipe_threshold)
134
+ self.gradient_checkpointing = bool(gradient_checkpointing)
135
+ self.use_cache = bool(use_cache)
136
+ self.labels_are_shifted = bool(labels_are_shifted)
137
+ self.mipe_compute_dtype = str(mipe_compute_dtype)
138
+ self.softmask_compute_dtype = str(softmask_compute_dtype)
139
+ self.strict_position_ids = bool(strict_position_ids)
140
+ self.zero_pad_hidden_states = bool(zero_pad_hidden_states)
141
+ self.initializer_range = float(initializer_range)
142
+
143
+ self._validate()
144
+
145
+ super().__init__(
146
+ bos_token_id=bos_token_id,
147
+ eos_token_id=eos_token_id,
148
+ pad_token_id=pad_token_id,
149
+ tie_word_embeddings=tie_word_embeddings,
150
+ is_decoder=True,
151
+ is_encoder_decoder=False,
152
+ use_cache=use_cache,
153
+ **kwargs,
154
+ )
155
+
156
+ # Useful when pushing a repo with these Python files to the Hub and
157
+ # loading with trust_remote_code=True.
158
+ if not getattr(self, "auto_map", None):
159
+ self.auto_map = {
160
+ "AutoConfig": "configuration_multiscreen.MultiscreenConfig",
161
+ "AutoModel": "modeling_multiscreen.MultiscreenModel",
162
+ "AutoModelForCausalLM": "modeling_multiscreen.MultiscreenForCausalLM",
163
+ }
164
+ if not getattr(self, "architectures", None):
165
+ self.architectures = ["MultiscreenForCausalLM"]
166
+
167
+ @staticmethod
168
+ def _resolve_alias(
169
+ *,
170
+ primary: int | None,
171
+ alias: int | None,
172
+ default: int,
173
+ primary_name: str,
174
+ alias_name: str,
175
+ ) -> int:
176
+ if primary is None and alias is None:
177
+ return default
178
+ if primary is None:
179
+ return int(alias) # type: ignore[arg-type]
180
+ if alias is None:
181
+ return int(primary)
182
+ if int(primary) != int(alias):
183
+ raise ValueError(
184
+ f"Conflicting values for {primary_name}={primary} and "
185
+ f"{alias_name}={alias}. Use only one or make them equal."
186
+ )
187
+ return int(primary)
188
+
189
+ @classmethod
190
+ def from_psi(
191
+ cls,
192
+ psi: int,
193
+ vocab_size: int = 50_257,
194
+ max_seq_len: int = 256,
195
+ **overrides: Any,
196
+ ) -> "MultiscreenConfig":
197
+ """Build a paper-style config from the supraparameter Psi.
198
+
199
+ The scaling rule used in the reference repo is ``N_L = N_H = Psi`` and
200
+ ``d_E = Psi²``.
201
+ """
202
+
203
+ return cls(
204
+ vocab_size=vocab_size,
205
+ hidden_size=psi * psi,
206
+ num_hidden_layers=psi,
207
+ num_attention_heads=psi,
208
+ max_position_embeddings=max_seq_len,
209
+ **overrides,
210
+ )
211
+
212
+ def _validate(self) -> None:
213
+ if self.vocab_size <= 0:
214
+ raise ValueError("vocab_size must be positive")
215
+ if self.hidden_size <= 0:
216
+ raise ValueError("hidden_size/hidden_dim must be positive")
217
+ if self.num_hidden_layers <= 0:
218
+ raise ValueError("num_hidden_layers/num_layers must be positive")
219
+ if self.num_attention_heads <= 0:
220
+ raise ValueError("num_attention_heads/num_heads must be positive")
221
+ if self.key_dim < 2:
222
+ raise ValueError("key_dim must be at least 2 because MiPE rotates the first two coordinates")
223
+ if self.value_dim <= 0:
224
+ raise ValueError("value_dim must be positive")
225
+ if self.max_position_embeddings <= 0:
226
+ raise ValueError("max_position_embeddings/max_seq_len must be positive")
227
+ if self.mipe_threshold <= 0:
228
+ raise ValueError("mipe_threshold must be positive")
229
+ allowed_compute_dtypes = {"fp32", "reference"}
230
+ if self.mipe_compute_dtype not in allowed_compute_dtypes:
231
+ raise ValueError(
232
+ "mipe_compute_dtype must be either 'fp32' or 'reference', "
233
+ f"got {self.mipe_compute_dtype!r}"
234
+ )
235
+ if self.softmask_compute_dtype not in allowed_compute_dtypes:
236
+ raise ValueError(
237
+ "softmask_compute_dtype must be either 'fp32' or 'reference', "
238
+ f"got {self.softmask_compute_dtype!r}"
239
+ )
240
+ if self.initializer_range <= 0:
241
+ raise ValueError("initializer_range must be positive")
242
+
243
+ @classmethod
244
+ def _normalize_alias_updates(cls, updates: dict[str, Any]) -> dict[str, Any]:
245
+ """Map original-repository aliases to canonical Transformers field names."""
246
+
247
+ normalized = dict(updates)
248
+ for alias, primary in cls._alias_to_primary.items():
249
+ if alias not in normalized:
250
+ continue
251
+ alias_value = normalized.pop(alias)
252
+ if primary in normalized and int(normalized[primary]) != int(alias_value):
253
+ raise ValueError(
254
+ f"Conflicting update values for {primary}={normalized[primary]} "
255
+ f"and {alias}={alias_value}. Use only one or make them equal."
256
+ )
257
+ normalized[primary] = alias_value
258
+ return normalized
259
+
260
+ def clone(self, **updates: Any) -> "MultiscreenConfig":
261
+ """Return a config copy with updated fields.
262
+
263
+ ``PreTrainedConfig.to_dict()`` contains both Transformers field names and
264
+ original-repository aliases such as ``hidden_size``/``hidden_dim``.
265
+ Reusing that dictionary directly can create alias conflicts when callers
266
+ update only one spelling, so this method rebuilds from canonical fields
267
+ and canonicalizes alias-style updates.
268
+ """
269
+
270
+ normalized_updates = self._normalize_alias_updates(updates)
271
+
272
+ # ``is_decoder`` and ``is_encoder_decoder`` are forced by this class and
273
+ # are passed explicitly to ``PreTrainedConfig`` in ``__init__``.
274
+ # Silently accepting the matching values makes clone robust to dicts
275
+ # produced by Transformers; conflicting values should fail loudly.
276
+ if "is_decoder" in normalized_updates:
277
+ if normalized_updates.pop("is_decoder") is not True:
278
+ raise ValueError("MultiscreenConfig requires is_decoder=True")
279
+ if "is_encoder_decoder" in normalized_updates:
280
+ if normalized_updates.pop("is_encoder_decoder") is not False:
281
+ raise ValueError("MultiscreenConfig requires is_encoder_decoder=False")
282
+
283
+ data: dict[str, Any] = {
284
+ "vocab_size": self.vocab_size,
285
+ "hidden_size": self.hidden_size,
286
+ "num_hidden_layers": self.num_hidden_layers,
287
+ "num_attention_heads": self.num_attention_heads,
288
+ "key_dim": self.key_dim,
289
+ "value_dim": self.value_dim,
290
+ "max_position_embeddings": self.max_position_embeddings,
291
+ "mipe_threshold": self.mipe_threshold,
292
+ "gradient_checkpointing": self.gradient_checkpointing,
293
+ "use_cache": self.use_cache,
294
+ "labels_are_shifted": self.labels_are_shifted,
295
+ "mipe_compute_dtype": self.mipe_compute_dtype,
296
+ "softmask_compute_dtype": self.softmask_compute_dtype,
297
+ "strict_position_ids": self.strict_position_ids,
298
+ "zero_pad_hidden_states": self.zero_pad_hidden_states,
299
+ "initializer_range": self.initializer_range,
300
+ "bos_token_id": self.bos_token_id,
301
+ "eos_token_id": self.eos_token_id,
302
+ "pad_token_id": self.pad_token_id,
303
+ "tie_word_embeddings": True,
304
+ }
305
+
306
+ # Preserve useful PreTrainedConfig extras without reintroducing explicit
307
+ # constructor arguments, aliases, or forced superclass kwargs.
308
+ skip_keys = set(data) | set(self._alias_to_primary) | {
309
+ "model_type",
310
+ "is_decoder",
311
+ "is_encoder_decoder",
312
+ "transformers_version",
313
+ }
314
+ for key, value in self.to_dict().items():
315
+ if key not in skip_keys:
316
+ data[key] = value
317
+
318
+ data.update(normalized_updates)
319
+ return self.__class__(**data)
320
+
321
+ @property
322
+ def num_params_estimate(self) -> int:
323
+ """Approximate parameter count, following the reference implementation.
324
+
325
+ The estimate assumes tied input/output embeddings and ignores the small
326
+ learned scalar parameters.
327
+ """
328
+
329
+ embed = self.vocab_size * self.hidden_size
330
+ per_tile = self.hidden_size * (2 * self.key_dim + 3 * self.value_dim)
331
+ total_tiles = self.num_hidden_layers * self.num_attention_heads
332
+ return int(embed + total_tiles * per_tile)
333
+
334
+ @property
335
+ def sqrt_hidden_size(self) -> float:
336
+ return math.sqrt(self.hidden_size)
data.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dataset utilities for Multiscreen causal LM training."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import Optional
7
+
8
+ import numpy as np
9
+ import torch
10
+ from torch.utils.data import Dataset
11
+
12
+
13
+ class PackedTextDataset(Dataset):
14
+ """In-memory packed dataset for autoregressive language-model training.
15
+
16
+ Texts are tokenized, separated by EOS, concatenated, and chunked into fixed
17
+ length sequences.
18
+
19
+ By default this dataset follows the original ``dieOD/multiscreen-pytorch``
20
+ trainer: each stored chunk has ``seq_len + 1`` tokens, ``input_ids`` are
21
+ ``chunk[:-1]``, and ``labels`` are ``chunk[1:]``. The item also includes a
22
+ scalar ``labels_are_shifted=True`` flag so a standard Transformers data
23
+ collator/Trainer can forward it to ``MultiscreenForCausalLM`` and avoid a
24
+ second internal next-token shift.
25
+
26
+ Set ``legacy_shifted_labels=False`` for conventional Hugging Face causal-LM
27
+ batches where ``labels == input_ids`` and the model performs the standard
28
+ internal shift. In that mode the dataset emits ``labels_are_shifted=False``.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ texts: Iterable[str],
34
+ tokenizer,
35
+ seq_len: int = 256,
36
+ eos_token_id: Optional[int] = None,
37
+ max_tokens: Optional[int] = None,
38
+ legacy_shifted_labels: bool = True,
39
+ return_labels_are_shifted: bool = True,
40
+ ) -> None:
41
+ if seq_len <= 0:
42
+ raise ValueError("seq_len must be positive")
43
+ self.seq_len = int(seq_len)
44
+ self.legacy_shifted_labels = bool(legacy_shifted_labels)
45
+ self.return_labels_are_shifted = bool(return_labels_are_shifted)
46
+
47
+ if eos_token_id is None:
48
+ eos_token_id = getattr(tokenizer, "eos_token_id", None)
49
+ if eos_token_id is None:
50
+ eos_token_id = 0
51
+ self.eos_token_id = int(eos_token_id)
52
+
53
+ all_ids: list[int] = []
54
+ for text in texts:
55
+ if not text:
56
+ continue
57
+ ids = tokenizer.encode(text, add_special_tokens=False)
58
+ all_ids.extend(int(i) for i in ids)
59
+ all_ids.append(self.eos_token_id)
60
+ if max_tokens is not None and len(all_ids) >= max_tokens:
61
+ all_ids = all_ids[:max_tokens]
62
+ break
63
+
64
+ chunk_size = self.seq_len + 1 if self.legacy_shifted_labels else self.seq_len
65
+ usable = (len(all_ids) // chunk_size) * chunk_size
66
+ if usable == 0:
67
+ raise ValueError(f"Not enough tokens for one chunk (need {chunk_size}, got {len(all_ids)})")
68
+
69
+ self.tokens = np.array(all_ids[:usable], dtype=np.int64).reshape(-1, chunk_size)
70
+
71
+ def __len__(self) -> int:
72
+ return int(self.tokens.shape[0])
73
+
74
+ def __getitem__(self, idx: int) -> dict[str, torch.Tensor]:
75
+ chunk = self.tokens[idx]
76
+ if self.legacy_shifted_labels:
77
+ input_ids = torch.from_numpy(chunk[:-1].copy())
78
+ labels = torch.from_numpy(chunk[1:].copy())
79
+ else:
80
+ input_ids = torch.from_numpy(chunk.copy())
81
+ labels = input_ids.clone()
82
+
83
+ item = {
84
+ "input_ids": input_ids,
85
+ "labels": labels,
86
+ "attention_mask": torch.ones_like(input_ids, dtype=torch.long),
87
+ }
88
+ if self.return_labels_are_shifted:
89
+ item["labels_are_shifted"] = torch.tensor(self.legacy_shifted_labels, dtype=torch.bool)
90
+ return item
91
+
92
+ @classmethod
93
+ def from_hf_dataset(
94
+ cls,
95
+ dataset_name: str,
96
+ tokenizer,
97
+ seq_len: int = 256,
98
+ split: str = "train",
99
+ text_column: str = "text",
100
+ config_name: Optional[str] = None,
101
+ max_tokens: Optional[int] = None,
102
+ legacy_shifted_labels: bool = True,
103
+ return_labels_are_shifted: bool = True,
104
+ cache_dir: Optional[str] = None,
105
+ data_files: Optional[str | list[str] | dict[str, str | list[str]]] = None,
106
+ data_dir: Optional[str] = None,
107
+ revision: Optional[str] = None,
108
+ ) -> "PackedTextDataset":
109
+ """Load and pack a Hugging Face dataset.
110
+
111
+ ``cache_dir`` is forwarded to :func:`datasets.load_dataset`, which is
112
+ useful when training from TinyStories or other Hub datasets on machines
113
+ with a dedicated dataset cache volume. ``data_files`` / ``data_dir`` /
114
+ ``revision`` are kept as narrow passthroughs for local or pinned data
115
+ sources while preserving the original in-memory packing behavior.
116
+ """
117
+
118
+ from datasets import load_dataset
119
+
120
+ load_kwargs = {
121
+ "split": split,
122
+ "cache_dir": cache_dir,
123
+ "data_files": data_files,
124
+ "data_dir": data_dir,
125
+ "revision": revision,
126
+ }
127
+ load_kwargs = {k: v for k, v in load_kwargs.items() if v is not None}
128
+ ds = load_dataset(dataset_name, config_name, **load_kwargs)
129
+ return cls(
130
+ texts=(row[text_column] for row in ds),
131
+ tokenizer=tokenizer,
132
+ seq_len=seq_len,
133
+ max_tokens=max_tokens,
134
+ legacy_shifted_labels=legacy_shifted_labels,
135
+ return_labels_are_shifted=return_labels_are_shifted,
136
+ )
modeling_multiscreen.py ADDED
@@ -0,0 +1,1084 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Transformers-compatible Multiscreen model.
2
+
3
+ The screening block is ported from ``dieOD/multiscreen-pytorch`` and wrapped in
4
+ Hugging Face ``PreTrainedModel`` classes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import weakref
11
+ from collections.abc import Mapping, Sequence
12
+ from typing import Any, Optional
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+ from torch.utils.checkpoint import checkpoint as grad_checkpoint
18
+ from transformers import PreTrainedModel
19
+ try: # Transformers >=4.50 separates generation helpers from PreTrainedModel.
20
+ from transformers.generation import GenerationMixin
21
+ except ImportError: # pragma: no cover - compatibility with older releases.
22
+ try:
23
+ from transformers.generation.utils import GenerationMixin
24
+ except ImportError: # pragma: no cover
25
+ class GenerationMixin: # type: ignore[no-redef]
26
+ pass
27
+ from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
28
+ from transformers.utils import logging
29
+
30
+ from .configuration_multiscreen import MultiscreenConfig
31
+
32
+ logger = logging.get_logger(__name__)
33
+
34
+ # Per-layer screening cache.
35
+ # K: (batch, num_heads, cached_length, key_dim), post-MiPE and unit-normalized.
36
+ # V: (batch, num_heads, cached_length, value_dim), unit-normalized.
37
+ ScreeningCache = tuple[torch.Tensor, torch.Tensor]
38
+
39
+
40
+ def convert_original_state_dict_for_causal_lm(
41
+ state_dict: Mapping[str, torch.Tensor],
42
+ *,
43
+ strip_module_prefix: bool = True,
44
+ ) -> dict[str, torch.Tensor]:
45
+ """Convert original ``dieOD/multiscreen-pytorch`` weights for HF CausalLM.
46
+
47
+ The original repository's language model stores parameters under bare keys
48
+ such as ``embed.weight`` and ``layers.0.block.q_proj.weight``. This
49
+ Transformers port keeps those modules inside ``MultiscreenForCausalLM`` as
50
+ ``self.multiscreen``; state dict keys are prefixed with ``multiscreen.``.
51
+ Loading the original checkpoint into ``MultiscreenForCausalLM`` requires
52
+ that prefix. Already
53
+ prefixed keys are left unchanged, so the helper is safe to call twice.
54
+
55
+ Args:
56
+ state_dict: State dict from the original implementation, or an already
57
+ converted state dict.
58
+ strip_module_prefix: Strip a leading ``module.`` prefix often added by
59
+ DataParallel/DDP wrappers before conversion.
60
+ """
61
+
62
+ converted: dict[str, torch.Tensor] = {}
63
+ for key, value in state_dict.items():
64
+ converted_key = key
65
+ if strip_module_prefix and converted_key.startswith("module."):
66
+ converted_key = converted_key[len("module.") :]
67
+ if not converted_key.startswith("multiscreen."):
68
+ converted_key = f"multiscreen.{converted_key}"
69
+ converted[converted_key] = value
70
+ return converted
71
+
72
+
73
+ def convert_original_state_dict_for_model(
74
+ state_dict: Mapping[str, torch.Tensor],
75
+ *,
76
+ strip_module_prefix: bool = True,
77
+ ) -> dict[str, torch.Tensor]:
78
+ """Convert original or CausalLM-prefixed weights for bare ``MultiscreenModel``.
79
+
80
+ Original ``dieOD/multiscreen-pytorch`` keys are already suitable for the bare
81
+ decoder. This helper mainly strips a leading ``module.`` or ``multiscreen.``
82
+ prefix when needed.
83
+ """
84
+
85
+ converted: dict[str, torch.Tensor] = {}
86
+ for key, value in state_dict.items():
87
+ converted_key = key
88
+ if strip_module_prefix and converted_key.startswith("module."):
89
+ converted_key = converted_key[len("module.") :]
90
+ if converted_key.startswith("multiscreen."):
91
+ converted_key = converted_key[len("multiscreen.") :]
92
+ converted[converted_key] = value
93
+ return converted
94
+
95
+
96
+ class MultiscreenPreTrainedModel(PreTrainedModel):
97
+ """Base class for Multiscreen Transformers models."""
98
+
99
+ config_class = MultiscreenConfig
100
+ base_model_prefix = "multiscreen"
101
+ # Transformers 5 expects tied-weight metadata to be a mapping, while older
102
+ # model classes often used a list of regex keys. Multiscreen has no
103
+ # duplicated output-head Parameter to tie or drop from the state dict:
104
+ # logits are computed directly from the normalized input embedding.
105
+ _tied_weights_keys: dict[str, str] = {}
106
+ supports_gradient_checkpointing = True
107
+ _no_split_modules = ["MultiscreenLayer"]
108
+ _skip_keys_device_placement = "past_key_values"
109
+ # Newer Transformers Trainer may pass ``num_items_in_batch`` to models whose
110
+ # forward signature has **kwargs. Multiscreen computes a standard mean CE
111
+ # loss internally and does not consume that normalization hint.
112
+ accepts_loss_kwargs = False
113
+
114
+ def get_expanded_tied_weights_keys(self, all_submodels: bool = False) -> dict[str, str]:
115
+ """Return no storage-level tied-parameter mapping for Multiscreen.
116
+
117
+ The input/output embedding relationship is implemented by construction
118
+ in ``_compute_logits`` / ``_NormalizedTiedLMHead`` instead of by
119
+ assigning a second registered output-head Parameter to the input
120
+ embedding Parameter. Returning an empty mapping keeps Transformers 5
121
+ tied-weight bookkeeping on the mapping code path and avoids legacy
122
+ list-vs-dict crashes.
123
+ """
124
+
125
+ return {}
126
+
127
+ def _init_weights(self, module: nn.Module) -> None: # pragma: no cover - post_init hook.
128
+ """No-op because modules initialize with the original Multiscreen rules.
129
+
130
+ The reference implementation uses per-projection initializers rather than
131
+ a single global initializer. Those are applied in each module's ``__init__``.
132
+ """
133
+
134
+ return None
135
+
136
+ def _set_gradient_checkpointing(
137
+ self,
138
+ module: nn.Module | None = None,
139
+ value: bool = False,
140
+ enable: bool | None = None,
141
+ gradient_checkpointing_func: Any | None = None,
142
+ ) -> None:
143
+ # Accept both the older Transformers hook signature
144
+ # _set_gradient_checkpointing(module, value=False)
145
+ # and the newer one
146
+ # _set_gradient_checkpointing(enable=True, gradient_checkpointing_func=...).
147
+ flag = value if enable is None else enable
148
+ if module is None:
149
+ for child in self.modules():
150
+ if isinstance(child, MultiscreenModel):
151
+ child.gradient_checkpointing = bool(flag)
152
+ return
153
+ if isinstance(module, MultiscreenModel):
154
+ module.gradient_checkpointing = bool(flag)
155
+
156
+
157
+ class MultiscreenModel(MultiscreenPreTrainedModel):
158
+ """Bare Multiscreen decoder model.
159
+
160
+ This returns hidden states, not vocabulary logits. Use
161
+ :class:`MultiscreenForCausalLM` for the original language-model behavior.
162
+ """
163
+
164
+ def __init__(self, config: MultiscreenConfig) -> None:
165
+ super().__init__(config)
166
+ self.config = config
167
+ self.gradient_checkpointing = bool(config.gradient_checkpointing)
168
+ self.zero_pad_hidden_states = bool(config.zero_pad_hidden_states)
169
+
170
+ d_e = config.hidden_size
171
+ self.embed = nn.Embedding(config.vocab_size, d_e)
172
+ self.s_E = nn.Parameter(torch.tensor(0.0))
173
+ self.s_F = nn.Parameter(torch.tensor(math.log(math.sqrt(d_e))))
174
+ self.layers = nn.ModuleList(
175
+ [MultiscreenLayer(config, layer_idx=i) for i in range(config.num_hidden_layers)]
176
+ )
177
+
178
+ # Original embedding initialization: N(0, 0.1 / sqrt(d_E)).
179
+ nn.init.normal_(self.embed.weight, mean=0.0, std=config.initializer_range / math.sqrt(d_e))
180
+ self.post_init()
181
+
182
+ def get_input_embeddings(self) -> nn.Embedding:
183
+ return self.embed
184
+
185
+ def set_input_embeddings(self, value: nn.Embedding) -> None:
186
+ self.embed = value
187
+ self.config.vocab_size = value.num_embeddings
188
+
189
+ def get_output_embeddings(self) -> nn.Embedding:
190
+ # Output is tied by construction via normalized input embedding.
191
+ return self.embed
192
+
193
+ def set_output_embeddings(self, value: nn.Embedding) -> None:
194
+ self.set_input_embeddings(value)
195
+
196
+ def tie_weights(self, *args: Any, **kwargs: Any) -> None:
197
+ # We do not create a separate lm_head; logits use self.embed.weight.
198
+ # Recent Transformers releases call tie_weights with keyword arguments
199
+ # such as recompute_mapping=... or missing_keys=...; accept and ignore
200
+ # them because Multiscreen ties weights by construction.
201
+ return None
202
+
203
+ def count_parameters(self) -> int:
204
+ return sum(p.numel() for p in self.parameters() if p.requires_grad)
205
+
206
+ def forward(
207
+ self,
208
+ input_ids: torch.LongTensor | None = None,
209
+ attention_mask: torch.Tensor | None = None,
210
+ position_ids: torch.LongTensor | None = None,
211
+ past_key_values: Sequence[ScreeningCache] | None = None,
212
+ inputs_embeds: torch.Tensor | None = None,
213
+ use_cache: bool | None = None,
214
+ output_attentions: bool | None = None,
215
+ output_hidden_states: bool | None = None,
216
+ return_dict: bool | None = None,
217
+ start_pos: int | None = None,
218
+ **kwargs: Any,
219
+ ) -> BaseModelOutputWithPast | tuple[torch.Tensor, ...]:
220
+ """Run the Multiscreen decoder.
221
+
222
+ Args follow Transformers conventions. ``start_pos`` is kept as an
223
+ explicit compatibility escape hatch for the original cache API.
224
+ """
225
+
226
+ kv_caches = kwargs.pop("kv_caches", None)
227
+ if kv_caches is not None:
228
+ if past_key_values is not None:
229
+ raise ValueError("Pass only one of `past_key_values` or original-api `kv_caches`, not both.")
230
+ past_key_values = kv_caches
231
+
232
+ # Compatibility with Trainer/TRL/Transformers call paths.
233
+ use_return_dict = kwargs.pop("use_return_dict", None)
234
+ if return_dict is None and use_return_dict is not None:
235
+ return_dict = bool(use_return_dict)
236
+ kwargs.pop("num_items_in_batch", None)
237
+
238
+ if past_key_values is not None and len(past_key_values) == 0:
239
+ past_key_values = None
240
+ if past_key_values is not None and len(past_key_values) != len(self.layers):
241
+ raise ValueError(
242
+ f"past_key_values must contain {len(self.layers)} layer caches, got {len(past_key_values)}."
243
+ )
244
+
245
+ if kwargs:
246
+ # Keep forward permissive, but surface likely typo/debug information.
247
+ # ``warning_once`` caches calls, so every argument must be hashable.
248
+ unused_kwargs = ", ".join(sorted(str(key) for key in kwargs.keys()))
249
+ logger.warning_once("Unused MultiscreenModel.forward kwargs: %s", unused_kwargs)
250
+
251
+ if output_attentions:
252
+ logger.warning_once(
253
+ "Multiscreen has no softmax attention weights; `output_attentions=True` returns None."
254
+ )
255
+
256
+ output_hidden_states = (
257
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
258
+ )
259
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
260
+ requested_cache = self.config.use_cache if use_cache is None else bool(use_cache)
261
+
262
+ if input_ids is not None and inputs_embeds is not None:
263
+ raise ValueError("Pass either input_ids or inputs_embeds, not both.")
264
+ if input_ids is None and inputs_embeds is None:
265
+ raise ValueError("You must pass input_ids or inputs_embeds.")
266
+ if inputs_embeds is not None:
267
+ raise ValueError(
268
+ "Multiscreen does not accept `inputs_embeds` through the public Transformers API. "
269
+ "The reference architecture normalizes token embedding weights before lookup, so raw "
270
+ "embeddings from `get_input_embeddings()` are not equivalent to `input_ids`. "
271
+ "Pass `input_ids` instead."
272
+ )
273
+
274
+ if input_ids is None:
275
+ raise ValueError("input_ids unexpectedly None")
276
+ input_shape = input_ids.shape
277
+ batch_size, seq_len = input_shape
278
+ W_norm = F.normalize(self.embed.weight, dim=-1)
279
+ hidden_states = F.embedding(input_ids, W_norm) * self.s_E.exp()
280
+
281
+ if past_key_values is not None and len(past_key_values) > 0:
282
+ past_length = int(past_key_values[0][0].shape[2])
283
+ else:
284
+ past_length = 0
285
+
286
+ if start_pos is None:
287
+ if position_ids is not None:
288
+ start_pos = self._start_pos_from_position_ids(
289
+ position_ids=position_ids,
290
+ seq_len=seq_len,
291
+ strict=bool(self.config.strict_position_ids),
292
+ )
293
+ else:
294
+ start_pos = past_length
295
+ elif position_ids is not None:
296
+ logger.warning_once(
297
+ "Multiscreen consumes a scalar `start_pos`; `position_ids` are ignored when `start_pos` is provided."
298
+ )
299
+
300
+ use_cache = requested_cache and (not self.training)
301
+ if requested_cache and self.training:
302
+ logger.warning_once("Multiscreen disables cache materialization while model.training is True.")
303
+
304
+ if self.gradient_checkpointing and self.training and use_cache:
305
+ logger.warning_once("use_cache=True is incompatible with gradient checkpointing; disabling cache.")
306
+ use_cache = False
307
+
308
+ total_length = past_length + seq_len
309
+ key_attention_mask, query_attention_mask = self._prepare_attention_masks(
310
+ attention_mask=attention_mask,
311
+ batch_size=batch_size,
312
+ past_length=past_length,
313
+ seq_len=seq_len,
314
+ total_length=total_length,
315
+ device=hidden_states.device,
316
+ )
317
+ query_mask_3d = (
318
+ query_attention_mask.to(dtype=hidden_states.dtype).unsqueeze(-1)
319
+ if query_attention_mask is not None
320
+ else None
321
+ )
322
+ if self.zero_pad_hidden_states and query_mask_3d is not None:
323
+ hidden_states = hidden_states * query_mask_3d
324
+
325
+ all_hidden_states: tuple[torch.Tensor, ...] | None = () if output_hidden_states else None
326
+ new_key_values: list[ScreeningCache] = []
327
+
328
+ for layer_idx, layer in enumerate(self.layers):
329
+ if output_hidden_states:
330
+ all_hidden_states = all_hidden_states + (hidden_states,) # type: ignore[operator]
331
+
332
+ past_layer = past_key_values[layer_idx] if past_key_values is not None else None
333
+
334
+ if self.gradient_checkpointing and self.training:
335
+ def custom_forward(
336
+ x: torch.Tensor,
337
+ layer_ref: MultiscreenLayer = layer,
338
+ start_pos_ref: int = start_pos,
339
+ key_attention_mask_ref: torch.Tensor | None = key_attention_mask,
340
+ query_attention_mask_ref: torch.Tensor | None = query_attention_mask,
341
+ ) -> torch.Tensor:
342
+ y, _ = layer_ref(
343
+ x,
344
+ start_pos=start_pos_ref,
345
+ past_kv=None,
346
+ use_cache=False,
347
+ key_attention_mask=key_attention_mask_ref,
348
+ query_attention_mask=query_attention_mask_ref,
349
+ )
350
+ return y
351
+
352
+ hidden_states = grad_checkpoint(custom_forward, hidden_states, use_reentrant=False)
353
+ new_kv = None
354
+ else:
355
+ hidden_states, new_kv = layer(
356
+ hidden_states,
357
+ start_pos=start_pos,
358
+ past_kv=past_layer,
359
+ use_cache=use_cache,
360
+ key_attention_mask=key_attention_mask,
361
+ query_attention_mask=query_attention_mask,
362
+ )
363
+
364
+ if self.zero_pad_hidden_states and query_mask_3d is not None:
365
+ hidden_states = hidden_states * query_mask_3d
366
+
367
+ if use_cache:
368
+ if new_kv is None:
369
+ raise RuntimeError("Layer did not return a cache while use_cache=True")
370
+ new_key_values.append(new_kv)
371
+
372
+ if output_hidden_states:
373
+ all_hidden_states = all_hidden_states + (hidden_states,) # type: ignore[operator]
374
+
375
+ past = tuple(new_key_values) if use_cache else None
376
+
377
+ if not return_dict:
378
+ outputs: tuple[Any, ...] = (hidden_states,)
379
+ if past is not None:
380
+ outputs += (past,)
381
+ if all_hidden_states is not None:
382
+ outputs += (all_hidden_states,)
383
+ return outputs # type: ignore[return-value]
384
+
385
+ return BaseModelOutputWithPast(
386
+ last_hidden_state=hidden_states,
387
+ past_key_values=past,
388
+ hidden_states=all_hidden_states,
389
+ attentions=None,
390
+ )
391
+
392
+ @staticmethod
393
+ def _start_pos_from_position_ids(
394
+ *,
395
+ position_ids: torch.LongTensor,
396
+ seq_len: int,
397
+ strict: bool,
398
+ ) -> int:
399
+ """Extract the scalar reference-style ``start_pos`` from position IDs.
400
+
401
+ Multiscreen's reference implementation uses one scalar ``start_pos`` for
402
+ every batch item. Arbitrary per-token or per-batch ``position_ids`` would
403
+ misalign MiPE and the distance softmask, so strict mode fails loudly.
404
+ """
405
+
406
+ if position_ids.dim() != 2:
407
+ raise ValueError("position_ids must have shape (batch, sequence_length)")
408
+ if int(position_ids.shape[1]) != seq_len:
409
+ raise ValueError(
410
+ f"position_ids length {position_ids.shape[1]} does not match input sequence length {seq_len}"
411
+ )
412
+ if seq_len == 0:
413
+ return 0
414
+
415
+ start_pos = int(position_ids[0, 0].item())
416
+ expected = torch.arange(
417
+ start_pos,
418
+ start_pos + seq_len,
419
+ device=position_ids.device,
420
+ dtype=position_ids.dtype,
421
+ ).unsqueeze(0).expand(position_ids.shape[0], -1)
422
+
423
+ if not torch.equal(position_ids, expected):
424
+ message = (
425
+ "Multiscreen only supports batch-shared contiguous position_ids, "
426
+ "because the reference cache API is based on a scalar start_pos. "
427
+ "Pass start_pos explicitly for reference-style decoding, or disable "
428
+ "config.strict_position_ids only if you intentionally want to use "
429
+ "position_ids[0, 0] and ignore the rest."
430
+ )
431
+ if strict:
432
+ raise ValueError(message)
433
+ logger.warning_once(message)
434
+ return start_pos
435
+
436
+ @staticmethod
437
+ def _prepare_attention_masks(
438
+ *,
439
+ attention_mask: torch.Tensor | None,
440
+ batch_size: int,
441
+ past_length: int,
442
+ seq_len: int,
443
+ total_length: int,
444
+ device: torch.device,
445
+ ) -> tuple[torch.Tensor | None, torch.Tensor | None]:
446
+ if attention_mask is None:
447
+ return None, None
448
+
449
+ if attention_mask.dim() != 2:
450
+ raise ValueError("attention_mask must have shape (batch, sequence_length)")
451
+ if attention_mask.shape[0] != batch_size:
452
+ raise ValueError(
453
+ f"attention_mask batch size {attention_mask.shape[0]} does not match input batch {batch_size}"
454
+ )
455
+
456
+ mask = attention_mask.to(device=device)
457
+ mask_len = int(mask.shape[1])
458
+
459
+ if past_length > 0 and mask_len != total_length:
460
+ logger.warning_once(
461
+ "Cached Multiscreen decoding received an attention_mask whose length (%s) "
462
+ "does not cover the full cache length (%s). Omitted past cache positions are "
463
+ "treated as valid. Pass a full-length attention_mask when cached prefixes "
464
+ "contain padding.",
465
+ mask_len,
466
+ total_length,
467
+ )
468
+
469
+ if mask_len == total_length:
470
+ key_mask = mask
471
+ query_mask = mask[:, -seq_len:]
472
+ elif mask_len == seq_len:
473
+ if past_length > 0:
474
+ prefix = torch.ones(batch_size, past_length, device=device, dtype=mask.dtype)
475
+ key_mask = torch.cat([prefix, mask], dim=1)
476
+ else:
477
+ key_mask = mask
478
+ query_mask = mask
479
+ elif mask_len > total_length:
480
+ key_mask = mask[:, -total_length:]
481
+ query_mask = key_mask[:, -seq_len:]
482
+ elif mask_len < total_length:
483
+ # If only a shorter mask is supplied during cached decoding, assume
484
+ # the missing older cache positions are valid.
485
+ prefix = torch.ones(batch_size, total_length - mask_len, device=device, dtype=mask.dtype)
486
+ key_mask = torch.cat([prefix, mask], dim=1)
487
+ query_mask = key_mask[:, -seq_len:]
488
+ else: # pragma: no cover - unreachable, kept for clarity.
489
+ key_mask = mask
490
+ query_mask = mask[:, -seq_len:]
491
+
492
+ return key_mask, query_mask
493
+
494
+
495
+
496
+
497
+ class _NormalizedTiedLMHead(nn.Module):
498
+ """Parameter-free lm_head proxy for trainers that expect ``model.lm_head``.
499
+
500
+ Multiscreen computes logits with the unit-normalized input embedding matrix
501
+ and the learned scalar ``s_F`` instead of a standalone Linear layer. Some
502
+ Hugging Face/TRL training paths look for ``model.lm_head.weight`` to avoid
503
+ materializing full logits. This proxy exposes the mathematically equivalent
504
+ dynamic weight while keeping the true parameters tied to ``embed.weight`` and
505
+ ``s_F``.
506
+ """
507
+
508
+ def __init__(self, owner: "MultiscreenForCausalLM") -> None:
509
+ super().__init__()
510
+ self._owner_ref = weakref.ref(owner)
511
+
512
+ @property
513
+ def weight(self) -> torch.Tensor:
514
+ owner = self._owner_ref()
515
+ if owner is None: # pragma: no cover - defensive only.
516
+ raise RuntimeError("Multiscreen lm_head owner has been garbage-collected")
517
+ W_norm = F.normalize(owner.multiscreen.embed.weight, dim=-1)
518
+ return W_norm * owner.multiscreen.s_F.exp()
519
+
520
+ @property
521
+ def bias(self) -> None:
522
+ return None
523
+
524
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
525
+ return F.linear(hidden_states, self.weight)
526
+
527
+
528
+ class MultiscreenForCausalLM(MultiscreenPreTrainedModel, GenerationMixin):
529
+ """Multiscreen decoder with normalized tied LM head."""
530
+
531
+ # ``self.lm_head`` is a parameter-free proxy whose dynamic ``weight``
532
+ # property is computed from ``multiscreen.embed.weight`` and ``s_F``. There
533
+ # is no registered ``lm_head.weight`` Parameter, so report no explicit tied
534
+ # parameter pair to Transformers.
535
+ _tied_weights_keys: dict[str, str] = {}
536
+
537
+ def __init__(self, config: MultiscreenConfig) -> None:
538
+ if not bool(getattr(config, "tie_word_embeddings", True)):
539
+ raise ValueError(
540
+ "Multiscreen uses normalized tied input/output embeddings; "
541
+ "tie_word_embeddings must be True."
542
+ )
543
+ super().__init__(config)
544
+ self.multiscreen = MultiscreenModel(config)
545
+ # Compatibility shim for TRL/SFTTrainer paths that expect a ``lm_head``
546
+ # attribute. It has no parameters; it dynamically reuses the normalized
547
+ # tied input embeddings exactly like ``_compute_logits``.
548
+ self.lm_head = _NormalizedTiedLMHead(self)
549
+ self.vocab_size = config.vocab_size
550
+ self.post_init()
551
+
552
+ def get_input_embeddings(self) -> nn.Embedding:
553
+ return self.multiscreen.get_input_embeddings()
554
+
555
+ def set_input_embeddings(self, value: nn.Embedding) -> None:
556
+ self.multiscreen.set_input_embeddings(value)
557
+ self.config.vocab_size = value.num_embeddings
558
+ self.vocab_size = value.num_embeddings
559
+
560
+ def get_output_embeddings(self) -> nn.Embedding:
561
+ return self.multiscreen.get_output_embeddings()
562
+
563
+ def set_output_embeddings(self, value: nn.Embedding) -> None:
564
+ self.set_input_embeddings(value)
565
+
566
+ def tie_weights(self, *args: Any, **kwargs: Any) -> None:
567
+ # Output logits directly reuse the normalized input embedding matrix.
568
+ # Recent Transformers releases call tie_weights with keyword arguments
569
+ # such as recompute_mapping=... or missing_keys=...; accept and ignore
570
+ # them because Multiscreen ties weights by construction.
571
+ return None
572
+
573
+ @staticmethod
574
+ def convert_original_state_dict(state_dict: Mapping[str, torch.Tensor]) -> dict[str, torch.Tensor]:
575
+ """Convert original ``multiscreen-pytorch`` checkpoint keys for this class."""
576
+
577
+ return convert_original_state_dict_for_causal_lm(state_dict)
578
+
579
+ def _compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
580
+ return self.lm_head(hidden_states)
581
+
582
+ @staticmethod
583
+ def _coerce_optional_bool(value: Any, name: str) -> bool | None:
584
+ """Convert Python or collated tensor booleans to a scalar bool.
585
+
586
+ ``PackedTextDataset`` can emit a scalar ``labels_are_shifted`` flag so a
587
+ standard Transformers data collator/Trainer forwards it to the model.
588
+ After collation this arrives as a batch tensor; mixed True/False values
589
+ in one batch are rejected because a single loss path must be chosen.
590
+ """
591
+
592
+ if value is None:
593
+ return None
594
+ if isinstance(value, torch.Tensor):
595
+ if value.numel() == 0:
596
+ return None
597
+ bool_values = value.detach().to(dtype=torch.bool).flatten()
598
+ has_true = bool(bool_values.any().item())
599
+ has_false = bool((~bool_values).any().item())
600
+ if has_true and has_false:
601
+ raise ValueError(f"{name} must be the same for every item in a batch.")
602
+ return has_true
603
+ return bool(value)
604
+
605
+ def forward(
606
+ self,
607
+ input_ids: torch.LongTensor | None = None,
608
+ attention_mask: torch.Tensor | None = None,
609
+ position_ids: torch.LongTensor | None = None,
610
+ past_key_values: Sequence[ScreeningCache] | None = None,
611
+ inputs_embeds: torch.Tensor | None = None,
612
+ labels: torch.LongTensor | None = None,
613
+ use_cache: bool | None = None,
614
+ output_attentions: bool | None = None,
615
+ output_hidden_states: bool | None = None,
616
+ return_dict: bool | None = None,
617
+ start_pos: int | None = None,
618
+ labels_are_shifted: bool | None = None,
619
+ legacy_shifted_labels: bool | None = None,
620
+ logits_to_keep: int = 0,
621
+ **kwargs: Any,
622
+ ) -> CausalLMOutputWithPast | tuple[torch.Tensor, ...]:
623
+ # Compatibility with Trainer/TRL/Transformers call paths.
624
+ # ``use_return_dict`` is a deprecated alias that may still be forwarded,
625
+ # and ``num_items_in_batch`` can be injected by recent Trainer versions
626
+ # when a model forward has **kwargs. Multiscreen does not consume it.
627
+ use_return_dict = kwargs.pop("use_return_dict", None)
628
+ if return_dict is None and use_return_dict is not None:
629
+ return_dict = bool(use_return_dict)
630
+ kwargs.pop("num_items_in_batch", None)
631
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
632
+
633
+ labels_are_shifted_value = self._coerce_optional_bool(labels_are_shifted, "labels_are_shifted")
634
+ legacy_shifted_labels_value = self._coerce_optional_bool(
635
+ legacy_shifted_labels, "legacy_shifted_labels"
636
+ )
637
+ if labels_are_shifted_value is not None and legacy_shifted_labels_value is not None:
638
+ if labels_are_shifted_value != legacy_shifted_labels_value:
639
+ raise ValueError("labels_are_shifted and legacy_shifted_labels disagree.")
640
+ if labels_are_shifted_value is None:
641
+ labels_are_shifted = (
642
+ legacy_shifted_labels_value
643
+ if legacy_shifted_labels_value is not None
644
+ else bool(getattr(self.config, "labels_are_shifted", False))
645
+ )
646
+ else:
647
+ labels_are_shifted = labels_are_shifted_value
648
+
649
+ kv_caches = kwargs.pop("kv_caches", None)
650
+ if kv_caches is not None:
651
+ if past_key_values is not None:
652
+ raise ValueError("Pass only one of `past_key_values` or original-api `kv_caches`, not both.")
653
+ past_key_values = kv_caches
654
+
655
+ if kwargs:
656
+ # Keep forward permissive for HF/TRL extras, but do not pass them to
657
+ # the bare decoder where they would only generate duplicate warnings.
658
+ unused_kwargs = ", ".join(sorted(str(key) for key in kwargs.keys()))
659
+ logger.warning_once("Unused MultiscreenForCausalLM.forward kwargs: %s", unused_kwargs)
660
+
661
+ model_outputs = self.multiscreen(
662
+ input_ids=input_ids,
663
+ attention_mask=attention_mask,
664
+ position_ids=position_ids,
665
+ past_key_values=past_key_values,
666
+ inputs_embeds=inputs_embeds,
667
+ use_cache=use_cache,
668
+ output_attentions=output_attentions,
669
+ output_hidden_states=output_hidden_states,
670
+ return_dict=True,
671
+ start_pos=start_pos,
672
+ )
673
+ hidden_states = model_outputs.last_hidden_state
674
+
675
+ if labels is None and logits_to_keep and logits_to_keep > 0:
676
+ logits_hidden_states = hidden_states[:, -logits_to_keep:, :]
677
+ else:
678
+ logits_hidden_states = hidden_states
679
+
680
+ logits = self._compute_logits(logits_hidden_states)
681
+ loss = None
682
+
683
+ if labels is not None:
684
+ if logits.shape[1] != labels.shape[1]:
685
+ # This can only happen if a caller forced logits_to_keep with labels.
686
+ logits = self._compute_logits(hidden_states)
687
+
688
+ loss_labels = labels.to(device=logits.device).clone()
689
+ loss_attention_mask = None
690
+ if attention_mask is not None:
691
+ loss_attention_mask = self._slice_loss_attention_mask(
692
+ attention_mask=attention_mask,
693
+ target_length=loss_labels.shape[1],
694
+ device=loss_labels.device,
695
+ )
696
+ loss_labels = loss_labels.masked_fill(loss_attention_mask == 0, -100)
697
+
698
+ loss_fct = nn.CrossEntropyLoss(ignore_index=-100)
699
+ if labels_are_shifted:
700
+ loss = loss_fct(
701
+ logits.reshape(-1, self.config.vocab_size),
702
+ loss_labels.reshape(-1),
703
+ )
704
+ else:
705
+ if logits.shape[1] < 2:
706
+ loss = logits.new_zeros(())
707
+ else:
708
+ shift_logits = logits[..., :-1, :].contiguous()
709
+ shift_labels = loss_labels[..., 1:].contiguous()
710
+ if loss_attention_mask is not None:
711
+ # Ignore predictions where either the query token or the
712
+ # target token is padding. This avoids a left-padding edge
713
+ # case where the last pad token predicts the first real token.
714
+ valid_shift = (loss_attention_mask[..., :-1] != 0) & (
715
+ loss_attention_mask[..., 1:] != 0
716
+ )
717
+ shift_labels = shift_labels.masked_fill(~valid_shift, -100)
718
+ loss = loss_fct(
719
+ shift_logits.reshape(-1, self.config.vocab_size),
720
+ shift_labels.reshape(-1),
721
+ )
722
+
723
+ if not return_dict:
724
+ output: tuple[Any, ...] = (logits,)
725
+ if model_outputs.past_key_values is not None:
726
+ output += (model_outputs.past_key_values,)
727
+ if model_outputs.hidden_states is not None:
728
+ output += (model_outputs.hidden_states,)
729
+ return ((loss,) + output) if loss is not None else output # type: ignore[return-value]
730
+
731
+ return CausalLMOutputWithPast(
732
+ loss=loss,
733
+ logits=logits,
734
+ past_key_values=model_outputs.past_key_values,
735
+ hidden_states=model_outputs.hidden_states,
736
+ attentions=None,
737
+ )
738
+
739
+ @staticmethod
740
+ def _slice_loss_attention_mask(
741
+ *,
742
+ attention_mask: torch.Tensor,
743
+ target_length: int,
744
+ device: torch.device,
745
+ ) -> torch.Tensor:
746
+ if attention_mask.dim() != 2:
747
+ raise ValueError("attention_mask must have shape (batch, sequence_length)")
748
+ mask = attention_mask.to(device=device)
749
+ mask_length = int(mask.shape[1])
750
+ if mask_length == target_length:
751
+ return mask
752
+ if mask_length > target_length:
753
+ return mask[:, -target_length:]
754
+ prefix = torch.ones(
755
+ mask.shape[0],
756
+ target_length - mask_length,
757
+ device=device,
758
+ dtype=mask.dtype,
759
+ )
760
+ return torch.cat([prefix, mask], dim=1)
761
+
762
+ def prepare_inputs_for_generation(
763
+ self,
764
+ input_ids: torch.LongTensor,
765
+ past_key_values: Sequence[ScreeningCache] | None = None,
766
+ attention_mask: torch.Tensor | None = None,
767
+ cache_position: torch.LongTensor | None = None,
768
+ position_ids: torch.LongTensor | None = None,
769
+ start_pos: int | None = None,
770
+ use_cache: bool | None = True,
771
+ **kwargs: Any,
772
+ ) -> dict[str, Any]:
773
+ """Prepare inputs for ``GenerationMixin.generate``.
774
+
775
+ Multiscreen keeps a simple tuple cache. When a cache is present, the
776
+ method slices ``input_ids`` to the new suffix and sets a scalar
777
+ ``start_pos`` equal to the cached sequence length.
778
+ """
779
+
780
+ kv_caches = kwargs.pop("kv_caches", None)
781
+ if kv_caches is not None:
782
+ if past_key_values is not None:
783
+ raise ValueError("Pass only one of `past_key_values` or original-api `kv_caches`, not both.")
784
+ past_key_values = kv_caches
785
+
786
+ if past_key_values is not None and len(past_key_values) > 0:
787
+ past_length = int(past_key_values[0][0].shape[2])
788
+ if input_ids.shape[1] > past_length:
789
+ input_ids = input_ids[:, past_length:]
790
+ else:
791
+ input_ids = input_ids[:, -1:]
792
+ # Cache length is the source of truth during generation. A stale
793
+ # explicit start_pos or arbitrary position_ids would misalign
794
+ # MiPE/softmask positions.
795
+ start_pos = past_length
796
+ else:
797
+ if start_pos is None:
798
+ if cache_position is not None and cache_position.numel() > 0:
799
+ start_pos = int(cache_position[0].item())
800
+ elif position_ids is not None:
801
+ start_pos = MultiscreenModel._start_pos_from_position_ids(
802
+ position_ids=position_ids,
803
+ seq_len=int(input_ids.shape[1]),
804
+ strict=bool(self.config.strict_position_ids),
805
+ )
806
+ else:
807
+ start_pos = 0
808
+
809
+ # The model consumes scalar `start_pos`; forwarding arbitrary
810
+ # `position_ids` would give the false impression that batch-specific
811
+ # offsets are honored.
812
+ position_ids = None
813
+
814
+ model_inputs = {
815
+ "input_ids": input_ids,
816
+ "attention_mask": attention_mask,
817
+ "position_ids": position_ids,
818
+ "past_key_values": past_key_values,
819
+ "use_cache": use_cache,
820
+ "start_pos": start_pos,
821
+ }
822
+ return model_inputs
823
+
824
+ @staticmethod
825
+ def _reorder_cache(
826
+ past_key_values: Sequence[ScreeningCache], beam_idx: torch.LongTensor
827
+ ) -> tuple[ScreeningCache, ...]:
828
+ """Beam-search cache reordering."""
829
+
830
+ reordered: list[ScreeningCache] = []
831
+ for key_cache, value_cache in past_key_values:
832
+ beam_idx_device = beam_idx.to(key_cache.device)
833
+ reordered.append(
834
+ (
835
+ key_cache.index_select(0, beam_idx_device),
836
+ value_cache.index_select(0, beam_idx_device.to(value_cache.device)),
837
+ )
838
+ )
839
+ return tuple(reordered)
840
+
841
+
842
+ class MultiscreenLayer(nn.Module):
843
+ """Single residual Multiscreen layer."""
844
+
845
+ def __init__(self, config: MultiscreenConfig, layer_idx: int) -> None:
846
+ super().__init__()
847
+ self.block = GatedScreeningBlock(config, layer_idx)
848
+
849
+ def forward(
850
+ self,
851
+ x: torch.Tensor,
852
+ start_pos: int = 0,
853
+ past_kv: ScreeningCache | None = None,
854
+ use_cache: bool = False,
855
+ key_attention_mask: torch.Tensor | None = None,
856
+ query_attention_mask: torch.Tensor | None = None,
857
+ ) -> tuple[torch.Tensor, ScreeningCache | None]:
858
+ block_out, new_kv = self.block(
859
+ x,
860
+ start_pos=start_pos,
861
+ past_kv=past_kv,
862
+ use_cache=use_cache,
863
+ key_attention_mask=key_attention_mask,
864
+ query_attention_mask=query_attention_mask,
865
+ )
866
+ if query_attention_mask is not None:
867
+ block_out = block_out * query_attention_mask.to(dtype=block_out.dtype).unsqueeze(-1)
868
+ return x + block_out, new_kv
869
+
870
+
871
+ class GatedScreeningBlock(nn.Module):
872
+ """Parallel gated screening tiles for one Multiscreen layer.
873
+
874
+ Each tile performs Q/K/V/G projection, unit normalization, MiPE, independent
875
+ screening, TanhNorm, bounded gating, per-head scaling, and output projection.
876
+ """
877
+
878
+ def __init__(self, config: MultiscreenConfig, layer_idx: int) -> None:
879
+ super().__init__()
880
+ d_e = config.hidden_size
881
+ d_k = config.key_dim
882
+ d_v = config.value_dim
883
+ num_heads = config.num_attention_heads
884
+ num_layers = config.num_hidden_layers
885
+
886
+ self.layer_idx = layer_idx
887
+ self.NH = num_heads
888
+ self.dK = d_k
889
+ self.dV = d_v
890
+ self.wth = float(config.mipe_threshold)
891
+ self.max_seq_len = int(config.max_position_embeddings)
892
+ self.mipe_compute_dtype = str(config.mipe_compute_dtype)
893
+ self.softmask_compute_dtype = str(config.softmask_compute_dtype)
894
+
895
+ self.q_proj = nn.Linear(d_e, num_heads * d_k, bias=False)
896
+ self.k_proj = nn.Linear(d_e, num_heads * d_k, bias=False)
897
+ self.v_proj = nn.Linear(d_e, num_heads * d_v, bias=False)
898
+ self.g_proj = nn.Linear(d_e, num_heads * d_v, bias=False)
899
+ self.o_proj = nn.Linear(num_heads * d_v, d_e, bias=False)
900
+
901
+ # Per-head learned parameters from the reference implementation.
902
+ self.sw = nn.Parameter(torch.linspace(0, math.log(self.wth), num_heads))
903
+ self.sr = nn.Parameter(torch.zeros(num_heads))
904
+ self.sO = nn.Parameter(torch.full((num_heads,), math.log(1.0 / math.sqrt(num_heads * num_layers))))
905
+
906
+ init = config.initializer_range
907
+ nn.init.normal_(self.q_proj.weight, mean=0.0, std=init / math.sqrt(d_k))
908
+ nn.init.normal_(self.k_proj.weight, mean=0.0, std=init / math.sqrt(d_k))
909
+ nn.init.normal_(self.v_proj.weight, mean=0.0, std=init / math.sqrt(d_v))
910
+ nn.init.normal_(self.g_proj.weight, mean=0.0, std=init)
911
+ nn.init.normal_(self.o_proj.weight, mean=0.0, std=init / math.sqrt(d_e))
912
+
913
+ def forward(
914
+ self,
915
+ x: torch.Tensor,
916
+ start_pos: int = 0,
917
+ past_kv: ScreeningCache | None = None,
918
+ use_cache: bool = False,
919
+ key_attention_mask: torch.Tensor | None = None,
920
+ query_attention_mask: torch.Tensor | None = None,
921
+ ) -> tuple[torch.Tensor, ScreeningCache | None]:
922
+ batch_size, seq_len, _ = x.shape
923
+
924
+ q = self.q_proj(x).view(batch_size, seq_len, self.NH, self.dK)
925
+ k_new = self.k_proj(x).view(batch_size, seq_len, self.NH, self.dK)
926
+ v_new = self.v_proj(x).view(batch_size, seq_len, self.NH, self.dV)
927
+ g = self.g_proj(x).view(batch_size, seq_len, self.NH, self.dV)
928
+
929
+ u, new_kv = self._screening(
930
+ q=q,
931
+ k_new=k_new,
932
+ v_new=v_new,
933
+ start_pos=start_pos,
934
+ past_kv=past_kv,
935
+ use_cache=use_cache,
936
+ key_attention_mask=key_attention_mask,
937
+ )
938
+
939
+ g_hat = torch.tanh(F.silu(g))
940
+ h = u * g_hat
941
+ if query_attention_mask is not None:
942
+ h = h * query_attention_mask.to(dtype=h.dtype).view(batch_size, seq_len, 1, 1)
943
+ h = h * self.sO.exp().view(1, 1, self.NH, 1)
944
+ h = h.reshape(batch_size, seq_len, self.NH * self.dV)
945
+ return self.o_proj(h), new_kv
946
+
947
+ def _screening(
948
+ self,
949
+ q: torch.Tensor,
950
+ k_new: torch.Tensor,
951
+ v_new: torch.Tensor,
952
+ start_pos: int = 0,
953
+ past_kv: ScreeningCache | None = None,
954
+ use_cache: bool = False,
955
+ key_attention_mask: torch.Tensor | None = None,
956
+ ) -> tuple[torch.Tensor, ScreeningCache | None]:
957
+ """Screening unit with optional per-layer KV cache."""
958
+
959
+ q = F.normalize(q, dim=-1)
960
+ k_new = F.normalize(k_new, dim=-1)
961
+ v_new = F.normalize(v_new, dim=-1)
962
+
963
+ w = self.sw.exp() + 1.0
964
+ r = self.sr.exp() + 1.0
965
+
966
+ q, k_new = self._apply_mipe(q, k_new, w, start_pos=start_pos)
967
+
968
+ q = q.transpose(1, 2)
969
+ k_new = k_new.transpose(1, 2)
970
+ v_new = v_new.transpose(1, 2)
971
+
972
+ if past_kv is not None:
973
+ past_k, past_v = past_kv
974
+ full_k = torch.cat([past_k, k_new], dim=2)
975
+ full_v = torch.cat([past_v, v_new], dim=2)
976
+ else:
977
+ full_k = k_new
978
+ full_v = v_new
979
+
980
+ seq_len = q.shape[2]
981
+ total_length = full_k.shape[2]
982
+
983
+ sim = torch.matmul(q, full_k.transpose(-2, -1))
984
+ mask = self._softmask(
985
+ T_new=seq_len,
986
+ T_total=total_length,
987
+ start_pos=start_pos,
988
+ w=w,
989
+ device=sim.device,
990
+ dtype=sim.dtype,
991
+ key_attention_mask=key_attention_mask,
992
+ )
993
+
994
+ rho_d = torch.clamp(
995
+ 1.0 - r.view(1, -1, 1, 1).to(dtype=sim.dtype) * (1.0 - sim),
996
+ min=0.0,
997
+ ).square_().mul_(mask)
998
+
999
+ h = torch.matmul(rho_d, full_v)
1000
+ h_norm = h.norm(dim=-1, keepdim=True).clamp(min=1e-8)
1001
+ u = (torch.tanh(h_norm) / h_norm) * h
1002
+
1003
+ new_kv = (full_k, full_v) if use_cache else None
1004
+ return u.transpose(1, 2), new_kv
1005
+
1006
+ @staticmethod
1007
+ def _select_compute_dtype(input_dtype: torch.dtype, mode: str) -> torch.dtype:
1008
+ if mode == "fp32":
1009
+ return torch.float32
1010
+ if mode == "reference":
1011
+ return input_dtype
1012
+ raise ValueError(f"Unknown Multiscreen compute dtype mode: {mode!r}")
1013
+
1014
+ def _apply_mipe(
1015
+ self,
1016
+ q: torch.Tensor,
1017
+ k: torch.Tensor,
1018
+ w: torch.Tensor,
1019
+ start_pos: int = 0,
1020
+ ) -> tuple[torch.Tensor, torch.Tensor]:
1021
+ """Minimal positional encoding on the first two Q/K coordinates."""
1022
+
1023
+ seq_len = q.shape[1]
1024
+ compute_dtype = self._select_compute_dtype(q.dtype, self.mipe_compute_dtype)
1025
+ w_float = w.to(device=q.device, dtype=compute_dtype)
1026
+ phi = torch.where(
1027
+ w_float < self.wth,
1028
+ 0.5 * (torch.cos(math.pi * w_float / self.wth) + 1.0),
1029
+ torch.zeros_like(w_float),
1030
+ )
1031
+
1032
+ positions = torch.arange(start_pos, start_pos + seq_len, device=q.device, dtype=compute_dtype)
1033
+ pos_2d = positions.unsqueeze(1)
1034
+ w_2d = w_float.unsqueeze(0)
1035
+ effective_pos = torch.where(pos_2d >= self.max_seq_len, pos_2d % w_2d, pos_2d)
1036
+ angles = effective_pos * (math.pi * phi / w_float).unsqueeze(0)
1037
+
1038
+ cos_a = torch.cos(angles).to(dtype=q.dtype)
1039
+ sin_a = torch.sin(angles).to(dtype=q.dtype)
1040
+
1041
+ q0, q1 = q[..., 0], q[..., 1]
1042
+ k0, k1 = k[..., 0], k[..., 1]
1043
+
1044
+ q_rot = torch.empty_like(q)
1045
+ q_rot[..., 0] = q0 * cos_a - q1 * sin_a
1046
+ q_rot[..., 1] = q0 * sin_a + q1 * cos_a
1047
+ q_rot[..., 2:] = q[..., 2:]
1048
+
1049
+ k_rot = torch.empty_like(k)
1050
+ k_rot[..., 0] = k0 * cos_a - k1 * sin_a
1051
+ k_rot[..., 1] = k0 * sin_a + k1 * cos_a
1052
+ k_rot[..., 2:] = k[..., 2:]
1053
+ return q_rot, k_rot
1054
+
1055
+ def _softmask(
1056
+ self,
1057
+ T_new: int,
1058
+ T_total: int,
1059
+ start_pos: int,
1060
+ w: torch.Tensor,
1061
+ device: torch.device,
1062
+ dtype: torch.dtype,
1063
+ key_attention_mask: torch.Tensor | None = None,
1064
+ ) -> torch.Tensor:
1065
+ """Causal distance-aware softmask for new queries over all keys."""
1066
+
1067
+ compute_dtype = self._select_compute_dtype(dtype, self.softmask_compute_dtype)
1068
+ q_pos = torch.arange(start_pos, start_pos + T_new, device=device, dtype=compute_dtype)
1069
+ k_pos = torch.arange(T_total, device=device, dtype=compute_dtype)
1070
+ rel = k_pos.unsqueeze(0) - q_pos.unsqueeze(1)
1071
+
1072
+ w_exp = w.to(device=device, dtype=compute_dtype).view(-1, 1, 1)
1073
+ valid = (rel <= 0) & (rel > -w_exp)
1074
+ mask = (0.5 * (torch.cos(math.pi * rel / w_exp) + 1.0)) * valid
1075
+ mask = mask.unsqueeze(0).to(dtype=dtype)
1076
+
1077
+ if key_attention_mask is not None:
1078
+ if key_attention_mask.shape[1] != T_total:
1079
+ raise ValueError(
1080
+ f"key_attention_mask length {key_attention_mask.shape[1]} must equal total key length {T_total}"
1081
+ )
1082
+ mask = mask * key_attention_mask.to(device=device, dtype=dtype).view(-1, 1, 1, T_total)
1083
+
1084
+ return mask