stefaniancu commited on
Commit
2250a44
·
verified ·
1 Parent(s): a4ea4d3

RoST-1B-Instruct-v2: frozen SFT-v2 arm C, step 365

Browse files

Exported from checkpoint model_000365.pt (sha256 397dcd5264e2e0dafadd20da3726d8105cafb0becf43ced607d1cd721b0df121), verified in fp32 against the source checkpoint with a maximum logit difference of 0.0, then cast to bfloat16. Bitwise identical on all 175 tensors to the export that produced the published benchmark numbers.

config.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vocab_size": 32768,
3
+ "n_layer": 24,
4
+ "n_head": 12,
5
+ "n_kv_head": 12,
6
+ "n_embd": 1536,
7
+ "sequence_len": 4096,
8
+ "rope_base": 100000,
9
+ "window_pattern": "SSSL",
10
+ "model_type": "rost",
11
+ "architectures": [
12
+ "RostForCausalLM"
13
+ ],
14
+ "auto_map": {
15
+ "AutoConfig": "configuration_rost.RostConfig",
16
+ "AutoModelForCausalLM": "modeling_rost.RostForCausalLM"
17
+ },
18
+ "torch_dtype": "bfloat16"
19
+ }
configuration_rost.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HuggingFace config for RoST.
2
+
3
+ Ships inside the published model repository and runs on the downloader's
4
+ machine, so it must not import anything from `nanochat`.
5
+
6
+ Field names mirror `nanochat.gpt.GPTConfig` exactly rather than being renamed
7
+ to Llama's vocabulary. A rename would need a mapping table that nothing checks,
8
+ and a silently wrong mapping produces a model that loads and computes the wrong
9
+ thing -- the one failure mode this whole export has to avoid.
10
+ """
11
+ from transformers.configuration_utils import PretrainedConfig
12
+
13
+
14
+ class RostConfig(PretrainedConfig):
15
+ model_type = "rost"
16
+ keys_to_ignore_at_inference = ["past_key_values"]
17
+
18
+ def __init__(
19
+ self,
20
+ vocab_size=32768,
21
+ n_layer=24,
22
+ n_head=12,
23
+ n_kv_head=12,
24
+ n_embd=1536,
25
+ sequence_len=4096,
26
+ rope_base=100000,
27
+ window_pattern="SSSL",
28
+ pad_vocab_size_to=64,
29
+ logit_softcap=15.0,
30
+ attention_scale=1.2,
31
+ ve_gate_channels=12,
32
+ smear_gate_channels=24,
33
+ bos_token_id=None,
34
+ eos_token_id=None,
35
+ **kwargs,
36
+ ):
37
+ self.vocab_size = vocab_size
38
+ self.n_layer = n_layer
39
+ self.n_head = n_head
40
+ self.n_kv_head = n_kv_head
41
+ self.n_embd = n_embd
42
+ self.sequence_len = sequence_len
43
+ self.rope_base = rope_base
44
+ self.window_pattern = window_pattern
45
+ self.pad_vocab_size_to = pad_vocab_size_to
46
+ # Constants in the training code, carried as config so a checkpoint
47
+ # trained under different ones cannot be served under these.
48
+ self.logit_softcap = logit_softcap
49
+ self.attention_scale = attention_scale
50
+ self.ve_gate_channels = ve_gate_channels
51
+ self.smear_gate_channels = smear_gate_channels
52
+ super().__init__(bos_token_id=bos_token_id, eos_token_id=eos_token_id, **kwargs)
53
+
54
+ @property
55
+ def padded_vocab_size(self):
56
+ pad = self.pad_vocab_size_to
57
+ return ((self.vocab_size + pad - 1) // pad) * pad
58
+
59
+ @property
60
+ def head_dim(self):
61
+ return self.n_embd // self.n_head
62
+
63
+ # Aliases so generic HuggingFace code (generation, device maps, pipelines)
64
+ # finds what it expects without the weights being renamed.
65
+ @property
66
+ def hidden_size(self):
67
+ return self.n_embd
68
+
69
+ @property
70
+ def num_attention_heads(self):
71
+ return self.n_head
72
+
73
+ @property
74
+ def num_key_value_heads(self):
75
+ return self.n_kv_head
76
+
77
+ @property
78
+ def num_hidden_layers(self):
79
+ return self.n_layer
80
+
81
+ @property
82
+ def max_position_embeddings(self):
83
+ return self.sequence_len
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:662bbaff33d1b1b27a67e50aa25cc1d2ba88d4c39f0a91b4035b44b7adc615d0
3
+ size 2768263260
modeling_rost.py ADDED
@@ -0,0 +1,325 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HuggingFace modelling code for RoST.
2
+
3
+ Ships inside the published model repository and runs on the downloader's
4
+ machine, so it imports nothing from `nanochat` and uses no FlashAttention-3.
5
+
6
+ This is a transcription of `nanochat/gpt.py`, not a reimplementation. Parameter
7
+ names, the order of operations and every constant are kept identical, because
8
+ the only thing that makes an export trustworthy is that it computes the same
9
+ function -- `tests/test_hf_export.py` asserts that against the source model.
10
+
11
+ RoST is not a Llama variant. It carries nine components with no equivalent in
12
+ standard architectures: smear, per-layer resid/x0 lambdas, gated value
13
+ embeddings on alternating layers, backout, QK-norm with double 1.2 scaling,
14
+ relu-squared MLP, parameter-free RMSNorm, logit softcap and a tiled sliding
15
+ window. Each is transcribed below with the reason it exists.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import torch
20
+ import torch.nn as nn
21
+ import torch.nn.functional as F
22
+ from transformers.cache_utils import DynamicCache
23
+ from transformers.generation.utils import GenerationMixin
24
+ from transformers.modeling_outputs import CausalLMOutputWithPast
25
+ from transformers.modeling_utils import PreTrainedModel
26
+
27
+ from .configuration_rost import RostConfig
28
+
29
+
30
+ def norm(x):
31
+ """RMSNorm with NO learnable scale. RoST has no norm parameters at all."""
32
+ return F.rms_norm(x, (x.size(-1),))
33
+
34
+
35
+ def has_ve(layer_idx, n_layer):
36
+ """Value embeddings sit on alternating layers, last layer always included."""
37
+ return layer_idx % 2 == (n_layer - 1) % 2
38
+
39
+
40
+ def apply_rotary_emb(x, cos, sin):
41
+ # Rotates by -theta, the transpose of the textbook convention. Only the
42
+ # relative q/k rotation matters so it is functionally equivalent, but it
43
+ # must be transcribed as-is or the loaded weights mean something else.
44
+ d = x.shape[3] // 2
45
+ x1, x2 = x[..., :d], x[..., d:]
46
+ y1 = x1 * cos + x2 * sin
47
+ y2 = x1 * (-sin) + x2 * cos
48
+ return torch.cat([y1, y2], 3)
49
+
50
+
51
+ def compute_window_sizes(config):
52
+ """Per-layer left-attention span, tiled from `window_pattern`.
53
+
54
+ S is a quarter of the context rounded up to 128; L is the full context. The
55
+ final layer is always L. Mirrors `GPT._compute_window_sizes`.
56
+ """
57
+ pattern = config.window_pattern.upper()
58
+ long_window = config.sequence_len
59
+ short_window = -(-long_window // 4 // 128) * 128
60
+ sizes = [long_window if pattern[i % len(pattern)] == "L" else short_window
61
+ for i in range(config.n_layer)]
62
+ sizes[-1] = long_window
63
+ return sizes
64
+
65
+
66
+ class RostAttention(nn.Module):
67
+ def __init__(self, config, layer_idx):
68
+ super().__init__()
69
+ self.layer_idx = layer_idx
70
+ self.n_head = config.n_head
71
+ self.n_kv_head = config.n_kv_head
72
+ self.head_dim = config.head_dim
73
+ self.attention_scale = config.attention_scale
74
+ self.c_q = nn.Linear(config.n_embd, self.n_head * self.head_dim, bias=False)
75
+ self.c_k = nn.Linear(config.n_embd, self.n_kv_head * self.head_dim, bias=False)
76
+ self.c_v = nn.Linear(config.n_embd, self.n_kv_head * self.head_dim, bias=False)
77
+ self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=False)
78
+ self.ve_gate_channels = config.ve_gate_channels
79
+ self.ve_gate = (nn.Linear(self.ve_gate_channels, self.n_kv_head, bias=False)
80
+ if has_ve(layer_idx, config.n_layer) else None)
81
+
82
+ def forward(self, x, ve, cos, sin, attn_mask, cache, layer_idx):
83
+ B, T, _ = x.size()
84
+ q = self.c_q(x).view(B, T, self.n_head, self.head_dim)
85
+ k = self.c_k(x).view(B, T, self.n_kv_head, self.head_dim)
86
+ v = self.c_v(x).view(B, T, self.n_kv_head, self.head_dim)
87
+
88
+ # Value residual (ResFormer): a per-token, per-kv-head gate in (0, 3)
89
+ # mixes a learned per-layer value embedding into v.
90
+ if ve is not None:
91
+ ve = ve.view(B, T, self.n_kv_head, self.head_dim)
92
+ gate = 3 * torch.sigmoid(self.ve_gate(x[..., :self.ve_gate_channels]))
93
+ v = v + gate.unsqueeze(-1) * ve
94
+
95
+ q, k = apply_rotary_emb(q, cos, sin), apply_rotary_emb(k, cos, sin)
96
+ q, k = norm(q), norm(k) # QK norm
97
+ # Sharper attention: the 1.2 is applied to BOTH q and k, so the effective
98
+ # logit scale is 1.44x the usual 1/sqrt(head_dim).
99
+ q = q * self.attention_scale
100
+ k = k * self.attention_scale
101
+
102
+ # (B, T, H, D) -> (B, H, T, D) for SDPA
103
+ q = q.transpose(1, 2)
104
+ k = k.transpose(1, 2)
105
+ v = v.transpose(1, 2)
106
+
107
+ # Append through the cache's own API rather than concatenating tensors
108
+ # by hand: `generate()` owns the cache object and expects to be the one
109
+ # tracking its length.
110
+ if cache is not None:
111
+ k, v = cache.update(k, v, layer_idx)
112
+
113
+ if self.n_kv_head != self.n_head:
114
+ repeat = self.n_head // self.n_kv_head
115
+ k = k.repeat_interleave(repeat, dim=1)
116
+ v = v.repeat_interleave(repeat, dim=1)
117
+
118
+ y = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
119
+ y = y.transpose(1, 2).contiguous().view(B, T, -1)
120
+ return self.c_proj(y)
121
+
122
+
123
+ class RostMLP(nn.Module):
124
+ """relu-squared at 4x expansion, not SwiGLU at 8/3x."""
125
+
126
+ def __init__(self, config):
127
+ super().__init__()
128
+ self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd, bias=False)
129
+ self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd, bias=False)
130
+
131
+ def forward(self, x):
132
+ return self.c_proj(F.relu(self.c_fc(x)).square())
133
+
134
+
135
+ class RostBlock(nn.Module):
136
+ def __init__(self, config, layer_idx):
137
+ super().__init__()
138
+ self.attn = RostAttention(config, layer_idx)
139
+ self.mlp = RostMLP(config)
140
+
141
+ def forward(self, x, ve, cos, sin, attn_mask, cache, layer_idx):
142
+ x = x + self.attn(norm(x), ve, cos, sin, attn_mask, cache, layer_idx)
143
+ x = x + self.mlp(norm(x))
144
+ return x
145
+
146
+
147
+ class RostCache(DynamicCache):
148
+ """A KV cache that also carries smear's previous-token embedding.
149
+
150
+ Smear mixes the previous token's embedding into the current one. During
151
+ incremental decoding that embedding is not in `input_ids`, and it is not a
152
+ key or a value, so there is nowhere in the standard cache to put it. It
153
+ rides along as an attribute here.
154
+
155
+ `generate()` builds its own `DynamicCache` rather than this subclass, so the
156
+ forward pass reads the attribute defensively with `getattr` and sets it on
157
+ whatever cache object it was handed. That works because a plain
158
+ `DynamicCache` accepts attribute assignment -- and it must keep working,
159
+ because the alternative failure is silent: without the previous embedding
160
+ every decoded token is smeared against nothing.
161
+ """
162
+
163
+ prev_embedding = None
164
+
165
+
166
+ class RostPreTrainedModel(PreTrainedModel):
167
+ config_class = RostConfig
168
+ base_model_prefix = "transformer"
169
+ supports_gradient_checkpointing = False
170
+ _no_split_modules = ["RostBlock"]
171
+
172
+
173
+ class RostForCausalLM(RostPreTrainedModel, GenerationMixin):
174
+ # GenerationMixin after PreTrainedModel, or `generate` is unavailable from
175
+ # transformers 4.50 onward.
176
+ def __init__(self, config):
177
+ super().__init__(config)
178
+ padded = config.padded_vocab_size
179
+ self.transformer = nn.ModuleDict({
180
+ "wte": nn.Embedding(padded, config.n_embd),
181
+ "h": nn.ModuleList([RostBlock(config, i) for i in range(config.n_layer)]),
182
+ })
183
+ self.lm_head = nn.Linear(config.n_embd, padded, bias=False)
184
+ # Per-layer scalars from modded-nanogpt: resid_lambdas rescales the
185
+ # residual stream, x0_lambdas blends the initial embedding back in.
186
+ self.resid_lambdas = nn.Parameter(torch.ones(config.n_layer))
187
+ self.x0_lambdas = nn.Parameter(torch.zeros(config.n_layer))
188
+ # Smear: mixes the previous token's embedding into the current one.
189
+ self.smear_gate = nn.Linear(config.smear_gate_channels, 1, bias=False)
190
+ self.smear_lambda = nn.Parameter(torch.zeros(1))
191
+ # Backout: removes the mid-layer residual before the logit projection.
192
+ self.backout_lambda = nn.Parameter(0.2 * torch.ones(1))
193
+ kv_dim = config.n_kv_head * config.head_dim
194
+ self.value_embeds = nn.ModuleDict({
195
+ str(i): nn.Embedding(padded, kv_dim)
196
+ for i in range(config.n_layer) if has_ve(i, config.n_layer)})
197
+
198
+ self.window_sizes = compute_window_sizes(config)
199
+ # Rotary tables are built on first use, not in __init__.
200
+ #
201
+ # They are derived from config, so they are absent from the checkpoint.
202
+ # `from_pretrained` initializes on the meta device and materializes only
203
+ # tensors the checkpoint supplies, so buffers registered here would stay
204
+ # meta and the model would return NaN -- silently, and only after a
205
+ # round trip through disk, which is exactly how a published model breaks
206
+ # while every in-memory test passes.
207
+ self._rotary_cache = None
208
+ self.post_init()
209
+
210
+ def _rotary(self, device, dtype, length):
211
+ cached = self._rotary_cache
212
+ if (cached is not None and cached[0].device == device
213
+ and cached[0].dtype == dtype and cached[0].size(1) >= length):
214
+ return cached
215
+ head_dim = self.config.head_dim
216
+ # Table length mirrors nanochat's 10x over-compute, so a sequence longer
217
+ # than the trained context still has rotations available rather than
218
+ # tripping an index error at serving time.
219
+ size = max(length, self.config.sequence_len * 10)
220
+ channel_range = torch.arange(0, head_dim, 2, dtype=torch.float32, device=device)
221
+ inv_freq = 1.0 / (self.config.rope_base ** (channel_range / head_dim))
222
+ t = torch.arange(size, dtype=torch.float32, device=device)
223
+ freqs = torch.outer(t, inv_freq)
224
+ cos = freqs.cos()[None, :, None, :].to(dtype)
225
+ sin = freqs.sin()[None, :, None, :].to(dtype)
226
+ self._rotary_cache = (cos, sin)
227
+ return self._rotary_cache
228
+
229
+ def get_input_embeddings(self):
230
+ return self.transformer["wte"]
231
+
232
+ def set_input_embeddings(self, value):
233
+ self.transformer["wte"] = value
234
+
235
+ def get_output_embeddings(self):
236
+ return self.lm_head
237
+
238
+ def _window_mask(self, window, q_len, kv_len, offset, device):
239
+ """Causal mask restricted to a left-window, matching FA3's semantics.
240
+
241
+ FA3's `window_size=(left, 0)` attends to keys in `[i - left, i]`
242
+ inclusive. A mask that dropped the `i - left` position, or that used the
243
+ window as a count rather than a span, would change what 18 of 24 layers
244
+ can see -- quietly, and only on long inputs.
245
+ """
246
+ q_pos = torch.arange(offset, offset + q_len, device=device).unsqueeze(1)
247
+ k_pos = torch.arange(kv_len, device=device).unsqueeze(0)
248
+ allowed = (k_pos <= q_pos) & (k_pos >= q_pos - window)
249
+ return allowed.unsqueeze(0).unsqueeze(0)
250
+
251
+ def forward(self, input_ids, attention_mask=None, past_key_values=None,
252
+ use_cache=None, labels=None, return_dict=True, **kwargs):
253
+ B, T = input_ids.size()
254
+ device = input_ids.device
255
+ use_cache = True if use_cache is None else use_cache
256
+ if use_cache and past_key_values is None:
257
+ past_key_values = RostCache()
258
+
259
+ # Position of this chunk in the sequence. Read from the cache rather
260
+ # than tracked separately: `generate()` supplies its own cache object,
261
+ # and a private counter would silently desynchronise from it.
262
+ offset = past_key_values.get_seq_length() if past_key_values is not None else 0
263
+
264
+ x = self.transformer["wte"](input_ids)
265
+ cos_table, sin_table = self._rotary(device, x.dtype, offset + T)
266
+ cos, sin = cos_table[:, offset:offset + T], sin_table[:, offset:offset + T]
267
+ x = norm(x)
268
+
269
+ # Smear. During incremental decoding the previous token's embedding is
270
+ # not in `input_ids`, so it is carried in the cache. HuggingFace's cache
271
+ # API has no slot for non-KV state, which is why the cache here is a
272
+ # plain dict rather than a `Cache` subclass.
273
+ prev = getattr(past_key_values, "prev_embedding", None)
274
+ gate_channels = self.config.smear_gate_channels
275
+ # Stored BEFORE smear is applied, matching nanochat, where
276
+ # `kv_cache.prev_embedding = x[:, -1:, :]` is assigned on the post-norm
277
+ # pre-smear activation.
278
+ new_prev = x[:, -1:, :]
279
+ if T > 1:
280
+ # Position 0 is left unsmeared even when a previous embedding
281
+ # exists. nanochat's prefill branch does the same; carrying `prev`
282
+ # in here would make a two-call prefill differ from a one-call one.
283
+ gate = self.smear_lambda.to(x.dtype) * torch.sigmoid(
284
+ self.smear_gate(x[:, 1:, :gate_channels]))
285
+ x = torch.cat([x[:, :1], x[:, 1:] + gate * x[:, :-1]], dim=1)
286
+ elif prev is not None:
287
+ gate = self.smear_lambda.to(x.dtype) * torch.sigmoid(
288
+ self.smear_gate(x[:, :, :gate_channels]))
289
+ x = x + gate * prev
290
+
291
+ x0 = x
292
+ n_layer = self.config.n_layer
293
+ backout_layer = n_layer // 2
294
+ x_backout = None
295
+ for i, block in enumerate(self.transformer["h"]):
296
+ x = self.resid_lambdas[i] * x + self.x0_lambdas[i] * x0
297
+ ve = (self.value_embeds[str(i)](input_ids).to(x.dtype)
298
+ if str(i) in self.value_embeds else None)
299
+ mask = self._window_mask(self.window_sizes[i], T, offset + T, offset, device)
300
+ x = block(x, ve, cos, sin, mask, past_key_values, i)
301
+ if i == backout_layer:
302
+ x_backout = x
303
+ if x_backout is not None:
304
+ x = x - self.backout_lambda.to(x.dtype) * x_backout
305
+ x = norm(x)
306
+
307
+ logits = self.lm_head(x)[..., :self.config.vocab_size].float()
308
+ softcap = self.config.logit_softcap
309
+ logits = softcap * torch.tanh(logits / softcap)
310
+
311
+ loss = None
312
+ if labels is not None:
313
+ loss = F.cross_entropy(logits[:, :-1].reshape(-1, logits.size(-1)),
314
+ labels[:, 1:].reshape(-1), ignore_index=-1)
315
+
316
+ if past_key_values is not None:
317
+ past_key_values.prev_embedding = new_prev
318
+ return CausalLMOutputWithPast(loss=loss, logits=logits,
319
+ past_key_values=past_key_values if use_cache else None)
320
+
321
+ def prepare_inputs_for_generation(self, input_ids, past_key_values=None, **kwargs):
322
+ # Feed only the new tokens once the cache holds the prefix.
323
+ if past_key_values is not None and past_key_values.get_seq_length() > 0:
324
+ input_ids = input_ids[:, past_key_values.get_seq_length():]
325
+ return {"input_ids": input_ids, "past_key_values": past_key_values, "use_cache": True}
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "tokenizer_class": "PreTrainedTokenizerFast",
3
+ "bos_token": "<|bos|>",
4
+ "eos_token": "<|assistant_end|>",
5
+ "model_max_length": 4096,
6
+ "clean_up_tokenization_spaces": false,
7
+ "chat_template": "{{- bos_token -}}{%- set ns = namespace(system='') -%}{%- for message in messages -%}{%- if message['role'] == 'system' -%}{%- set ns.system = message['content'] -%}{%- elif message['role'] == 'user' -%}{{- '<|user_start|>' -}}{%- if ns.system -%}{{- ns.system + '\n\n' -}}{%- set ns.system = '' -%}{%- endif -%}{{- message['content'] + '<|user_end|>' -}}{%- elif message['role'] == 'assistant' -%}{{- '<|assistant_start|>' + message['content'] + '<|assistant_end|>' -}}{%- endif -%}{%- endfor -%}{%- if add_generation_prompt -%}{{- '<|assistant_start|>' -}}{%- endif -%}"
8
+ }