ChrisMcCormick commited on
Commit
cd2099c
·
verified ·
1 Parent(s): c732da1

Adding source code

Browse files
layers/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ Subspace Decoder Layers
5
+
6
+ This module contains the layer implementations for the Shared Subspace Decoder,
7
+ including Multi-Head Latent Attention (MLA) and decomposed MLP layers.
8
+ """
9
+
10
+ # Import the main layer classes
11
+ from .mla import MultiheadLatentAttention, RotaryEmbedding
12
+ from .feedforward import SubspaceFeedForward
13
+ from .task_heads import SharedSpaceDecoderForCausalLM
14
+
15
+ __all__ = [
16
+ "MultiheadLatentAttention",
17
+ "RotaryEmbedding",
18
+ "SubspaceFeedForward",
19
+ "SharedSpaceDecoderForCausalLM",
20
+ ]
layers/feedforward.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """# ▂▂▂▂▂▂▂▂▂▂▂▂
2
+
3
+ # `feedforward.py`
4
+
5
+ Regarding dropout:
6
+
7
+ - I don't see it applied to the MoE in DeepSeek-V3, [here](https://huggingface.co/deepseek-ai/DeepSeek-R1/blob/main/modeling_deepseek.py).
8
+
9
+ - I don't see it applied in [modeling_llama.py](https://github.com/huggingface/transformers/blob/main/src/transformers/models/llama/modeling_llama.py#L140)
10
+
11
+ Norms:
12
+
13
+ * nn.RMSNorm [here](https://docs.pytorch.org/docs/stable/generated/torch.nn.RMSNorm.html)
14
+
15
+ ## FFN
16
+ """
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+ import torch.nn.functional as F
21
+ from ..models.shared_space_config import SharedSpaceDecoderConfig
22
+
23
+
24
+ def create_norm_layer(hidden_size: int, config: SharedSpaceDecoderConfig) -> nn.Module:
25
+ """
26
+ Create a normalization layer based on the config norm_type.
27
+
28
+ Args:
29
+ hidden_size: The dimension to normalize over
30
+ config: Configuration containing norm_type and epsilon values
31
+
32
+ Returns:
33
+ Either a LayerNorm or RMSNorm layer
34
+ """
35
+ if config.norm_type == "layernorm":
36
+ return nn.LayerNorm(hidden_size, eps=config.layer_norm_eps)
37
+ elif config.norm_type == "rmsnorm":
38
+ return DeepseekV3RMSNorm(hidden_size, eps=config.rms_norm_eps)
39
+ else:
40
+ # This should be caught by config validation, but being defensive
41
+ raise ValueError(f"Unknown norm_type: {config.norm_type}")
42
+
43
+
44
+ # TODO - Find a shared place to put this.
45
+ class DeepseekV3RMSNorm(nn.Module):
46
+ def __init__(self, hidden_size, eps=1e-6):
47
+ """
48
+ DeepseekV3RMSNorm is equivalent to T5LayerNorm
49
+ """
50
+ super().__init__()
51
+ self.weight = nn.Parameter(torch.ones(hidden_size))
52
+ self.variance_epsilon = eps
53
+
54
+ def forward(self, hidden_states):
55
+ input_dtype = hidden_states.dtype
56
+ hidden_states = hidden_states.to(torch.float32)
57
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
58
+ hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
59
+ return self.weight * hidden_states.to(input_dtype)
60
+
61
+ class SubspaceFeedForward(nn.Module):
62
+ """
63
+ Feed-forward block for SharedSpaceDecoder.
64
+
65
+ Implements SwiGLU:
66
+ FFN(x) = W_out( Swish(W_in(x)) ⊙ W_gate(x) ) + residual
67
+
68
+ Supports both dense and decomposed MLP variants.
69
+
70
+ Dense:
71
+ - W_in: Linear(hidden_dim → intermediate_dim)
72
+ - W_gate: Linear(hidden_dim → intermediate_dim)
73
+ - W_out: Linear(intermediate_dim → hidden_dim)
74
+
75
+ Decomposed:
76
+ - W_in_shared: Linear(hidden_dim → rank, bias=False)
77
+ - W_in_shared_norm: RMSNorm
78
+ - W_in: Linear(rank → intermediate_dim)
79
+ - W_gate_shared: Linear(hidden_dim → rank, bias=False)
80
+ - W_gate_shared_norm: RMSNorm
81
+ - W_gate: Linear(rank → intermediate_dim)
82
+ - W_out: Linear(intermediate_dim → rank, bias=False)
83
+ - W_out_shared: Linear(rank → hidden_dim)
84
+
85
+ Residual, dropout, and post-norm are handled inside the block.
86
+ """
87
+
88
+ def __init__(self, config, layer_idx):
89
+ super().__init__()
90
+
91
+
92
+ #dropout_prob = config.hidden_dropout_prob # TODO - Style -- don't define variables if only used once.
93
+
94
+ # Determine whether this is a dense or decomposed layer.
95
+ # It's dense if either:
96
+ # - ffn_decompose is disabled (no dense layers at all)
97
+ # - ffn_decompose is enabled, but this is one of the early dense layers.
98
+ self.is_dense = (not config.ffn_decompose) or (layer_idx < config.num_dense_layers)
99
+
100
+ hidden_dim = config.hidden_size
101
+ intermediate_dim = config.intermediate_size # TODO - Find something shorter, and use the same name.
102
+
103
+ # If it's one of the dense layers,
104
+ if self.is_dense:
105
+ # === Dense FFN Projections ===
106
+ self.W_in = nn.Linear(hidden_dim, intermediate_dim)
107
+ self.W_gate = nn.Linear(hidden_dim, intermediate_dim)
108
+ self.W_out = nn.Linear(intermediate_dim, hidden_dim)
109
+
110
+ # Define weights for the decomposed version.
111
+ else:
112
+ rank = config.ffn_rank
113
+
114
+ print("hidden_dim:", hidden_dim)
115
+ print("rank:", rank)
116
+
117
+ # === Input Projections ===
118
+ self.W_in_shared = nn.Linear(hidden_dim, rank, bias=False)
119
+ self.W_in_shared_norm = create_norm_layer(rank, config)
120
+ self.W_in = nn.Linear(rank, intermediate_dim, bias=True)
121
+
122
+ # === Gate Projections ===
123
+ self.W_gate_shared = nn.Linear(hidden_dim, rank, bias=False)
124
+ self.W_gate_shared_norm = create_norm_layer(rank, config)
125
+ self.W_gate = nn.Linear(rank, intermediate_dim, bias=True)
126
+
127
+ # === Output Projection ===
128
+ self.W_out = nn.Linear(intermediate_dim, rank, bias=False)
129
+ # TODO - Could experiment with this.
130
+ #self.W_out_shared_layernorm = DeepseekV3RMSNorm(rank, eps=config.eps)
131
+ self.W_out_shared = nn.Linear(rank, hidden_dim, bias=True)
132
+
133
+ # See notes no dropout
134
+ #self.dropout = nn.Dropout(config.hidden_dropout_prob)
135
+
136
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
137
+ # === Tensor Dimension Symbols ===
138
+ # B: batch_size — number of samples in the batch
139
+ # T: seq_len — number of tokens per sample
140
+ # D: hidden_dim — model embedding size
141
+ # R: ffn_rank — latent shared subspace dimension
142
+ # D_ff: intermediate_size — FFN hidden dimension
143
+
144
+ # =========================
145
+ # Gated Feedforward
146
+ # =========================
147
+
148
+ if self.is_dense:
149
+ # =============
150
+ # Dense
151
+ # =============
152
+
153
+ # Input: x [B, T, D]
154
+ # Output: x_proj [B, T, D_ff]
155
+ x_proj = self.W_in(x)
156
+
157
+ # Output: gate [B, T, D_ff]
158
+ gate = self.W_gate(x)
159
+
160
+ # SwiGLU nonlinearity
161
+ x = F.silu(x_proj) * gate # [B, T, D_ff]
162
+
163
+ # See notes on dropout
164
+ #x = self.dropout(x)
165
+
166
+ # Output: x [B, T, D]
167
+ x = self.W_out(x)
168
+
169
+ else:
170
+ # ==================
171
+ # Decomposed
172
+ # ==================
173
+
174
+ # Input: x [B, T, D]
175
+ # Output: x_proj [B, T, D_ff]
176
+ x_proj = self.W_in(self.W_in_shared_norm(self.W_in_shared(x)))
177
+
178
+ # Input: x [B, T, D]
179
+ # Output: gate [B, T, D_ff]
180
+ gate = self.W_gate(self.W_gate_shared_norm(self.W_gate_shared(x)))
181
+
182
+ # SwiGLU nonlinearity
183
+ x = F.silu(x_proj) * gate # [B, T, D_ff]
184
+
185
+ # See notes on dropout
186
+ #x = self.dropout(x)
187
+
188
+ # Output: x [B, T, D]
189
+ x = self.W_out_shared(self.W_out(x))
190
+
191
+
192
+ return x
193
+
194
+
195
+
196
+
layers/gla.py ADDED
@@ -0,0 +1,721 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """# ▂▂▂▂▂▂▂▂▂▂▂▂
2
+
3
+ # `gla.py`
4
+
5
+ Based on: https://huggingface.co/deepseek-ai/DeepSeek-R1/blob/main/modeling_deepseek.py
6
+
7
+ """
8
+
9
+ import torch
10
+ import torch.nn as nn
11
+ import torch.nn.functional as F
12
+ from typing import Optional
13
+ import math
14
+
15
+ from models.shared_space_config import SharedSpaceDecoderConfig
16
+
17
+
18
+ def create_norm_layer(hidden_size: int, config: SharedSpaceDecoderConfig) -> nn.Module:
19
+ """
20
+ Create a normalization layer based on the config norm_type.
21
+
22
+ If `hidden_size` is `None`, this returns an identity layer.
23
+
24
+ Args:
25
+ hidden_size: The dimension to normalize over
26
+ config: Configuration containing norm_type and epsilon values
27
+
28
+ Returns:
29
+ Either a LayerNorm or RMSNorm layer
30
+ """
31
+ if hidden_size is None:
32
+ return nn.Identity()
33
+ elif config.norm_type == "layernorm":
34
+ return nn.LayerNorm(hidden_size, eps=config.layer_norm_eps)
35
+ elif config.norm_type == "rmsnorm":
36
+ return DeepseekV3RMSNorm(hidden_size, eps=config.rms_norm_eps)
37
+ else:
38
+ # This should be caught by config validation, but being defensive
39
+ raise ValueError(f"Unknown norm_type: {config.norm_type}")
40
+
41
+
42
+ # TODO - Find a shared place to put this.
43
+ class DeepseekV3RMSNorm(nn.Module):
44
+ def __init__(self, hidden_size, eps=1e-6):
45
+ """
46
+ DeepseekV3RMSNorm is equivalent to T5LayerNorm
47
+ """
48
+ super().__init__()
49
+ self.weight = nn.Parameter(torch.ones(hidden_size))
50
+ self.variance_epsilon = eps
51
+
52
+ def forward(self, hidden_states):
53
+ input_dtype = hidden_states.dtype
54
+ hidden_states = hidden_states.to(torch.float32)
55
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
56
+ hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
57
+ return self.weight * hidden_states.to(input_dtype)
58
+
59
+
60
+ # Helper function needed because it's called twice during RoPE,
61
+ # but I dumped it in the comments there.
62
+ # TODO - Nah, screw it, just write it twice! At least then you get
63
+ # to use the word 'query' instead of 'x'.
64
+ def rotate_half(x):
65
+ """Rotates half the hidden dims of the input."""
66
+ x1 = x[..., : x.shape[-1] // 2]
67
+ x2 = x[..., x.shape[-1] // 2 :]
68
+ return torch.cat((-x2, x1), dim=-1)
69
+
70
+ class RotaryEmbedding(nn.Module):
71
+ """Precompute RoPE embeddings and store them as buffers."""
72
+
73
+ def __init__(self, config: SharedSpaceDecoderConfig) -> None:
74
+ super().__init__()
75
+
76
+ dim = config.rope_dims
77
+ seq_len = config.max_position_embeddings
78
+
79
+ # ------------------------------
80
+ # Compute inverse frequencies
81
+ # ------------------------------
82
+ # Shape: [dim // 2]
83
+ # inv_freq[i] = 1 / (theta^(i / dim))
84
+ inv_freq = 1.0 / (
85
+ config.rope_theta
86
+ ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim)
87
+ )
88
+
89
+ # ------------------------------
90
+ # Apply RoPE scaling if configured
91
+ # ------------------------------
92
+ if config.rope_scaling is not None:
93
+ scaling_type = config.rope_scaling.get("type", "linear")
94
+ scaling_factor = config.rope_scaling.get("factor", 1.0)
95
+
96
+ if scaling_type == "linear":
97
+ # Linear scaling: divide frequencies by scaling factor
98
+ inv_freq = inv_freq / scaling_factor
99
+ elif scaling_type == "dynamic":
100
+ # Dynamic scaling: adjust based on sequence length
101
+ # This is a simplified implementation
102
+ inv_freq = inv_freq / scaling_factor
103
+ else:
104
+ print(f"Warning: Unknown RoPE scaling type '{scaling_type}', using linear scaling")
105
+ inv_freq = inv_freq / scaling_factor
106
+
107
+ # ------------------------------
108
+ # Compute position indices
109
+ # ------------------------------
110
+ # Shape: [seq_len]
111
+ t = torch.arange(seq_len, dtype=torch.float32)
112
+
113
+ # ------------------------------
114
+ # Outer product: [seq_len, dim // 2]
115
+ # Each row i contains: t[i] * inv_freq
116
+ # ------------------------------
117
+ freqs = torch.outer(t, inv_freq)
118
+
119
+ # ------------------------------
120
+ # Duplicate for interleaved sin/cos: [seq_len, dim]
121
+ # This matches the common format: [sin_0, cos_0, sin_1, cos_1, ...]
122
+ # ------------------------------
123
+ emb = torch.cat((freqs, freqs), dim=-1)
124
+
125
+ # ------------------------------
126
+ # Register cos/sin as buffers
127
+ # - Stored in float32
128
+ # - Will be moved to correct device/dtype via model.to(...)
129
+ # - Not saved with state_dict (persistent=False)
130
+ # ------------------------------
131
+ self.register_buffer("cos", emb.cos(), persistent=False)
132
+ self.register_buffer("sin", emb.sin(), persistent=False)
133
+
134
+ def forward(self, position_ids: torch.LongTensor) -> tuple[torch.Tensor, torch.Tensor]:
135
+ """ """
136
+ return None # This function is not necessary.
137
+
138
+ """## GLA"""
139
+
140
+ class GroupedLatentAttention(nn.Module):
141
+ """
142
+ This version of Multihead Latent Attention applies the re-ordering trick from DeepSeekV3.
143
+ Instead of comparing the queries and keys in the query-key space, we compare them in the
144
+ kv-shared space.
145
+
146
+ For clarity, I've re-interpreted the naming of the heads, and am framing it as MQA.
147
+ What were previously labeled the query and key heads are now treated as a low-rank decomposition
148
+ of the query heads.
149
+ What we considered the "shared key/value space" is now a single key head that is also used as the
150
+ value head.
151
+ Finally, what we previously labeled the value and output heads are now treated as a low-rank
152
+ decomposition of the output heads.
153
+
154
+ This interpretation / implementation is designed to leverage the performance benefits of GQA.
155
+ The trade-off is that the query-key matching space is now larger--it will require a greater
156
+ number of calculations to match the queries to the keys. The hope is that the memory bandwidth
157
+ savings will outweigh the increased computational cost.
158
+
159
+ The same applies to the value-output space.
160
+
161
+ Note that, although the query-key and value-output spaces are now large, the low-rank
162
+ decomposition of the query heads and output heads ensures that the heads are still effectively
163
+ low rank / not over-parameterized.
164
+
165
+ Finally, note that this implementation also supports the optional use of shared spaces on
166
+ the query and output sides.
167
+
168
+ I've named the class "GroupedLatentAttention" because I may expand it to support multiple
169
+ key/value heads (i.e., multiple groups of query heads) in the future.
170
+
171
+ ==== Adding RoPE to VO ====
172
+
173
+ ### **Attempt**
174
+
175
+ We're extending Rotary Position Embeddings (RoPE) beyond the query-key interaction to the **value-output path** in Multihead Latent Attention (MLA).
176
+
177
+ * In DeepSeek-V3's MLA framing, the same **full-rank key/value head** provides both the keys (for patterns) and the values (for messages).
178
+ * Queries and output heads are low-rank bottlenecks, effectively serving as vocabularies of **pattern directions** (Q) and **message directions** (O).
179
+ * Standard RoPE only modulates the Q–K dot product. Our attempt is to also apply RoPE phases consistently in the V–O pathway, so that **positional dependence is preserved in both the matching (QK) and messaging (VO) sides**.
180
+
181
+ --
182
+
183
+ ### **Hypothesis**
184
+
185
+ If we rotate value vectors by their **source position phase** and then apply the **inverse rotation at the destination** before output projection, the model gains a clean **relative-position equivariance** in the message path, mirroring the property RoPE provides for queries and keys.
186
+
187
+ This should:
188
+
189
+ 1. Make the 1-to-1 correspondence between "pattern templates" (Q) and "message templates" (O) more consistent.
190
+ 2. Reduce the burden on output heads to learn ad-hoc positional compensation.
191
+ 3. Improve long-context generalization, since both attention matching *and* message passing would share the same relative-position geometry.
192
+
193
+
194
+ """
195
+
196
+ def __init__(self, config: SharedSpaceDecoderConfig, layer_idx: int):
197
+ super().__init__()
198
+
199
+ self.config = config
200
+
201
+ # Used to determine if this layer is dense or uses latents.
202
+ self.layer_idx = layer_idx
203
+ self.attention_dropout_prob = config.attention_dropout_prob
204
+
205
+ self.num_heads = config.num_attention_heads
206
+
207
+ self.rope_theta = config.rope_theta
208
+ self.rope_dims = config.rope_dims
209
+ self.nope_dims = config.nope_dims
210
+
211
+ self.q_shared_dim = config.q_shared_dim
212
+ # What was previously considered the key/value shared dimension is now the
213
+ # size of the MQA style single key/value head.
214
+ self.kv_head_dim = config.kv_shared_dim
215
+ self.o_shared_dim = config.o_shared_dim
216
+
217
+ # What was previously the query/key head size is now the size of
218
+ # the query head decomposition.
219
+ self.q_inner_dim = config.qk_private_dim
220
+
221
+ # What was previously the value/output head size is now the size of
222
+ # the output head decomposition.
223
+ self.o_inner_dim = config.vo_private_dim
224
+
225
+ self.hidden_size = config.hidden_size
226
+
227
+ # =========================
228
+ # Input Projections
229
+ # =========================
230
+
231
+ # If this is one of the dense layers,
232
+ if self.layer_idx < config.num_dense_layers:
233
+
234
+ # =========================
235
+ # Dense Attention
236
+ # =========================
237
+
238
+ # No latent projections.
239
+ self.latent_spaces = False
240
+
241
+ # Define the standard QKV projection
242
+ self.qkv_proj = nn.Linear(
243
+ config.hidden_size,
244
+ self.num_heads * (self.qk_private_dim * 2 + self.vo_private_dim),
245
+ bias=config.attention_bias,
246
+ )
247
+
248
+ # Dense output projection
249
+ self.o_proj = nn.Linear(
250
+ self.num_heads * self.vo_private_dim,
251
+ config.hidden_size,
252
+ bias=config.attention_bias,
253
+ )
254
+
255
+ # If we're past the dense layers,
256
+ else:
257
+
258
+ # =========================
259
+ # Latent Attention
260
+ # =========================
261
+
262
+ # Use latent projections.
263
+ self.latent_spaces = True
264
+
265
+ # Input latent projections
266
+
267
+ print("config.q_shared_dim", config.q_shared_dim)
268
+
269
+ # ==========================
270
+ # Shared Query Space
271
+ # ==========================
272
+
273
+ # If we're using a shared query subspace,
274
+ if config.q_shared_dim is not None:
275
+ # Set a flag that we'll check in `forward`.
276
+ self.query_shared = True
277
+
278
+ self.q_shared_proj = nn.Linear(
279
+ config.hidden_size,
280
+ self.q_shared_dim,
281
+ bias=config.attention_bias,
282
+ )
283
+
284
+ self.q_shared_norm = create_norm_layer(self.q_shared_dim, config)
285
+
286
+ else:
287
+ print("Using identity for shared projection.")
288
+ # Set a flag that we'll check in `forward`.
289
+ self.query_shared = False
290
+
291
+ self.q_shared_dim = config.hidden_size
292
+
293
+ #print("Updated self.q_shared_dim to", self.q_shared_dim)
294
+
295
+ # Use identity.
296
+ self.q_shared_proj = nn.Identity()
297
+ self.q_shared_norm = nn.Identity()
298
+
299
+ # ==========================
300
+ # Shared Output Space
301
+ # ==========================
302
+
303
+ # If we're using a shared output space,
304
+ if config.o_shared_dim is not None:
305
+ # Set a flag that we'll check in `forward`.
306
+ self.output_shared = True
307
+
308
+ # Shared output projection
309
+ # The head outputs from `o_private_proj` are first summed together (across
310
+ # heads) in the latent space.
311
+ # Then we project their combined outputs (a single vector per token)
312
+ # back to model space via `o_shared_proj`.
313
+ self.o_shared_proj = nn.Linear(
314
+ self.o_shared_dim,
315
+ self.hidden_size,
316
+ bias=config.attention_bias
317
+ )
318
+
319
+ self.o_shared_norm = create_norm_layer(self.o_shared_dim, config)
320
+
321
+ else:
322
+ # Set a flag that we'll check in `forward`.
323
+ self.output_shared = False
324
+ self.o_shared_dim = config.hidden_size
325
+
326
+ # Use identity.
327
+ self.o_shared_proj = nn.Identity()
328
+ self.o_shared_norm = nn.Identity()
329
+
330
+ # ================================
331
+ # Decomposed Query Heads
332
+ # ================================
333
+
334
+ # Query down projections.
335
+ # The query head inner dimension makes the head low rank, as usual.
336
+ self.q_priv_a_proj = nn.Linear(
337
+ self.q_shared_dim,
338
+ self.num_heads * self.q_inner_dim,
339
+ bias=False
340
+ )
341
+
342
+ # Query up projections.
343
+ # We project back to the larger key/value space.
344
+ # Rather than create a linear and break it apart, we can create our
345
+ # desired shapes.
346
+ # per-head Dq_c -> Dkv (store as [H, Dq_c, Dkv])
347
+ self.q_priv_b_weight = nn.Parameter(
348
+ torch.empty(self.num_heads, self.q_inner_dim, self.kv_head_dim)
349
+ )
350
+ nn.init.kaiming_uniform_(self.q_priv_b_weight, a=math.sqrt(5))
351
+
352
+ # ====================================
353
+ # Single Joint Key/Value Head
354
+ # ====================================
355
+
356
+ # The single joint key/value head.
357
+ self.kv_priv_proj = nn.Linear(
358
+ self.hidden_size,
359
+ self.kv_head_dim,
360
+ bias=False,
361
+ )
362
+
363
+ self.kv_priv_norm = create_norm_layer(self.kv_head_dim, config)
364
+
365
+ # ================================
366
+ # Decomposed Output Heads
367
+ # ================================
368
+
369
+ # Down: values [B,H,T,Dkv] -> per-head Do_c using weights [H, Dkv, Do_c]
370
+ self.o_priv_a_weight = nn.Parameter(
371
+ torch.empty(self.num_heads, self.kv_head_dim, self.o_inner_dim)
372
+ )
373
+ nn.init.kaiming_uniform_(self.o_priv_a_weight, a=math.sqrt(5))
374
+
375
+ # Output up projections.
376
+
377
+ # We project back to the larger output subspace (or the model space,
378
+ # if no subspace is used).
379
+ self.o_priv_b_proj = nn.Linear(
380
+ self.num_heads * self.o_inner_dim,
381
+ self.o_shared_dim,
382
+ bias=False
383
+ )
384
+
385
+ # Let SDPA choose 1/sqrt(E). If you want explicit: self.kv_head_dim ** -0.5
386
+ self.softmax_scale = None
387
+
388
+
389
+ def forward(
390
+ self,
391
+ hidden_states: torch.Tensor,
392
+ position_embeddings: tuple[torch.Tensor, torch.Tensor],
393
+ attention_mask: Optional[torch.Tensor],
394
+ #past_key_value: Optional[Cache] = None, # TODO - Can I remove this?
395
+ #cache_position: Optional[torch.LongTensor] = None, # TODO - Can I remove this?
396
+ **kwargs,
397
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor], Optional[tuple[torch.Tensor]]]:
398
+ # === Tensor Dimension Symbols ===
399
+ # B: batch_size — number of samples in the batch
400
+ # T: seq_len — number of tokens per sample
401
+ # H: n_heads — number of attention heads
402
+ # D: hidden_dim — model embedding size
403
+ # Dq_c: q_inner_dim - per-head decomposition dim for Q
404
+ Dq_c = self.q_inner_dim # per-head inner dim for Q
405
+ # Do_c: o_inner_dim - per-head decomposition dim for O
406
+ Do_c = self.o_inner_dim # per-head inner dim for O
407
+ # Dkv: kv_head_dim - Head size of the joint key/value head
408
+ Dkv = self.kv_head_dim # Head size of the joint key/value head
409
+ # Dr: rope_dims - The first Dr dimensions receive rope.
410
+ # Dq_s: q_shared_dim - query shared subspace size
411
+ Dq_s = self.q_shared_dim
412
+ # Do_s: o_shared_dim - output shared subspace size
413
+ Do_s = self.o_shared_dim
414
+
415
+ # Input token embeddings
416
+ # hidden_states: [B, T, D]
417
+ B, T = hidden_states.shape[:2]
418
+ H = self.num_heads
419
+
420
+
421
+
422
+ # =============================
423
+ # Shared Query Space
424
+ # =============================
425
+ # These are set to identity if no shared query space is used.
426
+
427
+ # Project token embeddings into shared latents
428
+ # Input:
429
+ # hidden_states [B, T, D]
430
+ # q_shared_proj [D, Dq_s]
431
+ # kv_shared_proj [D, Dkv]
432
+ # Output:
433
+ # q_shared [B, T, Dq_s]
434
+ # kv_shared [B, T, Dkv]
435
+ q_shared = self.q_shared_proj(hidden_states)
436
+
437
+ # Normalize latent vectors, shapes unchanged.
438
+ q_shared = self.q_shared_norm(q_shared)
439
+
440
+ # ================================
441
+ # Decomposed Query Heads
442
+ # ================================
443
+
444
+
445
+ # Project query latents onto decomposed query heads.
446
+ #
447
+ # Down projection ('a')
448
+ # Input:
449
+ # q_shared [B, T, Dq_s]
450
+ # q_priv_a_proj [Dq_s, H*Dq_c]
451
+ # Output:
452
+ # queries_c [B, T, H*Dq_c]
453
+ queries_c = self.q_priv_a_proj(q_shared)
454
+
455
+ # Split the vectors by head
456
+ # Input:
457
+ # queries_c [B, T, H*Dq_c]
458
+ # Output:
459
+ # queries_c [B, T, H, Dq_c]
460
+ queries_c = queries_c.view(B, T, H, Dq_c)
461
+
462
+ # Up projection ('b')
463
+ # Input:
464
+ # queries_c [B, T, H, Dq_c]
465
+ # q_priv_b_weight [H, Dq_c, Dkv]
466
+ # Output:
467
+ # queries [B, H, T, Dkv]
468
+ queries = torch.einsum("bthd,hdc->bhtc", queries_c, self.q_priv_b_weight)
469
+
470
+ # ===================================
471
+ # Single Joint Key/Value Head
472
+ # ===================================
473
+
474
+ # Project token embeddings into single joint key/value head.
475
+ # Input:
476
+ # hidden_states [B, T, D]
477
+ # kv_priv_proj [D, Dkv]
478
+ # Output:
479
+ # keyvalue [B, T, Dkv]
480
+ keyvalue = self.kv_priv_proj(hidden_states)
481
+
482
+ # Apply QK normalization.
483
+ keyvalue = self.kv_priv_norm(keyvalue)
484
+
485
+ # Prepare the queries and keyvalue vectors for RoPE and flash attention.
486
+ # We have multiple query heads, and the queries are in `queries`.
487
+ # We have a single key head, and the keyvector is in `keyvalue`.
488
+
489
+ # Move the head dimension to the front, so for each head, we have
490
+ # a series of vectors for each token in the sequence.
491
+ #
492
+ # Inputs:
493
+ # keyvalue [B, T, Dkv]
494
+ # Output:
495
+ # keyvalue [B, 1, T, Dkv]
496
+ keyvalue = keyvalue.unsqueeze(1)
497
+
498
+ # ==================
499
+ # RoPE
500
+ # ==================
501
+ # Apply rotary position embeddings to the first `self.rope_dims` of
502
+ # each head.
503
+ # The slice operations are free, but the concatenation is
504
+ # not, because the outputs of the rotation operation are new data
505
+ # occupying different memory. Still considered the best option,
506
+ # though.
507
+
508
+ # 1. Unpack the precomputed cosine and sine embeddings
509
+ # Position embeddings is a tuple of
510
+ # (cos [seq_len, rope_dims],
511
+ # sin [seq_len, rope_dims])
512
+ cos, sin = position_embeddings
513
+
514
+ # 2. Split the query and key heads into the part to rotate and the part
515
+ # to pass through (early columns get position info, later ones don't)
516
+ #
517
+ # (Using queries as example)
518
+ # Inputs:
519
+ # queries [B, H, T, Dkv] Dkv = rope_dims + not_rope_dims
520
+ # Outputs:
521
+ # q_rope [B, H, T, Dr]
522
+ # q_pass [B, H, T, Dkv-Dr]
523
+ q_rope, q_pass = queries[..., :self.rope_dims], queries[..., self.rope_dims:]
524
+ k_rope, k_pass = keyvalue[..., :self.rope_dims], keyvalue[..., self.rope_dims:]
525
+
526
+ # 3. Apply the rotary embedding to the designated slice
527
+ #
528
+ # To broadcast cos and sin across the batch and head dimensions, we unsqueeze them.
529
+ # Shape change: [T, Dr] -> [1, 1, T, Dr]
530
+ cos = cos.unsqueeze(0).unsqueeze(0)
531
+ sin = sin.unsqueeze(0).unsqueeze(0)
532
+
533
+ #print("q_rope.shape[-1] // 2:", (q_rope.shape[-1] // 2))
534
+ #print("x1 = x[..., :x.shape[-1] // 2 ].shape:", q_rope[..., :q_rope.shape[-1] // 2 ].shape)
535
+ #print("sin/cos.shape:", cos.shape)
536
+ #print("q_rope.shape:", q_rope.shape)
537
+ #print("(q_rope * cos).shape:", (q_rope * cos).shape)
538
+ #print("rotate_half(q_rope).shape:", rotate_half(q_rope).shape)
539
+ #print("(rotate_half(q_rope) * sin).shape:", (rotate_half(q_rope) * sin).shape)
540
+ """
541
+ In this example batch_size = 2, hum_heads = 8, seq_len = 65, rope_dims = 16
542
+
543
+ q_rope.shape[-1] // 2: 8
544
+ x1 = x[..., :x.shape[-1] // 2 ].shape: torch.Size([2, 8, 65, 8])
545
+
546
+ sin/cos.shape: torch.Size([1, 1, 65, 16]) # After double unsqueeze.
547
+ vq_rope.shape: torch.Size([2, 8, 65, 16])
548
+
549
+ (q_rope * cos).shape: torch.Size([2, 8, 65, 16])
550
+
551
+ rotate_half(q_rope).shape: torch.Size([2, 8, 65, 16])
552
+ (rotate_half(q_rope) * sin).shape: torch.Size([2, 8, 65, 16])
553
+ """
554
+
555
+
556
+ # Let's walk through the queries as the example.
557
+ # What does rotate half do?
558
+ # dim -1 is the row vectors, the queries
559
+ #
560
+ # Step 1: Split the vector in half.
561
+ # "q_rope.shape[-1] // 2" <- How much to select. Half the length of the q_rope vector
562
+ # x1 = x[..., :x.shape[-1] // 2 ] # Select the first half of the vector.
563
+ # x2 = x[..., x.shape[-1] // 2:] # Select the second half.
564
+ #
565
+ # Step 2:
566
+ # - Apply negative to the values in the second half.
567
+ # - Reverse the order of the halves.
568
+ # return torch.cat((-x2, x1), dim=-1)
569
+ #
570
+ # ---- (q_rope * cos) ----
571
+ # Element-wise multiply the values in each `cos` vector with the
572
+ # corresponding (i.e., same sequence position) `q_rope` vector.
573
+ #
574
+ # Inputs:
575
+ # q_rope [B, H, T, Dr]
576
+ # cos [1, 1, T, Dr]
577
+ #
578
+ # Outputs:
579
+ # x [B, H, T, Dr]
580
+ #
581
+ # ---- (rotate_half(q_rope)) ----
582
+ # TODO
583
+ #
584
+ # Inputs:
585
+ # q_rope [B, T, Dr]
586
+ #
587
+ # Outputs:
588
+ # rot_q_rope [B, T, Dr]
589
+ #
590
+ # ---- rotated * sin ----
591
+ # TODO
592
+ q_rotated = (q_rope * cos) + (rotate_half(q_rope) * sin)
593
+ k_rotated = (k_rope * cos) + (rotate_half(k_rope) * sin)
594
+
595
+ # 4. Concatenate the rotated and pass-through parts back together
596
+ # Input (each): [B, H, T, Dr] and [B, H, T, Dkv-Dr]
597
+ # Output (each): [B, H, T, Dkv]
598
+ # (Where h = 1 for the key head and h = num_heads for the query heads)
599
+ queries = torch.cat((q_rotated, q_pass), dim=-1)
600
+ keyvalue = torch.cat((k_rotated, k_pass), dim=-1)
601
+
602
+ # ====================
603
+ # GQA / MQA
604
+ # ====================
605
+ # GPT says that flash attention will infer the broadcasting, so `expand` is not needed.
606
+ #
607
+ # We need to use the `expand` operation to broadcast the keyvalue vector
608
+ # across the query heads.
609
+ # Input:
610
+ # keyvalue [B, 1, T, Dkv]
611
+ # Output:
612
+ # keyvalue [B, H, T, Dkv]
613
+ #keyvalue = keyvalue.expand(-1, H, -1, -1)
614
+
615
+ # ===================
616
+ # Attention
617
+ # ===================
618
+ # We're ready for the attention score calculation.
619
+
620
+ # Only apply dropout during training.
621
+ # self.training is a pytorch flag.
622
+ if self.training:
623
+ dropout_p = self.attention_dropout_prob
624
+ else:
625
+ dropout_p = 0.0
626
+
627
+ # Call SDPA / Flash Attention
628
+ # https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html
629
+ # Apply MQA / GQA. In this case, we have a single key head, and multiple query heads.
630
+ values = F.scaled_dot_product_attention(
631
+ queries,
632
+ keyvalue, # Single key vector (joint with value) for GQA / MQA.
633
+ keyvalue, # Single value vector (joint with key) for GQA / MQA.
634
+ attn_mask=None, # attention_mask,
635
+ dropout_p=dropout_p,
636
+ scale=self.softmax_scale,
637
+ is_causal=True, # This is a decoder - apply causal masking
638
+ )
639
+
640
+ # Attention outputs:
641
+ # values [B, H, T, Dkv]
642
+
643
+ # The final Dr dims of the value vectors carry RoPE information.
644
+ # We can either (1) add position dependence to the value-output process,
645
+ # or (2) we can strip off the RoPE information and only use the non-RoPE parts.
646
+
647
+ # Let's try option 1!
648
+
649
+ # Split the values into the RoPE and non-RoPE parts.
650
+ # Input:
651
+ # values [B, H, T, Dkv]
652
+ # Output:
653
+ # values_rope [B, H, T, Dr]
654
+ # values_pass [B, H, T, Dkv-Dr]
655
+ values_rope, values_pass = values[..., :self.rope_dims], values[..., self.rope_dims:]
656
+
657
+ # Fold the query RoPE information into the value vectors.
658
+ # Inverse rotation: R_{-θ} x = (x * cos) - (rotate_half(x) * sin)
659
+ # Input:
660
+ # values_rope [B, H, T, Dr]
661
+ # cos [1, 1, T, Dr]
662
+ # sin [1, 1, T, Dr]
663
+ # Output:
664
+ # values_unrot [B, H, T, Dr]
665
+ values_unrot = (values_rope * cos) - (rotate_half(values_rope) * sin)
666
+
667
+ # Now the values have the offset information in their rope dimensions,
668
+ # and the output heads can learn to use it.
669
+ values = torch.cat((values_unrot, values_pass), dim=-1) # [B,H,T,Dkv]
670
+
671
+ # =========================
672
+ # Output Projection
673
+ # =========================
674
+
675
+
676
+ # Project the values onto the decomposed output heads.
677
+ # Output down projection heads.
678
+ # Input:
679
+ # values [B, H, T, Dkv]
680
+ # o_priv_a_weight [H, Dkv, Do_c]
681
+ # Output:
682
+ # outputs_c [B, H, T, Do_c]
683
+ outputs_c = torch.einsum("bhtd,hdc->bhtc", values, self.o_priv_a_weight)
684
+
685
+ # For the up projection, we can concatenate the 'outputs_c' vectors by head,
686
+ # (in the same way we would usually concatenate the value vectors)
687
+ # Input:
688
+ # outputs_c [B, H, T, Do_c]
689
+ # Output:
690
+ # outputs_c [B, T, H*Do_c]
691
+
692
+ outputs_c = outputs_c.permute(0, 2, 1, 3).contiguous().view(B, T, H * Do_c)
693
+
694
+ # Project up to the shared output space and sum across the output heads.
695
+ # Input:
696
+ # outputs_c [B, T, H*Do_c]
697
+ # o_priv_b_proj [H*Do_c, Do_s]
698
+ # Output:
699
+ # output_s [B, T, Do_s]
700
+ output_s = self.o_priv_b_proj(outputs_c)
701
+
702
+ # Apply normalization to the output latents
703
+ output_s = self.o_shared_norm(output_s)
704
+
705
+ # Re-project the output latent representation back to model space.
706
+ # Input:
707
+ # output_s [B, T, Do_s]
708
+ # o_shared_proj [Do_s, D]
709
+ # Output:
710
+ # attn_output [B, T, D]
711
+ attn_output = self.o_shared_proj(output_s)
712
+
713
+ # TODO - Not currently supported.
714
+ # If this is a dense layer,
715
+ # Project the values back into model space.
716
+ # attn_output = self.o_proj(attn_output)
717
+
718
+ # -----------------------------------------
719
+
720
+ return attn_output
721
+
layers/mla.py ADDED
@@ -0,0 +1,616 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """# ▂▂▂▂▂▂▂▂▂▂▂▂
2
+
3
+ # `mla.py`
4
+
5
+ Based on: https://huggingface.co/deepseek-ai/DeepSeek-R1/blob/main/modeling_deepseek.py
6
+
7
+ ## RotaryEmbedding
8
+ """
9
+
10
+ import torch
11
+ import torch.nn as nn
12
+ import torch.nn.functional as F
13
+ from typing import Optional
14
+
15
+ from ..models.shared_space_config import SharedSpaceDecoderConfig
16
+
17
+
18
+ def create_norm_layer(hidden_size: int, config: SharedSpaceDecoderConfig) -> nn.Module:
19
+ """
20
+ Create a normalization layer based on the config norm_type.
21
+
22
+ If `hidden_size` is `None`, this returns an identity layer.
23
+
24
+ Args:
25
+ hidden_size: The dimension to normalize over
26
+ config: Configuration containing norm_type and epsilon values
27
+
28
+ Returns:
29
+ Either a LayerNorm or RMSNorm layer
30
+ """
31
+ if hidden_size is None:
32
+ return nn.Identity()
33
+ elif config.norm_type == "layernorm":
34
+ return nn.LayerNorm(hidden_size, eps=config.layer_norm_eps)
35
+ elif config.norm_type == "rmsnorm":
36
+ return DeepseekV3RMSNorm(hidden_size, eps=config.rms_norm_eps)
37
+ else:
38
+ # This should be caught by config validation, but being defensive
39
+ raise ValueError(f"Unknown norm_type: {config.norm_type}")
40
+
41
+
42
+ # TODO - Find a shared place to put this.
43
+ class DeepseekV3RMSNorm(nn.Module):
44
+ def __init__(self, hidden_size, eps=1e-6):
45
+ """
46
+ DeepseekV3RMSNorm is equivalent to T5LayerNorm
47
+ """
48
+ super().__init__()
49
+ self.weight = nn.Parameter(torch.ones(hidden_size))
50
+ self.variance_epsilon = eps
51
+
52
+ def forward(self, hidden_states):
53
+ input_dtype = hidden_states.dtype
54
+ hidden_states = hidden_states.to(torch.float32)
55
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
56
+ hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
57
+ return self.weight * hidden_states.to(input_dtype)
58
+
59
+
60
+ # Helper function needed because it's called twice during RoPE,
61
+ # but I dumped it in the comments there.
62
+ # TODO - Nah, screw it, just write it twice! At least then you get
63
+ # to use the word 'query' instead of 'x'.
64
+ def rotate_half(x):
65
+ """Rotates half the hidden dims of the input."""
66
+ x1 = x[..., : x.shape[-1] // 2]
67
+ x2 = x[..., x.shape[-1] // 2 :]
68
+ return torch.cat((-x2, x1), dim=-1)
69
+
70
+ class RotaryEmbedding(nn.Module):
71
+ """Precompute RoPE embeddings and store them as buffers."""
72
+
73
+ def __init__(self, config: SharedSpaceDecoderConfig) -> None:
74
+ super().__init__()
75
+
76
+ dim = config.rope_dims
77
+ seq_len = config.max_position_embeddings
78
+
79
+ # ------------------------------
80
+ # Compute inverse frequencies
81
+ # ------------------------------
82
+ # Shape: [dim // 2]
83
+ # inv_freq[i] = 1 / (theta^(i / dim))
84
+ inv_freq = 1.0 / (
85
+ config.rope_theta
86
+ ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim)
87
+ )
88
+
89
+ # ------------------------------
90
+ # Apply RoPE scaling if configured
91
+ # ------------------------------
92
+ if config.rope_scaling is not None:
93
+ scaling_type = config.rope_scaling.get("type", "linear")
94
+ scaling_factor = config.rope_scaling.get("factor", 1.0)
95
+
96
+ if scaling_type == "linear":
97
+ # Linear scaling: divide frequencies by scaling factor
98
+ inv_freq = inv_freq / scaling_factor
99
+ elif scaling_type == "dynamic":
100
+ # Dynamic scaling: adjust based on sequence length
101
+ # This is a simplified implementation
102
+ inv_freq = inv_freq / scaling_factor
103
+ else:
104
+ print(f"Warning: Unknown RoPE scaling type '{scaling_type}', using linear scaling")
105
+ inv_freq = inv_freq / scaling_factor
106
+
107
+ # ------------------------------
108
+ # Compute position indices
109
+ # ------------------------------
110
+ # Shape: [seq_len]
111
+ t = torch.arange(seq_len, dtype=torch.float32)
112
+
113
+ # ------------------------------
114
+ # Outer product: [seq_len, dim // 2]
115
+ # Each row i contains: t[i] * inv_freq
116
+ # ------------------------------
117
+ freqs = torch.outer(t, inv_freq)
118
+
119
+ # ------------------------------
120
+ # Duplicate for interleaved sin/cos: [seq_len, dim]
121
+ # This matches the common format: [sin_0, cos_0, sin_1, cos_1, ...]
122
+ # ------------------------------
123
+ emb = torch.cat((freqs, freqs), dim=-1)
124
+
125
+ # ------------------------------
126
+ # Register cos/sin as buffers
127
+ # - Stored in float32
128
+ # - Will be moved to correct device/dtype via model.to(...)
129
+ # - Not saved with state_dict (persistent=False)
130
+ # ------------------------------
131
+ self.register_buffer("cos", emb.cos(), persistent=False)
132
+ self.register_buffer("sin", emb.sin(), persistent=False)
133
+
134
+ def forward(self, position_ids: torch.LongTensor) -> tuple[torch.Tensor, torch.Tensor]:
135
+ """ """
136
+ return None # This function is not necessary.
137
+
138
+ """## MLA"""
139
+
140
+ class MultiheadLatentAttention(nn.Module):
141
+ """
142
+ A variant of MLA with:
143
+ - Simplified RoPE handling:
144
+ - A portion of the head dimensions are used for position information.
145
+ - Same number of queries as keys. (no MQA)
146
+ - Optional output subspace
147
+ """
148
+
149
+ def __init__(self, config: SharedSpaceDecoderConfig, layer_idx: int):
150
+ super().__init__()
151
+
152
+ self.config = config
153
+
154
+ # Used to determine if this layer is dense or uses latents.
155
+ self.layer_idx = layer_idx
156
+ self.attention_dropout_prob = config.attention_dropout_prob
157
+
158
+ self.num_heads = config.num_attention_heads
159
+
160
+ self.rope_theta = config.rope_theta
161
+ self.rope_dims = config.rope_dims
162
+ self.nope_dims = config.nope_dims
163
+
164
+ self.q_shared_dim = config.q_shared_dim
165
+ self.kv_shared_dim = config.kv_shared_dim
166
+ self.o_shared_dim = config.o_shared_dim
167
+
168
+ self.qk_private_dim = config.qk_private_dim
169
+ self.vo_private_dim = config.vo_private_dim
170
+
171
+ self.hidden_size = config.hidden_size
172
+
173
+ # =========================
174
+ # Input Projections
175
+ # =========================
176
+
177
+ # If this is one of the dense layers,
178
+ if self.layer_idx < config.num_dense_layers:
179
+
180
+ # =========================
181
+ # Dense Attention
182
+ # =========================
183
+
184
+ # No latent projections.
185
+ self.latent_spaces = False
186
+
187
+ # Define the standard QKV projection
188
+ self.qkv_proj = nn.Linear(
189
+ config.hidden_size,
190
+ self.num_heads * (self.qk_private_dim * 2 + self.vo_private_dim),
191
+ bias=config.attention_bias,
192
+ )
193
+
194
+ # Dense output projection
195
+ self.o_proj = nn.Linear(
196
+ self.num_heads * self.vo_private_dim,
197
+ config.hidden_size,
198
+ bias=config.attention_bias,
199
+ )
200
+
201
+ # If we're past the dense layers,
202
+ else:
203
+
204
+ # =========================
205
+ # Latent Attention
206
+ # =========================
207
+
208
+ # Use latent projections.
209
+ self.latent_spaces = True
210
+
211
+ # Input latent projections
212
+
213
+ # If we're using a shared query subspace,
214
+ if config.q_shared_dim is not None:
215
+ # Set a flag that we'll check in `forward`.
216
+ self.query_shared = True
217
+
218
+ self.q_shared_proj = nn.Linear(
219
+ config.hidden_size,
220
+ self.q_shared_dim,
221
+ bias=config.attention_bias,
222
+ )
223
+
224
+ self.q_shared_norm = create_norm_layer(self.q_shared_dim, config)
225
+
226
+ else:
227
+ # Set a flag that we'll check in `forward`.
228
+ self.query_shared = False
229
+
230
+ self.q_shared_dim = config.hidden_size
231
+
232
+ #print("Updated self.q_shared_dim to", self.q_shared_dim)
233
+
234
+ # Use identity.
235
+ self.q_shared_proj = nn.Identity()
236
+ self.q_shared_norm = nn.Identity()
237
+
238
+ # If we're using a shared key/value subspace,
239
+ if config.kv_shared_dim is not None:
240
+ # Set a flag that we'll check in `forward`.
241
+ self.keyvalue_shared = True
242
+
243
+ self.kv_shared_proj = nn.Linear(
244
+ config.hidden_size,
245
+ self.kv_shared_dim,
246
+ bias=config.attention_bias,
247
+ )
248
+
249
+ self.kv_shared_norm = create_norm_layer(self.kv_shared_dim, config)
250
+
251
+ else:
252
+ # Set a flag that we'll check in `forward`.
253
+ self.keyvalue_shared = False
254
+
255
+ self.kv_shared_dim = config.hidden_size
256
+
257
+ # Use identity.
258
+ self.kv_shared_proj = nn.Identity()
259
+ self.kv_shared_norm = nn.Identity()
260
+
261
+ #print("config.q_shared_dim", config.q_shared_dim)
262
+ #print("self.qk_private_dim", self.qk_private_dim)
263
+
264
+ # Query heads
265
+ self.q_private_proj = nn.Linear(
266
+ self.q_shared_dim,
267
+ self.num_heads * self.qk_private_dim,
268
+ bias=False # TODO
269
+ )
270
+
271
+ # Key and Value heads, concatenated
272
+ self.kv_private_proj = nn.Linear(
273
+ self.kv_shared_dim,
274
+ self.num_heads * (self.qk_private_dim + self.vo_private_dim),
275
+ bias=False,
276
+ )
277
+
278
+ # Use output subspace if o_shared_dim is specified
279
+ self.output_subspace = config.o_shared_dim is not None
280
+
281
+ # If we're using an output subspace,
282
+ if self.output_subspace:
283
+
284
+ # ==========================
285
+ # Output Subspace
286
+ # ==========================
287
+
288
+ self.o_shared_dim = config.o_shared_dim
289
+
290
+ # Per-head output projections
291
+ # (Similar to original W^O, but projects the scored value vectors
292
+ # into a latent space instead of back to the model)
293
+ self.o_private_proj = nn.Linear(
294
+ self.num_heads * self.vo_private_dim,
295
+ self.o_shared_dim,
296
+ bias=False
297
+ )
298
+
299
+ # Norm layer between o_private_proj and o_shared_proj
300
+ # Note: In previous ViT experiments, this norm step hurt performance, but was beneficial
301
+ # in the DeepSeekV3 experiments.
302
+ # However, we're making it configurable so it can be tested in different contexts.
303
+ self.o_private_norm = create_norm_layer(self.o_shared_dim, config)
304
+
305
+ # Shared output projection
306
+ # The head outputs from `o_private_proj` are first summed together (across
307
+ # heads) in the latent space.
308
+ # Then we project their combined outputs (a single vector per token)
309
+ # back to model space via `o_shared_proj`.
310
+ self.o_shared_proj = nn.Linear(
311
+ self.o_shared_dim,
312
+ self.hidden_size,
313
+ bias=config.attention_bias
314
+ )
315
+ else:
316
+ # Dense output projection
317
+ self.o_proj = nn.Linear(
318
+ self.num_heads * self.vo_private_dim,
319
+ config.hidden_size,
320
+ bias=config.attention_bias,
321
+ )
322
+
323
+ # Softmax scaling factor.
324
+ self.softmax_scale = self.qk_private_dim ** (-0.5)
325
+
326
+
327
+ def forward(
328
+ self,
329
+ hidden_states: torch.Tensor,
330
+ position_embeddings: tuple[torch.Tensor, torch.Tensor],
331
+ attention_mask: Optional[torch.Tensor],
332
+ #past_key_value: Optional[Cache] = None, # TODO - Can I remove this?
333
+ #cache_position: Optional[torch.LongTensor] = None, # TODO - Can I remove this?
334
+ **kwargs,
335
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor], Optional[tuple[torch.Tensor]]]:
336
+ # === Tensor Dimension Symbols ===
337
+ # B: batch_size — number of samples in the batch
338
+ # T: seq_len — number of tokens per sample
339
+ # H: n_heads — number of attention heads
340
+ # D: hidden_dim — model embedding size
341
+ # Dv: vo_private_dim - per-head value/output projection dimension
342
+ # Dr: rope_dims - The first Dr dimensions receive rope.
343
+ # Cq: q_shared_dim - query shared subspace size
344
+ # Ckv: kv_shared_dim - key-value shared subspace size
345
+ # Co: o_shared_dim - output shared subspace size
346
+
347
+ # Input token embeddings
348
+ # hidden_states: [B, T, D]
349
+ B, T = hidden_states.shape[:2]
350
+ H = self.num_heads
351
+ Dq = self.qk_private_dim # per-head dim for Q and K
352
+ Dv = self.vo_private_dim # per-head dim for V/O
353
+
354
+ Dc_q, Dc_kv = self.q_shared_dim, self.kv_shared_dim
355
+
356
+ # ==============================
357
+ # QKV Head Projections
358
+ # ==============================
359
+ # Project tokens into per-head query, key, and value vectors
360
+
361
+ # If this layer uses latent projections,
362
+ if self.latent_spaces:
363
+
364
+ # ================================
365
+ # Shared Space Projections
366
+ # ================================
367
+
368
+ # Project token embeddings into shared latents
369
+ # Input:
370
+ # hidden_states [B, T, D]
371
+ # q_shared_proj [D, Cq]
372
+ # kv_shared_proj [D, Ckv]
373
+ # Output:
374
+ # q_shared [B, T, Cq]
375
+ # kv_shared [B, T, Ckv]
376
+
377
+ # If we're using a shared query subspace,
378
+ if self.q_shared_dim is not None:
379
+ q_shared = self.q_shared_proj(hidden_states)
380
+
381
+ # Normalize latent vectors, shapes unchanged.
382
+ q_shared = self.q_shared_norm(q_shared)
383
+ # Otherwise,
384
+ else:
385
+ # Use the hidden states
386
+ q_shared = hidden_states
387
+
388
+ # If we're using a shared key/value subspace,
389
+ if self.kv_shared_dim is not None:
390
+
391
+ # Project token embeddings into shared subspace.
392
+ kv_shared = self.kv_shared_proj(hidden_states)
393
+
394
+ # Normalize latent vectors, shapes unchanged.
395
+ kv_shared = self.kv_shared_norm(kv_shared)
396
+ # Otherwise,
397
+ else:
398
+ # Use the hidden states
399
+ kv_shared = hidden_states
400
+
401
+ # ======================================
402
+ # Per-Head (Private) Projections
403
+ # ======================================
404
+
405
+ # Project query latents onto query heads.
406
+ # Input:
407
+ # q_shared [B, T, Cq]
408
+ # q_private_proj [Cq, H*Dh]
409
+ # Output:
410
+ # queries [B, T, H*Dh]
411
+ queries = self.q_private_proj(q_shared)
412
+
413
+ # Project key/value latents onto key and value heads.
414
+ # The key and value heads are all concatenated, each head occupies
415
+ # Dh columns of the kv_private_proj. This yields the key and value
416
+ # vectors concatenated in the same way.
417
+ #
418
+ # Input:
419
+ # kv_shared [B, T, Ckv]
420
+ # kv_private_proj [Ckv, 2*H*Dh]
421
+ # Output:
422
+ # keysvalues [B, T, 2*H*Dh]
423
+ keysvalues = self.kv_private_proj(kv_shared)
424
+
425
+ # Split into key and value tensors
426
+ # Each: [B, T, H * Dh]
427
+ keys, values = keysvalues.chunk(2, dim=-1)
428
+
429
+ # If this is a dense attention layer (no latent projections),
430
+ else:
431
+
432
+ # ====================
433
+ # Standard MHA
434
+ # ====================
435
+
436
+ # Standard QKV projection
437
+ # Input:
438
+ # hidden_states [B, T, D]
439
+ # qkv_proj [D, 3*H*Dh]
440
+ # Output:
441
+ # querieskeysvalues [B, T, 3*H*Dh]
442
+ querieskeysvalues = self.qkv_proj(hidden_states)
443
+
444
+ # Separate query, key, and value vectors
445
+ # Each: [B, T, H * Dh]
446
+ queries, keys, values = querieskeysvalues.chunk(3, dim=-1)
447
+
448
+ # Split up queries so that there's just one per row.
449
+ # Same for keys and values.
450
+ #
451
+ # Inputs:
452
+ # Each [B, T, H*Dh]
453
+ # Output:
454
+ # Each [B, H, T, Dh]
455
+ queries = queries.view(B, T, H, Dq).transpose(1, 2)
456
+ keys = keys.view(B, T, H, Dq).transpose(1, 2)
457
+ values = values.view(B, T, H, Dv).transpose(1, 2)
458
+
459
+ # ==================
460
+ # RoPE
461
+ # ==================
462
+ # Apply rotary position embeddings to the first `self.rope_dims` of
463
+ # each head.
464
+ # The slice operations are free, but the concatenation is
465
+ # not, because the outputs of the rotation operation are new data
466
+ # occupying different memory. Still considered the best option,
467
+ # though.
468
+
469
+ # 1. Unpack the precomputed cosine and sine embeddings
470
+ # Position embeddings is a tuple of
471
+ # (cos [seq_len, rope_dims],
472
+ # sin [seq_len, rope_dims])
473
+ cos, sin = position_embeddings
474
+
475
+ # 2. Split the query and key heads into the part to rotate and the part
476
+ # to pass through (early columns get position info, later ones don't)
477
+ #
478
+ # (Using queries as example)
479
+ # Inputs:
480
+ # queries [B, H, T, Dh] Dh = rope_dims + not_rope_dims
481
+ # Outputs:
482
+ # q_rope [B, H, T, Dr]
483
+ # q_pass [B, H, T, Dh-Dr]
484
+ q_rope, q_pass = queries[..., :self.rope_dims], queries[..., self.rope_dims:]
485
+ k_rope, k_pass = keys[..., :self.rope_dims], keys[..., self.rope_dims:]
486
+
487
+ # 3. Apply the rotary embedding to the designated slice
488
+ #
489
+ # To broadcast cos and sin across the batch and head dimensions, we unsqueeze them.
490
+ # Shape change: [T, Dr] -> [1, 1, T, Dr]
491
+ cos = cos.unsqueeze(0).unsqueeze(0)
492
+ sin = sin.unsqueeze(0).unsqueeze(0)
493
+
494
+ #print("q_rope.shape[-1] // 2:", (q_rope.shape[-1] // 2))
495
+ #print("x1 = x[..., :x.shape[-1] // 2 ].shape:", q_rope[..., :q_rope.shape[-1] // 2 ].shape)
496
+ #print("sin/cos.shape:", cos.shape)
497
+ #print("q_rope.shape:", q_rope.shape)
498
+ #print("(q_rope * cos).shape:", (q_rope * cos).shape)
499
+ #print("rotate_half(q_rope).shape:", rotate_half(q_rope).shape)
500
+ #print("(rotate_half(q_rope) * sin).shape:", (rotate_half(q_rope) * sin).shape)
501
+ """
502
+ In this example batch_size = 2, hum_heads = 8, seq_len = 65, rope_dims = 16
503
+
504
+ q_rope.shape[-1] // 2: 8
505
+ x1 = x[..., :x.shape[-1] // 2 ].shape: torch.Size([2, 8, 65, 8])
506
+
507
+ sin/cos.shape: torch.Size([1, 1, 65, 16]) # After double unsqueeze.
508
+ vq_rope.shape: torch.Size([2, 8, 65, 16])
509
+
510
+ (q_rope * cos).shape: torch.Size([2, 8, 65, 16])
511
+
512
+ rotate_half(q_rope).shape: torch.Size([2, 8, 65, 16])
513
+ (rotate_half(q_rope) * sin).shape: torch.Size([2, 8, 65, 16])
514
+ """
515
+
516
+
517
+ # Let's walk through the queries as the example.
518
+ # What does rotate half do?
519
+ # dim -1 is the row vectors, the queries
520
+ #
521
+ # Step 1: Split the vector in half.
522
+ # "q_rope.shape[-1] // 2" <- How much to select. Half the length of the q_rope vector
523
+ # x1 = x[..., :x.shape[-1] // 2 ] # Select the first half of the vector.
524
+ # x2 = x[..., x.shape[-1] // 2:] # Select the second half.
525
+ #
526
+ # Step 2:
527
+ # - Apply negative to the values in the second half.
528
+ # - Reverse the order of the halves.
529
+ # return torch.cat((-x2, x1), dim=-1)
530
+ #
531
+ # ---- (q_rope * cos) ----
532
+ # Element-wise multiply the values in each `cos` vector with the
533
+ # corresponding (i.e., same sequence position) `q_rope` vector.
534
+ #
535
+ # Inputs:
536
+ # q_rope [B, H, T, Dr]
537
+ # cos [1, 1, T, Dr]
538
+ #
539
+ # Outputs:
540
+ # x [B, H, T, Dr]
541
+ #
542
+ # ---- (rotate_half(q_rope)) ----
543
+ # TODO
544
+ #
545
+ # Inputs:
546
+ # q_rope [B, T, Dr]
547
+ #
548
+ # Outputs:
549
+ # rot_q_rope [B, T, Dr]
550
+ #
551
+ # ---- rotated * sin ----
552
+ # TODO
553
+ q_rotated = (q_rope * cos) + (rotate_half(q_rope) * sin)
554
+ k_rotated = (k_rope * cos) + (rotate_half(k_rope) * sin)
555
+
556
+ # 4. Concatenate the rotated and pass-through parts back together
557
+ # Input (each): [B, H, T, Dr] and [B, H, T, Dq-Dr]
558
+ # Output (each): [B, H, T, Dq]
559
+ queries = torch.cat((q_rotated, q_pass), dim=-1)
560
+ keys = torch.cat((k_rotated, k_pass), dim=-1)
561
+
562
+ # ===================
563
+ # Attention
564
+ # ===================
565
+ # The tensors (queries, keys, values) now have shape [B, H, T, Dq]
566
+ # and are ready for the attention score calculation.
567
+
568
+ # Only apply dropout during training.
569
+ # self.training is a pytorch flag.
570
+ if self.training:
571
+ dropout_p = self.attention_dropout_prob
572
+ else:
573
+ dropout_p = 0.0
574
+
575
+ # Call SDPA / Flash Attention
576
+ # https://docs.pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html
577
+ attn_output = F.scaled_dot_product_attention(
578
+ queries,
579
+ keys,
580
+ values,
581
+ attn_mask=None, # attention_mask,
582
+ dropout_p=dropout_p,
583
+ scale=self.softmax_scale,
584
+ is_causal=True, # This is a decoder - apply causal masking
585
+ )
586
+
587
+ # Reshape output back to [B, T, H * Dv] from [B, H, T, Dv]
588
+ attn_output = attn_output.transpose(1, 2).contiguous().view(B, T, H * Dv)
589
+
590
+ # =========================
591
+ # Output Projection
592
+ # =========================
593
+
594
+ # If we are using an output latent projection,
595
+ if self.latent_spaces and self.output_subspace:
596
+
597
+ # Project the attention output into the output latent space.
598
+ # This is analogous to the W^O matrix in standard attention but
599
+ # projects to an intermediate latent dimension.
600
+ attn_output = self.o_private_proj(attn_output)
601
+
602
+ # Apply normalization to the output latents
603
+ attn_output = self.o_private_norm(attn_output)
604
+
605
+ # Re-project the output latent representation back to model space.
606
+ attn_output = self.o_shared_proj(attn_output)
607
+
608
+ # If this is a dense layer,
609
+ else:
610
+ # Project the values back into model space.
611
+ attn_output = self.o_proj(attn_output)
612
+
613
+ # -----------------------------------------
614
+
615
+ return attn_output
616
+
layers/task_heads.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ from typing import Optional, Union
6
+
7
+ from transformers.modeling_outputs import CausalLMOutputWithPast
8
+
9
+ from ..models.shared_space_config import SharedSpaceDecoderConfig
10
+ from ..models.shared_space_decoder import (
11
+ SharedSpaceDecoderPreTrainedModel,
12
+ SharedSpaceDecoderModel,
13
+ DeepseekV3RMSNorm
14
+ )
15
+
16
+ def create_norm_layer(hidden_size: int, config: SharedSpaceDecoderConfig) -> nn.Module:
17
+ """
18
+ Create a normalization layer based on the config norm_type.
19
+
20
+ Args:
21
+ hidden_size: The dimension to normalize over
22
+ config: Configuration containing norm_type and epsilon values
23
+
24
+ Returns:
25
+ Either a LayerNorm or RMSNorm layer
26
+ """
27
+ if config.norm_type == "layernorm":
28
+ return nn.LayerNorm(hidden_size, eps=config.layer_norm_eps)
29
+ elif config.norm_type == "rmsnorm":
30
+ return DeepseekV3RMSNorm(hidden_size, eps=config.rms_norm_eps)
31
+ else:
32
+ # This should be caught by config validation, but being defensive
33
+ raise ValueError(f"Unknown norm_type: {config.norm_type}")
34
+
35
+
36
+ class SharedSpaceDecoderForCausalLM(SharedSpaceDecoderPreTrainedModel):
37
+ """
38
+ Subspace Decoder model with a causal language modeling head.
39
+
40
+ This model extends the SharedSpaceDecoderModel with:
41
+ - A language modeling head that projects hidden states to vocabulary logits
42
+ - Support for computing cross-entropy loss for language modeling
43
+ - Proper HuggingFace compatibility for causal language modeling tasks
44
+ - Decoder-specific initialization strategies
45
+
46
+ The model can be used for:
47
+ - Text generation
48
+ - Language modeling pretraining
49
+ - Fine-tuning on downstream tasks
50
+ """
51
+
52
+ def __init__(self, config: SharedSpaceDecoderConfig) -> None:
53
+ super().__init__(config)
54
+
55
+ # Initialize the base decoder model
56
+ self.model = SharedSpaceDecoderModel(config)
57
+
58
+ # Final layer norm before the language modeling head
59
+ self.norm = create_norm_layer(config.hidden_size, config)
60
+
61
+ # Language modeling head
62
+ # Projects from hidden_size to vocab_size to get logits for each token
63
+ self.lm_head = nn.Linear(
64
+ config.hidden_size,
65
+ config.vocab_size,
66
+ bias=False # Following common practice in modern LMs
67
+ )
68
+
69
+ # Initialize weights with decoder-specific strategy
70
+ # Note: tie_weights() will be called automatically by post_init() if config.tie_word_embeddings=True
71
+ self.post_init()
72
+
73
+ def _init_weights(self, module: nn.Module) -> None:
74
+ """
75
+ Decoder-specific weight initialization with special handling for language modeling head.
76
+
77
+ Key differences from encoder initialization:
78
+ - Language modeling head gets specialized initialization for stability
79
+ - Configurable normalization layers (LayerNorm or RMSNorm) are properly handled
80
+ - Weight tying considerations for embedding/lm_head relationship
81
+ """
82
+
83
+ # Use the base class initialization for most modules
84
+ super()._init_weights(module)
85
+
86
+ # Special handling for language modeling head
87
+ if module is self.lm_head:
88
+ # Use smaller initialization for the language modeling head
89
+ # This helps with training stability in autoregressive generation
90
+ # Common practice is to use std=initializer_range or smaller
91
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
92
+
93
+ # If weight tying is not used, we might want even smaller init
94
+ if self.model.vocab_proj is not None:
95
+ # For vocab subspace models where weights aren't tied,
96
+ # use a smaller scale to prevent initial logits from being too large
97
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range * 0.5)
98
+
99
+ def get_input_embeddings(self):
100
+ """Return the input embedding layer for compatibility with HuggingFace."""
101
+ return self.model.vocab_embed
102
+
103
+ def set_input_embeddings(self, value):
104
+ """Set the input embedding layer for compatibility with HuggingFace."""
105
+ self.model.vocab_embed = value
106
+
107
+ def get_output_embeddings(self):
108
+ """Return the output embedding layer (lm_head) for compatibility."""
109
+ return self.lm_head
110
+
111
+ def set_output_embeddings(self, new_embeddings):
112
+ """Set the output embedding layer for compatibility."""
113
+ self.lm_head = new_embeddings
114
+
115
+ def tie_weights(self):
116
+ """
117
+ Tie the input and output embedding weights.
118
+
119
+ This method sets the language modeling head's weight to be the same as
120
+ the input embedding weight. This reduces the number of parameters and
121
+ is a common practice in modern language models.
122
+
123
+ Note: For vocab subspace models, we need to handle the case where
124
+ input embeddings go through a projection layer.
125
+ """
126
+ # Only tie when embeddings live in model space (no vocab_proj)
127
+ if getattr(self.model, "vocab_proj", None) is None:
128
+ # Use HF utility for correct tying/cloning semantics
129
+ self._tie_or_clone_weights(self.lm_head, self.model.vocab_embed)
130
+ # else: leave untied for subspace case
131
+
132
+
133
+ def forward(
134
+ self,
135
+ input_ids: torch.LongTensor,
136
+ attention_mask: Optional[torch.Tensor] = None,
137
+ labels: Optional[torch.LongTensor] = None,
138
+ **kwargs,
139
+ ) -> Union[CausalLMOutputWithPast, tuple]:
140
+ """
141
+ Forward pass for causal language modeling.
142
+
143
+ Args:
144
+ input_ids: Token ids of shape [batch_size, seq_len]
145
+ attention_mask: Attention mask of shape [batch_size, seq_len]
146
+ (1 for real tokens, 0 for padding)
147
+ labels: Ground truth token ids for computing loss. Same shape as input_ids.
148
+ If provided, loss will be computed. Typically input_ids shifted by 1.
149
+
150
+ Returns:
151
+ CausalLMOutputWithPast containing:
152
+ - logits: Prediction logits of shape [batch_size, seq_len, vocab_size]
153
+ - loss: Cross-entropy loss if labels provided, else None
154
+ - hidden_states: Final layer hidden states [batch_size, seq_len, hidden_size]
155
+ """
156
+
157
+ # Run the base decoder model
158
+ # This applies all the transformer layers with causal attention
159
+ hidden_states = self.model(
160
+ input_ids=input_ids,
161
+ attention_mask=attention_mask,
162
+ **kwargs
163
+ )
164
+
165
+ # Apply final layer normalization
166
+ # This normalizes the final hidden states before the language modeling head
167
+ hidden_states = self.norm(hidden_states)
168
+
169
+ # Project to vocabulary logits
170
+ # Shape: [batch_size, seq_len, vocab_size]
171
+ logits = self.lm_head(hidden_states)
172
+
173
+ # Compute loss if labels are provided
174
+ # Previously, we had custom loss computation here, but now we use the
175
+ # standard HuggingFace loss function.
176
+ loss = None
177
+ if labels is not None:
178
+ # Flatten the tokens
179
+ loss = self.loss_function(
180
+ logits,
181
+ labels,
182
+ vocab_size=self.config.vocab_size,
183
+ **kwargs,
184
+ )
185
+
186
+ # Return in HuggingFace format
187
+ return CausalLMOutputWithPast(
188
+ loss=loss,
189
+ logits=logits,
190
+ past_key_values=None, # Not implementing KV cache yet
191
+ #hidden_states=hidden_states,
192
+ hidden_states=hidden_states if kwargs.get("output_hidden_states", False) else None,
193
+ attentions=None,
194
+ )
195
+
models/__init__.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """
4
+ Shared Subspace Decoder Models
5
+
6
+ This module contains the implementation of the Shared Subspace Decoder architecture,
7
+ including Multi-Head Latent Attention (MLA) and decomposed MLP layers.
8
+ """
9
+
10
+ from transformers import AutoConfig, AutoModel, AutoModelForCausalLM
11
+
12
+ from .shared_space_config import SharedSpaceDecoderConfig
13
+ from .shared_space_decoder import (
14
+ SharedSpaceDecoderPreTrainedModel,
15
+ SharedSpaceDecoderModel,
16
+ )
17
+
18
+ # Import from task_heads in layers directory
19
+ from ..layers.task_heads import SharedSpaceDecoderForCausalLM
20
+
21
+ # Register the configuration class with AutoConfig
22
+ AutoConfig.register("shared_space_decoder", SharedSpaceDecoderConfig)
23
+
24
+ # Register the model classes with AutoModel
25
+ AutoModel.register(SharedSpaceDecoderConfig, SharedSpaceDecoderModel)
26
+ AutoModelForCausalLM.register(SharedSpaceDecoderConfig, SharedSpaceDecoderForCausalLM)
27
+
28
+ __all__ = [
29
+ "SharedSpaceDecoderConfig",
30
+ "SharedSpaceDecoderPreTrainedModel",
31
+ "SharedSpaceDecoderModel",
32
+ "SharedSpaceDecoderForCausalLM",
33
+ ]
models/shared_space_config.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+
3
+ import torch
4
+ from torch import nn
5
+
6
+ from transformers.configuration_utils import PretrainedConfig
7
+ from transformers.modeling_utils import PreTrainedModel
8
+
9
+ class SharedSpaceDecoderConfig(PretrainedConfig):
10
+ r"""
11
+ Configuration class for SharedSpaceDecoderConfig.
12
+
13
+ Extends the HuggingFace `PretrainedConfig` to support architectural
14
+ variations including:
15
+ - Multi-Head Latent Attention (MLA)
16
+ - Decomposed MLPs (low-rank FFNs)
17
+ - Flexible attention backends (eager, flash, sdpa)
18
+ - Explicit shared subspaces for Q, K, V, and O projections
19
+
20
+ This config does not infer any defaults based on `hidden_size`. All
21
+ dimensions and ranks must be explicitly specified. If required values are
22
+ missing, a `ValueError` is raised during initialization.
23
+
24
+ ----------------------
25
+ Core Model Parameters:
26
+ ----------------------
27
+ - vocab_size (`int`) — Vocabulary size.
28
+ - hidden_size (`int`) — Model hidden dimension.
29
+ - num_hidden_layers (`int`) — Number of transformer blocks.
30
+ - intermediate_size (`int`) — Feed-forward hidden dimension.
31
+ - hidden_act (`str`) — Activation function.
32
+ - hidden_dropout_prob (`float`) — Dropout after projections and FFNs.
33
+ - attention_dropout_prob (`float`) — Dropout applied to attention scores.
34
+ - max_position_embeddings (`int`) — Max sequence length.
35
+ - initializer_range (`float`) — Stddev of weight init.
36
+
37
+ - layer_norm_eps (`float`) — Epsilon for LayerNorm.
38
+ - rms_norm_ps (`float`) — Epsilon for RMSNorm
39
+
40
+ - classifier_dropout (`float` or None) — Dropout for final classifier.
41
+
42
+ - vocab_subspace
43
+ - vocab_rank
44
+
45
+ ----------------------------------
46
+ Multi-Head Latent Attention (MLA):
47
+ ----------------------------------
48
+ - num_attention_heads (`int`) — Number of attention heads.
49
+
50
+ - q_shared_dim (`int`) — Rank of the shared query subspace.
51
+ - kv_shared_dim (`int`) — Rank of the shared key/value subspace.
52
+
53
+ - output_subspace (`bool`) — Whether to use a shared latent subspace for output projections.
54
+ - o_shared_dim (`int`) — Rank of the shared output subspace (required if `output_subspace=True`).
55
+ - qk_private_dim (`int`) — Query/key private dimension per head.
56
+ - vo_private_dim (`int`) — Value/output private dimension per head.
57
+
58
+ - rope_dims (`int`) — Number of head dimensions carrying RoPE.
59
+ - nope_dims (`int`) — Non-positional encoding dimensions.
60
+ - rope_theta (`float`) — Base frequency used for RoPE.
61
+ - rope_scaling (`dict` or None) — HF-style scaling dict for RoPE.
62
+ - attention_bias (`bool`) — Whether to include bias terms in Q/K/V projections.
63
+ - num_dense_layers (`int`) — Number of leading layers that do not use
64
+ subspaces for attention or FFNs.
65
+ - attention_backend (`str`) — Must be one of `"eager"`, `"flash_attention_2"`, or `"sdpa"`.
66
+
67
+ ----------------------
68
+ Decomposed MLP (Low-Rank FFN):
69
+ ----------------------
70
+ - ffn_decompose (`bool`) — Whether to enable low-rank FFNs.
71
+ - ffn_rank (`int`) — Rank of the shared FFN latent space (required if `ffn_decompose=True`).
72
+
73
+ ----------------------
74
+ Validation Behavior:
75
+ ----------------------
76
+ Raises `ValueError` at init time if:
77
+ - FFN decomposition is enabled without specifying `ffn_rank`.
78
+ - An unknown `attention_backend` is provided.
79
+ """
80
+
81
+ model_type = "shared_subspace_decoder"
82
+
83
+ def __init__(
84
+ self,
85
+
86
+ # === Core Model ===
87
+ vocab_size: int = 30522,
88
+ hidden_size: int = 512,
89
+ num_hidden_layers: int = 12,
90
+
91
+ intermediate_size: int = 3072,
92
+
93
+ hidden_dropout_prob=0.1,
94
+ attention_dropout_prob=0.1,
95
+ max_position_embeddings: int = 2048,
96
+ initializer_range=0.02,
97
+ layer_norm_eps=1e-12,
98
+ rms_norm_eps=1e-6, # Their default, but confirm in config.
99
+ norm_type="layernorm", # Choice between "layernorm" and "rmsnorm"
100
+ classifier_dropout=None,
101
+
102
+ vocab_subspace=False,
103
+ vocab_rank=None,
104
+ tie_word_embeddings=True,
105
+
106
+ # === Multi-Head Latent Attention ===
107
+ num_attention_heads: int = 16,
108
+ rope_dims: int = 16,
109
+
110
+ q_shared_dim: int = None,
111
+ kv_shared_dim: int = None,
112
+
113
+ o_shared_dim=None, # If None, no output subspace is used
114
+
115
+ # Private head dimensions
116
+ qk_private_dim: int = None, # Query/key private dimension per head
117
+ vo_private_dim: int = None, # Value/output private dimension per head
118
+ nope_dims: int = None, # Non-positional encoding dimensions
119
+
120
+ attention_backend="eager",
121
+ rope_theta=10000.0,
122
+ rope_scaling=None,
123
+ attention_bias=False,
124
+
125
+ # === MLA Composition ===
126
+ num_dense_layers=12, # dense MHA layers before MLA starts
127
+
128
+ # === Decomposed MLP ===
129
+ ffn_decompose=False,
130
+ ffn_rank=None,
131
+ **kwargs
132
+ ) -> None:
133
+ super().__init__(**kwargs)
134
+
135
+
136
+
137
+ # === Core Model ===
138
+ self.vocab_size = vocab_size
139
+ self.hidden_size = hidden_size
140
+ self.num_hidden_layers = num_hidden_layers
141
+ self.intermediate_size = intermediate_size
142
+ self.hidden_dropout_prob = hidden_dropout_prob
143
+ self.attention_dropout_prob = attention_dropout_prob
144
+ self.max_position_embeddings = max_position_embeddings
145
+ self.initializer_range = initializer_range
146
+ self.layer_norm_eps = layer_norm_eps
147
+ self.rms_norm_eps = rms_norm_eps
148
+ self.norm_type = norm_type
149
+ self.classifier_dropout = classifier_dropout
150
+
151
+ self.vocab_subspace = vocab_subspace
152
+ self.vocab_rank = vocab_rank
153
+ self.tie_word_embeddings = tie_word_embeddings
154
+
155
+ # === MLA ===
156
+ self.num_attention_heads = num_attention_heads
157
+ self.rope_dims = rope_dims
158
+
159
+ self.q_shared_dim = q_shared_dim
160
+ self.kv_shared_dim = kv_shared_dim
161
+ self.o_shared_dim = o_shared_dim
162
+
163
+ # Private head dimensions
164
+ self.qk_private_dim = qk_private_dim
165
+ self.vo_private_dim = vo_private_dim
166
+ self.nope_dims = nope_dims
167
+ self.rope_theta = rope_theta
168
+ self.rope_scaling = rope_scaling
169
+ self.attention_bias = attention_bias
170
+ self.num_dense_layers = num_dense_layers
171
+
172
+ # === Decomposed FFN ===
173
+ self.ffn_decompose = ffn_decompose
174
+ self.ffn_rank = ffn_rank
175
+
176
+ # === Attention backend ===
177
+ self.attention_backend = attention_backend
178
+
179
+ # === Validation ===
180
+ # TODO - Somewhere during training these get instantiated with bad
181
+ # values...
182
+ #self._validate()
183
+
184
+ #print(f" > SubEnc *Config.init: {make_shorthand(self)}\n")
185
+
186
+
187
+ def _validate(self):
188
+ # === Model ===
189
+ if self.num_dense_layers > self.num_hidden_layers:
190
+ raise ValueError("`num_dense_layers` must be <= `num_hidden_layers`")
191
+ if self.vocab_subspace and self.vocab_rank is None:
192
+ raise ValueError("`vocab_rank` must be set when `vocab_subspace=True`")
193
+
194
+ # === MLA Validation ===
195
+ # At least one of q_shared_dim or kv_shared_dim must be set if we have subspace layers
196
+ if self.num_dense_layers < self.num_hidden_layers and self.q_shared_dim is None and self.kv_shared_dim is None:
197
+ raise ValueError("At least one of q_shared_dim or kv_shared_dim must be set when there are subspace layers")
198
+
199
+ # Validate that private dimensions are set
200
+ if self.qk_private_dim is None or self.vo_private_dim is None:
201
+ raise ValueError("Must set qk_private_dim and vo_private_dim")
202
+ if self.nope_dims is None:
203
+ raise ValueError("Must set nope_dims")
204
+
205
+ # === Decomposed FFN ===
206
+ if self.ffn_decompose and self.ffn_rank is None:
207
+ raise ValueError("`ffn_rank` must be set when `ffn_decompose=True`")
208
+ if self.ffn_decompose and self.num_dense_layers >= self.num_hidden_layers:
209
+ raise ValueError("`ffn_decompose` was set but `num_dense` is >= number of layers")
210
+
211
+ # === Attention Backend ===
212
+ valid_backends = ["eager", "flash_attention_2", "sdpa"]
213
+ if self.attention_backend not in valid_backends:
214
+ raise ValueError(f"Unknown attention backend: {self.attention_backend}, options are {valid_backends}")
215
+
216
+ # === Norm Type ===
217
+ valid_norm_types = ["layernorm", "rmsnorm"]
218
+ if self.norm_type not in valid_norm_types:
219
+ raise ValueError(f"Unknown norm type: {self.norm_type}, options are {valid_norm_types}")
220
+
221
+
222
+ import json
223
+
224
+ def get_config(filename):
225
+
226
+ # Load the config file.
227
+ with open(filename) as f:
228
+ full_cfg = json.load(f)
229
+
230
+ # Strict key check on the model configuration.
231
+
232
+ # Get the list of keys allowed / required by `*Config`
233
+ valid_keys = SharedSpaceDecoderConfig.__init__.__code__.co_varnames
234
+ # Remove `self` and `kwargs`
235
+ valid_keys = set(valid_keys) - {"self", "kwargs"}
236
+
237
+ # Compare the set of keys in the json file vs `*Config`
238
+ extra_keys = set(full_cfg["model"]) - valid_keys
239
+ missing_keys = valid_keys - set(full_cfg["model"])
240
+
241
+ # If there any in the `json` that aren't in `*Config`,
242
+ if extra_keys:
243
+ # List them for the user.
244
+ raise ValueError(f"Unknown keys in config: {sorted(extra_keys)}")
245
+
246
+ # If the json config is missing required keys,
247
+ if missing_keys:
248
+ # List them for the user.
249
+ raise ValueError(f"config json is missing: {sorted(missing_keys)}")
250
+
251
+ # Will raise TypeError, by design, if required args are missing
252
+ # The asterisks unpack the dictionary into a list of keywords as though
253
+ # all of the settings were writting out individually.
254
+ model_cfg = SharedSpaceDecoderConfig(**full_cfg["model"])
255
+
256
+ return full_cfg, model_cfg
models/shared_space_decoder.py ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """# shared_subspace_encoder.py"""
4
+
5
+ from typing import Optional
6
+
7
+ import torch
8
+ from torch import nn
9
+
10
+ from transformers.configuration_utils import PretrainedConfig
11
+ from transformers.modeling_utils import PreTrainedModel
12
+ from transformers.modeling_attn_mask_utils import _prepare_4d_attention_mask_for_sdpa
13
+
14
+ from ..layers.mla import MultiheadLatentAttention, RotaryEmbedding
15
+ from ..layers.feedforward import SubspaceFeedForward
16
+ from ..models.shared_space_config import SharedSpaceDecoderConfig
17
+
18
+ """
19
+ RMSNorm
20
+ From: https://huggingface.co/deepseek-ai/DeepSeek-R1/blob/main/modeling_deepseek.py
21
+ """
22
+
23
+ class DeepseekV3RMSNorm(nn.Module):
24
+ def __init__(self, hidden_size, eps=1e-6):
25
+ """
26
+ DeepseekV3RMSNorm is equivalent to T5LayerNorm
27
+ """
28
+ super().__init__()
29
+ self.weight = nn.Parameter(torch.ones(hidden_size))
30
+ self.variance_epsilon = eps
31
+
32
+ def forward(self, hidden_states):
33
+ input_dtype = hidden_states.dtype
34
+ hidden_states = hidden_states.to(torch.float32)
35
+ variance = hidden_states.pow(2).mean(-1, keepdim=True)
36
+ hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
37
+ return self.weight * hidden_states.to(input_dtype)
38
+
39
+ def create_norm_layer(hidden_size: int, config: SharedSpaceDecoderConfig) -> nn.Module:
40
+ """
41
+ Create a normalization layer based on the config norm_type.
42
+
43
+ Args:
44
+ hidden_size: The dimension to normalize over
45
+ config: Configuration containing norm_type and epsilon values
46
+
47
+ Returns:
48
+ Either a LayerNorm or RMSNorm layer
49
+ """
50
+ if config.norm_type == "layernorm":
51
+ return nn.LayerNorm(hidden_size, eps=config.layer_norm_eps)
52
+ elif config.norm_type == "rmsnorm":
53
+ return DeepseekV3RMSNorm(hidden_size, eps=config.rms_norm_eps)
54
+ else:
55
+ # This should be caught by config validation, but being defensive
56
+ raise ValueError(f"Unknown norm_type: {config.norm_type}")
57
+
58
+ """#### *PreTrainedModel"""
59
+
60
+ class SharedSpaceDecoderPreTrainedModel(PreTrainedModel):
61
+ """
62
+ The **PreTrainedModel object:
63
+ - Is instantiated when TODO
64
+ - Initializes:
65
+ - TODO
66
+ - Provides access to TODO
67
+ - Executes TODO
68
+ """
69
+
70
+ config_class = SharedSpaceDecoderConfig
71
+ base_model_prefix = "model"
72
+
73
+ def _init_weights(self, module: nn.Module) -> None:
74
+ """Weight initialization hook used by :class:`PreTrainedModel`.
75
+
76
+ ``PreTrainedModel.post_init`` will recursively apply this function to
77
+ every submodule right after construction. HuggingFace models override
78
+ it so that creating a model from scratch yields the same initialization
79
+ as ``from_pretrained`` when no checkpoint is supplied.
80
+
81
+ This decoder-specific initialization strategy includes:
82
+ - Proper handling of configurable normalization layers (LayerNorm or RMSNorm)
83
+ - Special initialization for language modeling heads
84
+ - Considerations for causal attention and autoregressive modeling
85
+ - Support for both dense and decomposed vocabulary embeddings
86
+ """
87
+
88
+ if isinstance(module, nn.Linear):
89
+ # Standard linear layer initialization
90
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
91
+ if module.bias is not None:
92
+ module.bias.data.zero_()
93
+
94
+ elif isinstance(module, nn.Embedding):
95
+ # Initialize embeddings with normal distribution
96
+ module.weight.data.normal_(mean=0.0, std=self.config.initializer_range)
97
+ if module.padding_idx is not None:
98
+ module.weight.data[module.padding_idx].zero_()
99
+
100
+ elif isinstance(module, DeepseekV3RMSNorm):
101
+ # RMSNorm initialization: weight to 1.0, no bias term
102
+ module.weight.data.fill_(1.0)
103
+
104
+ elif isinstance(module, nn.LayerNorm):
105
+ # LayerNorm initialization: bias to 0, weight to 1.0
106
+ module.bias.data.zero_()
107
+ module.weight.data.fill_(1.0)
108
+
109
+
110
+ class SharedSpaceDecoderLayer(nn.Module):
111
+ """
112
+ The **Layer object:
113
+ - Is instantiated by :class:`SharedSpaceDecoderModel` for each
114
+ Transformer block in the decoder.
115
+ - Initializes:
116
+ - ``self_attn`` – multi-head latent attention implementing either
117
+ dense or latent projections depending on the configuration.
118
+ - ``ffn`` – a :class:`SubspaceFeedForward` block.
119
+ - RMSNorm layers for pre-attention and pre-FFN normalization.
120
+ - Provides access to the attention and feed-forward submodules via the
121
+ attributes ``self_attn`` and ``ffn``.
122
+ - Executes a single decoder block in :meth:`forward`.
123
+ """
124
+
125
+ def __init__(self, config: SharedSpaceDecoderConfig, layer_idx: int) -> None:
126
+
127
+ super().__init__()
128
+
129
+ # Norm applied prior to attention.
130
+ self.attn_input_norm = create_norm_layer(config.hidden_size, config)
131
+
132
+ # Attention block
133
+ self.self_attn = MultiheadLatentAttention(config, layer_idx)
134
+
135
+ # Norm applied prior to FFN
136
+ self.ffn_input_norm = create_norm_layer(config.hidden_size, config)
137
+
138
+ # Feed-forward network used after attention
139
+ self.ffn = SubspaceFeedForward(config, layer_idx)
140
+
141
+ def forward(
142
+ self,
143
+ hidden_states: torch.Tensor,
144
+ position_embeddings: tuple[torch.Tensor, torch.Tensor], # RoPE embeddings
145
+ attention_mask: Optional[torch.Tensor],
146
+ ) -> torch.Tensor:
147
+
148
+ # ========================
149
+ # Self Attention
150
+ # ========================
151
+ residual_strm = hidden_states
152
+
153
+ # Normalize the hidden states to create the input to attention.
154
+ attn_input = self.attn_input_norm(hidden_states)
155
+
156
+ # Evaluate
157
+ attn_output = self.self_attn(
158
+ attn_input,
159
+ position_embeddings,
160
+ attention_mask,
161
+ )
162
+
163
+ # Add the attention output (the residual) back to the non-normalized
164
+ # hidden_states.
165
+ hidden_states = residual_strm + attn_output
166
+
167
+ # ===========================
168
+ # Feed-Forward Network
169
+ # ===========================
170
+ residual_strm = hidden_states
171
+
172
+ # Normalize the updated hidden states prior to the FFN
173
+ ffn_input = self.ffn_input_norm(hidden_states)
174
+
175
+ # Evaluate
176
+ ffn_output = self.ffn(ffn_input)
177
+
178
+ # Add the output the un-normalized hidden states.
179
+ hidden_states = residual_strm + ffn_output
180
+
181
+ return hidden_states
182
+
183
+
184
+ class SharedSpaceDecoderModel(SharedSpaceDecoderPreTrainedModel):
185
+ """
186
+ The **Model object:
187
+ - Initializes:
188
+ - The vocabulary embeddings (and optional decomposition)
189
+ - Position embeddings (calculated in RotaryEmbedding)
190
+ - All of the **Layer objects.
191
+ - Provides interface to vocab embeddings.
192
+ - Executes the whole decoder model in `forward` with causal attention.
193
+
194
+ This is the base decoder without the language modeling head.
195
+ Use SubspaceDecoderForCausalLM for language modeling tasks.
196
+ """
197
+
198
+ def __init__(self, config: SharedSpaceDecoderConfig) -> None:
199
+ super().__init__(config)
200
+
201
+ # ============================
202
+ # Vocabulary Embeddings
203
+ # ============================
204
+ # Decomposing the vocabulary (if enabled) defines a shared projection
205
+ # which constrains the model to store semantic information (and
206
+ # whatever other static token knowledge) into a limited set of
207
+ # feature directions.
208
+
209
+ # If we're decomposing the token embeddings,
210
+ # TODO - Rename to vocab_subspace.
211
+ if config.vocab_subspace:
212
+
213
+ # Create the embedding table. Vocabulary embeddings are learned
214
+ # in a lower dimensional latent space.
215
+ self.vocab_embed = nn.Embedding(
216
+ config.vocab_size, # Number of tokens
217
+ config.vocab_rank # Subspace dimension
218
+ )
219
+
220
+ # Create a
221
+ # Selected token latents will be projected up to model size.
222
+ # vocab_proj has shape [vocab_rank x model_size]
223
+ self.vocab_proj = nn.Linear(
224
+ config.vocab_rank, # Size of latents
225
+ config.hidden_size, # Model size
226
+ bias=False
227
+ )
228
+
229
+ # Otherwise, for a dense vocabulary,
230
+ else:
231
+ # Create the dense embedding table in model space.
232
+ self.vocab_embed = nn.Embedding(
233
+ config.vocab_size, # Number of tokens
234
+ config.hidden_size # Model size
235
+ )
236
+
237
+ self.vocab_proj = None
238
+
239
+ # =====================
240
+ # RoPE Embeddings
241
+ # =====================
242
+
243
+ # Pre-computes the table of RoPE embeddings, leaving them in
244
+ # GPU memory.
245
+ self.rope = RotaryEmbedding(config)
246
+
247
+ # ===================
248
+ # Create Layers
249
+ # ===================
250
+
251
+ layers = []
252
+
253
+ # For each layer,
254
+ for i in range(config.num_hidden_layers):
255
+ # Create a **Layer, providing the config and indicating its number.
256
+ layers.append(
257
+ SharedSpaceDecoderLayer(
258
+ config,
259
+ layer_idx = i
260
+ )
261
+ )
262
+
263
+ # Wrap in torch ModuleList
264
+ self.layers = nn.ModuleList(layers)
265
+
266
+ # Whatever huggingface does behind the scenes...
267
+ self.post_init()
268
+
269
+ # Agents: Do not define boilerplate helpers, e.g., get/set_input_embeddings
270
+
271
+
272
+ def embed(self, input_ids: torch.LongTensor) -> torch.Tensor:
273
+ """
274
+ Return token embeddings for input ids.
275
+ This will perform the up projection to model space if the vocabulary is
276
+ decomposed.
277
+
278
+ input_ids have shape [batch_size, seq_len]
279
+ """
280
+
281
+ # If the vocabulary is decomposed,
282
+ if self.vocab_proj is not None:
283
+
284
+ # Retrieve the latents
285
+ # input_ids: [batch_size, seq_len]
286
+ # x: [batch_size, seq_len, latent_dim]
287
+ x = self.vocab_embed(input_ids)
288
+
289
+ # Project the latents back to model space and return.
290
+ return(self.vocab_proj(x))
291
+
292
+ # If the vocabulary is dense,
293
+ else:
294
+ # Just return the embeddings.
295
+ return self.vocab_embed(input_ids)
296
+
297
+ def forward(
298
+ self,
299
+ input_ids: torch.LongTensor,
300
+ attention_mask: Optional[torch.Tensor] = None,
301
+ **kwargs,
302
+ ) -> torch.Tensor:
303
+ """
304
+ Run the full decoder stack with causal attention.
305
+
306
+ Inputs:
307
+ input_ids [batch_size, seq_len]
308
+ attention_mask [batch_size, seq_len] - 1 for real tokens, 0 for padding
309
+
310
+ Returns:
311
+ Final decoder layer output [batch_size, seq_len, model_size]
312
+ """
313
+
314
+ # Retrieve the token embeddings for this sequence.
315
+ # These are model_size, regardless of whether the vocab is decompd.
316
+ hidden_states = self.embed(input_ids)
317
+
318
+ # Retrieve the rotary position embeddings for all of the positions in
319
+ # our current input sequence.
320
+
321
+ seq_len = hidden_states.size(1)
322
+
323
+ # Retrieves just the ones necessary for the sequence length of the
324
+ # input. These are vectors, two per token. Their length is the
325
+ # number of head dimensions we're applying RoPE to.
326
+ # Input
327
+ # cos: [max_seq_len, rope_dims]
328
+ # sin: [max_seq_len, rope_dims]
329
+ # Outputs:
330
+ # R_cos [seq_len, rope_dims]
331
+ # R_sin [seq_len, rope_dims]
332
+ R_cos = self.rope.cos[:seq_len]
333
+ R_sin = self.rope.sin[:seq_len]
334
+
335
+
336
+ # ===============================
337
+ # Attention Mask Conversion
338
+ # ===============================
339
+
340
+ """
341
+ use_sdpa_attention_masks = (
342
+ self.attn_implementation == "sdpa"
343
+ and self.position_embedding_type == "absolute"
344
+ and head_mask is None
345
+ and not output_attentions
346
+ )
347
+ """
348
+
349
+ # Expand the attention mask
350
+ #if use_sdpa_attention_masks and attention_mask.dim() == 2:
351
+ if True:
352
+ # Expand the attention mask for SDPA.
353
+ # [bsz, seq_len] -> [bsz, 1, seq_len, seq_len]
354
+ extended_attention_mask = _prepare_4d_attention_mask_for_sdpa(
355
+ attention_mask,
356
+ hidden_states.dtype,
357
+ tgt_len = seq_len
358
+ )
359
+ attention_mask = extended_attention_mask
360
+
361
+
362
+ # Run the model!
363
+
364
+ # For each decoder layer,
365
+ for layer_i, layer in enumerate(self.layers):
366
+
367
+ # Evaluate the layer
368
+ hidden_states = layer(
369
+ hidden_states, # Token embeddings
370
+ (R_cos, R_sin), # Rope embeddings, passed as a tuple.
371
+ attention_mask, # Attn mask
372
+ )
373
+
374
+ # Return the final output of the decoder stack.
375
+ return hidden_states
376
+