File size: 14,671 Bytes
f0975ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
"""Trainable PyTorch port of NVIDIA Dory1.0 (looped normalized-GPT).

Mirrors vLLM's inference implementation (vllm-project/vllm#60149,
vllm/model_executor/models/dory.py) op for op, replacing its paged attention and
tensor-parallel layers with plain modules so the model can be fine-tuned:

  * full-sequence causal attention with per-layer sliding windows; each recurrent
    loop attends over that loop's own keys/values, which is exactly vLLM's
    "per_loop" KV-cache mode
  * loop count selectable per forward pass (n_loops), for elastic-loop training
  * per-layer gradient checkpointing and a chunked cross-entropy over the 131k vocab
  * packed sequences (doc_ids + per-document position_ids); attention via dense-mask
    SDPA (any device, used for checks) or FlexAttention block masks (CUDA training)

Parameter names match the released checkpoint, so state dicts load and save as-is.
"""

import json
import math
from dataclasses import dataclass, fields
from pathlib import Path

import torch
import torch.nn.functional as F
from safetensors.torch import load_file
from torch import nn
from torch.utils.checkpoint import checkpoint

EPS = 1e-6


@dataclass
class DoryConfig:
    vocab_size: int = 131072
    hidden_size: int = 2560
    num_hidden_layers: int = 72
    intermediate_size: int = 7168
    num_attention_heads: int = 16
    num_key_value_heads: int = 4
    head_dim: int = 256
    initializer_range: float = 0.0177
    sliding_window: int = 1024
    rope_parameters: dict = None
    layer_types: list = None
    ngpt_alpha_init: float = 0.05
    attention_output_gate_logit_scale_init: float = None
    n_input_layers: int = 16
    n_recurrent_layers: int = 40
    n_output_layers: int = 16
    n_recurrent_loops: int = 2

    @classmethod
    def from_json(cls, path):
        raw = json.loads(Path(path).read_text())
        known = {f.name for f in fields(cls)}
        cfg = cls(**{k: v for k, v in raw.items() if k in known})
        assert cfg.n_input_layers + cfg.n_recurrent_layers + cfg.n_output_layers == cfg.num_hidden_layers
        assert not raw.get("tie_word_embeddings") and raw.get("hidden_act", "silu") == "silu"
        assert not raw.get("attention_bias") and not raw.get("mlp_bias")
        return cfg


def justnorm(x, dim=-1):
    """nGPT normalization: x * rsqrt(sum(x^2) + eps), fp32 accumulation."""
    xf = x.float()
    return (xf * torch.rsqrt(xf.pow(2).sum(dim=dim, keepdim=True) + EPS)).to(x.dtype)


def slerp_alpha(a, b, alpha, alpha_scale):
    ya, yb = justnorm(a).float(), justnorm(b).float()
    lr = (alpha.float() * alpha_scale).abs()
    return justnorm(ya + lr * (yb - ya)).to(a.dtype)


def rope_cos_sin(positions, head_dim, theta, device):
    inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device, dtype=torch.float32) / head_dim))
    freqs = positions.to(device=device, dtype=torch.float32)[..., None] * inv_freq
    return freqs.cos(), freqs.sin()  # [B, T, D/2]


def apply_rope_neox(x, cos, sin):
    """x: [B, T, H, D]; cos/sin: [B, T, D/2]. NeoX style (rotate halves)."""
    x1, x2 = x.float().chunk(2, dim=-1)
    c, s = cos[:, :, None, :], sin[:, :, None, :]
    return torch.cat([x1 * c - x2 * s, x2 * c + x1 * s], dim=-1).to(x.dtype)


def causal_mask(t, window, device, doc_ids=None):
    """Bool mask, True = attend: [T, T], or [B, 1, T, T] with doc_ids (packed sequences).
    Window w keeps keys with 0 <= i - j < w."""
    i = torch.arange(t, device=device)[:, None]
    j = torch.arange(t, device=device)[None, :]
    allowed = j <= i
    if window is not None:
        allowed &= (i - j) < window
    if doc_ids is not None:
        allowed = allowed[None] & (doc_ids[:, :, None] == doc_ids[:, None, :])
        allowed = allowed[:, None]
    return allowed


_flex = None


def flex_block_mask(t, window, device, doc_ids):
    from torch.nn.attention.flex_attention import create_block_mask

    def mask_mod(b, h, q, kv):
        ok = (kv <= q) & (doc_ids[b, q] == doc_ids[b, kv])
        if window is not None:
            ok = ok & ((q - kv) < window)
        return ok

    return create_block_mask(mask_mod, doc_ids.shape[0], None, t, t, device=device)


def flex_attention(q, k, v, block_mask, scale):
    global _flex
    if _flex is None:
        from torch.nn.attention.flex_attention import flex_attention as fa

        _flex = torch.compile(fa, dynamic=False)
    return _flex(q, k, v, block_mask=block_mask, scale=scale, enable_gqa=True)


class DoryAttention(nn.Module):
    """GQA attention with nGPT qk-norm (after RoPE), sigmoid output gate and justnorm."""

    def __init__(self, cfg: DoryConfig, layer_type: str):
        super().__init__()
        h, kv, d = cfg.num_attention_heads, cfg.num_key_value_heads, cfg.head_dim
        self.num_heads, self.num_kv_heads, self.head_dim = h, kv, d
        self.q_proj = nn.Linear(cfg.hidden_size, h * d, bias=False)
        self.k_proj = nn.Linear(cfg.hidden_size, kv * d, bias=False)
        self.v_proj = nn.Linear(cfg.hidden_size, kv * d, bias=False)
        self.g_proj = nn.Linear(cfg.hidden_size, h * d, bias=False)
        self.o_proj = nn.Linear(h * d, cfg.hidden_size, bias=False)
        self.sqk = nn.Parameter(torch.empty(h * d))
        self.s_gate = nn.Parameter(torch.empty(h * d))
        std = cfg.initializer_range
        self.sqk_factor = d**0.5 / std
        g0 = cfg.attention_output_gate_logit_scale_init or cfg.hidden_size**0.5
        self.s_gate_factor = g0**0.5 / std
        self.layer_type = layer_type

    def forward(self, x, cos, sin, mask):
        b, t, _ = x.shape
        h, kv, d = self.num_heads, self.num_kv_heads, self.head_dim
        q = self.q_proj(x).view(b, t, h, d)
        k = self.k_proj(x).view(b, t, kv, d)
        v = self.v_proj(x).view(b, t, kv, d)
        gate = self.g_proj(x)

        q, k = apply_rope_neox(q, cos, sin), apply_rope_neox(k, cos, sin)
        sqk = (self.sqk.float() * self.sqk_factor).view(h, d)
        q = (sqk.square() * justnorm(q).float()).to(q.dtype)
        k = justnorm(k)

        q, k, v = (z.transpose(1, 2) for z in (q, k, v))  # [B, H, T, D]
        if isinstance(mask, torch.Tensor):
            if kv != h:
                k = k.repeat_interleave(h // kv, dim=1)
                v = v.repeat_interleave(h // kv, dim=1)
            out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask, scale=1.0 / math.sqrt(d))
        else:  # FlexAttention BlockMask
            out = flex_attention(q, k, v, mask, 1.0 / math.sqrt(d))
        out = out.transpose(1, 2).reshape(b, t, h * d)

        sg = (self.s_gate.float() * self.s_gate_factor).square()
        out = (out.float() * torch.sigmoid(gate.float() * sg)).to(out.dtype)
        return self.o_proj(justnorm(out))


class DoryMLP(nn.Module):
    """SwiGLU with nGPT suv scaling and justnorm before down_proj."""

    def __init__(self, cfg: DoryConfig):
        super().__init__()
        self.gate_proj = nn.Linear(cfg.hidden_size, cfg.intermediate_size, bias=False)
        self.up_proj = nn.Linear(cfg.hidden_size, cfg.intermediate_size, bias=False)
        self.down_proj = nn.Linear(cfg.intermediate_size, cfg.hidden_size, bias=False)
        self.suv_gate = nn.Parameter(torch.empty(cfg.intermediate_size))
        self.suv_up = nn.Parameter(torch.empty(cfg.intermediate_size))
        self.suv_factor = cfg.hidden_size**0.5

    def forward(self, x):
        g = (self.gate_proj(x).float() * (self.suv_gate.float() * self.suv_factor)).to(x.dtype)
        u = (self.up_proj(x).float() * (self.suv_up.float() * self.suv_factor)).to(x.dtype)
        m = (F.silu(g.float()) * u.float()).to(x.dtype)
        return self.down_proj(justnorm(m))


class DoryLayer(nn.Module):
    """One physical layer: a single sublayer (attention OR mlp) plus slerp residual."""

    def __init__(self, cfg: DoryConfig, idx: int):
        super().__init__()
        self.layer_type = cfg.layer_types[idx]
        self.is_attention = self.layer_type != "mlp"
        self.alpha_scale = cfg.ngpt_alpha_init / cfg.initializer_range
        if self.is_attention:
            self.mixer = DoryAttention(cfg, self.layer_type)
            self.attn_alpha = nn.Parameter(torch.empty(cfg.hidden_size))
        else:
            self.mixer = DoryMLP(cfg)
            self.mlp_alpha = nn.Parameter(torch.empty(cfg.hidden_size))

    def forward(self, h, ctx):
        if self.is_attention:
            cos, sin = ctx["rope"][self.layer_type]
            out = self.mixer(h, cos, sin, ctx["mask"][self.layer_type])
            return slerp_alpha(h, out, self.attn_alpha, self.alpha_scale)
        return slerp_alpha(h, self.mixer(h), self.mlp_alpha, self.alpha_scale)


class DoryBackbone(nn.Module):
    def __init__(self, cfg: DoryConfig):
        super().__init__()
        self.cfg = cfg
        self.embeddings = nn.Embedding(cfg.vocab_size, cfg.hidden_size)
        self.layers = nn.ModuleList(DoryLayer(cfg, i) for i in range(cfg.num_hidden_layers))
        self.gradient_checkpointing = False

    def _run(self, layer, h, ctx):
        if self.gradient_checkpointing and self.training and torch.is_grad_enabled():
            return checkpoint(layer, h, ctx, use_reentrant=False)
        return layer(h, ctx)

    def forward(self, input_ids, position_ids=None, n_loops=None, doc_ids=None, attn_impl="sdpa"):
        cfg = self.cfg
        b, t = input_ids.shape
        dev = input_ids.device
        if position_ids is None:
            position_ids = torch.arange(t, device=dev).expand(b, t)
        if attn_impl == "flex" and doc_ids is None:
            doc_ids = torch.zeros(b, t, dtype=torch.int32, device=dev)
        ctx = {"rope": {}, "mask": {}}
        for lt in set(cfg.layer_types) - {"mlp"}:
            ctx["rope"][lt] = rope_cos_sin(position_ids, cfg.head_dim, cfg.rope_parameters[lt]["rope_theta"], dev)
            window = cfg.sliding_window if lt == "sliding_attention" else None
            if attn_impl == "flex":
                ctx["mask"][lt] = flex_block_mask(t, window, dev, doc_ids)
            else:
                ctx["mask"][lt] = causal_mask(t, window, dev, doc_ids)

        n_in, n_rec = cfg.n_input_layers, cfg.n_recurrent_layers
        h = self.embeddings(input_ids)
        for layer in self.layers[:n_in]:
            h = self._run(layer, h, ctx)
        input_h = h
        state = input_h
        for _ in range(n_loops or cfg.n_recurrent_loops):
            h = justnorm(state + input_h)
            for layer in self.layers[n_in : n_in + n_rec]:
                h = self._run(layer, h, ctx)
            state = justnorm(h)
        h = state
        for layer in self.layers[n_in + n_rec :]:
            h = self._run(layer, h, ctx)
        return h


class DoryForCausalLM(nn.Module):
    def __init__(self, cfg: DoryConfig):
        super().__init__()
        self.config = cfg
        self.backbone = DoryBackbone(cfg)
        self.lm_head = nn.Linear(cfg.hidden_size, cfg.vocab_size, bias=False)
        self.lm_head.logit_scale = nn.Parameter(torch.empty(cfg.vocab_size))

    @classmethod
    def from_pretrained(cls, path, dtype=torch.bfloat16, device="cpu"):
        path = Path(path)
        cfg = DoryConfig.from_json(path / "config.json")
        with torch.device("meta"):
            model = cls(cfg)
        state = {}
        for shard in sorted(path.glob("*.safetensors")):
            state.update(load_file(shard))
        state = {k: v.to(dtype) for k, v in state.items()}
        missing, unexpected = model.load_state_dict(state, strict=True, assign=True)
        assert not missing and not unexpected, (missing, unexpected)
        return model.to(device)

    def gradient_checkpointing_enable(self):
        self.backbone.gradient_checkpointing = True

    @torch.no_grad()
    def normalize_matrices_(self):
        """Project weights back onto the unit hypersphere after an optimizer step (nGPT).

        The released checkpoint has every matrix unit-norm along the hidden dimension:
        rows of embeddings/lm_head and of maps out of the residual stream (q/k/v/g,
        gate/up), columns of maps into it (o_proj, down_proj).
        """
        hidden = self.config.hidden_size
        for w in (self.backbone.embeddings.weight, self.lm_head.weight):
            w.copy_(justnorm(w, dim=1))
        for mod in self.backbone.modules():
            if isinstance(mod, nn.Linear):
                dim = 1 if mod.in_features == hidden else 0
                mod.weight.copy_(justnorm(mod.weight, dim=dim))

    def logits(self, hidden):
        scale = self.lm_head.logit_scale.float() / self.config.initializer_range
        return self.lm_head(hidden).float() * scale

    def forward(self, input_ids, labels=None, position_ids=None, n_loops=None, doc_ids=None,
                attn_impl="sdpa", loss_chunk=2048):
        """Without labels: returns float32 logits [B, T, V].
        With labels (same shape as input_ids, -100 = ignore; shifted internally, so a
        packed batch must set -100 on each document's first token):
        returns (mean_loss, n_target_tokens) via a chunked, checkpointed CE."""
        hidden = self.backbone(input_ids, position_ids=position_ids, n_loops=n_loops,
                               doc_ids=doc_ids, attn_impl=attn_impl)
        if labels is None:
            return self.logits(hidden)
        h = hidden[:, :-1].reshape(-1, hidden.shape[-1])
        y = labels[:, 1:].reshape(-1)
        keep = y != -100
        h, y = h[keep], y[keep]
        total = h.new_zeros((), dtype=torch.float32)

        def chunk_loss(hc, yc):
            return F.cross_entropy(self.logits(hc), yc, reduction="sum")

        for s in range(0, h.shape[0], loss_chunk):
            hc, yc = h[s : s + loss_chunk], y[s : s + loss_chunk]
            if self.training and torch.is_grad_enabled():
                total = total + checkpoint(chunk_loss, hc, yc, use_reentrant=False)
            else:
                total = total + chunk_loss(hc, yc)
        n = int(keep.sum())
        return total / max(n, 1), n

    @torch.no_grad()
    def generate(self, input_ids, max_new_tokens=128, n_loops=None, temperature=0.0, eos_ids=()):
        """Simple cache-free sampling, for correctness checks (not speed)."""
        ids = input_ids
        for _ in range(max_new_tokens):
            logits = self.forward(ids, n_loops=n_loops)[:, -1]
            if temperature > 0:
                nxt = torch.multinomial(torch.softmax(logits / temperature, -1), 1)
            else:
                nxt = logits.argmax(-1, keepdim=True)
            ids = torch.cat([ids, nxt], dim=1)
            if int(nxt[0, 0]) in eos_ids:
                break
        return ids[:, input_ids.shape[1] :]