abe123 commited on
Commit
2719787
·
verified ·
1 Parent(s): cede046

Publish final EMA checkpoint

Browse files
README.md CHANGED
@@ -1,3 +1,56 @@
1
  ---
2
  license: apache-2.0
 
 
 
 
 
 
 
 
 
 
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
+ library_name: transformers
4
+ pipeline_tag: text-generation
5
+ tags:
6
+ - babylm
7
+ - babylm-2026
8
+ - causal-lm
9
+ - dual-attention-transformer
10
+ - nextlat
11
+ - ema
12
+ - custom-code
13
  ---
14
+
15
+ # DAT Strict NextLat Final
16
+
17
+ DAT Strict NextLat Final is a causal Dual Attention Transformer language model trained from scratch for the BabyLM 2026 Strict track. It combines self-attention, relational attention, position-relative symbols, and a NextLat auxiliary training objective. The repository contains the tokenizer and custom HuggingFace Transformers code required by the Auto classes.
18
+
19
+ ## Architecture
20
+
21
+ - 16 transformer layers
22
+ - Hidden dimension 1,024
23
+ - 12 self-attention heads and 4 relational-attention heads
24
+ - Rotary token positional encoding
25
+ - Shared position-relative symbol retrieval without relative-symbol RoPE
26
+ - RCA relational attention
27
+ - SwiGLU feed-forward layers
28
+ - Vocabulary size 16,384
29
+ - Maximum model sequence length 514
30
+ - Untied token embedding and language-model head
31
+
32
+ ## Training
33
+
34
+ The model was trained for ten passes through the BabyLM 2026 Strict corpus under the 100 million word data limit. Training used batches of 42 sequences of 512 tokens, seed 1, Muon for hidden matrix parameters, and LambW for the remaining parameters. The Muon learning rate was 0.02, the auxiliary learning rate was 0.001, weight decay was 0.01, and the schedule used one percent warmup followed by cosine decay to ten percent of the initial rate.
35
+
36
+ NextLat used horizon 1 with cross-entropy weight 0.0, KL weight 0.5, and MSE weight 1.0. An exponential moving average with decay 0.999 was maintained during training. The published final model and checkpoint revisions use the EMA weights. No teacher model, synthetic data augmentation, or multimodal input was used.
37
+
38
+ ## Checkpoints
39
+
40
+ The final EMA model is stored on `main`. Intermediate EMA checkpoints use the official BabyLM Strict revision names from `chck_1M` through `chck_1000M`. These names represent cumulative words processed across repeated passes through the corpus.
41
+
42
+ ## Loading
43
+
44
+ ```python
45
+ from transformers import AutoModelForCausalLM, AutoTokenizer
46
+
47
+ repo_id = "abe123/babylm-dat-strict-nextlat-final"
48
+ tokenizer = AutoTokenizer.from_pretrained(repo_id, trust_remote_code=True)
49
+ model = AutoModelForCausalLM.from_pretrained(repo_id, trust_remote_code=True)
50
+ ```
51
+
52
+ Use an explicit immutable commit SHA for reproducible evaluation.
53
+
54
+ ## Intended use and limitations
55
+
56
+ This checkpoint is intended for BabyLM evaluation and language-model research. It uses custom model code and therefore requires `trust_remote_code=True` when loaded through HuggingFace Auto classes.
attention_config.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Shared attention configuration constants."""
2
+
3
+ SUPPORTED_SEQUENCE_BOUNDARY_POLICIES = frozenset(
4
+ {"eos_document", "none", "segment_document"}
5
+ )
config.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DatForCausalLM"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_dat.DatConfig",
7
+ "AutoModel": "modeling_dat.DatModel",
8
+ "AutoModelForCausalLM": "modeling_dat.DatForCausalLM",
9
+ "AutoModelForMaskedLM": "modeling_dat.DatForMaskedLM",
10
+ "AutoModelForSequenceClassification": "modeling_dat.DatForSequenceClassification"
11
+ },
12
+ "bos_token_id": 1,
13
+ "dff_factor": 4,
14
+ "dropout": 0.1,
15
+ "dtype": "float32",
16
+ "eos_token_id": 2,
17
+ "ffn_activation": "swiglu",
18
+ "ffn_hidden_dim_mode": "dff_factor",
19
+ "hidden_dim": 1024,
20
+ "init_range": 0.15,
21
+ "init_scheme": "normal_0_02_scaled_projection",
22
+ "max_position_embeddings": 514,
23
+ "max_rel_pos": 512,
24
+ "max_seq_len": 514,
25
+ "mlm_head_enabled": false,
26
+ "model_type": "dat",
27
+ "n_heads_ra": 4,
28
+ "n_heads_sa": 12,
29
+ "n_layers": 16,
30
+ "n_symbols": null,
31
+ "norm_first": true,
32
+ "norm_type": "layernorm",
33
+ "pad_token_id": 3,
34
+ "pe_type": "rope",
35
+ "positional_symbols_sinusoidal": false,
36
+ "ra_n_relations": null,
37
+ "ra_rel_activation": "identity",
38
+ "ra_symmetric_rels": false,
39
+ "ra_type": "rca",
40
+ "relative_symbols_rope": false,
41
+ "relsymbolic_dropout": 0.0,
42
+ "relsymbolic_include_self": false,
43
+ "relsymbolic_neighborhood_size": 2,
44
+ "relsymbolic_normalize_rels": true,
45
+ "relsymbolic_rel_n_heads": 4,
46
+ "relsymbolic_rel_scale": null,
47
+ "relsymbolic_symbolic_attn_n_heads": 4,
48
+ "relsymbolic_symbolic_attn_scale": null,
49
+ "relsymbolic_trainable_symbols": true,
50
+ "relsymbolic_use_bias": false,
51
+ "rope_theta": 10000.0,
52
+ "segment_boundary_token_id": null,
53
+ "sequence_boundary_policy": "eos_document",
54
+ "share_attn_params": false,
55
+ "shared_symbol_retriever": true,
56
+ "symbol_dim": null,
57
+ "symbol_retrieval": "relative",
58
+ "symbolic_attn_n_heads": null,
59
+ "symbolic_use_bias": false,
60
+ "tie_lm_head": false,
61
+ "tie_word_embeddings": false,
62
+ "transformers_version": "5.9.0",
63
+ "use_bias_ffn": true,
64
+ "use_bias_out": false,
65
+ "use_bias_qkv": false,
66
+ "vocab_size": 16384
67
+ }
configuration_dat.py ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HuggingFace PretrainedConfig for the dual-attention (DAT) decoder LM.
2
+
3
+ This is the source-of-truth copy. scripts/convert_dat_to_hf.py copies it into a
4
+ generated HF repository (alongside modeling_dat.py and the flattened model
5
+ source) so the model can be loaded with
6
+ ``AutoModelForCausalLM.from_pretrained(path, trust_remote_code=True)``.
7
+
8
+ The config simply carries every field of models.attention.dat.config.DatLMConfig
9
+ so that modeling_dat.py can rebuild the exact DatLMConfig at load time.
10
+ """
11
+
12
+ from transformers import PretrainedConfig
13
+
14
+
15
+ # Every field needed to rebuild DatLMConfig. New exported fields are appended
16
+ # after existing constructor parameters to preserve positional compatibility.
17
+ # modeling_dat.py rebuilds DatLMConfig as
18
+ # DatLMConfig(**{f: getattr(config, f) for f in DAT_LM_FIELDS}).
19
+ DAT_LM_FIELDS = (
20
+ "vocab_size",
21
+ "max_seq_len",
22
+ "pe_type",
23
+ "hidden_dim",
24
+ "n_heads_sa",
25
+ "n_heads_ra",
26
+ "n_layers",
27
+ "dropout",
28
+ "dff_factor",
29
+ "ffn_hidden_dim_mode",
30
+ "ffn_activation",
31
+ "rope_theta",
32
+ "max_rel_pos",
33
+ "init_range",
34
+ "init_scheme",
35
+ "norm_type",
36
+ "norm_first",
37
+ "use_bias_qkv",
38
+ "use_bias_out",
39
+ "use_bias_ffn",
40
+ "tie_lm_head",
41
+ "symbol_dim",
42
+ "n_symbols",
43
+ "symbolic_attn_n_heads",
44
+ "symbol_retrieval",
45
+ "symbolic_use_bias",
46
+ "shared_symbol_retriever",
47
+ "share_attn_params",
48
+ "positional_symbols_sinusoidal",
49
+ "relative_symbols_rope",
50
+ "relsymbolic_rel_n_heads",
51
+ "relsymbolic_symbolic_attn_n_heads",
52
+ "relsymbolic_neighborhood_size",
53
+ "relsymbolic_include_self",
54
+ "relsymbolic_normalize_rels",
55
+ "relsymbolic_trainable_symbols",
56
+ "relsymbolic_dropout",
57
+ "relsymbolic_rel_scale",
58
+ "relsymbolic_symbolic_attn_scale",
59
+ "relsymbolic_use_bias",
60
+ "ra_type",
61
+ "ra_n_relations",
62
+ "ra_rel_activation",
63
+ "ra_symmetric_rels",
64
+ "sequence_boundary_policy",
65
+ "segment_boundary_token_id",
66
+ "pad_token_id",
67
+ "bos_token_id",
68
+ "eos_token_id",
69
+ "mlm_head_enabled",
70
+ )
71
+
72
+
73
+ class DatConfig(PretrainedConfig):
74
+ model_type = "dat"
75
+ # Expose the model width under HF's conventional name so generic eval
76
+ # harnesses (e.g. babylm-eval's finetuning head, which reads
77
+ # config.hidden_size) can size their classification head. The model's own
78
+ # field is hidden_dim; this maps the alias onto it for both get and set.
79
+ attribute_map = {"hidden_size": "hidden_dim"}
80
+
81
+ def __init__(
82
+ self,
83
+ vocab_size: int = 16384,
84
+ max_seq_len: int = 513,
85
+ pe_type: str = "rope",
86
+ hidden_dim: int = 256,
87
+ n_heads_sa: int = 2,
88
+ n_heads_ra: int = 2,
89
+ n_layers: int = 4,
90
+ dropout: float = 0.0,
91
+ dff_factor: int = 4,
92
+ ffn_hidden_dim_mode: str = "dff_factor",
93
+ ffn_activation: str = "gelu",
94
+ rope_theta: float = 10000.0,
95
+ max_rel_pos: int | None = None,
96
+ init_range: float = 0.15,
97
+ init_scheme: str = "xavier_uniform",
98
+ norm_type: str = "rmsnorm",
99
+ norm_first: bool = True,
100
+ use_bias_qkv: bool = False,
101
+ use_bias_out: bool = True,
102
+ use_bias_ffn: bool = True,
103
+ tie_lm_head: bool = True,
104
+ symbol_dim: int | None = None,
105
+ n_symbols: int | None = None,
106
+ symbolic_attn_n_heads: int | None = None,
107
+ symbol_retrieval: str = "symbolic",
108
+ symbolic_use_bias: bool = False,
109
+ shared_symbol_retriever: bool = True,
110
+ share_attn_params: bool = False,
111
+ positional_symbols_sinusoidal: bool = False,
112
+ relative_symbols_rope: bool = False,
113
+ relsymbolic_rel_n_heads: int = 4,
114
+ relsymbolic_symbolic_attn_n_heads: int = 4,
115
+ relsymbolic_neighborhood_size: int = 2,
116
+ relsymbolic_include_self: bool = False,
117
+ relsymbolic_normalize_rels: bool = True,
118
+ relsymbolic_trainable_symbols: bool = True,
119
+ relsymbolic_dropout: float = 0.0,
120
+ relsymbolic_rel_scale: float | None = None,
121
+ relsymbolic_symbolic_attn_scale: float | None = None,
122
+ relsymbolic_use_bias: bool = False,
123
+ ra_type: str = "ra",
124
+ ra_n_relations: int | None = None,
125
+ ra_rel_activation: str = "identity",
126
+ ra_symmetric_rels: bool = False,
127
+ sequence_boundary_policy: str = "eos_document",
128
+ segment_boundary_token_id: int | None = None,
129
+ pad_token_id: int = 0,
130
+ bos_token_id: int = 1,
131
+ eos_token_id: int = 2,
132
+ mlm_head_enabled: bool = False,
133
+ **kwargs,
134
+ ) -> None:
135
+ self.vocab_size = vocab_size
136
+ self.max_seq_len = max_seq_len
137
+ self.pe_type = pe_type
138
+ self.hidden_dim = hidden_dim
139
+ self.n_heads_sa = n_heads_sa
140
+ self.n_heads_ra = n_heads_ra
141
+ self.n_layers = n_layers
142
+ self.dropout = dropout
143
+ self.dff_factor = dff_factor
144
+ self.ffn_hidden_dim_mode = ffn_hidden_dim_mode
145
+ self.ffn_activation = ffn_activation
146
+ self.rope_theta = rope_theta
147
+ self.max_rel_pos = max_rel_pos
148
+ self.init_range = init_range
149
+ self.init_scheme = init_scheme
150
+ self.norm_type = norm_type
151
+ self.norm_first = norm_first
152
+ self.use_bias_qkv = use_bias_qkv
153
+ self.use_bias_out = use_bias_out
154
+ self.use_bias_ffn = use_bias_ffn
155
+ self.tie_lm_head = tie_lm_head
156
+ self.symbol_dim = symbol_dim
157
+ self.n_symbols = n_symbols
158
+ self.symbolic_attn_n_heads = symbolic_attn_n_heads
159
+ self.symbol_retrieval = symbol_retrieval
160
+ self.symbolic_use_bias = symbolic_use_bias
161
+ self.shared_symbol_retriever = shared_symbol_retriever
162
+ self.share_attn_params = share_attn_params
163
+ self.positional_symbols_sinusoidal = positional_symbols_sinusoidal
164
+ self.relative_symbols_rope = relative_symbols_rope
165
+ self.relsymbolic_rel_n_heads = relsymbolic_rel_n_heads
166
+ self.relsymbolic_symbolic_attn_n_heads = relsymbolic_symbolic_attn_n_heads
167
+ self.relsymbolic_neighborhood_size = relsymbolic_neighborhood_size
168
+ self.relsymbolic_include_self = relsymbolic_include_self
169
+ self.relsymbolic_normalize_rels = relsymbolic_normalize_rels
170
+ self.relsymbolic_trainable_symbols = relsymbolic_trainable_symbols
171
+ self.relsymbolic_dropout = relsymbolic_dropout
172
+ self.relsymbolic_rel_scale = relsymbolic_rel_scale
173
+ self.relsymbolic_symbolic_attn_scale = relsymbolic_symbolic_attn_scale
174
+ self.relsymbolic_use_bias = relsymbolic_use_bias
175
+ self.ra_type = ra_type
176
+ self.ra_n_relations = ra_n_relations
177
+ self.ra_rel_activation = ra_rel_activation
178
+ self.ra_symmetric_rels = ra_symmetric_rels
179
+ self.sequence_boundary_policy = sequence_boundary_policy
180
+ self.segment_boundary_token_id = segment_boundary_token_id
181
+ self.mlm_head_enabled = mlm_head_enabled
182
+ # HF base also exposes max_position_embeddings for generation utilities.
183
+ self.max_position_embeddings = max_seq_len
184
+ # tie_lm_head is the source of truth for weight tying; keep HF's
185
+ # tie_word_embeddings in lockstep so from_pretrained never re-ties an
186
+ # untied head (or fails to tie a tied one). Drop any value coming in via
187
+ # kwargs (e.g. a serialized config.json) so the two cannot disagree.
188
+ # NOTE: tie_lm_head controls only lm_head <-> token_embeddings tying.
189
+ # The mlm_head.linear_out <-> lm_head tying is handled separately by
190
+ # the get_expanded_tied_weights_keys override in modeling_dat.py.
191
+ kwargs.pop("tie_word_embeddings", None)
192
+ super().__init__(
193
+ pad_token_id=pad_token_id,
194
+ bos_token_id=bos_token_id,
195
+ eos_token_id=eos_token_id,
196
+ tie_word_embeddings=tie_lm_head,
197
+ **kwargs,
198
+ )
dat_config.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration for the owned dual-attention decoder LM."""
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .attention_config import SUPPORTED_SEQUENCE_BOUNDARY_POLICIES
6
+
7
+
8
+ SUPPORTED_PE_TYPES = {"sinusoidal", "learned", "relative", "rope", "none"}
9
+ SUPPORTED_SYMBOL_RETRIEVAL = {"symbolic", "positional", "relative", "relsymbolic"}
10
+ SUPPORTED_RA_TYPES = {"ra", "rca", "disrca"}
11
+ SUPPORTED_RA_ACTIVATIONS = {"softmax", "identity", "relu", "tanh", "sigmoid", "gelu"}
12
+ SUPPORTED_FFN_ACTIVATIONS = {"gelu", "relu", "swiglu", "identity"}
13
+ SUPPORTED_FFN_HIDDEN_DIM_MODES = {"dff_factor", "swiglu_parameter_matched"}
14
+ SUPPORTED_INIT_SCHEMES = {"xavier_uniform", "normal_0_02_scaled_projection"}
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class DatLMConfig:
19
+ vocab_size: int
20
+ max_seq_len: int
21
+ pe_type: str = "rope"
22
+ hidden_dim: int = 256
23
+ n_heads_sa: int = 2
24
+ n_heads_ra: int = 2
25
+ n_layers: int = 4
26
+ dropout: float = 0.0
27
+ dff_factor: int = 4
28
+ ffn_hidden_dim_mode: str = "dff_factor"
29
+ ffn_activation: str = "gelu"
30
+ rope_theta: float = 10000.0
31
+ max_rel_pos: int | None = None
32
+ sequence_boundary_policy: str = "eos_document"
33
+ segment_boundary_token_id: int | None = None
34
+ init_range: float = 0.15
35
+ init_scheme: str = "xavier_uniform"
36
+ norm_type: str = "rmsnorm"
37
+ norm_first: bool = True
38
+ use_bias_qkv: bool = False
39
+ use_bias_out: bool = True
40
+ use_bias_ffn: bool = True
41
+ tie_lm_head: bool = True
42
+ symbol_dim: int | None = None
43
+ n_symbols: int | None = None
44
+ symbolic_attn_n_heads: int | None = None
45
+ symbol_retrieval: str = "symbolic"
46
+ symbolic_use_bias: bool = False
47
+ shared_symbol_retriever: bool = True
48
+ share_attn_params: bool = False
49
+ positional_symbols_sinusoidal: bool = False
50
+ relative_symbols_rope: bool = False
51
+ relsymbolic_rel_n_heads: int = 4
52
+ relsymbolic_symbolic_attn_n_heads: int = 4
53
+ relsymbolic_neighborhood_size: int = 2
54
+ relsymbolic_include_self: bool = False
55
+ relsymbolic_normalize_rels: bool = True
56
+ relsymbolic_trainable_symbols: bool = True
57
+ relsymbolic_dropout: float = 0.0
58
+ relsymbolic_rel_scale: float | None = None
59
+ relsymbolic_symbolic_attn_scale: float | None = None
60
+ relsymbolic_use_bias: bool = False
61
+ ra_type: str = "ra"
62
+ ra_n_relations: int | None = None
63
+ ra_rel_activation: str = "identity"
64
+ ra_symmetric_rels: bool = False
65
+ pad_token_id: int = 0
66
+ bos_token_id: int = 1
67
+ eos_token_id: int = 2
68
+ mlm_head_enabled: bool = False
69
+
70
+ def __post_init__(self) -> None:
71
+ if self.vocab_size <= 0:
72
+ raise ValueError(f"vocab_size must be positive, got {self.vocab_size}")
73
+ if self.max_seq_len <= 0:
74
+ raise ValueError(f"max_seq_len must be positive, got {self.max_seq_len}")
75
+ if self.pe_type not in SUPPORTED_PE_TYPES:
76
+ raise ValueError(f"Unsupported pe_type: {self.pe_type}")
77
+ if self.hidden_dim <= 0:
78
+ raise ValueError(f"hidden_dim must be positive, got {self.hidden_dim}")
79
+ if self.n_heads_sa <= 0:
80
+ raise ValueError(f"n_heads_sa must be positive for DAT, got {self.n_heads_sa}")
81
+ if self.n_heads_ra <= 0:
82
+ raise ValueError(f"n_heads_ra must be positive for DAT, got {self.n_heads_ra}")
83
+ total_heads = self.total_n_heads
84
+ if self.hidden_dim % total_heads != 0:
85
+ raise ValueError(
86
+ f"hidden_dim ({self.hidden_dim}) must be divisible by total DAT heads "
87
+ f"({total_heads} = {self.n_heads_sa} SA + {self.n_heads_ra} RA)"
88
+ )
89
+ if self.n_layers <= 0:
90
+ raise ValueError(f"n_layers must be positive, got {self.n_layers}")
91
+ if not 0.0 <= self.dropout < 1.0:
92
+ raise ValueError(f"dropout must be in [0.0, 1.0), got {self.dropout}")
93
+ if self.dff_factor <= 0:
94
+ raise ValueError(f"dff_factor must be positive, got {self.dff_factor}")
95
+ if self.ffn_hidden_dim_mode not in SUPPORTED_FFN_HIDDEN_DIM_MODES:
96
+ raise ValueError(f"Unsupported ffn_hidden_dim_mode for DAT: {self.ffn_hidden_dim_mode}")
97
+ if self.ffn_activation not in SUPPORTED_FFN_ACTIVATIONS:
98
+ raise ValueError(f"Unsupported ffn_activation for DAT: {self.ffn_activation}")
99
+ if self.ffn_hidden_dim_mode == "swiglu_parameter_matched" and self.ffn_activation != "swiglu":
100
+ raise ValueError(
101
+ "ffn_hidden_dim_mode='swiglu_parameter_matched' requires "
102
+ f"ffn_activation='swiglu', got {self.ffn_activation}"
103
+ )
104
+ if self.rope_theta <= 0.0:
105
+ raise ValueError(f"rope_theta must be positive, got {self.rope_theta}")
106
+ if self.max_rel_pos is not None and self.max_rel_pos <= 0:
107
+ raise ValueError(f"max_rel_pos must be positive when provided, got {self.max_rel_pos}")
108
+ if self.sequence_boundary_policy not in SUPPORTED_SEQUENCE_BOUNDARY_POLICIES:
109
+ raise ValueError(
110
+ "Unsupported sequence_boundary_policy for DAT: "
111
+ f"{self.sequence_boundary_policy}"
112
+ )
113
+ if self.sequence_boundary_policy == "segment_document":
114
+ if self.segment_boundary_token_id is None:
115
+ raise ValueError(
116
+ "segment_boundary_token_id is required when "
117
+ "sequence_boundary_policy='segment_document'"
118
+ )
119
+ if self.init_range <= 0.0:
120
+ raise ValueError(f"init_range must be positive, got {self.init_range}")
121
+ if self.init_scheme not in SUPPORTED_INIT_SCHEMES:
122
+ raise ValueError(f"Unsupported init_scheme for DAT: {self.init_scheme}")
123
+ if self.norm_type not in {"layernorm", "rmsnorm"}:
124
+ raise ValueError(
125
+ f"norm_type must be 'layernorm' or 'rmsnorm', got {self.norm_type}"
126
+ )
127
+ if self.pe_type == "rope" and self.head_dim % 2 != 0:
128
+ raise ValueError(
129
+ "RoPE requires even DAT head_dim, got "
130
+ f"{self.head_dim} from hidden_dim={self.hidden_dim}, total_heads={total_heads}"
131
+ )
132
+ if self.pe_type == "sinusoidal" and self.hidden_dim % 2 != 0:
133
+ raise ValueError(f"Sinusoidal encoding requires even hidden_dim, got {self.hidden_dim}")
134
+ if self.share_attn_params and self.n_heads_sa != self.n_heads_ra:
135
+ raise ValueError(
136
+ "share_attn_params=True requires n_heads_sa == n_heads_ra, "
137
+ f"got {self.n_heads_sa} and {self.n_heads_ra}"
138
+ )
139
+ if self.symbol_dim is not None and self.symbol_dim <= 0:
140
+ raise ValueError(f"symbol_dim must be positive when provided, got {self.symbol_dim}")
141
+ if self.symbolic_attn_n_heads is not None and self.symbolic_attn_n_heads <= 0:
142
+ raise ValueError(
143
+ "symbolic_attn_n_heads must be positive when provided, "
144
+ f"got {self.symbolic_attn_n_heads}"
145
+ )
146
+ if self.symbol_retrieval not in SUPPORTED_SYMBOL_RETRIEVAL:
147
+ raise ValueError(f"Unsupported symbol_retrieval for DAT: {self.symbol_retrieval}")
148
+ if self.symbol_retrieval == "symbolic":
149
+ symbolic_heads = self.resolved_symbolic_attn_n_heads
150
+ if self.hidden_dim % symbolic_heads != 0:
151
+ raise ValueError(
152
+ f"hidden_dim ({self.hidden_dim}) must be divisible by symbolic_attn_n_heads "
153
+ f"({symbolic_heads}) for symbolic retrieval"
154
+ )
155
+ if self.resolved_symbol_dim % symbolic_heads != 0:
156
+ raise ValueError(
157
+ f"symbol_dim ({self.resolved_symbol_dim}) must be divisible by symbolic_attn_n_heads "
158
+ f"({symbolic_heads}) for symbolic retrieval"
159
+ )
160
+ if (
161
+ self.symbol_retrieval == "positional"
162
+ and self.positional_symbols_sinusoidal
163
+ and self.resolved_symbol_dim % 2 != 0
164
+ ):
165
+ raise ValueError(
166
+ "Sinusoidal positional symbols require even symbol_dim, "
167
+ f"got {self.resolved_symbol_dim}"
168
+ )
169
+ if self.relative_symbols_rope:
170
+ if self.symbol_retrieval != "relative":
171
+ raise ValueError(
172
+ "relative_symbols_rope=True requires symbol_retrieval='relative', "
173
+ f"got {self.symbol_retrieval!r}"
174
+ )
175
+ if self.resolved_symbol_dim % 2 != 0:
176
+ raise ValueError(
177
+ "RoPE relative symbols require even symbol_dim, "
178
+ f"got {self.resolved_symbol_dim}"
179
+ )
180
+ if self.positional_symbols_sinusoidal and self.symbol_retrieval != "positional":
181
+ raise ValueError(
182
+ "positional_symbols_sinusoidal=True requires symbol_retrieval='positional', "
183
+ f"got {self.symbol_retrieval!r}"
184
+ )
185
+ if self.resolved_n_symbols <= 0:
186
+ raise ValueError(f"resolved_n_symbols must be positive, got {self.resolved_n_symbols}")
187
+ if self.symbol_retrieval == "relsymbolic":
188
+ if self.relsymbolic_rel_n_heads <= 0:
189
+ raise ValueError(
190
+ f"relsymbolic_rel_n_heads must be positive, got {self.relsymbolic_rel_n_heads}"
191
+ )
192
+ if self.hidden_dim % self.relsymbolic_rel_n_heads != 0:
193
+ raise ValueError(
194
+ f"hidden_dim ({self.hidden_dim}) must be divisible by "
195
+ f"relsymbolic_rel_n_heads ({self.relsymbolic_rel_n_heads})"
196
+ )
197
+ if self.relsymbolic_symbolic_attn_n_heads <= 0:
198
+ raise ValueError(
199
+ "relsymbolic_symbolic_attn_n_heads must be positive, "
200
+ f"got {self.relsymbolic_symbolic_attn_n_heads}"
201
+ )
202
+ if self.hidden_dim % self.relsymbolic_symbolic_attn_n_heads != 0:
203
+ raise ValueError(
204
+ f"hidden_dim ({self.hidden_dim}) must be divisible by "
205
+ f"relsymbolic_symbolic_attn_n_heads ({self.relsymbolic_symbolic_attn_n_heads})"
206
+ )
207
+ if self.resolved_symbol_dim % self.relsymbolic_symbolic_attn_n_heads != 0:
208
+ raise ValueError(
209
+ f"symbol_dim ({self.resolved_symbol_dim}) must be divisible by "
210
+ f"relsymbolic_symbolic_attn_n_heads ({self.relsymbolic_symbolic_attn_n_heads})"
211
+ )
212
+ if self.relsymbolic_neighborhood_size <= 0:
213
+ raise ValueError(
214
+ "relsymbolic_neighborhood_size must be positive, "
215
+ f"got {self.relsymbolic_neighborhood_size}"
216
+ )
217
+ if not 0.0 <= self.relsymbolic_dropout < 1.0:
218
+ raise ValueError(
219
+ f"relsymbolic_dropout must be in [0.0, 1.0), got {self.relsymbolic_dropout}"
220
+ )
221
+ if self.relsymbolic_rel_scale is not None and self.relsymbolic_rel_scale <= 0.0:
222
+ raise ValueError(
223
+ "relsymbolic_rel_scale must be positive when provided, "
224
+ f"got {self.relsymbolic_rel_scale}"
225
+ )
226
+ if (
227
+ self.relsymbolic_symbolic_attn_scale is not None
228
+ and self.relsymbolic_symbolic_attn_scale <= 0.0
229
+ ):
230
+ raise ValueError(
231
+ "relsymbolic_symbolic_attn_scale must be positive when provided, "
232
+ f"got {self.relsymbolic_symbolic_attn_scale}"
233
+ )
234
+ if self.ra_type not in SUPPORTED_RA_TYPES:
235
+ raise ValueError(f"Unsupported ra_type for DAT: {self.ra_type}")
236
+ if self.ra_n_relations is not None and self.ra_n_relations <= 0:
237
+ raise ValueError(
238
+ f"ra_n_relations must be positive when provided, got {self.ra_n_relations}"
239
+ )
240
+ if self.ra_type != "ra" and self.ra_n_relations is not None:
241
+ raise ValueError(f"ra_n_relations applies only to ra_type='ra', got {self.ra_type}")
242
+ if self.ra_type != "ra" and self.ra_symmetric_rels:
243
+ raise ValueError(f"ra_symmetric_rels applies only to ra_type='ra', got {self.ra_type}")
244
+ n_relations = self.resolved_ra_n_relations
245
+ if self.ra_type == "ra" and (self.head_dim * self.n_heads_ra) % n_relations != 0:
246
+ raise ValueError(
247
+ f"head_dim * n_heads_ra ({self.head_dim * self.n_heads_ra}) must be "
248
+ f"divisible by ra_n_relations ({n_relations})"
249
+ )
250
+ if self.ra_rel_activation not in SUPPORTED_RA_ACTIVATIONS:
251
+ raise ValueError(
252
+ f"Unsupported ra_rel_activation for DAT: {self.ra_rel_activation}"
253
+ )
254
+
255
+ @property
256
+ def total_n_heads(self) -> int:
257
+ return self.n_heads_sa + self.n_heads_ra
258
+
259
+ @property
260
+ def head_dim(self) -> int:
261
+ return self.hidden_dim // self.total_n_heads
262
+
263
+ @property
264
+ def resolved_symbol_dim(self) -> int:
265
+ return self.hidden_dim if self.symbol_dim is None else self.symbol_dim
266
+
267
+ @property
268
+ def resolved_symbolic_attn_n_heads(self) -> int:
269
+ return self.total_n_heads if self.symbolic_attn_n_heads is None else self.symbolic_attn_n_heads
270
+
271
+ @property
272
+ def resolved_ffn_hidden_dim(self) -> int:
273
+ if self.ffn_hidden_dim_mode == "swiglu_parameter_matched":
274
+ return int(8 / 3 * self.hidden_dim)
275
+ return self.hidden_dim * self.dff_factor
276
+
277
+ @property
278
+ def resolved_n_symbols(self) -> int:
279
+ return self.max_seq_len if self.n_symbols is None else self.n_symbols
280
+
281
+ @property
282
+ def resolved_ra_n_relations(self) -> int:
283
+ return self.n_heads_ra if self.ra_n_relations is None else self.ra_n_relations
dat_core.py ADDED
@@ -0,0 +1,328 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dual-attention primitives for decoder language models."""
2
+
3
+ import math
4
+ from typing import Optional
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+
9
+ from .transformer_components import PositionalInfo
10
+ from .transformer_core import MultiHeadAttentionBase
11
+
12
+
13
+ def _activate_scores(scores: torch.Tensor, activation: str) -> torch.Tensor:
14
+ if activation == "softmax":
15
+ return torch.softmax(scores, dim=-1)
16
+ if activation == "identity":
17
+ return scores
18
+ if activation == "relu":
19
+ return torch.relu(scores)
20
+ if activation == "tanh":
21
+ return torch.tanh(scores)
22
+ if activation == "sigmoid":
23
+ return torch.sigmoid(scores)
24
+ if activation == "gelu":
25
+ return torch.nn.functional.gelu(scores)
26
+ raise ValueError(f"Unsupported attention activation: {activation}")
27
+
28
+
29
+ class RelationalAttentionBase(MultiHeadAttentionBase):
30
+ def __init__(
31
+ self,
32
+ hidden_dim: int,
33
+ symbol_dim: int,
34
+ n_heads: int,
35
+ total_n_heads: int,
36
+ dropout: float = 0.0,
37
+ n_relations: Optional[int] = None,
38
+ rel_activation: str = "identity",
39
+ symmetric_rels: bool = False,
40
+ use_relative_positional_symbols: bool = False,
41
+ use_bias_qkv: bool = False,
42
+ use_bias_out: bool = True,
43
+ ):
44
+ head_dim = hidden_dim // total_n_heads
45
+ output_dim = n_heads * head_dim
46
+ super().__init__(
47
+ query_dim=hidden_dim,
48
+ output_dim=output_dim,
49
+ key_dim=hidden_dim,
50
+ value_dim=symbol_dim,
51
+ n_heads=n_heads,
52
+ hidden_dim=hidden_dim,
53
+ dropout=dropout,
54
+ total_n_heads=total_n_heads,
55
+ activation="softmax",
56
+ use_bias_qkv=use_bias_qkv,
57
+ use_bias_out=use_bias_out,
58
+ )
59
+ self.symbol_dim = symbol_dim
60
+ self.rel_activation = rel_activation
61
+ self.symmetric_rels = symmetric_rels
62
+ self.use_relative_positional_symbols = use_relative_positional_symbols
63
+ self.n_relations = n_heads if n_relations is None else n_relations
64
+ total_rel_dim = self.head_dim * n_heads
65
+ if total_rel_dim % self.n_relations != 0:
66
+ raise ValueError(
67
+ f"head_dim * n_heads ({total_rel_dim}) must be divisible by n_relations "
68
+ f"({self.n_relations})"
69
+ )
70
+ self.rel_proj_dim = total_rel_dim // self.n_relations
71
+ self.rel_scale = 1.0 / math.sqrt(self.rel_proj_dim)
72
+ rel_total_dim = self.n_relations * self.rel_proj_dim
73
+ self.wq_rel = nn.Linear(hidden_dim, rel_total_dim, bias=False)
74
+ self.wk_rel = self.wq_rel if symmetric_rels else nn.Linear(
75
+ hidden_dim,
76
+ rel_total_dim,
77
+ bias=False,
78
+ )
79
+ nn.init.xavier_uniform_(self.wq_rel.weight)
80
+ if self.wk_rel is not self.wq_rel:
81
+ nn.init.xavier_uniform_(self.wk_rel.weight)
82
+
83
+ def _compute_base_attention(
84
+ self,
85
+ x: torch.Tensor,
86
+ mask: Optional[torch.Tensor],
87
+ pos_info: Optional[PositionalInfo],
88
+ ) -> torch.Tensor:
89
+ batch_size, seq_len, _ = x.shape
90
+ q = self._reshape_for_multihead(self.q_proj(x), batch_size, seq_len)
91
+ k = self._reshape_for_multihead(self.k_proj(x), batch_size, seq_len)
92
+ scores = self._compute_attn_scores(q, k, pos_info)
93
+ return self._apply_activation_and_mask(scores, mask)
94
+
95
+ def compute_relational_scores(
96
+ self,
97
+ x: torch.Tensor,
98
+ mask: Optional[torch.Tensor],
99
+ return_as: str,
100
+ ) -> torch.Tensor:
101
+ batch_size, seq_len, _ = x.shape
102
+ q_rel = self.wq_rel(x).view(batch_size, seq_len, self.n_relations, self.rel_proj_dim)
103
+ k_rel = self.wk_rel(x).view(batch_size, seq_len, self.n_relations, self.rel_proj_dim)
104
+ q_rel = q_rel.transpose(1, 2)
105
+ k_rel = k_rel.transpose(1, 2)
106
+ relations = torch.matmul(q_rel, k_rel.transpose(-2, -1)) * self.rel_scale
107
+ processed_mask = self._process_mask(mask)
108
+ if self.rel_activation == "softmax" and processed_mask is not None:
109
+ relations.masked_fill_(~processed_mask, torch.finfo(relations.dtype).min)
110
+ relations = _activate_scores(relations, self.rel_activation)
111
+ if processed_mask is not None:
112
+ relations = relations.masked_fill(~processed_mask, 0.0)
113
+ if return_as == "vectors":
114
+ return relations.permute(0, 2, 3, 1)
115
+ if return_as == "scores":
116
+ return relations
117
+ raise ValueError(f"return_as must be 'vectors' or 'scores', got {return_as}")
118
+
119
+ def combine_attention_and_relations(
120
+ self,
121
+ attn_weights: torch.Tensor,
122
+ relation_scores: torch.Tensor,
123
+ ) -> torch.Tensor:
124
+ return attn_weights * relation_scores
125
+
126
+ def _process_symbols_with_attention(
127
+ self,
128
+ symbols: torch.Tensor,
129
+ attn_weights: torch.Tensor,
130
+ ) -> torch.Tensor:
131
+ batch_size, _, seq_len_q, seq_len_k = attn_weights.shape
132
+ values = self.v_proj(symbols)
133
+ if self.use_relative_positional_symbols:
134
+ values = values.view(seq_len_q, seq_len_k, self.n_heads, self.head_dim)
135
+ return torch.einsum("bhij,ijhd->bihd", attn_weights, values)
136
+
137
+ values = values.view(batch_size, seq_len_k, self.n_heads, self.head_dim)
138
+ values = values.transpose(1, 2)
139
+ output = torch.matmul(attn_weights, values)
140
+ return output.transpose(1, 2)
141
+
142
+ def _apply_output_projection(self, output: torch.Tensor) -> torch.Tensor:
143
+ batch_size, seq_len = output.shape[:2]
144
+ output = output.contiguous().view(batch_size, seq_len, self.output_dim)
145
+ output = self.o_proj(output)
146
+ output = self.dropout(output)
147
+ return output
148
+
149
+ def _validate_symbols(self, x: torch.Tensor, symbols: torch.Tensor) -> None:
150
+ if self.use_relative_positional_symbols:
151
+ seq_len = x.shape[1]
152
+ expected_shape = (seq_len, seq_len, self.symbol_dim)
153
+ if tuple(symbols.shape) != expected_shape:
154
+ raise ValueError(
155
+ f"Relative symbols must have shape {expected_shape}, got {tuple(symbols.shape)}"
156
+ )
157
+
158
+
159
+ class RelationalAttention(RelationalAttentionBase):
160
+ def __init__(
161
+ self,
162
+ hidden_dim: int,
163
+ symbol_dim: int,
164
+ n_heads: int,
165
+ total_n_heads: int,
166
+ n_relations: int,
167
+ dropout: float = 0.0,
168
+ rel_activation: str = "identity",
169
+ symmetric_rels: bool = False,
170
+ use_relative_positional_symbols: bool = False,
171
+ use_bias_qkv: bool = False,
172
+ use_bias_out: bool = True,
173
+ ):
174
+ super().__init__(
175
+ hidden_dim=hidden_dim,
176
+ symbol_dim=symbol_dim,
177
+ n_heads=n_heads,
178
+ total_n_heads=total_n_heads,
179
+ dropout=dropout,
180
+ n_relations=n_relations,
181
+ rel_activation=rel_activation,
182
+ symmetric_rels=symmetric_rels,
183
+ use_relative_positional_symbols=use_relative_positional_symbols,
184
+ use_bias_qkv=use_bias_qkv,
185
+ use_bias_out=use_bias_out,
186
+ )
187
+ self.wr_proj = nn.Parameter(torch.empty(n_heads, self.head_dim, n_relations))
188
+ nn.init.xavier_uniform_(self.wr_proj)
189
+
190
+ def forward(
191
+ self,
192
+ x: torch.Tensor,
193
+ symbols: torch.Tensor,
194
+ mask: Optional[torch.Tensor],
195
+ pos_info: Optional[PositionalInfo],
196
+ ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
197
+ self._validate_symbols(x, symbols)
198
+ attn_weights = self._compute_base_attention(x, mask, pos_info)
199
+ attn_weights = self.attn_dropout(attn_weights)
200
+ relation_vectors = self.compute_relational_scores(x, mask, return_as="vectors")
201
+ attended_symbols = self._process_symbols_with_attention(symbols, attn_weights)
202
+ projected_relations = torch.einsum(
203
+ "bhij,bijr,hdr->bihd",
204
+ attn_weights,
205
+ relation_vectors,
206
+ self.wr_proj,
207
+ )
208
+ output = self._apply_output_projection(attended_symbols + projected_relations)
209
+ self.last_attn_weights = attn_weights.detach()
210
+ return output, {"attention": attn_weights, "relations": relation_vectors}
211
+
212
+
213
+ class RelationalCrossAttention(MultiHeadAttentionBase):
214
+ def __init__(
215
+ self,
216
+ hidden_dim: int,
217
+ symbol_dim: int,
218
+ n_heads: int,
219
+ total_n_heads: int,
220
+ dropout: float = 0.0,
221
+ activation: str = "identity",
222
+ use_relative_positional_symbols: bool = False,
223
+ use_bias_qkv: bool = False,
224
+ use_bias_out: bool = True,
225
+ ):
226
+ head_dim = hidden_dim // total_n_heads
227
+ super().__init__(
228
+ query_dim=hidden_dim,
229
+ output_dim=n_heads * head_dim,
230
+ key_dim=hidden_dim,
231
+ value_dim=symbol_dim,
232
+ n_heads=n_heads,
233
+ hidden_dim=hidden_dim,
234
+ dropout=dropout,
235
+ total_n_heads=total_n_heads,
236
+ activation=activation,
237
+ use_bias_qkv=use_bias_qkv,
238
+ use_bias_out=use_bias_out,
239
+ )
240
+ self.symbol_dim = symbol_dim
241
+ self.use_relative_positional_symbols = use_relative_positional_symbols
242
+
243
+ def forward(
244
+ self,
245
+ x: torch.Tensor,
246
+ symbols: torch.Tensor,
247
+ mask: Optional[torch.Tensor],
248
+ pos_info: Optional[PositionalInfo],
249
+ ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
250
+ if self.use_relative_positional_symbols:
251
+ batch_size, seq_len, _ = x.shape
252
+ expected_shape = (seq_len, seq_len, self.symbol_dim)
253
+ if tuple(symbols.shape) != expected_shape:
254
+ raise ValueError(
255
+ f"Relative symbols must have shape {expected_shape}, got {tuple(symbols.shape)}"
256
+ )
257
+
258
+ q = self._reshape_for_multihead(self.q_proj(x), batch_size, seq_len)
259
+ k = self._reshape_for_multihead(self.k_proj(x), batch_size, seq_len)
260
+ values = self.v_proj(symbols).view(seq_len, seq_len, self.n_heads, self.head_dim)
261
+ scores = self._compute_attn_scores(q, k, pos_info)
262
+ weights = self._apply_activation_and_mask(scores, mask)
263
+ weights = self.attn_dropout(weights)
264
+ output = torch.einsum("bhij,ijhd->bihd", weights, values)
265
+ output = output.contiguous().view(batch_size, seq_len, self.output_dim)
266
+ output = self.o_proj(output)
267
+ output = self.dropout(output)
268
+ self.last_attn_weights = weights.detach()
269
+ return output, {"attention": weights}
270
+
271
+ output, weights = super().forward(
272
+ query=x,
273
+ key=x,
274
+ value=symbols,
275
+ mask=mask,
276
+ pos_info=pos_info,
277
+ )
278
+ return output, {"attention": weights}
279
+
280
+
281
+ class DisentangledRelationalCrossAttention(RelationalAttentionBase):
282
+ def __init__(
283
+ self,
284
+ hidden_dim: int,
285
+ symbol_dim: int,
286
+ n_heads: int,
287
+ total_n_heads: int,
288
+ dropout: float = 0.0,
289
+ rel_activation: str = "identity",
290
+ use_relative_positional_symbols: bool = False,
291
+ use_bias_qkv: bool = False,
292
+ use_bias_out: bool = True,
293
+ ):
294
+ super().__init__(
295
+ hidden_dim=hidden_dim,
296
+ symbol_dim=symbol_dim,
297
+ n_heads=n_heads,
298
+ total_n_heads=total_n_heads,
299
+ dropout=dropout,
300
+ n_relations=None,
301
+ rel_activation=rel_activation,
302
+ symmetric_rels=False,
303
+ use_relative_positional_symbols=use_relative_positional_symbols,
304
+ use_bias_qkv=use_bias_qkv,
305
+ use_bias_out=use_bias_out,
306
+ )
307
+
308
+ def forward(
309
+ self,
310
+ x: torch.Tensor,
311
+ symbols: torch.Tensor,
312
+ mask: Optional[torch.Tensor],
313
+ pos_info: Optional[PositionalInfo],
314
+ ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:
315
+ self._validate_symbols(x, symbols)
316
+ attn_weights = self._compute_base_attention(x, mask, pos_info)
317
+ relation_scores = self.compute_relational_scores(x, mask, return_as="scores")
318
+ combined_weights = self.attn_dropout(
319
+ self.combine_attention_and_relations(attn_weights, relation_scores)
320
+ )
321
+ output = self._process_symbols_with_attention(symbols, combined_weights)
322
+ output = self._apply_output_projection(output)
323
+ self.last_attn_weights = attn_weights.detach()
324
+ return output, {
325
+ "attention": attn_weights,
326
+ "relations": relation_scores,
327
+ "combined": combined_weights,
328
+ }
dat_lm.py ADDED
@@ -0,0 +1,475 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Decoder-only LM built from dual-attention blocks."""
2
+
3
+ import math
4
+ from typing import Optional
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+ import torch.nn.functional as F
9
+ from torch.nn.utils.rnn import pad_sequence
10
+
11
+ from .dat_config import DatLMConfig
12
+ from .dat_core import (
13
+ DisentangledRelationalCrossAttention,
14
+ RelationalAttention,
15
+ RelationalCrossAttention,
16
+ )
17
+ from .transformer_core import SelfAttention
18
+ from .dat_symbols import (
19
+ PositionalSymbolRetriever,
20
+ RelationalSymbolicAttentionRetriever,
21
+ RelativePositionalSymbolRetriever,
22
+ SymbolicAttentionRetriever,
23
+ )
24
+ from .masks import build_decoder_attention_mask
25
+ from .mlm import MaskClassifier
26
+ from .transformer_components import FeedForward, PositionalEncoding, PositionalInfo, RMSNorm
27
+
28
+
29
+ class DatDecoderBlock(nn.Module):
30
+ def __init__(self, config: DatLMConfig):
31
+ super().__init__()
32
+ self.norm_first = config.norm_first
33
+ # input_dim is the block state width; hidden_dim is the total DAT width
34
+ # used with total_n_heads to derive the shared SA/RA head_dim.
35
+ self.sensory_attention = SelfAttention(
36
+ input_dim=config.hidden_dim,
37
+ n_heads=config.n_heads_sa,
38
+ hidden_dim=config.hidden_dim,
39
+ total_n_heads=config.total_n_heads,
40
+ dropout=config.dropout,
41
+ supports_relative=config.pe_type == "relative",
42
+ use_bias_qkv=config.use_bias_qkv,
43
+ use_bias_out=config.use_bias_out,
44
+ )
45
+ self.relational_attention = _build_relational_attention(config)
46
+ if config.share_attn_params:
47
+ self.sensory_attention.q_proj = self.relational_attention.q_proj
48
+ self.sensory_attention.k_proj = self.relational_attention.k_proj
49
+ self.feed_forward = FeedForward(
50
+ input_dim=config.hidden_dim,
51
+ hidden_dim=config.resolved_ffn_hidden_dim,
52
+ dropout=config.dropout,
53
+ activation=config.ffn_activation,
54
+ use_bias=config.use_bias_ffn,
55
+ )
56
+ self.norm1 = _create_norm(config.norm_type, config.hidden_dim)
57
+ self.norm2 = _create_norm(config.norm_type, config.hidden_dim)
58
+ self.dropout = nn.Dropout(config.dropout)
59
+
60
+ def forward(
61
+ self,
62
+ x: torch.Tensor,
63
+ symbol_retriever: nn.Module,
64
+ mask: torch.Tensor,
65
+ pos_info: Optional[PositionalInfo],
66
+ ) -> torch.Tensor:
67
+ if self.norm_first:
68
+ normed_x = self.norm1(x)
69
+ # Retrieve symbols at the block boundary so pre-norm attention and
70
+ # input-dependent symbol retrieval use the same representation.
71
+ symbols = symbol_retriever(normed_x)
72
+ sensory_output, _ = self.sensory_attention(normed_x, mask=mask, pos_info=pos_info)
73
+ relational_output, _ = self.relational_attention(
74
+ normed_x,
75
+ symbols,
76
+ mask=mask,
77
+ pos_info=pos_info,
78
+ )
79
+ x = x + self.dropout(torch.cat((sensory_output, relational_output), dim=-1))
80
+ x = x + self.dropout(self.feed_forward(self.norm2(x)))
81
+ return x
82
+
83
+ symbols = symbol_retriever(x)
84
+ sensory_output, _ = self.sensory_attention(x, mask=mask, pos_info=pos_info)
85
+ relational_output, _ = self.relational_attention(
86
+ x,
87
+ symbols,
88
+ mask=mask,
89
+ pos_info=pos_info,
90
+ )
91
+ x = self.norm1(x + self.dropout(torch.cat((sensory_output, relational_output), dim=-1)))
92
+ x = self.norm2(x + self.dropout(self.feed_forward(x)))
93
+ return x
94
+
95
+
96
+ class DatDecoderLM(nn.Module):
97
+ def __init__(self, config: DatLMConfig):
98
+ super().__init__()
99
+ self.config = config
100
+ self.token_embeddings = nn.Embedding(config.vocab_size, config.hidden_dim)
101
+ self.embedding_dropout = nn.Dropout(config.dropout)
102
+ position_dim = (
103
+ config.hidden_dim if config.pe_type in {"sinusoidal", "learned", "none"}
104
+ else config.head_dim
105
+ )
106
+ self.position_encoder = PositionalEncoding(
107
+ embedding_dim=position_dim,
108
+ pe_type=config.pe_type,
109
+ max_len=config.max_seq_len,
110
+ theta=config.rope_theta,
111
+ max_rel_pos=config.max_rel_pos,
112
+ init_range=config.init_range,
113
+ )
114
+ self.symbol_retrievers = _build_symbol_retrievers(config)
115
+ self.layers = nn.ModuleList(DatDecoderBlock(config) for _ in range(config.n_layers))
116
+ self.final_norm = _create_norm(config.norm_type, config.hidden_dim)
117
+ self.lm_head = nn.Linear(config.hidden_dim, config.vocab_size, bias=False)
118
+ if config.mlm_head_enabled:
119
+ self.mlm_head = MaskClassifier(
120
+ hidden_dim=config.hidden_dim,
121
+ vocab_size=config.vocab_size,
122
+ norm_type=config.norm_type,
123
+ ffn_activation=config.ffn_activation,
124
+ use_bias_ffn=config.use_bias_ffn,
125
+ dropout=config.dropout,
126
+ word_embedding=self.lm_head.weight if not config.tie_lm_head else self.token_embeddings.weight,
127
+ )
128
+ self._init_weights()
129
+
130
+ @property
131
+ def device(self) -> torch.device:
132
+ return self.token_embeddings.weight.device
133
+
134
+ def _init_weights(self) -> None:
135
+ if self.config.init_scheme == "xavier_uniform":
136
+ nn.init.xavier_uniform_(
137
+ self.token_embeddings.weight,
138
+ gain=nn.init.calculate_gain("linear"),
139
+ )
140
+ if self.config.tie_lm_head:
141
+ self.lm_head.weight = self.token_embeddings.weight
142
+ else:
143
+ nn.init.xavier_uniform_(
144
+ self.lm_head.weight,
145
+ gain=nn.init.calculate_gain("linear"),
146
+ )
147
+ return
148
+
149
+ if self.config.init_scheme == "normal_0_02_scaled_projection":
150
+ self._init_normal_0_02_scaled_projection()
151
+ if self.config.tie_lm_head:
152
+ self.lm_head.weight = self.token_embeddings.weight
153
+ return
154
+
155
+ raise ValueError(f"Unsupported init_scheme: {self.config.init_scheme}")
156
+
157
+ def _init_normal_0_02_scaled_projection(self) -> None:
158
+ for module in self.modules():
159
+ if isinstance(module, nn.Linear):
160
+ nn.init.normal_(module.weight, mean=0.0, std=0.02)
161
+ if module.bias is not None:
162
+ nn.init.zeros_(module.bias)
163
+ elif isinstance(module, nn.Embedding):
164
+ nn.init.normal_(module.weight, mean=0.0, std=0.02)
165
+
166
+ for module in self.modules():
167
+ if isinstance(module, RelationalAttention):
168
+ nn.init.normal_(module.wr_proj, mean=0.0, std=0.02)
169
+ elif isinstance(module, SymbolicAttentionRetriever):
170
+ nn.init.normal_(module.template_features, mean=0.0, std=1.0)
171
+ nn.init.normal_(module.symbol_library, mean=0.0, std=1.0)
172
+ elif isinstance(module, RelativePositionalSymbolRetriever) and not module.rope:
173
+ nn.init.xavier_uniform_(module.position_encoder.rel_pos_embeddings_table.weight)
174
+
175
+ scaled_std = 0.02 / math.sqrt(2 * self.config.n_layers)
176
+ ffn_scaled_suffix = (
177
+ "feed_forward.w_up.weight"
178
+ if self.config.ffn_activation == "swiglu"
179
+ else "feed_forward.linear2.weight"
180
+ )
181
+ for name, parameter in self.named_parameters():
182
+ # Exclude the MLM head from depth-scaled init: its feed_forward
183
+ # down-projection shares the same suffix as decoder layers but
184
+ # should use the regular 0.02 initialization.
185
+ if name.startswith("mlm_head"):
186
+ continue
187
+ if name.endswith("o_proj.weight") or name.endswith(ffn_scaled_suffix):
188
+ nn.init.normal_(parameter, mean=0.0, std=scaled_std)
189
+
190
+ def _build_attention_mask(
191
+ self,
192
+ input_ids: torch.Tensor,
193
+ attention_mask: Optional[torch.Tensor] = None,
194
+ bidirectional: bool = False,
195
+ ) -> torch.Tensor:
196
+ return build_decoder_attention_mask(
197
+ input_ids=input_ids,
198
+ pad_token_id=self.config.pad_token_id,
199
+ eos_token_id=self.config.eos_token_id,
200
+ sequence_boundary_policy=self.config.sequence_boundary_policy,
201
+ attention_mask=attention_mask,
202
+ segment_boundary_token_id=self.config.segment_boundary_token_id,
203
+ bidirectional=bidirectional,
204
+ )
205
+
206
+ def encode_for_objective(
207
+ self,
208
+ input_ids: torch.Tensor,
209
+ attention_mask: Optional[torch.Tensor] = None,
210
+ bidirectional: bool = False,
211
+ ) -> tuple[torch.Tensor, torch.Tensor]:
212
+ if input_ids.dim() != 2:
213
+ raise ValueError(f"input_ids must be rank-2 [batch, seq], got shape {tuple(input_ids.shape)}")
214
+ if input_ids.shape[1] > self.config.max_seq_len:
215
+ raise ValueError(
216
+ f"Sequence length {input_ids.shape[1]} exceeds max_seq_len {self.config.max_seq_len}"
217
+ )
218
+
219
+
220
+ mask = self._build_attention_mask(input_ids, attention_mask, bidirectional=bidirectional)
221
+ token_embeddings = self.token_embeddings(input_ids)
222
+ hidden_states = token_embeddings
223
+ pos_info = self.position_encoder.get_positional_info(input_ids.shape[1], input_ids.device)
224
+ if pos_info.apply_to_embeddings:
225
+ if pos_info.embeddings is None:
226
+ raise ValueError(f"Embedding-level pe_type {pos_info.pe_type} did not provide embeddings.")
227
+ hidden_states = hidden_states + pos_info.embeddings.unsqueeze(0)
228
+ hidden_states = self.embedding_dropout(hidden_states)
229
+
230
+ for symbol_retriever, layer in zip(self.symbol_retrievers, self.layers):
231
+ hidden_states = layer(
232
+ hidden_states,
233
+ symbol_retriever=symbol_retriever,
234
+ mask=mask,
235
+ pos_info=pos_info,
236
+ )
237
+
238
+ hidden_states = self.final_norm(hidden_states)
239
+ return token_embeddings, hidden_states
240
+
241
+ def forward(
242
+ self,
243
+ input_ids: torch.Tensor,
244
+ targets: Optional[torch.Tensor] = None,
245
+ attention_mask: Optional[torch.Tensor] = None,
246
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
247
+ _, hidden_states = self.encode_for_objective(input_ids, attention_mask=attention_mask)
248
+ logits = self.lm_head(hidden_states)
249
+
250
+ loss = None
251
+ if targets is not None:
252
+ if targets.shape != input_ids.shape:
253
+ raise ValueError(
254
+ f"targets shape must match input_ids shape, got {tuple(targets.shape)} "
255
+ f"vs {tuple(input_ids.shape)}"
256
+ )
257
+ loss = F.cross_entropy(
258
+ logits.reshape(-1, logits.size(-1)),
259
+ targets.reshape(-1),
260
+ ignore_index=-1,
261
+ )
262
+
263
+ return logits, loss
264
+
265
+ def forward_mlm(
266
+ self,
267
+ input_ids: torch.Tensor,
268
+ attention_mask: Optional[torch.Tensor] = None,
269
+ ) -> torch.Tensor:
270
+ """Encode bidirectionally and apply the MLM head.
271
+
272
+ Args:
273
+ input_ids: Masked input token ids [batch, seq].
274
+ attention_mask: Validity mask [batch, seq].
275
+
276
+ Returns:
277
+ MLM logits at every position [batch, seq, vocab].
278
+ """
279
+ if not self.config.mlm_head_enabled:
280
+ raise ValueError(
281
+ "forward_mlm requires mlm_head_enabled=True; "
282
+ "the MLM head is not instantiated."
283
+ )
284
+ _, hidden_states = self.encode_for_objective(
285
+ input_ids, attention_mask=attention_mask, bidirectional=True
286
+ )
287
+ return self.mlm_head(hidden_states)
288
+
289
+ @torch.no_grad()
290
+ def generate(
291
+ self,
292
+ input_ids: torch.Tensor,
293
+ attention_mask: Optional[torch.Tensor] = None,
294
+ max_new_tokens: int = 100,
295
+ temperature: float = 1.0,
296
+ top_k: Optional[int] = None,
297
+ do_sample: bool = True,
298
+ eos_token_id: Optional[int] = None,
299
+ ) -> torch.Tensor:
300
+ if temperature <= 0.0:
301
+ raise ValueError(f"temperature must be positive, got {temperature}")
302
+ if max_new_tokens < 0:
303
+ raise ValueError(f"max_new_tokens must be non-negative, got {max_new_tokens}")
304
+ if top_k is not None and top_k <= 0:
305
+ raise ValueError(f"top_k must be positive when provided, got {top_k}")
306
+ if eos_token_id is None:
307
+ eos_token_id = self.config.eos_token_id
308
+
309
+ if attention_mask is None:
310
+ generated_sequences = [row.clone() for row in input_ids]
311
+ else:
312
+ current_attention_mask = attention_mask.bool()
313
+ generated_sequences = []
314
+ for row, row_mask in zip(input_ids, current_attention_mask):
315
+ generated_sequence = row[row_mask]
316
+ if generated_sequence.numel() == 0:
317
+ raise ValueError("Each input row must contain at least one unmasked token for generation.")
318
+ generated_sequences.append(generated_sequence)
319
+
320
+ finished = torch.tensor(
321
+ [sequence[-1].item() == eos_token_id for sequence in generated_sequences],
322
+ dtype=torch.bool,
323
+ device=input_ids.device,
324
+ )
325
+
326
+ for _ in range(max_new_tokens):
327
+ context_sequences = [sequence[-self.config.max_seq_len :] for sequence in generated_sequences]
328
+ context_ids = pad_sequence(
329
+ context_sequences,
330
+ batch_first=True,
331
+ padding_value=self.config.pad_token_id,
332
+ )
333
+ context_mask = pad_sequence(
334
+ [
335
+ torch.ones(sequence.shape[0], dtype=torch.bool, device=input_ids.device)
336
+ for sequence in context_sequences
337
+ ],
338
+ batch_first=True,
339
+ padding_value=False,
340
+ )
341
+ logits, _ = self(context_ids, attention_mask=context_mask)
342
+ last_positions = context_mask.long().sum(dim=1) - 1
343
+ batch_indices = torch.arange(logits.shape[0], device=logits.device)
344
+ next_token_logits = logits[batch_indices, last_positions, :] / temperature
345
+
346
+ if top_k is not None:
347
+ k = min(top_k, next_token_logits.size(-1))
348
+ top_values, _ = torch.topk(next_token_logits, k=k)
349
+ cutoff = top_values[:, -1].unsqueeze(-1)
350
+ next_token_logits = next_token_logits.masked_fill(next_token_logits < cutoff, float("-inf"))
351
+
352
+ if do_sample:
353
+ probs = F.softmax(next_token_logits, dim=-1)
354
+ next_token = torch.multinomial(probs, num_samples=1)
355
+ else:
356
+ next_token = torch.argmax(next_token_logits, dim=-1, keepdim=True)
357
+
358
+ for row_index in range(next_token.shape[0]):
359
+ if finished[row_index]:
360
+ continue
361
+ generated_sequences[row_index] = torch.cat(
362
+ [generated_sequences[row_index], next_token[row_index]],
363
+ )
364
+ if next_token[row_index, 0].item() == eos_token_id:
365
+ finished[row_index] = True
366
+
367
+ if torch.all(finished):
368
+ break
369
+
370
+ return pad_sequence(
371
+ generated_sequences,
372
+ batch_first=True,
373
+ padding_value=self.config.pad_token_id,
374
+ )
375
+
376
+
377
+ def _build_symbol_retrievers(config: DatLMConfig) -> nn.ModuleList:
378
+ retriever = _build_symbol_retriever(config)
379
+ if config.shared_symbol_retriever:
380
+ return nn.ModuleList([retriever] * config.n_layers)
381
+ return nn.ModuleList(_build_symbol_retriever(config) for _ in range(config.n_layers))
382
+
383
+
384
+ def _build_symbol_retriever(config: DatLMConfig) -> nn.Module:
385
+ if config.symbol_retrieval == "symbolic":
386
+ return SymbolicAttentionRetriever(
387
+ hidden_dim=config.hidden_dim,
388
+ symbol_dim=config.resolved_symbol_dim,
389
+ n_symbols=config.resolved_n_symbols,
390
+ n_heads=config.resolved_symbolic_attn_n_heads,
391
+ dropout=config.dropout,
392
+ use_bias=config.symbolic_use_bias,
393
+ )
394
+ if config.symbol_retrieval == "positional":
395
+ return PositionalSymbolRetriever(
396
+ symbol_dim=config.resolved_symbol_dim,
397
+ max_len=config.max_seq_len,
398
+ sinusoidal=config.positional_symbols_sinusoidal,
399
+ )
400
+ if config.symbol_retrieval == "relative":
401
+ max_rel_distance = config.max_rel_pos if config.max_rel_pos is not None else max(1, config.max_seq_len // 2)
402
+ return RelativePositionalSymbolRetriever(
403
+ symbol_dim=config.resolved_symbol_dim,
404
+ max_rel_distance=max_rel_distance,
405
+ rope=config.relative_symbols_rope,
406
+ theta=config.rope_theta,
407
+ )
408
+ if config.symbol_retrieval == "relsymbolic":
409
+ return RelationalSymbolicAttentionRetriever(
410
+ hidden_dim=config.hidden_dim,
411
+ symbol_dim=config.resolved_symbol_dim,
412
+ rel_n_heads=config.relsymbolic_rel_n_heads,
413
+ symbolic_attn_n_heads=config.relsymbolic_symbolic_attn_n_heads,
414
+ n_symbols=config.resolved_n_symbols,
415
+ neighborhood_size=config.relsymbolic_neighborhood_size,
416
+ include_self=config.relsymbolic_include_self,
417
+ normalize_rels=config.relsymbolic_normalize_rels,
418
+ dropout=config.relsymbolic_dropout,
419
+ trainable_symbols=config.relsymbolic_trainable_symbols,
420
+ rel_scale=config.relsymbolic_rel_scale,
421
+ symbolic_attn_scale=config.relsymbolic_symbolic_attn_scale,
422
+ use_bias=config.relsymbolic_use_bias,
423
+ )
424
+ raise ValueError(f"Unsupported symbol_retrieval: {config.symbol_retrieval}")
425
+
426
+
427
+ def _build_relational_attention(config: DatLMConfig) -> nn.Module:
428
+ use_relative_symbols = config.symbol_retrieval == "relative"
429
+ if config.ra_type == "ra":
430
+ return RelationalAttention(
431
+ hidden_dim=config.hidden_dim,
432
+ symbol_dim=config.resolved_symbol_dim,
433
+ n_heads=config.n_heads_ra,
434
+ total_n_heads=config.total_n_heads,
435
+ n_relations=config.resolved_ra_n_relations,
436
+ dropout=config.dropout,
437
+ rel_activation=config.ra_rel_activation,
438
+ symmetric_rels=config.ra_symmetric_rels,
439
+ use_relative_positional_symbols=use_relative_symbols,
440
+ use_bias_qkv=config.use_bias_qkv,
441
+ use_bias_out=config.use_bias_out,
442
+ )
443
+ if config.ra_type == "rca":
444
+ return RelationalCrossAttention(
445
+ hidden_dim=config.hidden_dim,
446
+ symbol_dim=config.resolved_symbol_dim,
447
+ n_heads=config.n_heads_ra,
448
+ total_n_heads=config.total_n_heads,
449
+ dropout=config.dropout,
450
+ activation=config.ra_rel_activation,
451
+ use_relative_positional_symbols=use_relative_symbols,
452
+ use_bias_qkv=config.use_bias_qkv,
453
+ use_bias_out=config.use_bias_out,
454
+ )
455
+ if config.ra_type == "disrca":
456
+ return DisentangledRelationalCrossAttention(
457
+ hidden_dim=config.hidden_dim,
458
+ symbol_dim=config.resolved_symbol_dim,
459
+ n_heads=config.n_heads_ra,
460
+ total_n_heads=config.total_n_heads,
461
+ dropout=config.dropout,
462
+ rel_activation=config.ra_rel_activation,
463
+ use_relative_positional_symbols=use_relative_symbols,
464
+ use_bias_qkv=config.use_bias_qkv,
465
+ use_bias_out=config.use_bias_out,
466
+ )
467
+ raise ValueError(f"Unsupported ra_type: {config.ra_type}")
468
+
469
+
470
+ def _create_norm(norm_type: str, hidden_dim: int) -> nn.Module:
471
+ if norm_type == "layernorm":
472
+ return nn.LayerNorm(hidden_dim)
473
+ if norm_type == "rmsnorm":
474
+ return RMSNorm(hidden_dim)
475
+ raise ValueError(f"Unsupported norm_type: {norm_type}")
dat_symbols.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Symbol retrieval modules for the owned DAT backend."""
2
+
3
+ import math
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+ from .transformer_components import PositionalEncoding
10
+
11
+
12
+ class SymbolicAttentionRetriever(nn.Module):
13
+ def __init__(
14
+ self,
15
+ hidden_dim: int,
16
+ symbol_dim: int,
17
+ n_symbols: int,
18
+ n_heads: int,
19
+ dropout: float = 0.0,
20
+ trainable_symbols: bool = True,
21
+ scale: float | None = None,
22
+ use_bias: bool = False,
23
+ ):
24
+ super().__init__()
25
+ if hidden_dim % n_heads != 0:
26
+ raise ValueError(f"hidden_dim ({hidden_dim}) must be divisible by n_heads ({n_heads})")
27
+ if symbol_dim % n_heads != 0:
28
+ raise ValueError(f"symbol_dim ({symbol_dim}) must be divisible by n_heads ({n_heads})")
29
+ if scale is not None and scale <= 0.0:
30
+ raise ValueError(f"scale must be positive when provided, got {scale}")
31
+
32
+ self.hidden_dim = hidden_dim
33
+ self.symbol_dim = symbol_dim
34
+ self.n_symbols = n_symbols
35
+ self.n_heads = n_heads
36
+ self.head_dim = hidden_dim // n_heads
37
+ self.symbol_head_dim = symbol_dim // n_heads
38
+ self.scale = 1.0 / math.sqrt(self.head_dim) if scale is None else scale
39
+
40
+ self.q_proj = nn.Linear(hidden_dim, hidden_dim, bias=use_bias)
41
+ self.template_features = nn.Parameter(torch.empty(n_symbols, hidden_dim))
42
+ self.symbol_library = nn.Parameter(
43
+ torch.empty(n_symbols, symbol_dim),
44
+ requires_grad=trainable_symbols,
45
+ )
46
+ self.dropout = nn.Dropout(dropout)
47
+ self._init_weights()
48
+
49
+ def _init_weights(self) -> None:
50
+ nn.init.normal_(self.template_features, mean=0.0, std=0.9)
51
+ nn.init.normal_(self.symbol_library, mean=0.0, std=0.9)
52
+
53
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
54
+ batch_size, seq_len, _ = x.shape
55
+
56
+ queries = self.q_proj(x)
57
+ queries = queries.view(batch_size, seq_len, self.n_heads, self.head_dim)
58
+ queries = queries.transpose(1, 2)
59
+
60
+ keys = self.template_features.view(self.n_symbols, self.n_heads, self.head_dim)
61
+ keys = keys.transpose(0, 1).unsqueeze(0).expand(batch_size, -1, -1, -1)
62
+
63
+ values = self.symbol_library.view(self.n_symbols, self.n_heads, self.symbol_head_dim)
64
+ values = values.transpose(0, 1).unsqueeze(0).expand(batch_size, -1, -1, -1)
65
+
66
+ scores = torch.matmul(queries, keys.transpose(-2, -1)) * self.scale
67
+ weights = F.softmax(scores, dim=-1)
68
+ weights = self.dropout(weights)
69
+ retrieved = torch.matmul(weights, values)
70
+ retrieved = retrieved.transpose(1, 2).contiguous()
71
+ return retrieved.view(batch_size, seq_len, self.symbol_dim)
72
+
73
+
74
+ class PositionalSymbolRetriever(nn.Module):
75
+ def __init__(
76
+ self,
77
+ symbol_dim: int,
78
+ max_len: int,
79
+ sinusoidal: bool = False,
80
+ ):
81
+ super().__init__()
82
+ self.symbol_dim = symbol_dim
83
+ self.max_len = max_len
84
+ self.sinusoidal = sinusoidal
85
+ pe_type = "sinusoidal" if sinusoidal else "learned"
86
+ self.position_encoder = PositionalEncoding(
87
+ embedding_dim=symbol_dim,
88
+ pe_type=pe_type,
89
+ max_len=max_len,
90
+ )
91
+
92
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
93
+ batch_size, seq_len, _ = x.shape
94
+ pos_info = self.position_encoder.get_positional_info(seq_len, x.device)
95
+ if pos_info.embeddings is None:
96
+ raise ValueError(f"Positional symbol retrieval produced no embeddings for seq_len={seq_len}")
97
+ return pos_info.embeddings.unsqueeze(0).expand(batch_size, -1, -1)
98
+
99
+
100
+ class RelativePositionalSymbolRetriever(nn.Module):
101
+ def __init__(
102
+ self,
103
+ symbol_dim: int,
104
+ max_rel_distance: int,
105
+ rope: bool = False,
106
+ theta: float = 10000.0,
107
+ ):
108
+ super().__init__()
109
+ if rope and symbol_dim % 2 != 0:
110
+ raise ValueError(f"RoPE relative symbols require even symbol_dim, got {symbol_dim}")
111
+ if theta <= 0.0:
112
+ raise ValueError(f"theta must be positive, got {theta}")
113
+ self.symbol_dim = symbol_dim
114
+ self.max_rel_distance = max_rel_distance
115
+ self.rope = rope
116
+ self.theta = theta
117
+ if rope:
118
+ self.register_buffer("_rope_relative_cache", torch.empty(0), persistent=False)
119
+ else:
120
+ self.position_encoder = PositionalEncoding(
121
+ embedding_dim=symbol_dim,
122
+ pe_type="relative",
123
+ max_len=max_rel_distance * 2,
124
+ max_rel_pos=max_rel_distance,
125
+ )
126
+
127
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
128
+ seq_len = x.shape[1]
129
+ if self.rope:
130
+ return self._rope_relative_symbols(seq_len, x.device, x.dtype)
131
+
132
+ pos_info = self.position_encoder.get_positional_info(seq_len, x.device)
133
+ if pos_info.rel_embeddings is None:
134
+ raise ValueError(f"Relative symbol retrieval produced no embeddings for seq_len={seq_len}")
135
+ return pos_info.rel_embeddings
136
+
137
+ def _rope_relative_symbols(
138
+ self,
139
+ seq_len: int,
140
+ device: torch.device,
141
+ dtype: torch.dtype,
142
+ ) -> torch.Tensor:
143
+ cached = self._rope_relative_cache
144
+ if (
145
+ cached.shape[0] >= seq_len
146
+ and cached.device == device
147
+ and cached.dtype == dtype
148
+ ):
149
+ return cached[:seq_len, :seq_len, :]
150
+
151
+ positions = torch.arange(seq_len, device=device)
152
+ distances = positions[None, :] - positions[:, None]
153
+ if self.max_rel_distance is not None:
154
+ distances = torch.clamp(distances, -self.max_rel_distance, self.max_rel_distance)
155
+ inv_freq = PositionalEncoding._rope_inv_freq(self.symbol_dim, self.theta, device=device)
156
+ phases = distances.to(torch.float32).unsqueeze(-1) * inv_freq
157
+ symbols = torch.empty(seq_len, seq_len, self.symbol_dim, device=device)
158
+ symbols[..., 0::2] = torch.cos(phases)
159
+ symbols[..., 1::2] = torch.sin(phases)
160
+ self._rope_relative_cache = symbols.to(dtype=dtype)
161
+ return self._rope_relative_cache
162
+
163
+
164
+ class RelationalSymbolicAttentionRetriever(nn.Module):
165
+ def __init__(
166
+ self,
167
+ hidden_dim: int,
168
+ symbol_dim: int,
169
+ rel_n_heads: int,
170
+ symbolic_attn_n_heads: int,
171
+ n_symbols: int,
172
+ neighborhood_size: int = 2,
173
+ include_self: bool = False,
174
+ normalize_rels: bool = True,
175
+ dropout: float = 0.0,
176
+ trainable_symbols: bool = True,
177
+ rel_scale: float | None = None,
178
+ symbolic_attn_scale: float | None = None,
179
+ use_bias: bool = False,
180
+ ):
181
+ super().__init__()
182
+ if hidden_dim % rel_n_heads != 0:
183
+ raise ValueError(
184
+ f"hidden_dim ({hidden_dim}) must be divisible by rel_n_heads ({rel_n_heads})"
185
+ )
186
+ if rel_scale is not None and rel_scale <= 0.0:
187
+ raise ValueError(f"rel_scale must be positive when provided, got {rel_scale}")
188
+ if symbolic_attn_scale is not None and symbolic_attn_scale <= 0.0:
189
+ raise ValueError(
190
+ "symbolic_attn_scale must be positive when provided, "
191
+ f"got {symbolic_attn_scale}"
192
+ )
193
+
194
+ self.hidden_dim = hidden_dim
195
+ self.symbol_dim = symbol_dim
196
+ self.rel_n_heads = rel_n_heads
197
+ self.neighborhood_size = neighborhood_size
198
+ self.include_self = include_self
199
+ self.normalize_rels = normalize_rels
200
+ self.neighborhood_dim = neighborhood_size + (1 if include_self else 0)
201
+ self.rel_feature_dim = rel_n_heads * self.neighborhood_dim
202
+ rel_head_dim = hidden_dim // rel_n_heads
203
+ self.rel_scale = 1.0 / math.sqrt(rel_head_dim) if rel_scale is None else rel_scale
204
+
205
+ self.q_proj = nn.Linear(hidden_dim, hidden_dim, bias=use_bias)
206
+ self.k_proj = nn.Linear(hidden_dim, hidden_dim, bias=use_bias)
207
+ self.rel_to_hidden = nn.Linear(self.rel_feature_dim, hidden_dim, bias=True)
208
+ self.symbolic_attention = SymbolicAttentionRetriever(
209
+ hidden_dim=hidden_dim,
210
+ symbol_dim=symbol_dim,
211
+ n_symbols=n_symbols,
212
+ n_heads=symbolic_attn_n_heads,
213
+ dropout=dropout,
214
+ trainable_symbols=trainable_symbols,
215
+ scale=symbolic_attn_scale,
216
+ use_bias=use_bias,
217
+ )
218
+ self._init_weights()
219
+
220
+ def _init_weights(self) -> None:
221
+ nn.init.xavier_uniform_(self.q_proj.weight)
222
+ nn.init.xavier_uniform_(self.k_proj.weight)
223
+ if self.q_proj.bias is not None:
224
+ nn.init.zeros_(self.q_proj.bias)
225
+ if self.k_proj.bias is not None:
226
+ nn.init.zeros_(self.k_proj.bias)
227
+ nn.init.xavier_uniform_(self.rel_to_hidden.weight)
228
+ nn.init.zeros_(self.rel_to_hidden.bias)
229
+
230
+ def _compute_neighborhood_indices(self, seq_len: int, device: torch.device) -> torch.Tensor:
231
+ positions = torch.arange(seq_len, device=device).unsqueeze(1)
232
+ # Decoder-LM adaptation: DSSL uses bidirectional neighborhoods, but this
233
+ # owned backend must keep symbol retrieval causal. Keep the old causal
234
+ # neighborhood order: current/immediate-past positions before older ones.
235
+ if self.include_self:
236
+ offsets = torch.arange(0, self.neighborhood_size + 1, device=device).unsqueeze(0)
237
+ else:
238
+ offsets = torch.arange(1, self.neighborhood_size + 1, device=device).unsqueeze(0)
239
+ return (positions - offsets).clamp(0, seq_len - 1)
240
+
241
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
242
+ batch_size, seq_len, _ = x.shape
243
+ head_dim = self.hidden_dim // self.rel_n_heads
244
+
245
+ queries = self.q_proj(x).view(batch_size, seq_len, self.rel_n_heads, head_dim)
246
+ keys = self.k_proj(x).view(batch_size, seq_len, self.rel_n_heads, head_dim)
247
+ queries = queries.transpose(1, 2)
248
+ keys = keys.transpose(1, 2)
249
+
250
+ neighbor_indices = self._compute_neighborhood_indices(seq_len, x.device)
251
+ neighborhood_keys = keys[:, :, neighbor_indices]
252
+ neighborhood_relations = torch.einsum("bhid,bhijd->bhij", queries, neighborhood_keys)
253
+ if self.normalize_rels:
254
+ neighborhood_relations = F.softmax(neighborhood_relations * self.rel_scale, dim=-1)
255
+
256
+ neighborhood_relations = neighborhood_relations.permute(0, 2, 3, 1)
257
+ neighborhood_relations = neighborhood_relations.contiguous().view(
258
+ batch_size,
259
+ seq_len,
260
+ self.rel_feature_dim,
261
+ )
262
+ relational_features = self.rel_to_hidden(neighborhood_relations)
263
+ return self.symbolic_attention(relational_features)
masks.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Attention-mask helpers shared by repo-owned decoder LMs."""
2
+
3
+ from typing import Optional
4
+
5
+ import torch
6
+
7
+
8
+ def build_decoder_attention_mask(
9
+ input_ids: torch.Tensor,
10
+ pad_token_id: int,
11
+ eos_token_id: int,
12
+ sequence_boundary_policy: str,
13
+ attention_mask: Optional[torch.Tensor] = None,
14
+ segment_boundary_token_id: Optional[int] = None,
15
+ bidirectional: bool = False,
16
+ ) -> torch.Tensor:
17
+ if input_ids.ndim != 2:
18
+ raise ValueError(
19
+ "input_ids must be rank-2 [batch, seq], "
20
+ f"got shape {tuple(input_ids.shape)}"
21
+ )
22
+ _, seq_len = input_ids.shape
23
+ if attention_mask is None:
24
+ valid_tokens = input_ids != pad_token_id
25
+ else:
26
+ valid_tokens = attention_mask.bool()
27
+
28
+ query_mask = valid_tokens.unsqueeze(2)
29
+ key_mask = valid_tokens.unsqueeze(1)
30
+ if bidirectional:
31
+ mask = query_mask & key_mask
32
+ else:
33
+ directionality = torch.tril(
34
+ torch.ones(seq_len, seq_len, dtype=torch.bool, device=input_ids.device)
35
+ ).unsqueeze(0)
36
+ mask = directionality & query_mask & key_mask
37
+
38
+ if sequence_boundary_policy == "none":
39
+ return mask
40
+ if sequence_boundary_policy == "segment_document":
41
+ if segment_boundary_token_id is None:
42
+ raise ValueError(
43
+ "segment_boundary_token_id is required when "
44
+ "sequence_boundary_policy='segment_document'"
45
+ )
46
+ # The boundary token starts the next segment: cumsum increments on the
47
+ # boundary position, so the marker attends with the following tokens.
48
+ segment_ids = torch.cumsum(
49
+ input_ids == segment_boundary_token_id, dim=1
50
+ )
51
+ same_segment = segment_ids.unsqueeze(1) == segment_ids.unsqueeze(2)
52
+ return mask & same_segment
53
+ if sequence_boundary_policy != "eos_document":
54
+ raise ValueError(f"Unsupported sequence_boundary_policy: {sequence_boundary_policy}")
55
+
56
+ # Next-token training predicts EOS from the preceding document token.
57
+ # Once EOS is present as an input token, it starts the next segment so the
58
+ # following document is not predicted with prior-document context.
59
+ document_ids = torch.cumsum(input_ids == eos_token_id, dim=1)
60
+ same_document = document_ids.unsqueeze(1) == document_ids.unsqueeze(2)
61
+ return mask & same_document
mlm.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MLM head for the hybrid MLM/CLM objective.
2
+
3
+ Ported from temp/gpt-bert-main/pretraining/model.py MaskClassifier, but
4
+ configurable to match the trunk's customizability:
5
+ - norm_type: "layernorm" or "rmsnorm" (reuses the existing field).
6
+ - ffn_activation: "gelu", "swiglu", "relu", or "identity" (reuses the
7
+ existing field). The head's nonlinearity follows this choice.
8
+ - use_bias_ffn: whether the linear layers in the head use bias.
9
+ - weight tying: the final linear is tied to the word embedding, matching
10
+ gpt-bert's MaskClassifier and our lm_head.
11
+
12
+ The head is controlled by model config (mlm_head_enabled) so checkpoint
13
+ loading and HF export remain strict and honest.
14
+ """
15
+
16
+ import torch
17
+ import torch.nn as nn
18
+
19
+ from .transformer_components import RMSNorm, FeedForward
20
+
21
+
22
+ def _create_norm(norm_type: str, hidden_dim: int) -> nn.Module:
23
+ if norm_type == "layernorm":
24
+ return nn.LayerNorm(hidden_dim)
25
+ if norm_type == "rmsnorm":
26
+ return RMSNorm(hidden_dim)
27
+ raise ValueError(f"Unsupported norm_type: {norm_type}")
28
+
29
+
30
+ class MaskClassifier(nn.Module):
31
+ """MLM prediction head with configurable norm and activation.
32
+
33
+ Architecture (matching gpt-bert's MaskClassifier):
34
+ norm -> FeedForward (activation, bias configurable) -> norm -> dropout -> linear (tied)
35
+
36
+ The final linear is tied to the word embedding weight, matching
37
+ gpt-bert's MaskClassifier and our lm_head.
38
+
39
+ Args:
40
+ hidden_dim: Model hidden dimension.
41
+ vocab_size: Vocabulary size for the output projection.
42
+ norm_type: "layernorm" or "rmsnorm".
43
+ ffn_activation: "gelu", "swiglu", "relu", or "identity".
44
+ use_bias_ffn: Whether the intermediate linear layers use bias.
45
+ dropout: Dropout probability.
46
+ word_embedding: The word embedding weight to tie the final linear to.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ hidden_dim: int,
52
+ vocab_size: int,
53
+ norm_type: str,
54
+ ffn_activation: str,
55
+ use_bias_ffn: bool,
56
+ dropout: float,
57
+ word_embedding: nn.Parameter,
58
+ ):
59
+ super().__init__()
60
+ self.hidden_dim = hidden_dim
61
+ self.vocab_size = vocab_size
62
+ self.norm_type = norm_type
63
+ self.ffn_activation = ffn_activation
64
+ self.use_bias_ffn = use_bias_ffn
65
+ self.dropout_p = dropout
66
+
67
+ self.norm1 = _create_norm(norm_type, hidden_dim)
68
+ self.feed_forward = FeedForward(
69
+ input_dim=hidden_dim,
70
+ hidden_dim=hidden_dim,
71
+ dropout=dropout,
72
+ activation=ffn_activation,
73
+ use_bias=use_bias_ffn,
74
+ )
75
+ self.norm2 = _create_norm(norm_type, hidden_dim)
76
+ self.dropout = nn.Dropout(dropout)
77
+ self.linear_out = nn.Linear(hidden_dim, vocab_size, bias=False)
78
+ self.linear_out.weight = word_embedding
79
+
80
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
81
+ """Apply the MLM head to contextualized embeddings.
82
+
83
+ Args:
84
+ x: Contextualized embeddings [batch, seq_len, hidden_dim] or
85
+ flattened [num_masked, hidden_dim].
86
+
87
+ Returns:
88
+ Logits [batch, seq_len, vocab_size] or [num_masked, vocab_size].
89
+ """
90
+ x = self.norm1(x)
91
+ x = self.feed_forward(x)
92
+ x = self.norm2(x)
93
+ x = self.dropout(x)
94
+ x = self.linear_out(x)
95
+ return x
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8af548664053b171d726ec05dd80f7bf2f9cb46933fcf2e27071d3cc29fa4531
3
+ size 1187885624
modeling_dat.py ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HuggingFace PreTrainedModel wrapper for the dual-attention (DAT) decoder LM.
2
+
3
+ Source-of-truth copy. scripts/convert_dat_to_hf.py copies it into a generated HF
4
+ repository together with configuration_dat.py and the flattened model source
5
+ (dat_config.py, dat_core.py, dat_symbols.py, dat_lm.py, transformer_core.py,
6
+ transformer_components.py).
7
+
8
+ Why every model module is imported directly here: with trust_remote_code on a
9
+ local directory, transformers 4.46.3 only copies the entry module's *direct*
10
+ relative imports into its dynamic-module cache (it does not recurse). Importing
11
+ all flattened modules here forces every one of them to be copied, after which
12
+ their own relative imports resolve because the files are co-located.
13
+ """
14
+
15
+ import collections
16
+
17
+ import torch
18
+ from transformers import PreTrainedModel
19
+ from transformers.modeling_outputs import BaseModelOutput, CausalLMOutput, MaskedLMOutput, SequenceClassifierOutput
20
+ from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss
21
+
22
+ # Force every flattened module to be copied into the dynamic-module cache.
23
+ from .attention_config import SUPPORTED_SEQUENCE_BOUNDARY_POLICIES # noqa: F401
24
+ from .masks import build_decoder_attention_mask # noqa: F401
25
+ from .mlm import MaskClassifier # noqa: F401
26
+ from .transformer_components import FeedForward, PositionalEncoding # noqa: F401
27
+ from .transformer_core import SelfAttention # noqa: F401
28
+ from .dat_symbols import SymbolicAttentionRetriever # noqa: F401
29
+ from .dat_core import RelationalAttention # noqa: F401
30
+ from .dat_config import DatLMConfig
31
+ from .dat_lm import DatDecoderLM
32
+ from .configuration_dat import DatConfig, DAT_LM_FIELDS
33
+
34
+
35
+ def _refresh_rope_buffers(model: DatDecoderLM, rope_theta: float) -> None:
36
+ pe = model.position_encoder
37
+ if hasattr(pe, "pe_type") and pe.pe_type == "rope":
38
+ cos, sin = pe._precompute_rope_freqs(pe.embedding_dim, pe.max_len, rope_theta)
39
+ pe.register_buffer("rope_cos", cos, persistent=False)
40
+ pe.register_buffer("rope_sin", sin, persistent=False)
41
+ pe._rope_uninitialized = True
42
+
43
+
44
+ def _collect_shared_weight_keys(module: torch.nn.Module) -> dict[str, str]:
45
+ # remove_duplicate=False is essential: by default named_parameters yields
46
+ # each shared tensor only once, which would hide exactly the duplicates we
47
+ # need to declare (and which state_dict still emits under every name).
48
+ names_by_storage: dict[int, list[str]] = collections.defaultdict(list)
49
+ for name, parameter in module.named_parameters(remove_duplicate=False):
50
+ names_by_storage[id(parameter)].append(name)
51
+ shared_keys: dict[str, str] = {}
52
+ for names in names_by_storage.values():
53
+ if len(names) > 1:
54
+ names = sorted(names)
55
+ # Prefer token_embeddings.weight as the canonical source
56
+ # (HF convention: output weights tied to input embeddings).
57
+ source = names[0]
58
+ for n in names:
59
+ if n.endswith("token_embeddings.weight"):
60
+ source = n
61
+ break
62
+ for target in names:
63
+ if target != source:
64
+ shared_keys[target] = source
65
+ return shared_keys
66
+
67
+
68
+ class _ExpandedTiedWeightsMixin:
69
+ def get_expanded_tied_weights_keys(self, all_submodels: bool = False) -> dict:
70
+ # HF's default returns {} when tie_word_embeddings=False, which would
71
+ # discard the mlm_head.linear_out <-> lm_head tie for NextLat models
72
+ # (tie_lm_head=False). Return the dynamically computed mapping directly
73
+ # so all shared weights are properly tied on load.
74
+ return dict(self._tied_weights_keys)
75
+
76
+
77
+ class DatModel(_ExpandedTiedWeightsMixin, PreTrainedModel):
78
+ config_class = DatConfig
79
+ base_model_prefix = ""
80
+
81
+ def __init__(self, config: DatConfig) -> None:
82
+ super().__init__(config)
83
+ dat_config = DatLMConfig(**{field: getattr(config, field) for field in DAT_LM_FIELDS})
84
+ self.model = DatDecoderLM(dat_config)
85
+ self._tied_weights_keys = _collect_shared_weight_keys(self)
86
+ self.post_init()
87
+
88
+ def post_init(self) -> None:
89
+ super().post_init()
90
+ # Work around accelerate/transformers leaving persistent=False buffers uninitialized
91
+ _refresh_rope_buffers(self.model, self.config.rope_theta)
92
+
93
+ def get_input_embeddings(self) -> torch.nn.Module:
94
+ return self.model.token_embeddings
95
+
96
+ def set_input_embeddings(self, value: torch.nn.Module) -> None:
97
+ self.model.token_embeddings = value
98
+
99
+ def forward(
100
+ self,
101
+ input_ids: torch.Tensor,
102
+ attention_mask: torch.Tensor | None = None,
103
+ **kwargs,
104
+ ) -> BaseModelOutput:
105
+ _, hidden_states = self.model.encode_for_objective(input_ids, attention_mask=attention_mask)
106
+ return BaseModelOutput(last_hidden_state=hidden_states)
107
+
108
+
109
+ class DatForCausalLM(_ExpandedTiedWeightsMixin, PreTrainedModel):
110
+ config_class = DatConfig
111
+ base_model_prefix = "model"
112
+ _tied_weights_keys = {"model.lm_head.weight": "model.token_embeddings.weight"}
113
+
114
+ def __init__(self, config: DatConfig) -> None:
115
+ super().__init__(config)
116
+ dat_config = DatLMConfig(**{field: getattr(config, field) for field in DAT_LM_FIELDS})
117
+ self.model = DatDecoderLM(dat_config)
118
+ self._tied_weights_keys = _collect_shared_weight_keys(self)
119
+ self.post_init()
120
+
121
+ def post_init(self) -> None:
122
+ super().post_init()
123
+ # Work around accelerate/transformers leaving persistent=False buffers uninitialized
124
+ _refresh_rope_buffers(self.model, self.config.rope_theta)
125
+
126
+ def get_input_embeddings(self) -> torch.nn.Module:
127
+ return self.model.token_embeddings
128
+
129
+ def set_input_embeddings(self, value: torch.nn.Module) -> None:
130
+ self.model.token_embeddings = value
131
+
132
+ def get_output_embeddings(self) -> torch.nn.Module:
133
+ return self.model.lm_head
134
+
135
+ def set_output_embeddings(self, new_embeddings: torch.nn.Module) -> None:
136
+ self.model.lm_head = new_embeddings
137
+
138
+ def forward(
139
+ self,
140
+ input_ids: torch.Tensor,
141
+ attention_mask: torch.Tensor | None = None,
142
+ labels: torch.Tensor | None = None,
143
+ **kwargs,
144
+ ) -> CausalLMOutput:
145
+ logits, _ = self.model(input_ids, attention_mask=attention_mask)
146
+
147
+ loss = None
148
+ if labels is not None:
149
+ shift_logits = logits[:, :-1, :].contiguous()
150
+ shift_labels = labels[:, 1:].contiguous()
151
+ loss = torch.nn.functional.cross_entropy(
152
+ shift_logits.view(-1, shift_logits.size(-1)),
153
+ shift_labels.view(-1),
154
+ ignore_index=-100,
155
+ )
156
+
157
+ return CausalLMOutput(loss=loss, logits=logits)
158
+
159
+
160
+ class DatForMaskedLM(_ExpandedTiedWeightsMixin, PreTrainedModel):
161
+ config_class = DatConfig
162
+ base_model_prefix = "model"
163
+
164
+ def __init__(self, config: DatConfig) -> None:
165
+ super().__init__(config)
166
+ dat_config = DatLMConfig(**{field: getattr(config, field) for field in DAT_LM_FIELDS})
167
+ self.model = DatDecoderLM(dat_config)
168
+ self._tied_weights_keys = _collect_shared_weight_keys(self)
169
+ self.post_init()
170
+
171
+ def post_init(self) -> None:
172
+ super().post_init()
173
+ _refresh_rope_buffers(self.model, self.config.rope_theta)
174
+
175
+ def get_input_embeddings(self) -> torch.nn.Module:
176
+ return self.model.token_embeddings
177
+
178
+ def set_input_embeddings(self, value: torch.nn.Module) -> None:
179
+ self.model.token_embeddings = value
180
+
181
+ def get_output_embeddings(self) -> torch.nn.Module:
182
+ return self.model.mlm_head.linear_out
183
+
184
+ def set_output_embeddings(self, new_embeddings: torch.nn.Module) -> None:
185
+ self.model.mlm_head.linear_out = new_embeddings
186
+ # Keep lm_head in sync with mlm_head.linear_out so that
187
+ # resize_token_embeddings updates both tied heads.
188
+ self.model.lm_head.weight = new_embeddings.weight
189
+
190
+ def forward(
191
+ self,
192
+ input_ids: torch.Tensor,
193
+ attention_mask: torch.Tensor | None = None,
194
+ labels: torch.Tensor | None = None,
195
+ **kwargs,
196
+ ) -> MaskedLMOutput | tuple:
197
+ return_dict = kwargs.pop("return_dict", None)
198
+ if return_dict is None:
199
+ return_dict = self.config.return_dict
200
+ if kwargs:
201
+ raise TypeError(f"Unexpected keyword argument(s): {list(kwargs)}")
202
+ logits = self.model.forward_mlm(input_ids, attention_mask=attention_mask)
203
+
204
+ loss = None
205
+ if labels is not None:
206
+ loss = CrossEntropyLoss(ignore_index=-100)(
207
+ logits.view(-1, logits.size(-1)),
208
+ labels.view(-1),
209
+ )
210
+
211
+ if not return_dict:
212
+ output = (logits,)
213
+ return ((loss,) + output) if loss is not None else output
214
+
215
+ return MaskedLMOutput(loss=loss, logits=logits)
216
+
217
+ class DatForSequenceClassification(_ExpandedTiedWeightsMixin, PreTrainedModel):
218
+ config_class = DatConfig
219
+ base_model_prefix = "model"
220
+
221
+ def __init__(self, config: DatConfig) -> None:
222
+ super().__init__(config)
223
+ self.num_labels = getattr(config, "num_labels", 2)
224
+ dat_config = DatLMConfig(**{field: getattr(config, field) for field in DAT_LM_FIELDS})
225
+ self.model = DatDecoderLM(dat_config)
226
+ self.score = torch.nn.Linear(config.hidden_dim, self.num_labels, bias=False)
227
+ self._tied_weights_keys = _collect_shared_weight_keys(self)
228
+ self.post_init()
229
+
230
+ def post_init(self) -> None:
231
+ super().post_init()
232
+ _refresh_rope_buffers(self.model, self.config.rope_theta)
233
+
234
+ def get_input_embeddings(self) -> torch.nn.Module:
235
+ return self.model.token_embeddings
236
+
237
+ def set_input_embeddings(self, value: torch.nn.Module) -> None:
238
+ self.model.token_embeddings = value
239
+
240
+ def forward(
241
+ self,
242
+ input_ids: torch.Tensor,
243
+ attention_mask: torch.Tensor | None = None,
244
+ labels: torch.Tensor | None = None,
245
+ **kwargs,
246
+ ) -> SequenceClassifierOutput:
247
+ _, hidden_states = self.model.encode_for_objective(input_ids, attention_mask=attention_mask)
248
+ logits = self.score(hidden_states)
249
+
250
+ batch_size = input_ids.shape[0]
251
+
252
+ if self.config.pad_token_id is None:
253
+ last_non_pad_token = -1
254
+ else:
255
+ non_pad_mask = (input_ids != self.config.pad_token_id).to(logits.device, torch.int32)
256
+ if (non_pad_mask.sum(-1) == 0).any().item():
257
+ raise ValueError("Cannot pool sequence-classification logits for all-padding sequences")
258
+ token_indices = torch.arange(input_ids.shape[-1], device=logits.device, dtype=torch.int32)
259
+ last_non_pad_token = (token_indices * non_pad_mask).argmax(-1)
260
+
261
+ pooled_logits = logits[torch.arange(batch_size, device=logits.device), last_non_pad_token]
262
+
263
+ loss = None
264
+ if labels is not None:
265
+ if getattr(self.config, "problem_type", None) is None:
266
+ if self.num_labels == 1:
267
+ self.config.problem_type = "regression"
268
+ elif self.num_labels > 1 and (labels.dtype == torch.long or labels.dtype == torch.int):
269
+ self.config.problem_type = "single_label_classification"
270
+ else:
271
+ self.config.problem_type = "multi_label_classification"
272
+
273
+ if self.config.problem_type == "regression":
274
+ loss_fct = MSELoss()
275
+ if self.num_labels == 1:
276
+ loss = loss_fct(pooled_logits.squeeze(), labels.squeeze())
277
+ else:
278
+ loss = loss_fct(pooled_logits, labels)
279
+ elif self.config.problem_type == "single_label_classification":
280
+ loss_fct = CrossEntropyLoss()
281
+ loss = loss_fct(pooled_logits.view(-1, self.num_labels), labels.view(-1))
282
+ elif self.config.problem_type == "multi_label_classification":
283
+ loss_fct = BCEWithLogitsLoss()
284
+ loss = loss_fct(pooled_logits, labels)
285
+
286
+ return SequenceClassifierOutput(
287
+ loss=loss,
288
+ logits=pooled_logits,
289
+ hidden_states=hidden_states,
290
+ )
special_tokens_map.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"bos_token": "<s>", "eos_token": "</s>", "unk_token": "<unk>", "sep_token": "</s>", "pad_token": "<pad>", "cls_token": "<s>", "mask_token": "<mask>"}
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tokenizer_class": "PreTrainedTokenizerFast",
3
+ "bos_token": "<s>",
4
+ "eos_token": "</s>",
5
+ "unk_token": "<unk>",
6
+ "sep_token": "</s>",
7
+ "pad_token": "<pad>",
8
+ "cls_token": "<s>",
9
+ "mask_token": "<mask>"
10
+ }
transformer_components.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal transformer components for decoder LM training."""
2
+
3
+ import math
4
+ from dataclasses import dataclass
5
+ from typing import Optional, Tuple
6
+
7
+ import torch
8
+ import torch.nn as nn
9
+ from torch import Tensor
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class PositionalInfo:
14
+ pe_type: str
15
+ embeddings: Optional[Tensor] = None
16
+ rope_freqs: Optional[Tuple[Tensor, Tensor]] = None
17
+ rel_embeddings: Optional[Tensor] = None
18
+ apply_to_embeddings: bool = False
19
+
20
+
21
+ class RMSNorm(nn.Module):
22
+ def __init__(self, dim: int, eps: float = 1e-5):
23
+ super().__init__()
24
+ self.eps = eps
25
+ self.weight = nn.Parameter(torch.ones(dim))
26
+
27
+ def _norm(self, x: Tensor) -> Tensor:
28
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
29
+
30
+ def forward(self, x: Tensor) -> Tensor:
31
+ if x.dtype in (torch.float16, torch.bfloat16):
32
+ output = self._norm(x.float()).type_as(x)
33
+ else:
34
+ output = self._norm(x)
35
+ return output * self.weight
36
+
37
+
38
+ class FeedForward(nn.Module):
39
+ def __init__(
40
+ self,
41
+ input_dim: int,
42
+ hidden_dim: int,
43
+ dropout: float = 0.0,
44
+ activation: str = "gelu",
45
+ use_bias: bool = True,
46
+ ):
47
+ super().__init__()
48
+ self.activation = activation
49
+ self.use_swiglu = activation == "swiglu"
50
+ self.dropout = nn.Dropout(dropout)
51
+ if self.use_swiglu:
52
+ self.w_gate = nn.Linear(input_dim, hidden_dim, bias=use_bias)
53
+ self.w_up = nn.Linear(input_dim, hidden_dim, bias=use_bias)
54
+ self.w_down = nn.Linear(hidden_dim, input_dim, bias=use_bias)
55
+ self.activation_fn = nn.SiLU()
56
+ else:
57
+ self.linear1 = nn.Linear(input_dim, hidden_dim, bias=use_bias)
58
+ self.linear2 = nn.Linear(hidden_dim, input_dim, bias=use_bias)
59
+ if activation == "gelu":
60
+ self.activation_fn = nn.GELU()
61
+ elif activation == "relu":
62
+ self.activation_fn = nn.ReLU()
63
+ elif activation == "identity":
64
+ self.activation_fn = nn.Identity()
65
+ else:
66
+ raise ValueError(f"Unsupported ffn activation: {activation}")
67
+ self._init_weights()
68
+
69
+ def _init_weights(self) -> None:
70
+ for module in self.modules():
71
+ if isinstance(module, nn.Linear):
72
+ nn.init.xavier_uniform_(module.weight, gain=1.0)
73
+ if module.bias is not None:
74
+ nn.init.zeros_(module.bias)
75
+
76
+ def forward(self, x: Tensor) -> Tensor:
77
+ if self.use_swiglu:
78
+ gate = self.activation_fn(self.w_gate(x))
79
+ up = self.w_up(x)
80
+ x = gate * up
81
+ x = self.dropout(x)
82
+ return self.w_down(x)
83
+
84
+ x = self.linear1(x)
85
+ x = self.activation_fn(x)
86
+ x = self.dropout(x)
87
+ x = self.linear2(x)
88
+ return x
89
+
90
+
91
+ class PositionalEncoding(nn.Module):
92
+ def __init__(
93
+ self,
94
+ embedding_dim: int,
95
+ pe_type: str,
96
+ max_len: int,
97
+ theta: float = 10000.0,
98
+ max_rel_pos: Optional[int] = None,
99
+ init_range: float = 0.15,
100
+ ):
101
+ super().__init__()
102
+ if embedding_dim <= 0:
103
+ raise ValueError(f"embedding_dim must be positive, got {embedding_dim}")
104
+ if max_len <= 0:
105
+ raise ValueError(f"max_len must be positive, got {max_len}")
106
+ if pe_type not in {"sinusoidal", "learned", "relative", "rope", "none"}:
107
+ raise ValueError(f"Unsupported pe_type: {pe_type}")
108
+ if theta <= 0.0:
109
+ raise ValueError(f"theta must be positive, got {theta}")
110
+ if init_range <= 0.0:
111
+ raise ValueError(f"init_range must be positive, got {init_range}")
112
+
113
+ self.embedding_dim = embedding_dim
114
+ self.pe_type = pe_type
115
+ self.max_len = max_len
116
+
117
+ if pe_type == "sinusoidal":
118
+ if embedding_dim % 2 != 0:
119
+ raise ValueError(f"Sinusoidal encoding requires even embedding_dim, got {embedding_dim}")
120
+ pe = self._precompute_sinusoidal_embeddings(
121
+ embedding_dim,
122
+ max_len,
123
+ device=torch.empty(0).device,
124
+ )
125
+ self.register_buffer("sinusoidal_embeddings", pe, persistent=False)
126
+ self._sinusoidal_uninitialized = True
127
+ elif pe_type == "learned":
128
+ self.position_embeddings = nn.Embedding(max_len, embedding_dim)
129
+ nn.init.uniform_(self.position_embeddings.weight, -init_range, init_range)
130
+ elif pe_type == "relative":
131
+ self.max_relative_position = max_rel_pos if max_rel_pos is not None else max_len // 2
132
+ if self.max_relative_position <= 0:
133
+ raise ValueError(
134
+ f"max_relative_position must be positive, got {self.max_relative_position}"
135
+ )
136
+ num_embeddings = 2 * self.max_relative_position + 1
137
+ self.rel_pos_embeddings_table = nn.Embedding(num_embeddings, embedding_dim)
138
+ nn.init.uniform_(self.rel_pos_embeddings_table.weight, -init_range, init_range)
139
+ elif pe_type == "rope":
140
+ if embedding_dim % 2 != 0:
141
+ raise ValueError(f"RoPE requires even embedding_dim, got {embedding_dim}")
142
+ self.theta = theta
143
+ cos, sin = self._precompute_rope_freqs(embedding_dim, max_len, theta)
144
+ self.register_buffer("rope_cos", cos, persistent=False)
145
+ self.register_buffer("rope_sin", sin, persistent=False)
146
+ self._rope_uninitialized = True
147
+
148
+ def get_positional_info(self, seq_len: int, device: torch.device) -> PositionalInfo:
149
+ if self.pe_type == "none":
150
+ return PositionalInfo(pe_type="none")
151
+ if self.pe_type == "sinusoidal":
152
+ if seq_len > self.sinusoidal_embeddings.size(0):
153
+ raise ValueError(
154
+ f"seq_len {seq_len} exceeds precomputed sinusoidal length {self.sinusoidal_embeddings.size(0)}"
155
+ )
156
+ if (
157
+ getattr(self, "_sinusoidal_uninitialized", False)
158
+ or self.sinusoidal_embeddings.device.type == "meta"
159
+ ):
160
+ embeddings = self._precompute_sinusoidal_embeddings(
161
+ self.embedding_dim,
162
+ self.max_len,
163
+ device=device,
164
+ )
165
+ self.register_buffer("sinusoidal_embeddings", embeddings, persistent=False)
166
+ self._sinusoidal_uninitialized = False
167
+ return PositionalInfo(
168
+ pe_type="sinusoidal",
169
+ embeddings=self.sinusoidal_embeddings[:seq_len].to(device),
170
+ apply_to_embeddings=True,
171
+ )
172
+ if self.pe_type == "learned":
173
+ if seq_len > self.max_len:
174
+ raise ValueError(
175
+ f"seq_len {seq_len} exceeds maximum learned position length {self.max_len}"
176
+ )
177
+ positions = torch.arange(seq_len, dtype=torch.long, device=device)
178
+ return PositionalInfo(
179
+ pe_type="learned",
180
+ embeddings=self.position_embeddings(positions),
181
+ apply_to_embeddings=True,
182
+ )
183
+ if self.pe_type == "relative":
184
+ return PositionalInfo(
185
+ pe_type="relative",
186
+ rel_embeddings=self._generate_relative_embeddings(seq_len, device),
187
+ apply_to_embeddings=False,
188
+ )
189
+ if seq_len > self.rope_cos.size(0):
190
+ # Dynamically extend RoPE buffer (standard practice for extrapolation)
191
+ cos, sin = self._precompute_rope_freqs(self.embedding_dim, seq_len, self.theta)
192
+ self.register_buffer("rope_cos", cos, persistent=False)
193
+ self.register_buffer("rope_sin", sin, persistent=False)
194
+ self._rope_uninitialized = False
195
+ if getattr(self, "_rope_uninitialized", True) or torch.isnan(self.rope_cos[0, 0]):
196
+ cos, sin = self._precompute_rope_freqs(self.embedding_dim, self.max_len, self.theta)
197
+ self.rope_cos.copy_(cos)
198
+ self.rope_sin.copy_(sin)
199
+ self._rope_uninitialized = False
200
+ return PositionalInfo(
201
+ pe_type="rope",
202
+ rope_freqs=(
203
+ self.rope_cos[:seq_len].to(device),
204
+ self.rope_sin[:seq_len].to(device),
205
+ ),
206
+ apply_to_embeddings=False,
207
+ )
208
+
209
+ @staticmethod
210
+ def _rope_inv_freq(dim: int, theta: float, device: torch.device | str) -> Tensor:
211
+ return 1.0 / (theta ** (torch.arange(0, dim, 2, device=device).float() / dim))
212
+
213
+ @staticmethod
214
+ def _precompute_sinusoidal_embeddings(
215
+ dim: int,
216
+ end: int,
217
+ device: torch.device | str,
218
+ ) -> Tensor:
219
+ pe = torch.zeros(end, dim, device=device)
220
+ position = torch.arange(0, end, dtype=torch.float, device=device).unsqueeze(1)
221
+ div_term = PositionalEncoding._rope_inv_freq(dim, 10000.0, device=device)
222
+ pe[:, 0::2] = torch.sin(position * div_term)
223
+ pe[:, 1::2] = torch.cos(position * div_term)
224
+ return pe
225
+
226
+ @staticmethod
227
+ def _precompute_rope_freqs(dim: int, end: int, theta: float) -> Tuple[Tensor, Tensor]:
228
+ freqs = PositionalEncoding._rope_inv_freq(dim, theta, device="cpu")
229
+ positions = torch.arange(end, device="cpu")
230
+ freqs = torch.outer(positions, freqs).float()
231
+ return torch.cos(freqs), torch.sin(freqs)
232
+
233
+ def _generate_relative_embeddings(self, seq_len: int, device: torch.device) -> Tensor:
234
+ range_q = torch.arange(seq_len, device=device)
235
+ range_k = torch.arange(seq_len, device=device)
236
+ distance_mat = range_k[None, :] - range_q[:, None]
237
+ distance_mat_clipped = torch.clamp(
238
+ distance_mat,
239
+ -self.max_relative_position,
240
+ self.max_relative_position,
241
+ )
242
+ final_indices = distance_mat_clipped + self.max_relative_position
243
+ return self.rel_pos_embeddings_table(final_indices.long())
244
+
245
+
246
+ def reshape_for_broadcast(freqs_cis: Tensor, x: Tensor) -> Tensor:
247
+ ndim = x.ndim
248
+ if ndim < 2:
249
+ raise ValueError(f"Input tensor x must have at least 2 dimensions, got {ndim}")
250
+ if x.shape[-2] != freqs_cis.shape[0] or x.shape[-1] != freqs_cis.shape[-1]:
251
+ raise ValueError(
252
+ f"Shape mismatch for RoPE broadcasting: freqs_cis {freqs_cis.shape}, x {x.shape}"
253
+ )
254
+ shape = [1] * ndim
255
+ shape[-2] = x.shape[-2]
256
+ shape[-1] = x.shape[-1]
257
+ return freqs_cis.view(shape)
258
+
259
+
260
+ def apply_rotary_emb(xq: Tensor, xk: Tensor, freqs_cos: Tensor, freqs_sin: Tensor) -> Tuple[Tensor, Tensor]:
261
+ if xq.shape[-1] % 2 != 0:
262
+ raise ValueError(f"Query feature dimension must be even for RoPE, got {xq.shape[-1]}")
263
+ if xk.shape[-1] % 2 != 0:
264
+ raise ValueError(f"Key feature dimension must be even for RoPE, got {xk.shape[-1]}")
265
+
266
+ xq_r, xq_i = xq.float().reshape(xq.shape[:-1] + (-1, 2)).unbind(-1)
267
+ xk_r, xk_i = xk.float().reshape(xk.shape[:-1] + (-1, 2)).unbind(-1)
268
+
269
+ freqs_cos_q = reshape_for_broadcast(freqs_cos[: xq.shape[-2]], xq_r)
270
+ freqs_sin_q = reshape_for_broadcast(freqs_sin[: xq.shape[-2]], xq_r)
271
+ freqs_cos_k = reshape_for_broadcast(freqs_cos[: xk.shape[-2]], xk_r)
272
+ freqs_sin_k = reshape_for_broadcast(freqs_sin[: xk.shape[-2]], xk_r)
273
+
274
+ xq_out_r = xq_r * freqs_cos_q - xq_i * freqs_sin_q
275
+ xq_out_i = xq_r * freqs_sin_q + xq_i * freqs_cos_q
276
+ xk_out_r = xk_r * freqs_cos_k - xk_i * freqs_sin_k
277
+ xk_out_i = xk_r * freqs_sin_k + xk_i * freqs_cos_k
278
+
279
+ xq_out = torch.stack([xq_out_r, xq_out_i], dim=-1).flatten(-2)
280
+ xk_out = torch.stack([xk_out_r, xk_out_i], dim=-1).flatten(-2)
281
+
282
+ return xq_out.type_as(xq), xk_out.type_as(xk)
transformer_core.py ADDED
@@ -0,0 +1,327 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Minimal self-attention core for decoder language models."""
2
+
3
+ import math
4
+ from typing import Optional, Tuple
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+
9
+ from .transformer_components import PositionalInfo, apply_rotary_emb
10
+
11
+
12
+ def _activate_scores(scores: torch.Tensor, activation: str) -> torch.Tensor:
13
+ if activation == "softmax":
14
+ return torch.softmax(scores, dim=-1)
15
+ if activation == "identity":
16
+ return scores
17
+ if activation == "relu":
18
+ return torch.relu(scores)
19
+ if activation == "tanh":
20
+ return torch.tanh(scores)
21
+ if activation == "sigmoid":
22
+ return torch.sigmoid(scores)
23
+ if activation == "gelu":
24
+ return torch.nn.functional.gelu(scores)
25
+ raise ValueError(f"Unsupported attention activation: {activation}")
26
+
27
+
28
+ class MultiHeadAttentionBase(nn.Module):
29
+ def __init__(
30
+ self,
31
+ query_dim: int,
32
+ output_dim: int,
33
+ key_dim: Optional[int] = None,
34
+ value_dim: Optional[int] = None,
35
+ n_heads: int = 4,
36
+ hidden_dim: int = 64,
37
+ dropout: float = 0.0,
38
+ total_n_heads: Optional[int] = None,
39
+ activation: str = "softmax",
40
+ use_bias_qkv: bool = False,
41
+ use_bias_out: bool = True,
42
+ ):
43
+ super().__init__()
44
+ self.query_dim = query_dim
45
+ self.key_dim = query_dim if key_dim is None else key_dim
46
+ self.value_dim = self.key_dim if value_dim is None else value_dim
47
+ self.output_dim = output_dim
48
+ self.n_heads = n_heads
49
+ self.hidden_dim = hidden_dim
50
+ self.total_n_heads = n_heads if total_n_heads is None else total_n_heads
51
+ self.activation = activation
52
+ self.use_bias_qkv = use_bias_qkv
53
+ self.use_bias_out = use_bias_out
54
+
55
+ if self.total_n_heads <= 0:
56
+ raise ValueError(f"total_n_heads must be positive, got {self.total_n_heads}")
57
+ if hidden_dim % self.total_n_heads != 0:
58
+ raise ValueError(
59
+ f"hidden_dim ({hidden_dim}) must be divisible by total_n_heads ({self.total_n_heads})"
60
+ )
61
+
62
+ self.head_dim = hidden_dim // self.total_n_heads
63
+ self.scale = 1.0 / math.sqrt(self.head_dim)
64
+ projected_dim = n_heads * self.head_dim
65
+ self.q_proj = nn.Linear(self.query_dim, projected_dim, bias=use_bias_qkv)
66
+ self.k_proj = nn.Linear(self.key_dim, projected_dim, bias=use_bias_qkv)
67
+ self.v_proj = nn.Linear(self.value_dim, projected_dim, bias=use_bias_qkv)
68
+ self.o_proj = nn.Linear(projected_dim, output_dim, bias=use_bias_out)
69
+ self.dropout = nn.Dropout(dropout)
70
+ self.attn_dropout = nn.Dropout(dropout)
71
+ self.last_attn_weights = None
72
+
73
+ self._init_weights()
74
+
75
+ def _init_weights(self) -> None:
76
+ gain = 1.0 / math.sqrt(2.0)
77
+ for projection in (self.q_proj, self.k_proj, self.v_proj, self.o_proj):
78
+ nn.init.xavier_uniform_(projection.weight, gain=gain)
79
+ if projection.bias is not None:
80
+ nn.init.zeros_(projection.bias)
81
+
82
+ def _reshape_for_multihead(self, x: torch.Tensor, batch_size: int, seq_len: int) -> torch.Tensor:
83
+ x = x.view(batch_size, seq_len, self.n_heads, self.head_dim)
84
+ return x.transpose(1, 2)
85
+
86
+ def _process_mask(self, mask: Optional[torch.Tensor]) -> Optional[torch.Tensor]:
87
+ if mask is None:
88
+ return None
89
+ if mask.dim() == 2:
90
+ if mask.shape[0] != mask.shape[1]:
91
+ raise ValueError(
92
+ "2D decoder attention masks must be square [seq, seq]; "
93
+ f"got {tuple(mask.shape)}. Expand padding masks at the LM boundary."
94
+ )
95
+ return mask.bool().unsqueeze(0).unsqueeze(0)
96
+ if mask.dim() == 3:
97
+ return mask.bool().unsqueeze(1)
98
+ raise ValueError(f"Mask must be 2D or 3D, got {mask.dim()}D")
99
+
100
+ def _compute_attn_scores(
101
+ self,
102
+ q: torch.Tensor,
103
+ k: torch.Tensor,
104
+ pos_info: Optional[PositionalInfo] = None,
105
+ ) -> torch.Tensor:
106
+ if pos_info is not None and pos_info.rope_freqs is not None:
107
+ freqs_cos, freqs_sin = pos_info.rope_freqs
108
+ q, k = apply_rotary_emb(q, k, freqs_cos, freqs_sin)
109
+ return torch.matmul(q, k.transpose(-2, -1)) * self.scale
110
+
111
+ def _apply_activation_and_mask(self, scores: torch.Tensor, mask: Optional[torch.Tensor]) -> torch.Tensor:
112
+ processed_mask = self._process_mask(mask)
113
+ if processed_mask is not None and self.activation in {"softmax", "sigmoid", "tanh"}:
114
+ scores.masked_fill_(~processed_mask, torch.finfo(scores.dtype).min)
115
+ weights = _activate_scores(scores, self.activation)
116
+ if processed_mask is not None:
117
+ weights = weights.masked_fill(~processed_mask, 0.0)
118
+ return weights
119
+
120
+ def _apply_mask(self, scores: torch.Tensor, mask: Optional[torch.Tensor]) -> torch.Tensor:
121
+ return self._apply_activation_and_mask(scores, mask)
122
+
123
+ def forward(
124
+ self,
125
+ query: torch.Tensor,
126
+ key: Optional[torch.Tensor] = None,
127
+ value: Optional[torch.Tensor] = None,
128
+ mask: Optional[torch.Tensor] = None,
129
+ pos_info: Optional[PositionalInfo] = None,
130
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
131
+ batch_size, seq_len, _ = query.shape
132
+ key = query if key is None else key
133
+ value = key if value is None else value
134
+ key_len = key.shape[1]
135
+
136
+ if key.shape[0] != batch_size or value.shape[0] != batch_size:
137
+ raise ValueError(
138
+ f"Batch size mismatch: query={batch_size}, key={key.shape[0]}, value={value.shape[0]}"
139
+ )
140
+ if key.shape[1] != value.shape[1]:
141
+ raise ValueError(
142
+ f"Key and value sequence length mismatch: {key.shape[1]} vs {value.shape[1]}"
143
+ )
144
+
145
+ q = self.q_proj(query)
146
+ k = self.k_proj(key)
147
+ v = self.v_proj(value)
148
+
149
+ q = self._reshape_for_multihead(q, batch_size, seq_len)
150
+ k = self._reshape_for_multihead(k, batch_size, key_len)
151
+ v = self._reshape_for_multihead(v, batch_size, key_len)
152
+
153
+ attn_scores = self._compute_attn_scores(q, k, pos_info)
154
+ attn_weights = self._apply_activation_and_mask(attn_scores, mask)
155
+ attn_weights = self.attn_dropout(attn_weights)
156
+
157
+ attn_output = torch.matmul(attn_weights, v)
158
+ attn_output = attn_output.transpose(1, 2).contiguous()
159
+ attn_output = attn_output.view(batch_size, seq_len, self.n_heads * self.head_dim)
160
+ attn_output = self.o_proj(attn_output)
161
+ attn_output = self.dropout(attn_output)
162
+
163
+ self.last_attn_weights = attn_weights.detach()
164
+ return attn_output, attn_weights
165
+
166
+
167
+ class SelfAttention(MultiHeadAttentionBase):
168
+ def __init__(
169
+ self,
170
+ input_dim: int,
171
+ n_heads: int = 4,
172
+ hidden_dim: int = 64,
173
+ dropout: float = 0.0,
174
+ supports_relative: bool = False,
175
+ total_n_heads: Optional[int] = None,
176
+ use_bias_qkv: bool = False,
177
+ use_bias_out: bool = True,
178
+ ):
179
+ head_dim = hidden_dim // (n_heads if total_n_heads is None else total_n_heads)
180
+ super().__init__(
181
+ query_dim=input_dim,
182
+ output_dim=n_heads * head_dim,
183
+ key_dim=input_dim,
184
+ value_dim=input_dim,
185
+ n_heads=n_heads,
186
+ hidden_dim=hidden_dim,
187
+ dropout=dropout,
188
+ total_n_heads=total_n_heads,
189
+ activation="softmax",
190
+ use_bias_qkv=use_bias_qkv,
191
+ use_bias_out=use_bias_out,
192
+ )
193
+ self.supports_relative = supports_relative
194
+ if self.supports_relative:
195
+ self.rel_k_proj = nn.Linear(self.head_dim, self.head_dim, bias=False)
196
+ self.rel_v_proj = nn.Linear(self.head_dim, self.head_dim, bias=False)
197
+ nn.init.xavier_uniform_(self.rel_k_proj.weight)
198
+ nn.init.xavier_uniform_(self.rel_v_proj.weight)
199
+
200
+ def forward(
201
+ self,
202
+ x: torch.Tensor,
203
+ mask: Optional[torch.Tensor] = None,
204
+ pos_info: Optional[PositionalInfo] = None,
205
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
206
+ if self.supports_relative and pos_info is not None and pos_info.rel_embeddings is not None:
207
+ batch_size, seq_len, _ = x.shape
208
+ rel_embeddings = pos_info.rel_embeddings
209
+ if rel_embeddings.shape[:2] != (seq_len, seq_len):
210
+ raise ValueError(
211
+ f"Relative embeddings shape {rel_embeddings.shape} does not match sequence length {seq_len}"
212
+ )
213
+ if rel_embeddings.shape[2] != self.head_dim:
214
+ raise ValueError(
215
+ f"Relative embeddings dim {rel_embeddings.shape[2]} does not match head_dim {self.head_dim}"
216
+ )
217
+
218
+ rel_k = self.rel_k_proj(rel_embeddings)
219
+ rel_v = self.rel_v_proj(rel_embeddings)
220
+
221
+ q = self.q_proj(x)
222
+ k = self.k_proj(x)
223
+ v = self.v_proj(x)
224
+
225
+ q = self._reshape_for_multihead(q, batch_size, seq_len)
226
+ k = self._reshape_for_multihead(k, batch_size, seq_len)
227
+ v = self._reshape_for_multihead(v, batch_size, seq_len)
228
+
229
+ attn_scores = self._compute_attn_scores(q, k, pos_info)
230
+ rel_scores = torch.einsum("bhid,ijd->bhij", q, rel_k) * self.scale
231
+ attn_scores = attn_scores + rel_scores
232
+
233
+ attn_weights = self._apply_mask(attn_scores, mask)
234
+ attn_weights = self.attn_dropout(attn_weights)
235
+
236
+ attn_output = torch.matmul(attn_weights, v)
237
+ rel_output = torch.einsum("bhij,ijd->bhid", attn_weights, rel_v)
238
+ attn_output = attn_output + rel_output
239
+
240
+ attn_output = attn_output.transpose(1, 2).contiguous()
241
+ attn_output = attn_output.view(batch_size, seq_len, self.n_heads * self.head_dim)
242
+ attn_output = self.o_proj(attn_output)
243
+ attn_output = self.dropout(attn_output)
244
+ self.last_attn_weights = attn_weights.detach()
245
+ return attn_output, attn_weights
246
+
247
+ return super().forward(query=x, mask=mask, pos_info=pos_info)
248
+
249
+
250
+ class CrossAttention(MultiHeadAttentionBase):
251
+ def __init__(
252
+ self,
253
+ input_dim: int,
254
+ output_dim: int,
255
+ context_dim: Optional[int] = None,
256
+ n_heads: int = 8,
257
+ hidden_dim: int = 64,
258
+ dropout: float = 0.0,
259
+ supports_relative: bool = False,
260
+ use_bias_qkv: bool = False,
261
+ use_bias_out: bool = True,
262
+ ):
263
+ resolved_context_dim = input_dim if context_dim is None else context_dim
264
+ super().__init__(
265
+ query_dim=input_dim,
266
+ key_dim=resolved_context_dim,
267
+ value_dim=resolved_context_dim,
268
+ output_dim=output_dim,
269
+ n_heads=n_heads,
270
+ hidden_dim=hidden_dim,
271
+ dropout=dropout,
272
+ activation="softmax",
273
+ use_bias_qkv=use_bias_qkv,
274
+ use_bias_out=use_bias_out,
275
+ )
276
+ self.supports_relative = supports_relative
277
+ if supports_relative:
278
+ self.rel_k_proj = nn.Linear(self.head_dim, self.head_dim, bias=False)
279
+ self.rel_v_proj = nn.Linear(self.head_dim, self.head_dim, bias=False)
280
+ nn.init.xavier_uniform_(self.rel_k_proj.weight)
281
+ nn.init.xavier_uniform_(self.rel_v_proj.weight)
282
+
283
+ def forward(
284
+ self,
285
+ inputs: torch.Tensor,
286
+ context: torch.Tensor,
287
+ mask: Optional[torch.Tensor] = None,
288
+ pos_info: Optional[PositionalInfo] = None,
289
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
290
+ if self.supports_relative and pos_info is not None and pos_info.rel_embeddings is not None:
291
+ batch_size, seq_len_q, _ = inputs.shape
292
+ _, seq_len_k, _ = context.shape
293
+ rel_embeddings = pos_info.rel_embeddings
294
+ if rel_embeddings.shape[:2] != (seq_len_q, seq_len_k):
295
+ raise ValueError(
296
+ f"Relative embeddings shape {rel_embeddings.shape} does not match "
297
+ f"sequence lengths {(seq_len_q, seq_len_k)}"
298
+ )
299
+ if rel_embeddings.shape[2] != self.head_dim:
300
+ raise ValueError(
301
+ f"Relative embeddings dim {rel_embeddings.shape[2]} does not match head_dim {self.head_dim}"
302
+ )
303
+
304
+ rel_k = self.rel_k_proj(rel_embeddings)
305
+ rel_v = self.rel_v_proj(rel_embeddings)
306
+ q = self._reshape_for_multihead(self.q_proj(inputs), batch_size, seq_len_q)
307
+ k = self._reshape_for_multihead(self.k_proj(context), batch_size, seq_len_k)
308
+ v = self._reshape_for_multihead(self.v_proj(context), batch_size, seq_len_k)
309
+ attn_scores = self._compute_attn_scores(q, k, pos_info)
310
+ attn_scores = attn_scores + torch.einsum("bhid,ijd->bhij", q, rel_k) * self.scale
311
+ attn_weights = self._apply_activation_and_mask(attn_scores, mask)
312
+ attn_weights = self.attn_dropout(attn_weights)
313
+ self.last_attn_weights = attn_weights.detach()
314
+ attn_output = torch.matmul(attn_weights, v)
315
+ attn_output = attn_output + torch.einsum("bhij,ijd->bhid", attn_weights, rel_v)
316
+ attn_output = attn_output.transpose(1, 2).contiguous().view(batch_size, seq_len_q, -1)
317
+ attn_output = self.o_proj(attn_output)
318
+ attn_output = self.dropout(attn_output)
319
+ return attn_output, attn_weights
320
+
321
+ return super().forward(
322
+ query=inputs,
323
+ key=context,
324
+ value=context,
325
+ mask=mask,
326
+ pos_info=pos_info,
327
+ )