kerzgrr commited on
Commit
5c63f8f
·
verified ·
1 Parent(s): 01a0b23

Monody-Exp: monody-pass-7 EMA weights

Browse files
README.md ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ library_name: pytorch
6
+ pipeline_tag: text-generation
7
+ datasets:
8
+ - HuggingFaceFW/fineweb-edu
9
+ tags:
10
+ - proof-of-concept
11
+ - base-model
12
+ - causal-lm
13
+ - gated-deltanet-2
14
+ - parallax-attention
15
+ ---
16
+
17
+ # Monody-Exp
18
+
19
+ **A proof of concept, published for comparison only.** Monody-Exp is a
20
+ 353M-parameter autoregressive language model, the baseline twin of the
21
+ masked-diffusion model [Rondo-Exp](https://huggingface.co/KerzgrAI/Rondo-Exp),
22
+ trained side by side with it to compare how each uses the same data over
23
+ repeated passes. It is a small base model: it continues text, follows no
24
+ instructions, and is not meant for real use.
25
+
26
+ Both models trained on the same 88M FineWeb-Edu tokens, repeated 7 times (617M tokens), with 2,048-token windows and identical optimizer settings. These are the EMA weights after pass 7; held-out loss is on 101M unseen tokens.
27
+
28
+ | | Rondo-Exp | Monody-Exp |
29
+ |---|---|---|
30
+ | Objective | masked diffusion | next token |
31
+ | Token mixers | 12 bidirectional GDN-2, 4 bidirectional attention | 12 GDN-2, 4 Parallax attention |
32
+ | Held-out loss | ≤ 3.810 (ELBO bound) | 3.632 (exact NLL) |
33
+ | Held-out minus training loss | 0.13 | 1.59 |
34
+ | Zero-shot accuracy, mean of 6 tasks | 44.8% | 45.6% |
35
+
36
+ The six tasks are LAMBADA, ARC-Easy, PIQA, SciQ, HellaSwag and WinoGrande (1,000 examples each). Rondo's loss is an upper bound on its NLL, so it is not directly comparable to Monody's.
37
+
38
+ ## Run
39
+
40
+ Linux with an NVIDIA GPU:
41
+
42
+ ```bash
43
+ hf download KerzgrAI/Monody-Exp --local-dir Monody-Exp
44
+ pip install -r Monody-Exp/requirements.txt
45
+ python Monody-Exp/inference.py --prompt "The water cycle is"
46
+ ```
47
+
48
+ The first run installs flash-linear-attention if it is missing. Options:
49
+ `--max-new-tokens`, `--temperature` (0 is greedy), `--top-p`, `--top-k` and
50
+ `--seed`.
config.json ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "allow_negative_eigenvalues": false,
3
+ "architecture": "Senryu2ForCausalLM",
4
+ "attention_dropout": 0.0,
5
+ "attention_head_dim": 128,
6
+ "attention_layer_type": "parallax",
7
+ "bos_token_id": 0,
8
+ "eos_token_id": 1,
9
+ "full_attention_interval": 4,
10
+ "hidden_size": 1024,
11
+ "initializer_range": 0.02,
12
+ "intermediate_size": 2816,
13
+ "kda_lower_bound": -5.0,
14
+ "kda_safe_gate": true,
15
+ "layer_types": [
16
+ "gdn2",
17
+ "gdn2",
18
+ "gdn2",
19
+ "parallax",
20
+ "gdn2",
21
+ "gdn2",
22
+ "gdn2",
23
+ "parallax",
24
+ "gdn2",
25
+ "gdn2",
26
+ "gdn2",
27
+ "parallax",
28
+ "gdn2",
29
+ "gdn2",
30
+ "gdn2",
31
+ "parallax"
32
+ ],
33
+ "linear_conv_kernel_dim": 4,
34
+ "linear_expand_v": 1.0,
35
+ "linear_full_rank_gate": true,
36
+ "linear_head_dim": 128,
37
+ "linear_layer_type": "gdn2",
38
+ "linear_num_heads": 8,
39
+ "linear_num_value_heads": 8,
40
+ "mask_token_id": null,
41
+ "max_position_embeddings": 8192,
42
+ "mla_kv_lora_rank": 256,
43
+ "mla_q_lora_rank": 512,
44
+ "mla_qk_nope_head_dim": 128,
45
+ "mla_v_head_dim": 128,
46
+ "mlp_activation": "situ_glu",
47
+ "model_type": "senryu2",
48
+ "mtp_adapter_rank": 128,
49
+ "mtp_loss_weight": 0.0,
50
+ "mtp_num_heads": 0,
51
+ "ngram_conv_kernel": 4,
52
+ "ngram_embedding_dim": 128,
53
+ "ngram_heads_per_order": 4,
54
+ "ngram_layer_index": 1,
55
+ "ngram_orders": [],
56
+ "ngram_table_rows": 2097152,
57
+ "num_attention_heads": 8,
58
+ "num_hidden_layers": 16,
59
+ "num_key_value_heads": 4,
60
+ "pad_token_id": 2,
61
+ "partial_rotary_factor": 0.5,
62
+ "residual_branches": 4,
63
+ "residual_gate_rank": 128,
64
+ "residual_mode": "gated_residual",
65
+ "rms_norm_eps": 1e-06,
66
+ "rope_theta": 1000000.0,
67
+ "shared_layer_indices": [],
68
+ "situ_gate_cap": 4.0,
69
+ "situ_up_cap": 25.0,
70
+ "tie_word_embeddings": true,
71
+ "training_sequence_length": 2048,
72
+ "unk_token_id": 3,
73
+ "use_block_attn_res": false,
74
+ "vocab_size": 65536,
75
+ "model_name": "Monody-Exp",
76
+ "developer": "KerzgrAI",
77
+ "torch_dtype": "bfloat16",
78
+ "weights": "ema",
79
+ "stage": "monody-pass-7"
80
+ }
evaluation.json ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "elapsed_seconds": 1349.062306778971,
3
+ "evaluated_at_unix": 1793127069.5127065,
4
+ "held_out_tokens": 101191373,
5
+ "model": "monody",
6
+ "objective": "next-token NLL",
7
+ "optimizer_step": 2352,
8
+ "pass": 7,
9
+ "stage": "monody-pass-7",
10
+ "training_tokens": 616562688,
11
+ "unique_training_tokens": 88080384,
12
+ "weights": {
13
+ "ema": {
14
+ "downstream": {
15
+ "arc_easy": {
16
+ "accuracy": 0.475,
17
+ "accuracy_norm": 0.433,
18
+ "chance": 0.25043333333333334,
19
+ "elapsed_seconds": 7.513357853982598,
20
+ "examples": 1000,
21
+ "gold_bits_per_byte": 1.2626438911998918
22
+ },
23
+ "hellaswag": {
24
+ "accuracy": 0.264,
25
+ "accuracy_norm": 0.279,
26
+ "chance": 0.25,
27
+ "elapsed_seconds": 8.59502274915576,
28
+ "examples": 1000,
29
+ "gold_bits_per_byte": 1.1487319894695152
30
+ },
31
+ "lambada": {
32
+ "accuracy": 0.217,
33
+ "accuracy_norm": null,
34
+ "chance": 0.0,
35
+ "elapsed_seconds": 3.444128228817135,
36
+ "examples": 1000,
37
+ "gold_bits_per_byte": 1.2708033027176069,
38
+ "perplexity": 355.8159137348601
39
+ },
40
+ "mean_accuracy": 0.45649999999999996,
41
+ "mean_gold_bits_per_byte": 1.3049644023078721,
42
+ "piqa": {
43
+ "accuracy": 0.564,
44
+ "accuracy_norm": 0.564,
45
+ "chance": 0.5,
46
+ "elapsed_seconds": 5.2499302718788385,
47
+ "examples": 1000,
48
+ "gold_bits_per_byte": 1.3786440568762732
49
+ },
50
+ "sciq": {
51
+ "accuracy": 0.726,
52
+ "accuracy_norm": 0.636,
53
+ "chance": 0.25,
54
+ "elapsed_seconds": 8.13590192887932,
55
+ "examples": 1000,
56
+ "gold_bits_per_byte": 1.408172402357161
57
+ },
58
+ "winogrande": {
59
+ "accuracy": 0.493,
60
+ "accuracy_norm": null,
61
+ "chance": 0.5,
62
+ "elapsed_seconds": 4.800854941364378,
63
+ "examples": 1000,
64
+ "gold_bits_per_byte": 1.3607907712267844
65
+ }
66
+ },
67
+ "generalization_gap": 1.5875091574716764,
68
+ "held_out": {
69
+ "elapsed_seconds": 534.5517211458646,
70
+ "loss": 3.6317696267661397,
71
+ "loss_95ci": [
72
+ 3.6271775943594977,
73
+ 3.636572309692387
74
+ ],
75
+ "objective": "next-token NLL",
76
+ "perplexity": 37.77961331207202,
77
+ "tokens": 101191373,
78
+ "tokens_per_second": 189301.3697216918,
79
+ "windows": 49410
80
+ },
81
+ "memorization": {
82
+ "held_out": {
83
+ "continuation_tokens": 32,
84
+ "exact_match": 0.0,
85
+ "prefix_tokens": 64,
86
+ "token_accuracy": 0.0364990234375,
87
+ "windows": 256
88
+ },
89
+ "train": {
90
+ "continuation_tokens": 32,
91
+ "exact_match": 0.00390625,
92
+ "prefix_tokens": 64,
93
+ "token_accuracy": 0.0614013671875,
94
+ "windows": 256
95
+ }
96
+ },
97
+ "memorization_gap": 0.00390625,
98
+ "train_probe": {
99
+ "elapsed_seconds": 54.62738667288795,
100
+ "loss": 2.0442604692944633,
101
+ "loss_95ci": [
102
+ 2.036346034331813,
103
+ 2.052173914243236
104
+ ],
105
+ "objective": "next-token NLL",
106
+ "perplexity": 7.72344470088251,
107
+ "tokens": 10000384,
108
+ "tokens_per_second": 183065.39281995854,
109
+ "windows": 4883
110
+ }
111
+ },
112
+ "normal": {
113
+ "downstream": null,
114
+ "generalization_gap": 1.5069047089964025,
115
+ "held_out": {
116
+ "elapsed_seconds": 637.9913326767273,
117
+ "loss": 3.740345572211005,
118
+ "loss_95ci": [
119
+ 3.7358146551118874,
120
+ 3.745179232388174
121
+ ],
122
+ "objective": "next-token NLL",
123
+ "perplexity": 42.11254057450087,
124
+ "tokens": 101191373,
125
+ "tokens_per_second": 158609.32244243208,
126
+ "windows": 49410
127
+ },
128
+ "memorization": {
129
+ "held_out": {
130
+ "continuation_tokens": 32,
131
+ "exact_match": 0.0,
132
+ "prefix_tokens": 64,
133
+ "token_accuracy": 0.0335693359375,
134
+ "windows": 256
135
+ },
136
+ "train": {
137
+ "continuation_tokens": 32,
138
+ "exact_match": 0.00390625,
139
+ "prefix_tokens": 64,
140
+ "token_accuracy": 0.0570068359375,
141
+ "windows": 256
142
+ }
143
+ },
144
+ "memorization_gap": 0.00390625,
145
+ "train_probe": {
146
+ "elapsed_seconds": 67.42786940094084,
147
+ "loss": 2.2334408632146023,
148
+ "loss_95ci": [
149
+ 2.223836386290301,
150
+ 2.2425792741355606
151
+ ],
152
+ "objective": "next-token NLL",
153
+ "perplexity": 9.331920762841568,
154
+ "tokens": 10000384,
155
+ "tokens_per_second": 148312.32380390863,
156
+ "windows": 4883
157
+ }
158
+ }
159
+ }
160
+ }
inference.py ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Text continuation for KerzgrAI/Rondo-Exp and KerzgrAI/Monody-Exp.
3
+
4
+ Both are proof-of-concept base models: they continue text and follow no
5
+ instructions. Rondo-Exp is masked diffusion: the continuation starts as masks
6
+ and is revealed in random order, with up to --drafts tokens drafted and checked
7
+ per forward (exact self-speculative decoding). Monody-Exp is its
8
+ autoregressive twin and samples one token per forward with a cache.
9
+
10
+ Run from a downloaded repo, which this script reads its model from:
11
+ python inference.py --prompt "The water cycle is"
12
+ python inference.py --prompt "Photosynthesis" --max-new-tokens 128 --temperature 0.8
13
+ Or anywhere, downloading the model first:
14
+ python inference.py --repo KerzgrAI/Monody-Exp --prompt "The water cycle is"
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import importlib
21
+ import subprocess
22
+ import sys
23
+ import time
24
+ import warnings
25
+ from pathlib import Path
26
+ from types import ModuleType
27
+
28
+ import torch
29
+ from huggingface_hub import snapshot_download
30
+ from safetensors.torch import load_file
31
+ from tokenizers import Tokenizer
32
+
33
+ REPOS = ("KerzgrAI/Rondo-Exp", "KerzgrAI/Monody-Exp")
34
+ # The models trained on this flash-linear-attention commit (GDN-2 and Parallax attention).
35
+ FLA_COMMIT = "cbb0a72efb55c18ca0ef4f298298317573ad2cb3"
36
+ FLA_REPO = "https://github.com/fla-org/flash-linear-attention.git"
37
+ FLA_MODULES = (
38
+ "fla.ops.gdn2",
39
+ "fla.layers.gdn2",
40
+ "fla.modules.rotary",
41
+ "fla.ops.parallax.parallel",
42
+ )
43
+ TINY_GDN_FILES = (
44
+ "tiny_gdn/__init__.py",
45
+ "tiny_gdn/config.py",
46
+ "tiny_gdn/detokenize.py",
47
+ "tiny_gdn/fugue.py",
48
+ "tiny_gdn/fugue_sampling.py",
49
+ "tiny_gdn/fused_ops.py",
50
+ "tiny_gdn/gated_residual_ops.py",
51
+ "tiny_gdn/haiku_layers.py",
52
+ "tiny_gdn/kdn/__init__.py",
53
+ "tiny_gdn/kdn/kernels.py",
54
+ "tiny_gdn/kdn/layer.py",
55
+ "tiny_gdn/kdn/recurrence.py",
56
+ "tiny_gdn/model.py",
57
+ "tiny_gdn/ngram.py",
58
+ "tiny_gdn/nn_common.py",
59
+ "tiny_gdn/parallax_ops.py",
60
+ "tiny_gdn/senryu2.py",
61
+ )
62
+
63
+
64
+ def parse_args() -> argparse.Namespace:
65
+ parser = argparse.ArgumentParser(
66
+ description="Rondo-Exp / Monody-Exp text continuation"
67
+ )
68
+ parser.add_argument("--prompt", required=True, help="Text to continue")
69
+ parser.add_argument(
70
+ "--repo",
71
+ default=None,
72
+ help=f"Hub repo or local folder to load ({' or '.join(REPOS)}); default: this script's folder",
73
+ )
74
+ parser.add_argument("--max-new-tokens", type=int, default=256)
75
+ parser.add_argument("--temperature", type=float, default=1.0, help="0 is greedy")
76
+ parser.add_argument("--top-p", type=float, default=0.9)
77
+ parser.add_argument("--top-k", type=int, default=0, help="0 keeps every token")
78
+ parser.add_argument("--seed", type=int, default=0)
79
+ parser.add_argument(
80
+ "--drafts",
81
+ type=int,
82
+ default=8,
83
+ help="Rondo-Exp: tokens drafted per forward; 0 reveals one token per forward",
84
+ )
85
+ args = parser.parse_args()
86
+ if args.max_new_tokens <= 0:
87
+ parser.error("--max-new-tokens must be positive")
88
+ if (
89
+ args.temperature < 0
90
+ or not 0 < args.top_p <= 1
91
+ or args.top_k < 0
92
+ or args.drafts < 0
93
+ ):
94
+ parser.error(
95
+ "--temperature >= 0, 0 < --top-p <= 1, --top-k >= 0 and --drafts >= 0"
96
+ )
97
+ return args
98
+
99
+
100
+ def model_directory(repo: str | None) -> Path:
101
+ here = Path(__file__).resolve().parent
102
+ if repo is None:
103
+ if (here / "config.json").is_file():
104
+ return here
105
+ raise SystemExit(
106
+ f"No model next to {Path(__file__).name}; pass --repo {REPOS[0]} or --repo {REPOS[1]}"
107
+ )
108
+ if Path(repo).is_dir():
109
+ return Path(repo).resolve()
110
+ return Path(snapshot_download(repo))
111
+
112
+
113
+ def ensure_fla() -> None:
114
+ try:
115
+ for name in FLA_MODULES:
116
+ importlib.import_module(name)
117
+ return
118
+ except Exception as error: # noqa: BLE001
119
+ print(
120
+ f"Installing flash-linear-attention {FLA_COMMIT[:7]} ({error}) …",
121
+ file=sys.stderr,
122
+ flush=True,
123
+ )
124
+ pip = [sys.executable, "-m", "pip", "install"]
125
+ subprocess.check_call([*pip, "einops", "transformers>=4.45.0"])
126
+ subprocess.check_call(
127
+ [*pip, "--no-deps", "--force-reinstall", f"git+{FLA_REPO}@{FLA_COMMIT}"]
128
+ )
129
+ for name in [
130
+ name for name in sys.modules if name == "fla" or name.startswith("fla.")
131
+ ]:
132
+ del sys.modules[name]
133
+ importlib.invalidate_caches()
134
+ for name in FLA_MODULES:
135
+ importlib.import_module(name)
136
+
137
+
138
+ def load_model(
139
+ directory: Path, device: torch.device
140
+ ) -> tuple[torch.nn.Module, ModuleType | None]:
141
+ """The model in ``directory`` on ``device`` in bfloat16, and fugue_sampling when it is Rondo."""
142
+
143
+ missing = [name for name in TINY_GDN_FILES if not (directory / name).is_file()]
144
+ if missing:
145
+ raise SystemExit(f"{directory} lacks the bundled code: {', '.join(missing)}")
146
+ sys.path.insert(0, str(directory))
147
+ config = importlib.import_module("tiny_gdn.config").TinyGDNConfig.from_json(
148
+ directory / "config.json"
149
+ )
150
+ if config.model_type == "fugue":
151
+ model_class = importlib.import_module("tiny_gdn.fugue").FugueForMaskedDiffusion
152
+ sampling = importlib.import_module("tiny_gdn.fugue_sampling")
153
+ else:
154
+ model_class = importlib.import_module("tiny_gdn.senryu2").Senryu2ForCausalLM
155
+ sampling = None
156
+ with torch.device(device):
157
+ model = model_class(config)
158
+ model = model.to(dtype=torch.bfloat16)
159
+ model.load_state_dict(
160
+ load_file(directory / "model.safetensors", device="cpu"), strict=True
161
+ )
162
+ return model.eval(), sampling
163
+
164
+
165
+ def sample(
166
+ logits: torch.Tensor, args: argparse.Namespace, generator: torch.Generator
167
+ ) -> int:
168
+ logits = logits.float()
169
+ if args.temperature == 0:
170
+ return int(logits.argmax())
171
+ logits = logits / args.temperature
172
+ if 0 < args.top_k < logits.numel():
173
+ logits = logits.masked_fill(
174
+ logits < torch.topk(logits, args.top_k).values[-1], -torch.inf
175
+ )
176
+ if args.top_p < 1:
177
+ ordered, order = torch.sort(logits, descending=True)
178
+ drop = torch.softmax(ordered, dim=-1).cumsum(-1) > args.top_p
179
+ drop[1:] = drop[:-1].clone()
180
+ drop[0] = False
181
+ logits = logits.scatter(0, order, ordered.masked_fill(drop, -torch.inf))
182
+ return int(torch.multinomial(torch.softmax(logits, dim=-1), 1, generator=generator))
183
+
184
+
185
+ @torch.inference_mode()
186
+ def continue_autoregressive(model, prompt: list[int], args, generator, emit) -> int:
187
+ eos = int(model.config.eos_token_id)
188
+ room = model.config.max_position_embeddings - len(prompt)
189
+ output = model(
190
+ torch.tensor([prompt], device=generator.device),
191
+ return_logits=True,
192
+ logits_to_keep=1,
193
+ use_cache=True,
194
+ )
195
+ produced = 0
196
+ for _ in range(min(args.max_new_tokens, room)):
197
+ token = sample(output.logits[0, -1], args, generator)
198
+ if token == eos:
199
+ break
200
+ emit(token)
201
+ produced += 1
202
+ output = model(
203
+ torch.tensor([[token]], device=generator.device),
204
+ return_logits=True,
205
+ logits_to_keep=1,
206
+ past_key_values=output.past_key_values,
207
+ use_cache=True,
208
+ )
209
+ return produced
210
+
211
+
212
+ @torch.inference_mode()
213
+ def continue_diffusion(
214
+ model, sampling: ModuleType, prompt: list[int], args, generator, emit
215
+ ) -> int:
216
+ eos = int(model.config.eos_token_id)
217
+ mask = model.mask_token_id
218
+ settings = sampling.DiffusionSampling(
219
+ new_tokens=args.max_new_tokens,
220
+ block_length=None,
221
+ threshold=None,
222
+ order="random",
223
+ steps_per_block=None if args.drafts else args.max_new_tokens,
224
+ draft_tokens=args.drafts,
225
+ temperature=args.temperature,
226
+ top_k=args.top_k,
227
+ top_p=args.top_p,
228
+ )
229
+ shown = 0
230
+ forwards = 0
231
+
232
+ def show(canvas: torch.Tensor) -> bool:
233
+ nonlocal shown
234
+ committed = sampling.committed_prefix(canvas, mask).tolist()
235
+ for token in committed[shown:]:
236
+ if token == eos:
237
+ return True
238
+ emit(token)
239
+ shown += 1
240
+ return False
241
+
242
+ def on_step(state) -> bool:
243
+ nonlocal forwards
244
+ forwards = state.step
245
+ return show(state.tokens[0])
246
+
247
+ canvas = sampling.diffusion_generate(
248
+ model,
249
+ torch.tensor([prompt], device=generator.device),
250
+ settings,
251
+ generator=generator,
252
+ eos_token_id=eos,
253
+ on_step=on_step,
254
+ )
255
+ show(canvas[0])
256
+ print(f"\n[{forwards} forwards]", file=sys.stderr, end="", flush=True)
257
+ return shown
258
+
259
+
260
+ def main() -> None:
261
+ args = parse_args()
262
+ warnings.filterwarnings("ignore", message=r"tl\.make_block_ptr is deprecated")
263
+ if not torch.cuda.is_available():
264
+ raise SystemExit(
265
+ "An NVIDIA GPU is required: the GDN-2 kernels run in Triton on CUDA."
266
+ )
267
+ device = torch.device("cuda")
268
+ directory = model_directory(args.repo)
269
+ ensure_fla()
270
+ model, sampling = load_model(directory, device)
271
+ tokenizer = Tokenizer.from_file(str(directory / "tokenizer.json"))
272
+ decoder = importlib.import_module("tiny_gdn.detokenize").IncrementalUtf8Decoder(
273
+ tokenizer
274
+ )
275
+ prompt = tokenizer.encode(args.prompt, add_special_tokens=False).ids
276
+ if not prompt:
277
+ raise SystemExit("The prompt produced no tokens")
278
+ room = model.config.max_position_embeddings - args.max_new_tokens
279
+ if room < 1:
280
+ raise SystemExit(
281
+ f"--max-new-tokens leaves no room in {model.config.max_position_embeddings} positions"
282
+ )
283
+ prompt = prompt[-room:]
284
+ generator = torch.Generator(device=device).manual_seed(args.seed)
285
+
286
+ def emit(token: int) -> None:
287
+ print(decoder.push(token), end="", flush=True)
288
+
289
+ print(args.prompt, end="", flush=True)
290
+ started = time.perf_counter()
291
+ if sampling is None:
292
+ produced = continue_autoregressive(model, prompt, args, generator, emit)
293
+ else:
294
+ produced = continue_diffusion(model, sampling, prompt, args, generator, emit)
295
+ shown = len(decoder.text)
296
+ print(decoder.finalize()[shown:], end="")
297
+ elapsed = time.perf_counter() - started
298
+ print(f"\n[{produced} tokens in {elapsed:.1f}s]", file=sys.stderr, flush=True)
299
+
300
+
301
+ if __name__ == "__main__":
302
+ main()
merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a1f9a29ebdc0bfb39c4fa0cdffdee0e6754420f821896d0d39824095f2a28f8b
3
+ size 706364032
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch>=2.7.0
2
+ triton>=3.3
3
+ safetensors>=0.4.0
4
+ tokenizers>=0.20.0
5
+ huggingface_hub>=0.26.0
6
+ transformers>=4.45.0
7
+ einops>=0.8.0
8
+ numpy>=1.26.0
9
+
10
+ # flash-linear-attention (commit cbb0a72, with GDN-2 and Parallax attention)
11
+ # is installed by inference.py on first run when it is missing.
special_tokens_map.json ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": "<|begin_of_text|>",
3
+ "eos_token": "<|end_of_text|>",
4
+ "pad_token": "<|padding|>",
5
+ "unk_token": "<|unknown|>",
6
+ "additional_special_tokens": [
7
+ "<|im_start|>",
8
+ "<|im_end|>",
9
+ "<|tool_call|>",
10
+ "<|tool_response|>",
11
+ "<think>",
12
+ "</think>",
13
+ "<|fim_prefix|>",
14
+ "<|fim_middle|>",
15
+ "<|fim_suffix|>",
16
+ "<|fim_pad|>",
17
+ "<|no_think|>",
18
+ "<|think|>",
19
+ "<|mask|>",
20
+ "<|reserved_009|>",
21
+ "<|reserved_010|>",
22
+ "<|reserved_011|>",
23
+ "<|reserved_012|>",
24
+ "<|reserved_013|>",
25
+ "<|reserved_014|>",
26
+ "<|reserved_015|>",
27
+ "<|reserved_016|>",
28
+ "<|reserved_017|>",
29
+ "<|reserved_018|>",
30
+ "<|reserved_019|>",
31
+ "<|reserved_020|>",
32
+ "<|reserved_021|>",
33
+ "<|reserved_022|>",
34
+ "<|reserved_023|>",
35
+ "<|reserved_024|>",
36
+ "<|reserved_025|>",
37
+ "<|reserved_026|>",
38
+ "<|reserved_027|>",
39
+ "<|reserved_028|>",
40
+ "<|reserved_029|>",
41
+ "<|reserved_030|>",
42
+ "<|reserved_031|>",
43
+ "<|reserved_032|>",
44
+ "<|reserved_033|>",
45
+ "<|reserved_034|>",
46
+ "<|reserved_035|>",
47
+ "<|reserved_036|>",
48
+ "<|reserved_037|>",
49
+ "<|reserved_038|>",
50
+ "<|reserved_039|>",
51
+ "<|reserved_040|>",
52
+ "<|reserved_041|>",
53
+ "<|reserved_042|>",
54
+ "<|reserved_043|>",
55
+ "<|reserved_044|>",
56
+ "<|reserved_045|>",
57
+ "<|reserved_046|>",
58
+ "<|reserved_047|>",
59
+ "<|reserved_048|>",
60
+ "<|reserved_049|>"
61
+ ],
62
+ "mask_token": "<|mask|>"
63
+ }
stage.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "checkpoint-00002352",
3
+ "completed_at_unix": 1793125720.1193864,
4
+ "model": "monody",
5
+ "optimizer_step": 2352,
6
+ "pass": 7,
7
+ "skipped_optimizer_steps": 0,
8
+ "stage": "monody-pass-7",
9
+ "training_tokens": 616562688,
10
+ "unique_training_tokens": 88080384
11
+ }
tiny_gdn/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tiny_gdn.config import (
2
+ TinyGDNConfig,
3
+ haiku_config,
4
+ haiku_kdn_config,
5
+ prompttercet_config,
6
+ senryu2_1b_config,
7
+ senryu_config,
8
+ )
9
+ from tiny_gdn.model import TinyGDNForCausalLM, TinyGDNOutput
10
+
11
+ __all__ = [
12
+ "TinyGDNConfig",
13
+ "TinyGDNForCausalLM",
14
+ "TinyGDNOutput",
15
+ "haiku_config",
16
+ "haiku_kdn_config",
17
+ "prompttercet_config",
18
+ "senryu2_1b_config",
19
+ "senryu_config",
20
+ ]
tiny_gdn/config.py ADDED
@@ -0,0 +1,488 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import asdict, dataclass, fields
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ LINEAR_LAYER_TYPES = frozenset({"gdn2", "kda", "diag_kdn", "bigdn2"})
9
+ ATTENTION_LAYER_TYPES = frozenset({"full_attention", "gated_mla", "parallax", "bidirectional"})
10
+ # Gated-residual token-mixer pairs: Senryu-2's causal one, and Fugue's bidirectional one.
11
+ GATED_RESIDUAL_MIXERS = frozenset({("gdn2", "parallax"), ("bigdn2", "bidirectional")})
12
+ FUGUE_MODEL_TYPE = "fugue"
13
+ RESIDUAL_MODES = frozenset({"pre_norm", "gated_residual"})
14
+ DIAG_KDN_HEAD_DIMS = (64, 128)
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class TinyGDNConfig:
19
+ architecture: str = "TinyGDNForCausalLM"
20
+ model_type: str = "tiny_gdn"
21
+
22
+ vocab_size: int = 49_152
23
+ # Deep-thin sizing is deliberate: controlled sub-billion studies find
24
+ # depth materially more valuable than width around the 125M-150M scale.
25
+ hidden_size: int = 512
26
+ intermediate_size: int = 1_472
27
+ num_hidden_layers: int = 32
28
+
29
+ num_attention_heads: int = 4
30
+ num_key_value_heads: int = 1
31
+ attention_head_dim: int = 128
32
+ full_attention_interval: int = 4
33
+ attention_dropout: float = 0.0
34
+ partial_rotary_factor: float = 0.5
35
+ rope_theta: float = 1_000_000.0
36
+
37
+ linear_num_heads: int = 4
38
+ linear_num_value_heads: int = 4
39
+ linear_head_dim: int = 128
40
+ linear_expand_v: float = 1.0
41
+ linear_conv_kernel_dim: int = 4
42
+ allow_negative_eigenvalues: bool = False
43
+ linear_layer_type: str = "gdn2"
44
+ attention_layer_type: str = "full_attention"
45
+ mlp_activation: str = "swiglu"
46
+ use_block_attn_res: bool = False
47
+ kda_safe_gate: bool = True
48
+ kda_lower_bound: float = -5.0
49
+ mla_q_lora_rank: int | None = 512
50
+ mla_kv_lora_rank: int = 256
51
+ mla_qk_nope_head_dim: int = 128
52
+ mla_v_head_dim: int = 128
53
+ situ_gate_cap: float = 4.0
54
+ situ_up_cap: float = 25.0
55
+
56
+ # "gated_residual" replaces pre-norm with Qwen's multi-branch gated
57
+ # residual stream: each sublayer reads a gated mix of `residual_branches`
58
+ # normalized branches and writes its output back with per-branch scales.
59
+ residual_mode: str = "pre_norm"
60
+ residual_branches: int = 4
61
+ residual_gate_rank: int = 128
62
+ # GDN-2 output gate as one full-rank projection (Kimi K3) instead of fla's
63
+ # low-rank pair through head_v_dim.
64
+ linear_full_rank_gate: bool = False
65
+ # Hashed n-gram memory (Engram): `ngram_heads_per_order` tables per order,
66
+ # each with a distinct prime row count near `ngram_table_rows`, injected
67
+ # before block `ngram_layer_index`. An empty `ngram_orders` disables it.
68
+ ngram_orders: tuple[int, ...] = ()
69
+ ngram_heads_per_order: int = 4
70
+ ngram_table_rows: int = 2_097_152
71
+ ngram_embedding_dim: int = 128
72
+ ngram_layer_index: int = 1
73
+ ngram_conv_kernel: int = 4
74
+
75
+ max_position_embeddings: int = 32_768
76
+ training_sequence_length: int = 2_048
77
+ rms_norm_eps: float = 1e-6
78
+ initializer_range: float = 0.02
79
+ tie_word_embeddings: bool = True
80
+ shared_layer_indices: tuple[int, ...] = ()
81
+
82
+ # MTP is an opt-in ablation at this scale; static MTP is not assumed to
83
+ # improve a 150M model without a controlled pilot.
84
+ mtp_num_heads: int = 0
85
+ mtp_adapter_rank: int = 128
86
+ mtp_loss_weight: float = 0.0
87
+
88
+ bos_token_id: int = 0
89
+ eos_token_id: int = 1
90
+ pad_token_id: int = 2
91
+ unk_token_id: int = 3
92
+ # Masked-diffusion (Fugue) models replace noised tokens with this id.
93
+ mask_token_id: int | None = None
94
+ # Fugue only: 0 denoises whole sequences bidirectionally (Rondo); G > 0
95
+ # generates G-token blocks left to right over a causal clean stream
96
+ # (Fugue-1, docs/architecture.md "Fugue-1").
97
+ fugue_block_size: int = 0
98
+
99
+ def __post_init__(self) -> None:
100
+ for name in ("shared_layer_indices", "ngram_orders"):
101
+ object.__setattr__(self, name, tuple(getattr(self, name)))
102
+ if self.vocab_size <= 0 or self.vocab_size > 65_536:
103
+ raise ValueError("vocab_size must fit the uint16 token dataset")
104
+ if self.hidden_size != self.num_attention_heads * self.attention_head_dim:
105
+ raise ValueError("hidden_size must equal num_attention_heads * attention_head_dim")
106
+ if self.hidden_size != self.linear_num_heads * self.linear_head_dim:
107
+ raise ValueError("hidden_size must equal linear_num_heads * linear_head_dim")
108
+ if self.linear_num_value_heads < self.linear_num_heads:
109
+ raise ValueError("linear_num_value_heads must be at least linear_num_heads")
110
+ if self.linear_num_value_heads % self.linear_num_heads != 0:
111
+ raise ValueError("linear_num_value_heads must be divisible by linear_num_heads")
112
+ if self.num_attention_heads % self.num_key_value_heads != 0:
113
+ raise ValueError("num_attention_heads must be divisible by num_key_value_heads")
114
+ if self.num_hidden_layers % self.full_attention_interval != 0:
115
+ raise ValueError("num_hidden_layers must be divisible by full_attention_interval")
116
+ if not 0.0 < self.partial_rotary_factor <= 1.0:
117
+ raise ValueError("partial_rotary_factor must be in (0, 1]")
118
+ rotary_dim = int(self.attention_head_dim * self.partial_rotary_factor)
119
+ if rotary_dim <= 0 or rotary_dim % 2:
120
+ raise ValueError("The partial rotary dimension must be positive and even")
121
+ if self.training_sequence_length > self.max_position_embeddings:
122
+ raise ValueError("training_sequence_length exceeds max_position_embeddings")
123
+ if len(set(self.shared_layer_indices)) != len(self.shared_layer_indices):
124
+ raise ValueError("shared_layer_indices must be unique")
125
+ if any(
126
+ index < 0 or index >= self.num_hidden_layers
127
+ for index in self.shared_layer_indices
128
+ ):
129
+ raise ValueError("shared_layer_indices contains an invalid layer")
130
+ if self.mtp_num_heads < 0:
131
+ raise ValueError("mtp_num_heads cannot be negative")
132
+ if self.mtp_num_heads and self.mtp_adapter_rank <= 0:
133
+ raise ValueError("mtp_adapter_rank must be positive when MTP is enabled")
134
+ if not 0.0 <= self.mtp_loss_weight <= 1.0:
135
+ raise ValueError("mtp_loss_weight must be between zero and one")
136
+ for token_id in (
137
+ self.bos_token_id,
138
+ self.eos_token_id,
139
+ self.pad_token_id,
140
+ self.unk_token_id,
141
+ ):
142
+ if not 0 <= token_id < self.vocab_size:
143
+ raise ValueError(f"Special token ID {token_id} is outside the vocabulary")
144
+ if self.linear_layer_type not in LINEAR_LAYER_TYPES:
145
+ raise ValueError(
146
+ f"linear_layer_type must be one of {sorted(LINEAR_LAYER_TYPES)}"
147
+ )
148
+ if self.linear_layer_type == "diag_kdn":
149
+ if self.linear_head_dim not in DIAG_KDN_HEAD_DIMS:
150
+ raise ValueError(
151
+ f"diag_kdn needs linear_head_dim in {DIAG_KDN_HEAD_DIMS}"
152
+ )
153
+ if self.linear_num_value_heads != self.linear_num_heads:
154
+ raise ValueError("diag_kdn needs linear_num_value_heads == linear_num_heads")
155
+ if self.linear_expand_v != 1.0:
156
+ raise ValueError("diag_kdn needs linear_expand_v == 1.0")
157
+ if self.allow_negative_eigenvalues:
158
+ raise ValueError("diag_kdn derives its gain from the covariance; negative eigenvalues are unsupported")
159
+ if self.attention_layer_type not in ATTENTION_LAYER_TYPES:
160
+ raise ValueError(
161
+ f"attention_layer_type must be one of {sorted(ATTENTION_LAYER_TYPES)}"
162
+ )
163
+ if self.residual_mode not in RESIDUAL_MODES:
164
+ raise ValueError(f"residual_mode must be one of {sorted(RESIDUAL_MODES)}")
165
+ gated = self.residual_mode == "gated_residual"
166
+ if gated:
167
+ if self.residual_branches < 2:
168
+ raise ValueError("gated_residual needs at least two residual_branches")
169
+ if self.residual_gate_rank <= 0:
170
+ raise ValueError("residual_gate_rank must be positive")
171
+ if self.use_block_attn_res:
172
+ raise ValueError("gated_residual replaces block attention residuals")
173
+ if self.mtp_num_heads:
174
+ raise ValueError("gated_residual models do not support MTP heads")
175
+ if self.shared_layer_indices:
176
+ raise ValueError("gated_residual models do not support shared layers")
177
+ if (self.linear_layer_type, self.attention_layer_type) not in GATED_RESIDUAL_MIXERS:
178
+ raise ValueError(
179
+ "gated_residual models pair gdn2 with parallax attention, or bigdn2 with bidirectional"
180
+ )
181
+ elif (
182
+ self.attention_layer_type in {"parallax", "bidirectional"}
183
+ or self.linear_layer_type == "bigdn2"
184
+ or self.ngram_orders
185
+ ):
186
+ raise ValueError(
187
+ "parallax and bidirectional mixers and n-gram memory need residual_mode 'gated_residual'"
188
+ )
189
+ fugue = self.model_type == FUGUE_MODEL_TYPE
190
+ if fugue != (self.linear_layer_type == "bigdn2"):
191
+ raise ValueError("Fugue models, and only Fugue models, use bidirectional bigdn2 mixers")
192
+ if fugue:
193
+ if self.mask_token_id is None or not 0 <= self.mask_token_id < self.vocab_size:
194
+ raise ValueError("Fugue models need a mask_token_id inside the vocabulary")
195
+ if self.ngram_orders:
196
+ raise ValueError("Fugue models have no n-gram memory")
197
+ if self.fugue_block_size < 0:
198
+ raise ValueError("fugue_block_size cannot be negative")
199
+ if self.fugue_block_size and self.training_sequence_length % self.fugue_block_size:
200
+ raise ValueError("training_sequence_length must be a multiple of fugue_block_size")
201
+ elif self.fugue_block_size:
202
+ raise ValueError("fugue_block_size applies to Fugue models only")
203
+ if self.linear_full_rank_gate and self.linear_layer_type not in {"gdn2", "bigdn2"}:
204
+ raise ValueError("linear_full_rank_gate applies to gdn2 and bigdn2 layers")
205
+ if self.ngram_orders:
206
+ if any(order < 2 for order in self.ngram_orders):
207
+ raise ValueError("ngram_orders must be at least 2")
208
+ if tuple(sorted(set(self.ngram_orders))) != tuple(self.ngram_orders):
209
+ raise ValueError("ngram_orders must be unique and ascending")
210
+ if self.ngram_heads_per_order <= 0 or self.ngram_embedding_dim <= 0:
211
+ raise ValueError("n-gram heads and embedding width must be positive")
212
+ if self.ngram_table_rows <= self.vocab_size:
213
+ raise ValueError("ngram_table_rows must exceed the vocabulary")
214
+ if not 0 <= self.ngram_layer_index < self.num_hidden_layers:
215
+ raise ValueError("ngram_layer_index must name a block")
216
+ if self.ngram_conv_kernel <= 0:
217
+ raise ValueError("ngram_conv_kernel must be positive")
218
+ if self.mlp_activation not in {"swiglu", "situ_glu"}:
219
+ raise ValueError("mlp_activation must be 'swiglu' or 'situ_glu'")
220
+ if self.mla_kv_lora_rank <= 0:
221
+ raise ValueError("mla_kv_lora_rank must be positive")
222
+ if self.mla_q_lora_rank is not None and self.mla_q_lora_rank <= 0:
223
+ raise ValueError("mla_q_lora_rank must be positive or None")
224
+ if self.mla_qk_nope_head_dim <= 0 or self.mla_v_head_dim <= 0:
225
+ raise ValueError("MLA head dimensions must be positive")
226
+ if self.kda_lower_bound >= 0.0:
227
+ raise ValueError("kda_lower_bound must be negative log-decay")
228
+ if self.situ_gate_cap <= 0.0 or self.situ_up_cap <= 0.0:
229
+ raise ValueError("SiTU caps must be positive")
230
+
231
+ @property
232
+ def layer_types(self) -> tuple[str, ...]:
233
+ return tuple(
234
+ self.attention_layer_type
235
+ if (index + 1) % self.full_attention_interval == 0
236
+ else self.linear_layer_type
237
+ for index in range(self.num_hidden_layers)
238
+ )
239
+
240
+ @property
241
+ def rotary_dim(self) -> int:
242
+ return int(self.attention_head_dim * self.partial_rotary_factor)
243
+
244
+ @property
245
+ def effective_num_layers(self) -> int:
246
+ return self.num_hidden_layers + len(self.shared_layer_indices)
247
+
248
+ @property
249
+ def ngram_table_count(self) -> int:
250
+ return len(self.ngram_orders) * self.ngram_heads_per_order
251
+
252
+ @property
253
+ def ngram_memory_dim(self) -> int:
254
+ return self.ngram_table_count * self.ngram_embedding_dim
255
+
256
+ def to_dict(self) -> dict[str, Any]:
257
+ payload = asdict(self)
258
+ payload["layer_types"] = list(self.layer_types)
259
+ return payload
260
+
261
+ def save_json(self, path: Path) -> None:
262
+ path.parent.mkdir(parents=True, exist_ok=True)
263
+ path.write_text(
264
+ json.dumps(self.to_dict(), indent=2, sort_keys=True) + "\n",
265
+ encoding="utf-8",
266
+ )
267
+
268
+ @classmethod
269
+ def from_json(cls, path: Path) -> TinyGDNConfig:
270
+ payload = json.loads(path.read_text(encoding="utf-8"))
271
+ payload.pop("layer_types", None)
272
+ for name in ("shared_layer_indices", "ngram_orders"):
273
+ if name in payload:
274
+ payload[name] = tuple(payload[name])
275
+ allowed = {item.name for item in fields(cls)}
276
+ return cls(**{key: value for key, value in payload.items() if key in allowed})
277
+
278
+
279
+ def haiku_config(**overrides: Any) -> TinyGDNConfig:
280
+ """650M Haiku: 3×KDA + 1×Gated-MLA, AttnRes, SiTU-GLU, MTP."""
281
+
282
+ payload = {
283
+ "architecture": "HaikuForCausalLM",
284
+ "model_type": "haiku",
285
+ "vocab_size": 65_536,
286
+ "hidden_size": 1024,
287
+ "intermediate_size": 3840,
288
+ "num_hidden_layers": 36,
289
+ "num_attention_heads": 8,
290
+ "num_key_value_heads": 8,
291
+ "attention_head_dim": 128,
292
+ "full_attention_interval": 4,
293
+ "linear_num_heads": 8,
294
+ "linear_num_value_heads": 8,
295
+ "linear_head_dim": 128,
296
+ "linear_layer_type": "kda",
297
+ "attention_layer_type": "gated_mla",
298
+ "mlp_activation": "situ_glu",
299
+ "use_block_attn_res": True,
300
+ "mtp_num_heads": 2,
301
+ "mtp_adapter_rank": 128,
302
+ "mtp_loss_weight": 0.2,
303
+ "max_position_embeddings": 32_768,
304
+ "training_sequence_length": 2048,
305
+ }
306
+ payload.update(overrides)
307
+ return TinyGDNConfig(**payload)
308
+
309
+
310
+ def haiku_kdn_config(**overrides: Any) -> TinyGDNConfig:
311
+ """Haiku with Diagonal KDN in place of KDA in the 27 linear layers.
312
+
313
+ Opt-in: at equal tokens it trailed KDA in Haiku pilots and trains ~23%
314
+ slower at full depth (docs/architecture.md, "Haiku v2").
315
+ """
316
+
317
+ return haiku_config(**{"linear_layer_type": "diag_kdn", **overrides})
318
+
319
+
320
+ def senryu_config(**overrides: Any) -> TinyGDNConfig:
321
+ """800M Senryu: 3×Gated DeltaNet-2 + 1×Gated-MLA over 44 layers, AttnRes, SiTU-GLU.
322
+
323
+ Haiku's width, heads, tokenizer, and residual design with GDN-2's
324
+ channel-wise erase and write gates in the 33 linear layers. A rank-512
325
+ query LoRA costs as many parameters as a full 1024×1024 query, so queries
326
+ are full rank, and the KV latent doubles to 512. The feed-forward narrows
327
+ to 3392 to pay for the depth. Next-token only: MTP has no pilot win at
328
+ this scale (docs/architecture.md, "Senryu").
329
+ """
330
+
331
+ return haiku_config(
332
+ **{
333
+ "architecture": "SenryuForCausalLM",
334
+ "model_type": "senryu",
335
+ "intermediate_size": 3392,
336
+ "num_hidden_layers": 44,
337
+ "linear_layer_type": "gdn2",
338
+ "mla_q_lora_rank": None,
339
+ "mla_kv_lora_rank": 512,
340
+ "mtp_num_heads": 0,
341
+ "mtp_loss_weight": 0.0,
342
+ **overrides,
343
+ }
344
+ )
345
+
346
+
347
+ def senryu2_1b_config(**overrides: Any) -> TinyGDNConfig:
348
+ """Senryu-2-1B: a 1.09B Senryu-2 backbone plus a 2.2B host-side n-gram memory.
349
+
350
+ 28 layers of 3×Gated DeltaNet-2 (full-rank output gate) + 1×Parallax
351
+ attention, width 1536, SiTU-GLU 3584, a 4-branch gated residual stream,
352
+ and orders-2/3 hashed n-gram tables injected before block 1
353
+ (docs/architecture.md, "Senryu-2-1B").
354
+ """
355
+
356
+ payload: dict[str, Any] = {
357
+ "architecture": "Senryu2ForCausalLM",
358
+ "model_type": "senryu2",
359
+ "vocab_size": 65_536,
360
+ "hidden_size": 1536,
361
+ "intermediate_size": 3584,
362
+ "num_hidden_layers": 28,
363
+ "num_attention_heads": 12,
364
+ "num_key_value_heads": 4,
365
+ "attention_head_dim": 128,
366
+ "full_attention_interval": 4,
367
+ "partial_rotary_factor": 0.5,
368
+ "rope_theta": 1_000_000.0,
369
+ "linear_num_heads": 12,
370
+ "linear_num_value_heads": 12,
371
+ "linear_head_dim": 128,
372
+ "linear_layer_type": "gdn2",
373
+ "attention_layer_type": "parallax",
374
+ "mlp_activation": "situ_glu",
375
+ "residual_mode": "gated_residual",
376
+ "residual_branches": 4,
377
+ "residual_gate_rank": 128,
378
+ "linear_full_rank_gate": True,
379
+ "ngram_orders": (2, 3),
380
+ "ngram_heads_per_order": 4,
381
+ "ngram_table_rows": 2_097_152,
382
+ "ngram_embedding_dim": 128,
383
+ "ngram_layer_index": 1,
384
+ "ngram_conv_kernel": 4,
385
+ "max_position_embeddings": 32_768,
386
+ "training_sequence_length": 32_768,
387
+ "mtp_num_heads": 0,
388
+ "mtp_loss_weight": 0.0,
389
+ }
390
+ payload.update(overrides)
391
+ return TinyGDNConfig(**payload)
392
+
393
+
394
+ def monody_config(**overrides: Any) -> TinyGDNConfig:
395
+ """Monody: the autoregressive control in the Rondo/Monody data-efficiency experiment.
396
+
397
+ Senryu-2's backbone at width 1024 with no n-gram memory: 16 layers of
398
+ 3×Gated DeltaNet-2 + 1×Parallax attention, SiTU-GLU 2816, 8 query and 4 KV
399
+ heads, trained on 2,048-token windows (docs/architecture.md, "Fugue").
400
+ """
401
+
402
+ return senryu2_1b_config(
403
+ **{
404
+ "hidden_size": 1024,
405
+ "intermediate_size": 2816,
406
+ "num_hidden_layers": 16,
407
+ "num_attention_heads": 8,
408
+ "num_key_value_heads": 4,
409
+ "linear_num_heads": 8,
410
+ "linear_num_value_heads": 8,
411
+ "ngram_orders": (),
412
+ "max_position_embeddings": 8192,
413
+ "training_sequence_length": 2048,
414
+ **overrides,
415
+ }
416
+ )
417
+
418
+
419
+ def rondo_config(**overrides: Any) -> TinyGDNConfig:
420
+ """Rondo: the first Fugue model, a masked-diffusion LM on Monody's backbone.
421
+
422
+ Monody's widths, depth and heads with every token mixer bidirectional: 12
423
+ bidirectional Gated DeltaNet-2 layers and 4 bidirectional softmax attention
424
+ layers, denoising `<|mask|>` (tokenizer v2, id 22). The backward scans'
425
+ own convolutions and decay projections leave it within 0.3% of Monody's
426
+ parameter count (docs/architecture.md, "Fugue").
427
+ """
428
+
429
+ return monody_config(
430
+ **{
431
+ "architecture": "FugueForMaskedDiffusion",
432
+ "model_type": FUGUE_MODEL_TYPE,
433
+ "linear_layer_type": "bigdn2",
434
+ "attention_layer_type": "bidirectional",
435
+ "mask_token_id": 22,
436
+ **overrides,
437
+ }
438
+ )
439
+
440
+
441
+ def fugue1_1b_config(**overrides: Any) -> TinyGDNConfig:
442
+ """Fugue-1-1B: a 1.1B block-diffusion LM on Senryu-2-1B's backbone.
443
+
444
+ Senryu-2-1B's widths, depth and heads without n-gram memory, as a Fugue
445
+ model that writes 32-token blocks left to right: a causal clean stream
446
+ (next-token loss, and the exact cache every block reads) and a noisy stream
447
+ denoising each block bidirectionally against that cache, trained on
448
+ 4,096-token windows (docs/architecture.md, "Fugue-1").
449
+ """
450
+
451
+ return senryu2_1b_config(
452
+ **{
453
+ "architecture": "FugueForMaskedDiffusion",
454
+ "model_type": FUGUE_MODEL_TYPE,
455
+ "linear_layer_type": "bigdn2",
456
+ "attention_layer_type": "bidirectional",
457
+ "mask_token_id": 22,
458
+ "fugue_block_size": 32,
459
+ "ngram_orders": (),
460
+ "max_position_embeddings": 32_768,
461
+ "training_sequence_length": 4096,
462
+ **overrides,
463
+ }
464
+ )
465
+
466
+
467
+ def prompttercet_config(**overrides: Any) -> TinyGDNConfig:
468
+ """PromptTercet: a 501M Senryu-2 prompter, which writes the user's side of a chat.
469
+
470
+ Senryu-2-1B's layout at width 1024: 24 layers of 3×Gated DeltaNet-2 +
471
+ 1×Parallax attention, SiTU-GLU 2816, 8 query and 4 KV heads, and n-gram
472
+ tables of 1,048,576 rows, 1.09B host-side parameters, half of Senryu-2-1B's
473
+ (docs/architecture.md, "PromptTercet").
474
+ """
475
+
476
+ return senryu2_1b_config(
477
+ **{
478
+ "hidden_size": 1024,
479
+ "intermediate_size": 2816,
480
+ "num_hidden_layers": 24,
481
+ "num_attention_heads": 8,
482
+ "num_key_value_heads": 4,
483
+ "linear_num_heads": 8,
484
+ "linear_num_value_heads": 8,
485
+ "ngram_table_rows": 1_048_576,
486
+ **overrides,
487
+ }
488
+ )
tiny_gdn/detokenize.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Byte-accurate incremental decode for GPT-2-style byte-level BPE.
2
+
3
+ `tokenizer.decode(ids)` UTF-8-replaces incomplete sequences. Streaming that
4
+ as a string prefix then breaks on the next byte: decode([20492]) is a
5
+ replacement character and decode([20492, 294]) is `` ≈``, so the
6
+ accumulated text is no longer a prefix and the UI re-emits the whole
7
+ think block.
8
+
9
+ This decoder concatenates raw BPE bytes and only emits complete UTF-8
10
+ characters. The completing byte of ``≈`` yields `` ≈``; nothing is rewound.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Iterable, Mapping, Sequence
16
+ from typing import Any
17
+
18
+
19
+ def gpt2_bytes_to_unicode() -> dict[int, str]:
20
+ raw = (
21
+ list(range(ord("!"), ord("~") + 1))
22
+ + list(range(ord("¡"), ord("¬") + 1))
23
+ + list(range(ord("®"), ord("ÿ") + 1))
24
+ )
25
+ mapped = raw[:]
26
+ extra = 0
27
+ for byte in range(256):
28
+ if byte not in raw:
29
+ raw.append(byte)
30
+ mapped.append(256 + extra)
31
+ extra += 1
32
+ return dict(zip(raw, [chr(code) for code in mapped], strict=True))
33
+
34
+
35
+ def unicode_to_gpt2_bytes() -> dict[str, int]:
36
+ return {char: byte for byte, char in gpt2_bytes_to_unicode().items()}
37
+
38
+
39
+ def utf8_char_length(lead: int) -> int | None:
40
+ if lead < 0x80:
41
+ return 1
42
+ if 0xC2 <= lead <= 0xDF:
43
+ return 2
44
+ if 0xE0 <= lead <= 0xEF:
45
+ return 3
46
+ if 0xF0 <= lead <= 0xF4:
47
+ return 4
48
+ return None
49
+
50
+
51
+ def split_complete_utf8(buffer: bytes) -> tuple[str, bytes]:
52
+ """Return (decoded prefix, leftover incomplete or invalid-lead bytes)."""
53
+ index = 0
54
+ end = len(buffer)
55
+ while index < end:
56
+ size = utf8_char_length(buffer[index])
57
+ if size is None:
58
+ break
59
+ if index + size > end:
60
+ break
61
+ chunk = buffer[index : index + size]
62
+ try:
63
+ chunk.decode("utf-8")
64
+ except UnicodeDecodeError:
65
+ break
66
+ index += size
67
+ if index == 0:
68
+ return "", buffer
69
+ return buffer[:index].decode("utf-8"), buffer[index:]
70
+
71
+
72
+ def piece_to_bytes(piece: str, unicode_to_byte: Mapping[str, int]) -> bytes:
73
+ raw = bytearray()
74
+ for char in piece:
75
+ mapped = unicode_to_byte.get(char)
76
+ if mapped is None:
77
+ raw.extend(char.encode("utf-8"))
78
+ else:
79
+ raw.append(mapped)
80
+ return bytes(raw)
81
+
82
+
83
+ class IncrementalUtf8Decoder:
84
+ """Push token ids; read only complete Unicode as it becomes available."""
85
+
86
+ def __init__(
87
+ self,
88
+ tokenizer: Any,
89
+ *,
90
+ skip_special_ids: Iterable[int] | None = None,
91
+ ) -> None:
92
+ vocab = tokenizer.get_vocab()
93
+ self._id_to_piece = {tid: piece for piece, tid in vocab.items()}
94
+ self._unicode_to_byte = unicode_to_gpt2_bytes()
95
+ self._skip_special_ids = set(skip_special_ids or ())
96
+ self._pending = b""
97
+ self._text = ""
98
+
99
+ @property
100
+ def text(self) -> str:
101
+ return self._text
102
+
103
+ def reset(self) -> None:
104
+ self._pending = b""
105
+ self._text = ""
106
+
107
+ def token_bytes(self, token_id: int) -> bytes:
108
+ piece = self._id_to_piece.get(token_id, "")
109
+ return piece_to_bytes(piece, self._unicode_to_byte)
110
+
111
+ def push(self, token_id: int) -> str:
112
+ if token_id in self._skip_special_ids:
113
+ return ""
114
+ self._pending += self.token_bytes(token_id)
115
+ complete, self._pending = split_complete_utf8(self._pending)
116
+ self._text += complete
117
+ return complete
118
+
119
+ def push_many(self, token_ids: Sequence[int]) -> str:
120
+ delta = []
121
+ for token_id in token_ids:
122
+ piece = self.push(token_id)
123
+ if piece:
124
+ delta.append(piece)
125
+ return "".join(delta)
126
+
127
+ def finalize(self) -> str:
128
+ if self._pending:
129
+ self._text += self._pending.decode("utf-8", errors="replace")
130
+ self._pending = b""
131
+ return self._text
tiny_gdn/fugue.py ADDED
@@ -0,0 +1,1012 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fugue: masked-diffusion language models on a bidirectional Gated DeltaNet-2 hybrid.
2
+
3
+ Fugue keeps Senryu-2's block (4-branch Gated Residual stream, SiTU-GLU,
4
+ full-rank sigmoid output gates) and makes every token mixer bidirectional:
5
+
6
+ - Bidirectional GDN-2 reads each segment left to right and right to left over
7
+ one set of q/k/v, erase/write and output projections. Each direction has its
8
+ own short convolutions and low-rank decay (ViG). The backward scan runs on
9
+ the segment reversed in place, so both scans share ``cu_seqlens``, and it
10
+ reads its state before the current token (Hydra), so each token's own key
11
+ and value enter its output once.
12
+ - Bidirectional attention is grouped-query softmax attention with per-head
13
+ QK-norm, partial RoPE and a sigmoid output gate.
14
+
15
+ Masked diffusion (MDLM, LLaDA): each window draws a mask ratio t and every
16
+ token becomes ``<|mask|>`` with probability t. The model predicts the
17
+ originals; ``<|mask|>`` itself always gets probability zero. There is no time
18
+ input; the mask count carries it. The trainer picks the per-token weight: 1/t
19
+ for the ELBO, or 1 (unweighted masked cross-entropy). Evaluation always uses
20
+ the ELBO (docs/architecture.md, "Fugue").
21
+
22
+ Decoding runs equal-length canvases as a batch rather than packed, with no
23
+ host sync, so a CUDA graph can capture each call: ``canvas_logits`` scores
24
+ picked positions. It can also cache everything outside the block being
25
+ denoised (Fast-dLLM's DualCache): ``refresh_block`` runs the whole canvas once
26
+ and keeps, per layer, each scan's state and conv window where it enters the
27
+ block and the attention keys and values; ``block_logits`` then runs only the
28
+ block's tokens against them. Text outside the block keeps its features from
29
+ the refresh.
30
+
31
+ Fugue-1 (``fugue_block_size`` > 0) writes fixed-size blocks left to right
32
+ instead, over a causal clean stream whose cache is exact; ``tiny_gdn.fugue_block``
33
+ holds that mode's training forward and decoding, and the whole-sequence
34
+ methods here refuse it.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ import itertools
40
+ import math
41
+ from collections.abc import Callable, Iterator
42
+ from dataclasses import dataclass, field
43
+ from pathlib import Path
44
+ from typing import Any
45
+
46
+ import torch
47
+ import torch.nn.functional as F
48
+ from einops import rearrange, repeat
49
+ from fla.modules import FusedRMSNormGated, ShortConvolution
50
+ from fla.modules.rotary import rotary_embedding
51
+ from fla.ops.gdn2 import chunk_gdn2
52
+ from fla.ops.gdn2.fused_recurrent import fused_recurrent_gdn2_fwd
53
+ from safetensors.torch import save_model
54
+ from torch import nn
55
+ from torch.utils.checkpoint import checkpoint
56
+
57
+ from tiny_gdn.config import FUGUE_MODEL_TYPE, TinyGDNConfig
58
+ from tiny_gdn.fused_ops import weighted_linear_cross_entropy
59
+ from tiny_gdn.haiku_layers import SiTUGLU
60
+ from tiny_gdn.model import TinyGDNOutput
61
+ from tiny_gdn.nn_common import RMSNorm
62
+ from tiny_gdn.senryu2 import (
63
+ GatedResidual,
64
+ RotaryTables,
65
+ mark_adamw_only,
66
+ mark_gain,
67
+ mark_muon_row_blocks,
68
+ )
69
+
70
+ IGNORE_INDEX = -100
71
+ # Smallest mask ratio drawn in training; the 1/t weight stays below 1,000.
72
+ MIN_MASK_RATIO = 1e-3
73
+
74
+
75
+ # --------------------------------------------------------------------------- layout
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class SegmentLayout:
80
+ """Segments packed back to back in one row, and index maps within each segment."""
81
+
82
+ cu_seqlens: torch.Tensor
83
+ # Each position's mirror inside its segment; applying it twice is the identity.
84
+ reverse_index: torch.Tensor
85
+ # The next and previous position in the same segment; segment ends map to themselves.
86
+ next_index: torch.Tensor
87
+ previous_index: torch.Tensor
88
+ segment_start: torch.Tensor
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class ScanEdge:
93
+ """One scan direction of a GDN-2 layer where it enters the block: its state and q/k/v conv windows."""
94
+
95
+ state: torch.Tensor
96
+ convs: tuple[torch.Tensor, torch.Tensor, torch.Tensor]
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class GatedDeltaNetEdges:
101
+ forward: ScanEdge
102
+ backward: ScanEdge
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class AttentionContext:
107
+ """Rotated keys and values of the whole canvas; each block step overwrites the block's rows."""
108
+
109
+ keys: torch.Tensor
110
+ values: torch.Tensor
111
+
112
+
113
+ LayerCache = GatedDeltaNetEdges | AttentionContext
114
+
115
+
116
+ @dataclass
117
+ class BlockCapture:
118
+ """What a decoding forward keeps for later block-only steps.
119
+
120
+ Per scan direction, in scan order, ``lead`` marks the tokens the scan reads
121
+ before it reaches the block (the prefix forwards, the suffix backwards) and
122
+ ``lead_count`` counts them. Both are tensors, so one CUDA graph serves every block.
123
+ """
124
+
125
+ lead: tuple[torch.Tensor, torch.Tensor]
126
+ lead_count: tuple[torch.Tensor, torch.Tensor]
127
+ edges: dict[tuple[int, int], ScanEdge] = field(default_factory=dict)
128
+ attention: dict[int, AttentionContext] = field(default_factory=dict)
129
+
130
+ @classmethod
131
+ def around(cls, positions: torch.Tensor, length: int) -> BlockCapture:
132
+ """The capture for the block at consecutive ``positions`` of ``length``-token rows."""
133
+
134
+ scan = torch.arange(length, device=positions.device)
135
+ before = positions[0]
136
+ after = length - 1 - positions[-1]
137
+ return cls(lead=(scan < before, scan < after), lead_count=(before, after))
138
+
139
+
140
+ @dataclass(frozen=True)
141
+ class BlockCache:
142
+ """Everything outside the block ``[start, stop)`` as the last full forward saw it.
143
+
144
+ ``offsets`` (the block start per row) and ``positions`` are tensors, so a
145
+ CUDA graph captured against one cache serves every block of the canvas
146
+ once ``copy_`` has loaded that block's cache into it.
147
+ """
148
+
149
+ layers: tuple[LayerCache, ...]
150
+ rotary: tuple[torch.Tensor, torch.Tensor]
151
+ offsets: torch.Tensor
152
+ positions: torch.Tensor
153
+
154
+ @property
155
+ def rows(self) -> int:
156
+ return self.offsets.shape[0]
157
+
158
+ @property
159
+ def width(self) -> int:
160
+ return self.positions.shape[0]
161
+
162
+ def tensors(self) -> Iterator[torch.Tensor]:
163
+ for layer in self.layers:
164
+ if isinstance(layer, AttentionContext):
165
+ yield from (layer.keys, layer.values)
166
+ else:
167
+ for edge in (layer.forward, layer.backward):
168
+ yield edge.state
169
+ yield from edge.convs
170
+ yield from self.rotary
171
+ yield from (self.offsets, self.positions)
172
+
173
+ def clone(self) -> BlockCache:
174
+ def edge(source: ScanEdge) -> ScanEdge:
175
+ q, k, v = (conv.clone() for conv in source.convs)
176
+ return ScanEdge(state=source.state.clone(), convs=(q, k, v))
177
+
178
+ return BlockCache(
179
+ layers=tuple(
180
+ AttentionContext(keys=layer.keys.clone(), values=layer.values.clone())
181
+ if isinstance(layer, AttentionContext)
182
+ else GatedDeltaNetEdges(forward=edge(layer.forward), backward=edge(layer.backward))
183
+ for layer in self.layers
184
+ ),
185
+ rotary=(self.rotary[0].clone(), self.rotary[1].clone()),
186
+ offsets=self.offsets.clone(),
187
+ positions=self.positions.clone(),
188
+ )
189
+
190
+ def copy_(self, source: BlockCache) -> None:
191
+ for target, value in zip(self.tensors(), source.tensors(), strict=True):
192
+ target.copy_(value)
193
+
194
+
195
+ @dataclass
196
+ class FugueContext:
197
+ """Per-forward layout shared by every block.
198
+
199
+ Training and evaluation pack all rows into one ``[1, length]`` sequence
200
+ split by ``layout.cu_seqlens``. Decoding runs ``[rows, length]`` canvases
201
+ that are each one whole segment as a batch (``packed=False``), so no kernel
202
+ needs the boundaries on the host.
203
+ """
204
+
205
+ length: int
206
+ layout: SegmentLayout
207
+ # Set when every segment has this length, so attention runs as one batched call.
208
+ segment_length: int | None
209
+ rotary: tuple[torch.Tensor, torch.Tensor]
210
+ capture: BlockCapture | None = None
211
+ rows: int = 1
212
+ packed: bool = True
213
+
214
+ @property
215
+ def cu_seqlens(self) -> torch.Tensor | None:
216
+ return self.layout.cu_seqlens if self.packed else None
217
+
218
+
219
+ def whole_row_layout(length: int, device: torch.device) -> SegmentLayout:
220
+ """The layout of rows that are each one segment of ``length``, built without a host sync."""
221
+
222
+ positions = torch.arange(length, device=device)
223
+ return SegmentLayout(
224
+ cu_seqlens=torch.arange(2, device=device, dtype=torch.int32) * length,
225
+ reverse_index=positions.flip(0),
226
+ next_index=(positions + 1).clamp(max=length - 1),
227
+ previous_index=(positions - 1).clamp(min=0),
228
+ segment_start=positions.eq(0),
229
+ )
230
+
231
+
232
+ def segment_layout(lengths: torch.Tensor) -> SegmentLayout:
233
+ lengths = lengths.to(torch.long)
234
+ cu_seqlens = F.pad(lengths.cumsum(0), (1, 0))
235
+ total = int(cu_seqlens[-1])
236
+ positions = torch.arange(total, device=lengths.device)
237
+ segment = torch.repeat_interleave(torch.arange(lengths.numel(), device=lengths.device), lengths)
238
+ offset = positions - cu_seqlens[segment]
239
+ segment_start = offset.eq(0)
240
+ segment_end = offset.eq(lengths[segment] - 1)
241
+ return SegmentLayout(
242
+ cu_seqlens=cu_seqlens.to(torch.int32),
243
+ reverse_index=cu_seqlens[segment] + cu_seqlens[segment + 1] - 1 - positions,
244
+ next_index=torch.where(segment_end, positions, positions + 1),
245
+ previous_index=torch.where(segment_start, positions, positions - 1),
246
+ segment_start=segment_start,
247
+ )
248
+
249
+
250
+ # ------------------------------------------------------------------- token mixers
251
+
252
+
253
+ class BidirectionalGatedDeltaNet2(nn.Module):
254
+ """GDN-2 in both directions over shared projections, averaged before the output gate.
255
+
256
+ S_t = (I - k_t (b_t ⊙ k_t)ᵀ) Diag(exp g_t) S_{t-1} + k_t (w_t ⊙ v_t)ᵀ runs
257
+ left to right and right to left; o_t = ½ q_tᵀ (S→_t + S←_{t+1}). Index 0 of
258
+ each per-direction module or parameter is the forward scan, index 1 the
259
+ backward one.
260
+ """
261
+
262
+ def __init__(self, config: TinyGDNConfig, layer_index: int) -> None:
263
+ super().__init__()
264
+ hidden = config.hidden_size
265
+ self.layer_index = layer_index
266
+ self.num_heads = config.linear_num_heads
267
+ self.num_v_heads = config.linear_num_value_heads
268
+ self.head_k_dim = config.linear_head_dim
269
+ self.head_v_dim = int(config.linear_head_dim * config.linear_expand_v)
270
+ self.key_dim = self.num_heads * self.head_k_dim
271
+ self.value_dim = self.num_v_heads * self.head_v_dim
272
+ self.allow_neg_eigval = config.allow_negative_eigenvalues
273
+ kernel = config.linear_conv_kernel_dim
274
+
275
+ self.q_proj = nn.Linear(hidden, self.key_dim, bias=False)
276
+ self.k_proj = nn.Linear(hidden, self.key_dim, bias=False)
277
+ self.v_proj = nn.Linear(hidden, self.value_dim, bias=False)
278
+ self.q_conv1d = nn.ModuleList(
279
+ ShortConvolution(self.key_dim, kernel, bias=False, activation="silu") for _ in range(2)
280
+ )
281
+ self.k_conv1d = nn.ModuleList(
282
+ ShortConvolution(self.key_dim, kernel, bias=False, activation="silu") for _ in range(2)
283
+ )
284
+ self.v_conv1d = nn.ModuleList(
285
+ ShortConvolution(self.value_dim, kernel, bias=False, activation="silu") for _ in range(2)
286
+ )
287
+ self.f_proj = nn.ModuleList(
288
+ nn.Sequential(
289
+ nn.Linear(hidden, self.head_v_dim, bias=False),
290
+ nn.Linear(self.head_v_dim, self.key_dim, bias=False),
291
+ )
292
+ for _ in range(2)
293
+ )
294
+ self.b_proj = nn.Linear(hidden, self.key_dim, bias=False)
295
+ self.w_proj = nn.Linear(hidden, self.value_dim, bias=False)
296
+ self.A_log = nn.Parameter(torch.empty(2, self.num_heads, dtype=torch.float32).uniform_(1, 16).log())
297
+ dt = torch.exp(
298
+ torch.rand(2, self.key_dim, dtype=torch.float32) * (math.log(0.1) - math.log(0.001)) + math.log(0.001)
299
+ ).clamp(min=1e-4)
300
+ self.dt_bias = nn.Parameter(dt + torch.log(-torch.expm1(-dt)))
301
+ if config.linear_full_rank_gate:
302
+ self.g_proj: nn.Module = nn.Linear(hidden, self.value_dim, bias=True)
303
+ mark_adamw_only(self.g_proj.weight)
304
+ else:
305
+ self.g_proj = nn.Sequential(
306
+ nn.Linear(hidden, self.head_v_dim, bias=False),
307
+ nn.Linear(self.head_v_dim, self.value_dim, bias=True),
308
+ )
309
+ mark_adamw_only(*(layer.weight for layer in self.g_proj))
310
+ self.o_norm = FusedRMSNormGated(self.head_v_dim, activation="sigmoid", eps=config.rms_norm_eps)
311
+ self.o_proj = nn.Linear(self.value_dim, hidden, bias=False)
312
+
313
+ mark_gain(self.A_log, self.dt_bias)
314
+ mark_adamw_only(*(layer.weight for decay in self.f_proj for layer in decay))
315
+ for projection in (self.q_proj, self.k_proj, self.b_proj):
316
+ mark_muon_row_blocks(projection.weight, self.num_heads)
317
+ for projection in (self.v_proj, self.w_proj):
318
+ mark_muon_row_blocks(projection.weight, self.num_v_heads)
319
+
320
+ def convolutions(self, direction: int) -> tuple[ShortConvolution, ShortConvolution, ShortConvolution]:
321
+ return self.q_conv1d[direction], self.k_conv1d[direction], self.v_conv1d[direction]
322
+
323
+ def heads(
324
+ self,
325
+ direction: int,
326
+ q: torch.Tensor,
327
+ k: torch.Tensor,
328
+ v: torch.Tensor,
329
+ f: torch.Tensor,
330
+ b: torch.Tensor,
331
+ w: torch.Tensor,
332
+ ) -> tuple[torch.Tensor, ...]:
333
+ """Per-head ``(q, k, v, g, b, w)`` for the scan kernels, decay applied and key heads repeated."""
334
+
335
+ g = F.softplus(f + self.dt_bias[direction])
336
+ q, k, g, b = (rearrange(tensor, "... (h d) -> ... h d", d=self.head_k_dim) for tensor in (q, k, g, b))
337
+ v, w = (rearrange(tensor, "... (h d) -> ... h d", d=self.head_v_dim) for tensor in (v, w))
338
+ g = -self.A_log[direction].float().exp().unsqueeze(-1) * g
339
+ if self.num_v_heads > self.num_heads:
340
+ groups = self.num_v_heads // self.num_heads
341
+ q, k, g, b = (repeat(tensor, "... h d -> ... (h g) d", g=groups) for tensor in (q, k, g, b))
342
+ return q, k, v, g, b, w
343
+
344
+ def scan(
345
+ self,
346
+ direction: int,
347
+ hidden_states: torch.Tensor,
348
+ projected: tuple[torch.Tensor, ...],
349
+ layout: SegmentLayout,
350
+ cu_seqlens: torch.Tensor | None,
351
+ capture: BlockCapture | None = None,
352
+ ) -> torch.Tensor:
353
+ """One direction over every segment of ``layout``; ``cu_seqlens`` None scans rows that are one segment each."""
354
+
355
+ f = self.f_proj[direction](hidden_states).float()
356
+ q, k, v, f, b, w = (*projected[:3], f, *projected[3:])
357
+ if direction == 1:
358
+ q, k, v, f, b, w = (tensor.index_select(1, layout.reverse_index) for tensor in (q, k, v, f, b, w))
359
+ unconvolved = (q, k, v)
360
+ q_conv, k_conv, v_conv = self.convolutions(direction)
361
+ q, _ = q_conv(x=q, cu_seqlens=cu_seqlens)
362
+ k, _ = k_conv(x=k, cu_seqlens=cu_seqlens)
363
+ v, _ = v_conv(x=v, cu_seqlens=cu_seqlens)
364
+ if direction == 1:
365
+ # Each query reads the state one step early; its output moves back a step below.
366
+ q = q.index_select(1, layout.next_index)
367
+ q, k, v, g, b, w = self.heads(direction, q, k, v, f, b, w)
368
+ if capture is not None:
369
+ self._capture_edge(direction, unconvolved, (k, v, g, b, w), capture)
370
+ out, _ = chunk_gdn2(
371
+ q=q,
372
+ k=k,
373
+ v=v,
374
+ g=g,
375
+ b=b,
376
+ w=w,
377
+ initial_state=None,
378
+ output_final_state=False,
379
+ use_qk_l2norm_in_kernel=True,
380
+ cu_seqlens=cu_seqlens,
381
+ )
382
+ if direction == 0:
383
+ return out
384
+ out = out.index_select(1, layout.previous_index).masked_fill(layout.segment_start[None, :, None, None], 0.0)
385
+ return out.index_select(1, layout.reverse_index)
386
+
387
+ def _capture_edge(
388
+ self,
389
+ direction: int,
390
+ unconvolved: tuple[torch.Tensor, torch.Tensor, torch.Tensor],
391
+ scanned: tuple[torch.Tensor, ...],
392
+ capture: BlockCapture,
393
+ ) -> None:
394
+ """Keep this direction's state and conv windows after the tokens it reads before the block.
395
+
396
+ The scan reruns over the whole row with zero key and decay past the
397
+ lead, which leaves the state unchanged, so its shape is the same for
398
+ every block.
399
+ """
400
+
401
+ lead, count = capture.lead[direction], capture.lead_count[direction]
402
+ width = self.q_conv1d[direction].kernel_size[0]
403
+ # A conv window holds the last `width` inputs, oldest first, zeros before the row starts.
404
+ slots = count - width + torch.arange(width, device=lead.device)
405
+ present = slots.ge(0)[None, :, None]
406
+ q_window, k_window, v_window = (
407
+ (tensor.index_select(1, slots.clamp(min=0)) * present).transpose(1, 2).contiguous()
408
+ for tensor in unconvolved
409
+ )
410
+ k, v, g, b, w = scanned
411
+ keep = lead[None, :, None, None]
412
+ k = k * keep
413
+ _, state = chunk_gdn2(
414
+ q=k, k=k, v=v, g=g * keep, b=b, w=w, output_final_state=True, use_qk_l2norm_in_kernel=True
415
+ )
416
+ capture.edges[(self.layer_index, direction)] = ScanEdge(state=state, convs=(q_window, k_window, v_window))
417
+
418
+ def scan_block(
419
+ self,
420
+ direction: int,
421
+ hidden_states: torch.Tensor,
422
+ projected: tuple[torch.Tensor, ...],
423
+ edge: ScanEdge,
424
+ ) -> torch.Tensor:
425
+ """``scan`` over ``[rows, width]`` block tokens, entering from ``edge``."""
426
+
427
+ f = self.f_proj[direction](hidden_states).float()
428
+ q, k, v, f, b, w = (*projected[:3], f, *projected[3:])
429
+ if direction == 1:
430
+ q, k, v, f, b, w = (tensor.flip(1) for tensor in (q, k, v, f, b, w))
431
+ # The kernels update conv windows in place; the cache must stay as the refresh left it.
432
+ q_conv, k_conv, v_conv = self.convolutions(direction)
433
+ q, _ = q_conv(x=q, cache=edge.convs[0].clone())
434
+ k, _ = k_conv(x=k, cache=edge.convs[1].clone())
435
+ v, _ = v_conv(x=v, cache=edge.convs[2].clone())
436
+ q, k, v, g, b, w = self.heads(direction, q, k, v, f, b, w)
437
+ if direction == 1:
438
+ # A leading slot with zero key, value and decay passes the edge state
439
+ # through, so query i reads the state before token i (Hydra).
440
+ q = F.pad(q, (0, 0, 0, 0, 0, 1))
441
+ k, v, g, b, w = (F.pad(tensor, (0, 0, 0, 0, 1, 0)) for tensor in (k, v, g, b, w))
442
+ out, _ = fused_recurrent_gdn2_fwd(
443
+ q=q.contiguous(),
444
+ k=k.contiguous(),
445
+ v=v.contiguous(),
446
+ g=g.contiguous(),
447
+ b=b.contiguous(),
448
+ w=w.contiguous(),
449
+ initial_state=edge.state,
450
+ use_qk_l2norm_in_kernel=True,
451
+ )
452
+ if direction == 0:
453
+ return out
454
+ return out[:, :-1].flip(1)
455
+
456
+ def project(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, ...]:
457
+ b = self.b_proj(hidden_states).sigmoid()
458
+ if self.allow_neg_eigval:
459
+ b = b * 2.0
460
+ return (
461
+ self.q_proj(hidden_states),
462
+ self.k_proj(hidden_states),
463
+ self.v_proj(hidden_states),
464
+ b,
465
+ self.w_proj(hidden_states).sigmoid(),
466
+ )
467
+
468
+ def gated_output(self, out: torch.Tensor, hidden_states: torch.Tensor) -> torch.Tensor:
469
+ gate = rearrange(self.g_proj(hidden_states), "... (h d) -> ... h d", d=self.head_v_dim)
470
+ out = self.o_norm(out, gate)
471
+ return self.o_proj(rearrange(out, "b t h d -> b t (h d)"))
472
+
473
+ def forward(self, hidden_states: torch.Tensor, context: FugueContext) -> torch.Tensor:
474
+ projected = self.project(hidden_states)
475
+ out = 0.5 * (
476
+ self.scan(0, hidden_states, projected, context.layout, context.cu_seqlens, context.capture)
477
+ + self.scan(1, hidden_states, projected, context.layout, context.cu_seqlens, context.capture)
478
+ )
479
+ return self.gated_output(out, hidden_states)
480
+
481
+ def forward_block(self, hidden_states: torch.Tensor, cache: LayerCache, block: BlockCache) -> torch.Tensor:
482
+ """``forward`` for ``[rows, width]`` block tokens between the cached edges."""
483
+
484
+ if not isinstance(cache, GatedDeltaNetEdges):
485
+ raise TypeError(f"layer {self.layer_index} needs GDN-2 edges, got {type(cache).__name__}")
486
+ projected = self.project(hidden_states)
487
+ out = 0.5 * (
488
+ self.scan_block(0, hidden_states, projected, cache.forward)
489
+ + self.scan_block(1, hidden_states, projected, cache.backward)
490
+ )
491
+ return self.gated_output(out, hidden_states)
492
+
493
+
494
+ class BidirectionalAttention(nn.Module):
495
+ """Grouped-query softmax attention over each whole segment.
496
+
497
+ Per-head zero-centered RMSNorm on q and k, partial RoPE, and a full-rank
498
+ sigmoid output gate, as in Senryu-2's Parallax layers without the causal mask.
499
+ """
500
+
501
+ def __init__(self, config: TinyGDNConfig, layer_index: int) -> None:
502
+ super().__init__()
503
+ self.layer_index = layer_index
504
+ self.num_heads = config.num_attention_heads
505
+ self.num_kv_heads = config.num_key_value_heads
506
+ self.head_dim = config.attention_head_dim
507
+ self.scale = self.head_dim**-0.5
508
+ query_width = self.num_heads * self.head_dim
509
+ kv_width = self.num_kv_heads * self.head_dim
510
+ hidden = config.hidden_size
511
+ self.q_proj = nn.Linear(hidden, query_width, bias=False)
512
+ self.k_proj = nn.Linear(hidden, kv_width, bias=False)
513
+ self.v_proj = nn.Linear(hidden, kv_width, bias=False)
514
+ self.g_proj = nn.Linear(hidden, query_width, bias=True)
515
+ self.o_proj = nn.Linear(query_width, hidden, bias=False)
516
+ self.q_norm = RMSNorm(self.head_dim, config.rms_norm_eps)
517
+ self.k_norm = RMSNorm(self.head_dim, config.rms_norm_eps)
518
+ mark_muon_row_blocks(self.q_proj.weight, self.num_heads)
519
+ for projection in (self.k_proj, self.v_proj):
520
+ mark_muon_row_blocks(projection.weight, self.num_kv_heads)
521
+ mark_adamw_only(self.g_proj.weight)
522
+
523
+ def attend(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
524
+ """``[n, T, H, D]`` queries over ``[n, T, H_kv, D]`` keys/values, no mask."""
525
+
526
+ out = F.scaled_dot_product_attention(
527
+ q.transpose(1, 2),
528
+ k.transpose(1, 2),
529
+ v.transpose(1, 2),
530
+ scale=self.scale,
531
+ enable_gqa=self.num_kv_heads != self.num_heads,
532
+ )
533
+ return out.transpose(1, 2)
534
+
535
+ def forward(self, hidden_states: torch.Tensor, context: FugueContext) -> torch.Tensor:
536
+ rows, length = hidden_states.shape[:2]
537
+ q = self.q_norm(self.q_proj(hidden_states).view(rows, length, self.num_heads, self.head_dim))
538
+ k = self.k_norm(self.k_proj(hidden_states).view(rows, length, self.num_kv_heads, self.head_dim))
539
+ v = self.v_proj(hidden_states).view(rows, length, self.num_kv_heads, self.head_dim)
540
+ cosine, sine = context.rotary
541
+ q, k = (
542
+ rotary_embedding(tensor, cosine, sine, seqlen_offsets=0, cu_seqlens=context.cu_seqlens)
543
+ for tensor in (q, k)
544
+ )
545
+ if context.capture is not None:
546
+ context.capture.attention[self.layer_index] = AttentionContext(keys=k, values=v)
547
+ if context.segment_length is not None:
548
+ segments = rows * length // context.segment_length
549
+ out = self.attend(
550
+ q.view(segments, context.segment_length, self.num_heads, self.head_dim),
551
+ k.view(segments, context.segment_length, self.num_kv_heads, self.head_dim),
552
+ v.view(segments, context.segment_length, self.num_kv_heads, self.head_dim),
553
+ ).reshape(rows, length, self.num_heads, self.head_dim)
554
+ else:
555
+ bounds = context.layout.cu_seqlens.tolist()
556
+ out = torch.cat(
557
+ [
558
+ self.attend(q[:, start:stop], k[:, start:stop], v[:, start:stop])
559
+ for start, stop in itertools.pairwise(bounds)
560
+ ],
561
+ dim=1,
562
+ )
563
+ gate = torch.sigmoid(self.g_proj(hidden_states)).view(rows, length, self.num_heads, self.head_dim)
564
+ return self.o_proj((out * gate).reshape(rows, length, self.num_heads * self.head_dim))
565
+
566
+ def forward_block(self, hidden_states: torch.Tensor, cache: LayerCache, block: BlockCache) -> torch.Tensor:
567
+ """``[rows, width]`` block queries over the cached canvas, with the block's own keys and values."""
568
+
569
+ if not isinstance(cache, AttentionContext):
570
+ raise TypeError(f"layer {self.layer_index} needs attention context, got {type(cache).__name__}")
571
+ rows, width = hidden_states.shape[:2]
572
+ q = self.q_norm(self.q_proj(hidden_states).view(rows, width, self.num_heads, self.head_dim))
573
+ k = self.k_norm(self.k_proj(hidden_states).view(rows, width, self.num_kv_heads, self.head_dim))
574
+ v = self.v_proj(hidden_states).view(rows, width, self.num_kv_heads, self.head_dim)
575
+ cosine, sine = block.rotary
576
+ q, k = (rotary_embedding(tensor, cosine, sine, seqlen_offsets=block.offsets) for tensor in (q, k))
577
+ cache.keys.index_copy_(1, block.positions, k)
578
+ cache.values.index_copy_(1, block.positions, v)
579
+ out = self.attend(q, cache.keys, cache.values)
580
+ gate = torch.sigmoid(self.g_proj(hidden_states)).view(rows, width, self.num_heads, self.head_dim)
581
+ return self.o_proj((out * gate).reshape(rows, width, self.num_heads * self.head_dim))
582
+
583
+
584
+ # ------------------------------------------------------------------------- blocks
585
+
586
+
587
+ class FugueBlock(nn.Module):
588
+ def __init__(self, config: TinyGDNConfig, layer_index: int) -> None:
589
+ super().__init__()
590
+ self.layer_index = layer_index
591
+ self.layer_type = config.layer_types[layer_index]
592
+ residual = (config.residual_branches, config.hidden_size, config.residual_gate_rank, config.rms_norm_eps)
593
+ self.mixer_residual = GatedResidual(*residual, write=True)
594
+ self.mlp_residual = GatedResidual(*residual, write=True)
595
+ if self.layer_type == "bigdn2":
596
+ self.token_mixer: nn.Module = BidirectionalGatedDeltaNet2(config, layer_index)
597
+ elif self.layer_type == "bidirectional":
598
+ self.token_mixer = BidirectionalAttention(config, layer_index)
599
+ else:
600
+ raise ValueError(f"Fugue has no {self.layer_type} layers")
601
+ self.mlp = SiTUGLU(config)
602
+ mark_muon_row_blocks(self.mlp.gate_up_proj.weight, 2)
603
+
604
+ def mix(self, residual: torch.Tensor, mixer: Callable[[torch.Tensor], torch.Tensor]) -> torch.Tensor:
605
+ hidden_states, scales = self.mixer_residual.read(residual)
606
+ residual = GatedResidual.write(residual, scales, mixer(hidden_states).reshape(hidden_states.shape))
607
+ hidden_states, scales = self.mlp_residual.read(residual)
608
+ return GatedResidual.write(residual, scales, self.mlp(hidden_states))
609
+
610
+ def forward(self, residual: torch.Tensor, context: FugueContext) -> torch.Tensor:
611
+ return self.mix(
612
+ residual, lambda hidden: self.token_mixer(hidden.view(context.rows, context.length, -1), context)
613
+ )
614
+
615
+ def forward_block(self, residual: torch.Tensor, cache: LayerCache, block: BlockCache) -> torch.Tensor:
616
+ return self.mix(
617
+ residual,
618
+ lambda hidden: self.token_mixer.forward_block(hidden.view(block.rows, block.width, -1), cache, block),
619
+ )
620
+
621
+
622
+ # -------------------------------------------------------------------------- noise
623
+
624
+
625
+ def stratified_mask_ratios(
626
+ count: int,
627
+ *,
628
+ generator: torch.Generator,
629
+ device: torch.device | str,
630
+ minimum: float = MIN_MASK_RATIO,
631
+ ) -> torch.Tensor:
632
+ """``count`` low-discrepancy mask ratios in ``[minimum, 1]``: one uniform offset, evenly spaced."""
633
+
634
+ offset = torch.rand((), generator=generator, device=device)
635
+ strata = (offset + torch.arange(count, device=device, dtype=torch.float32) / count) % 1.0
636
+ order = torch.randperm(count, generator=generator, device=device)
637
+ return minimum + (1.0 - minimum) * strata[order]
638
+
639
+
640
+ LOSS_WEIGHTINGS = ("elbo", "unweighted")
641
+
642
+
643
+ def mask_tokens(
644
+ tokens: torch.Tensor,
645
+ valid: torch.Tensor,
646
+ ratios: torch.Tensor,
647
+ *,
648
+ mask_token_id: int,
649
+ generator: torch.Generator,
650
+ weighting: str = "elbo",
651
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
652
+ """Forward noise: ``(noisy ids, labels with -100 where kept, per-token weights)`` for ``[B, T]`` tokens.
653
+
654
+ Weights are 1/t for the ELBO and 1 for unweighted masked cross-entropy.
655
+ """
656
+
657
+ if weighting not in LOSS_WEIGHTINGS:
658
+ raise ValueError(f"weighting must be one of: {', '.join(LOSS_WEIGHTINGS)}")
659
+ draws = torch.rand(tokens.shape, generator=generator, device=tokens.device)
660
+ masked = (draws < ratios[:, None]) & valid
661
+ noisy = torch.where(masked, torch.full_like(tokens, mask_token_id), tokens)
662
+ labels = torch.where(masked, tokens, torch.full_like(tokens, IGNORE_INDEX))
663
+ row_weights = 1.0 / ratios if weighting == "elbo" else torch.ones_like(ratios)
664
+ return noisy, labels, row_weights[:, None].expand_as(tokens)
665
+
666
+
667
+ def mask_exact_counts(
668
+ tokens: torch.Tensor,
669
+ valid: torch.Tensor,
670
+ counts: torch.Tensor,
671
+ *,
672
+ mask_token_id: int,
673
+ generator: torch.Generator,
674
+ ) -> tuple[torch.Tensor, torch.Tensor]:
675
+ """Mask exactly ``counts[b]`` valid positions of each row, uniformly: ``(noisy ids, labels)``.
676
+
677
+ With ``l ~ U{1..L}`` masked and weight L/l, this is LLaDA's lower-variance
678
+ ELBO estimator; per token it is the mean CE over the masked positions.
679
+ """
680
+
681
+ draws = torch.rand(tokens.shape, generator=generator, device=tokens.device).masked_fill(~valid, 2.0)
682
+ ranks = draws.argsort(dim=1).argsort(dim=1)
683
+ masked = ranks < counts[:, None]
684
+ noisy = torch.where(masked, torch.full_like(tokens, mask_token_id), tokens)
685
+ labels = torch.where(masked, tokens, torch.full_like(tokens, IGNORE_INDEX))
686
+ return noisy, labels
687
+
688
+
689
+ # -------------------------------------------------------------------------- model
690
+
691
+
692
+ class FugueForMaskedDiffusion(nn.Module):
693
+ """Bidirectional GDN-2 hybrid that predicts the tokens hidden behind ``<|mask|>``."""
694
+
695
+ def __init__(self, config: TinyGDNConfig) -> None:
696
+ super().__init__()
697
+ if config.model_type != FUGUE_MODEL_TYPE or config.mask_token_id is None:
698
+ raise ValueError("FugueForMaskedDiffusion needs a Fugue config with a mask_token_id")
699
+ self.config = config
700
+ self.mask_token_id = config.mask_token_id
701
+ # Fugue-1's block size; 0 for whole-sequence denoising (Rondo).
702
+ self.block_size = config.fugue_block_size
703
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
704
+ self.layers = nn.ModuleList(FugueBlock(config, index) for index in range(config.num_hidden_layers))
705
+ self.final_residual = GatedResidual(
706
+ config.residual_branches,
707
+ config.hidden_size,
708
+ config.residual_gate_rank,
709
+ config.rms_norm_eps,
710
+ write=False,
711
+ )
712
+ self.rotary = RotaryTables(config.rotary_dim, config.rope_theta)
713
+ self.checkpointed_layers: frozenset[int] = frozenset()
714
+ self.apply(self._initialize_module)
715
+ self._initialize_residual_projections()
716
+
717
+ # ---------------------------------------------------------------- setup
718
+ def _initialize_module(self, module: nn.Module) -> None:
719
+ if isinstance(module, nn.Linear):
720
+ nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
721
+ if module.bias is not None:
722
+ nn.init.zeros_(module.bias)
723
+ elif isinstance(module, nn.Embedding):
724
+ nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
725
+
726
+ def _initialize_residual_projections(self) -> None:
727
+ residual_std = self.config.initializer_range / math.sqrt(2 * self.config.num_hidden_layers)
728
+ for layer in self.layers:
729
+ nn.init.normal_(layer.token_mixer.o_proj.weight, mean=0.0, std=residual_std)
730
+ nn.init.normal_(layer.mlp.down_proj.weight, mean=0.0, std=residual_std)
731
+
732
+ def enable_gradient_checkpointing(self, enabled: bool = True, *, layers: int | None = None) -> None:
733
+ """Recompute block activations in backward; ``layers`` picks GDN-2 blocks first."""
734
+
735
+ if not enabled:
736
+ if layers is not None:
737
+ raise ValueError("layers needs gradient checkpointing enabled")
738
+ self.checkpointed_layers = frozenset()
739
+ return
740
+ count = len(self.layers) if layers is None else layers
741
+ if not 1 <= count <= len(self.layers):
742
+ raise ValueError(f"layers must be between 1 and {len(self.layers)}")
743
+ cheapest_replay_first = sorted(
744
+ range(len(self.layers)),
745
+ key=lambda index: self.layers[index].layer_type != "bigdn2",
746
+ )
747
+ self.checkpointed_layers = frozenset(cheapest_replay_first[:count])
748
+
749
+ def describe_gradient_checkpointing(self) -> str:
750
+ if not self.checkpointed_layers:
751
+ return "off"
752
+ layer_types = sorted({self.layers[index].layer_type for index in self.checkpointed_layers})
753
+ return f"{len(self.checkpointed_layers)}/{len(self.layers)} blocks ({', '.join(layer_types)})"
754
+
755
+ def project_to_vocabulary(self, hidden_states: torch.Tensor) -> torch.Tensor:
756
+ """Logits over the vocabulary; ``<|mask|>`` is never a prediction (logit -inf)."""
757
+
758
+ logits = F.linear(hidden_states, self.embed_tokens.weight)
759
+ logits[..., self.mask_token_id] = float("-inf")
760
+ return logits
761
+
762
+ # -------------------------------------------------------------- forward
763
+ def _require_whole_sequence(self) -> None:
764
+ if self.block_size:
765
+ raise ValueError(
766
+ "this Fugue model denoises blocks over a causal clean stream "
767
+ f"(fugue_block_size={self.block_size}); train and decode it with tiny_gdn.fugue_block"
768
+ )
769
+
770
+ def _run_block(self, index: int, residual: torch.Tensor, context: FugueContext) -> torch.Tensor:
771
+ layer = self.layers[index]
772
+ if self.training and index in self.checkpointed_layers:
773
+ return checkpoint(layer, residual, context, use_reentrant=False)
774
+ return layer(residual, context)
775
+
776
+ def forward(
777
+ self,
778
+ input_ids: torch.Tensor,
779
+ attention_mask: torch.Tensor | None = None,
780
+ *,
781
+ labels: torch.Tensor | None = None,
782
+ label_weights: torch.Tensor | None = None,
783
+ loss_normalizer: float | None = None,
784
+ z_loss_coefficient: float = 0.0,
785
+ return_logits: bool = True,
786
+ logits_mask: torch.Tensor | None = None,
787
+ return_hidden_states: bool = False,
788
+ ) -> TinyGDNOutput:
789
+ """Score masked positions of ``[B, T]`` rows; each row is one bidirectional segment.
790
+
791
+ ``labels`` holds the original token at scored positions and -100
792
+ elsewhere; ``loss = Σ w·CE / loss_normalizer`` (default: the number of
793
+ scored positions). ``logits_mask`` limits logits to its true positions
794
+ and returns them flattened ``[selected, vocab]`` in row-major order.
795
+ """
796
+
797
+ self._require_whole_sequence()
798
+ if input_ids.ndim != 2:
799
+ raise ValueError("input_ids must have shape [batch, sequence]")
800
+ batch, length = input_ids.shape
801
+ if length > self.config.max_position_embeddings:
802
+ raise ValueError("Input exceeds max_position_embeddings")
803
+ device = input_ids.device
804
+ if attention_mask is None or bool(attention_mask.all()):
805
+ index = None
806
+ lengths = torch.full((batch,), length, dtype=torch.long, device=device)
807
+ segment_length: int | None = length
808
+ else:
809
+ if attention_mask.shape != input_ids.shape:
810
+ raise ValueError("attention_mask must match input_ids")
811
+ valid = attention_mask.reshape(-1).bool()
812
+ index = valid.nonzero().flatten()
813
+ lengths = attention_mask.bool().sum(dim=1)
814
+ if bool((lengths == 0).any()):
815
+ raise ValueError("every row needs at least one unmasked token")
816
+ segment_length = None
817
+
818
+ def pack(tensor: torch.Tensor) -> torch.Tensor:
819
+ flat = tensor.reshape(batch * length, *tensor.shape[2:])
820
+ return flat if index is None else flat.index_select(0, index)
821
+
822
+ tokens = pack(input_ids)
823
+ embeddings = self.embed_tokens(tokens)
824
+ context = FugueContext(
825
+ length=tokens.numel(),
826
+ layout=segment_layout(lengths),
827
+ segment_length=segment_length,
828
+ rotary=self.rotary.get(tokens.numel(), device, embeddings.dtype),
829
+ )
830
+ residual = embeddings.unsqueeze(1).expand(-1, self.config.residual_branches, -1)
831
+ for layer_index in range(len(self.layers)):
832
+ residual = self._run_block(layer_index, residual, context)
833
+ hidden_states, _ = self.final_residual.read(residual)
834
+
835
+ total_loss = main_loss = z_loss = None
836
+ if labels is not None:
837
+ if labels.shape != input_ids.shape:
838
+ raise ValueError("labels must match input_ids")
839
+ targets = pack(labels)
840
+ scored = targets.ne(IGNORE_INDEX).nonzero().flatten()
841
+ weights = (
842
+ torch.ones(scored.numel(), dtype=torch.float32, device=device)
843
+ if label_weights is None
844
+ else pack(label_weights).index_select(0, scored).float()
845
+ )
846
+ normalizer = float(scored.numel()) if loss_normalizer is None else float(loss_normalizer)
847
+ if scored.numel() == 0:
848
+ total_loss = hidden_states.sum() * 0.0
849
+ main_loss = total_loss.detach()
850
+ z_loss = total_loss.detach()
851
+ else:
852
+ objective, losses, log_partitions = weighted_linear_cross_entropy(
853
+ hidden_states.index_select(0, scored),
854
+ self.embed_tokens.weight,
855
+ targets.index_select(0, scored),
856
+ weights,
857
+ z_loss_coefficient=z_loss_coefficient,
858
+ excluded_token_id=self.mask_token_id,
859
+ )
860
+ total_loss = objective / max(normalizer, 1.0)
861
+ main_loss = (weights * losses).sum() / max(normalizer, 1.0)
862
+ z_loss = log_partitions.square().mean()
863
+
864
+ logits = None
865
+ if return_logits:
866
+ if logits_mask is not None:
867
+ selected = pack(logits_mask).nonzero().flatten()
868
+ logits = self.project_to_vocabulary(hidden_states.index_select(0, selected))
869
+ elif index is None:
870
+ logits = self.project_to_vocabulary(hidden_states).view(batch, length, -1)
871
+ else:
872
+ full = hidden_states.new_zeros(batch * length, hidden_states.shape[-1])
873
+ full.index_copy_(0, index, hidden_states)
874
+ logits = self.project_to_vocabulary(full).view(batch, length, -1)
875
+ return TinyGDNOutput(
876
+ loss=total_loss,
877
+ logits=logits,
878
+ main_loss=main_loss,
879
+ mtp_loss=None,
880
+ z_loss=z_loss if z_loss_coefficient > 0.0 else None,
881
+ hidden_states=hidden_states if return_hidden_states else None,
882
+ )
883
+
884
+ # ------------------------------------------------------------- decoding
885
+ def _decode(self, canvas: torch.Tensor, capture: BlockCapture | None) -> torch.Tensor:
886
+ """Final hidden states ``[rows, T, hidden]`` of unpadded canvases, run as a batch without host syncs."""
887
+
888
+ self._require_whole_sequence()
889
+ rows, length = canvas.shape
890
+ if length > self.config.max_position_embeddings:
891
+ raise ValueError("Input exceeds max_position_embeddings")
892
+ embeddings = self.embed_tokens(canvas.reshape(-1))
893
+ context = FugueContext(
894
+ length=length,
895
+ layout=whole_row_layout(length, canvas.device),
896
+ segment_length=length,
897
+ rotary=self.rotary.get(length, canvas.device, embeddings.dtype),
898
+ capture=capture,
899
+ rows=rows,
900
+ packed=False,
901
+ )
902
+ residual = embeddings.unsqueeze(1).expand(-1, self.config.residual_branches, -1)
903
+ for layer in self.layers:
904
+ residual = layer(residual, context)
905
+ hidden_states, _ = self.final_residual.read(residual)
906
+ return hidden_states.view(rows, length, -1)
907
+
908
+ def canvas_logits(self, canvas: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
909
+ """Logits ``[rows, picks, vocab]`` at ``positions`` ``[rows, picks]`` of ``[rows, T]`` canvases."""
910
+
911
+ hidden_states = self._decode(canvas, None)
912
+ picked = hidden_states.gather(1, positions.unsqueeze(-1).expand(-1, -1, hidden_states.shape[-1]))
913
+ return self.project_to_vocabulary(picked)
914
+
915
+ def refresh_block(self, canvas: torch.Tensor, positions: torch.Tensor) -> tuple[torch.Tensor, BlockCache]:
916
+ """Full forward of ``[rows, T]`` canvases: logits ``[rows, width, vocab]`` for the block and its cache.
917
+
918
+ ``positions`` holds the block's consecutive canvas positions, after at
919
+ least one token. The cache keeps ``positions`` itself, so a graph that
920
+ refreshes from a static tensor caches a block that later calls can move.
921
+ """
922
+
923
+ rows, length = canvas.shape
924
+ capture = BlockCapture.around(positions, length)
925
+ hidden_states = self._decode(canvas, capture)
926
+ logits = self.project_to_vocabulary(hidden_states.index_select(1, positions))
927
+ layers: list[LayerCache] = []
928
+ for index, layer in enumerate(self.layers):
929
+ if layer.layer_type == "bigdn2":
930
+ layers.append(GatedDeltaNetEdges(forward=capture.edges[(index, 0)], backward=capture.edges[(index, 1)]))
931
+ else:
932
+ layers.append(capture.attention[index])
933
+ cache = BlockCache(
934
+ layers=tuple(layers),
935
+ rotary=self.rotary.get(length, canvas.device, self.embed_tokens.weight.dtype),
936
+ offsets=positions[:1].to(torch.int32).repeat(rows),
937
+ positions=positions,
938
+ )
939
+ return logits, cache
940
+
941
+ def block_logits(self, block_ids: torch.Tensor, cache: BlockCache) -> torch.Tensor:
942
+ """Logits ``[rows, width, vocab]`` for the block's current ``[rows, width]`` tokens against ``cache``."""
943
+
944
+ self._require_whole_sequence()
945
+ if block_ids.shape != (cache.rows, cache.width):
946
+ raise ValueError("block_ids must match the cached block")
947
+ embeddings = self.embed_tokens(block_ids.reshape(-1))
948
+ residual = embeddings.unsqueeze(1).expand(-1, self.config.residual_branches, -1)
949
+ for layer, layer_cache in zip(self.layers, cache.layers, strict=True):
950
+ residual = layer.forward_block(residual, layer_cache, cache)
951
+ hidden_states, _ = self.final_residual.read(residual)
952
+ return self.project_to_vocabulary(hidden_states).view(cache.rows, cache.width, -1)
953
+
954
+ # ------------------------------------------------------------ reporting
955
+ def parameter_report(self) -> dict[str, int]:
956
+ total = sum(parameter.numel() for parameter in self.parameters())
957
+ embeddings = self.embed_tokens.weight.numel()
958
+ return {
959
+ "deployable_core": total,
960
+ "training_total": total,
961
+ "embedding": embeddings,
962
+ "mtp_auxiliary": 0,
963
+ "non_embedding_core": total - embeddings,
964
+ "ngram_table": 0,
965
+ }
966
+
967
+ def save_checkpoint(self, output_dir: Path) -> None:
968
+ output_dir.mkdir(parents=True, exist_ok=True)
969
+ self.config.save_json(output_dir / "config.json")
970
+ save_model(self, output_dir / "model.safetensors")
971
+
972
+ def extra_repr(self) -> str:
973
+ return f"core_parameters={self.parameter_report()['deployable_core']:,}"
974
+
975
+ def get_architecture_metadata(self) -> dict[str, Any]:
976
+ if self.block_size:
977
+ objective = [
978
+ (
979
+ f"block diffusion over {self.block_size}-token blocks: a causal clean stream with a next-token "
980
+ "loss, and a noisy stream denoising each block against the clean prefix (exact block cache)"
981
+ ),
982
+ "token editing: corrupted visible tokens are trained back to the originals",
983
+ "3:1 Gated DeltaNet-2 + attention hybrid, bidirectional inside the active block",
984
+ (
985
+ "GDN-2 noisy reads: the clean forward state where the block starts, plus forward and "
986
+ "backward scans inside the block (exclusive backward read, Hydra)"
987
+ ),
988
+ ]
989
+ else:
990
+ objective = [
991
+ "masked diffusion (MDLM/LLaDA, SUBS: mask never predicted, low-discrepancy mask ratios, no time input)",
992
+ "3:1 bidirectional Gated DeltaNet-2 + bidirectional attention hybrid",
993
+ (
994
+ "bidirectional GDN-2: shared projections, per-direction short convs and low-rank decay, "
995
+ "exclusive backward read (Hydra)"
996
+ ),
997
+ ]
998
+ return {
999
+ "architecture": self.config.architecture,
1000
+ "layer_types": list(self.config.layer_types),
1001
+ "effective_num_layers": self.config.effective_num_layers,
1002
+ "shared_layer_indices": [],
1003
+ "parameter_report": self.parameter_report(),
1004
+ "features": [
1005
+ *objective,
1006
+ "4-branch Gated Residual stream (gated read, per-branch write scales)",
1007
+ "full-rank sigmoid output gates",
1008
+ "partial RoPE, per-head QK normalization",
1009
+ "SiTU-GLU",
1010
+ "tied input-output embeddings",
1011
+ ],
1012
+ }
tiny_gdn/fugue_sampling.py ADDED
@@ -0,0 +1,654 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generation for Fugue masked-diffusion models.
2
+
3
+ The continuation starts as ``<|mask|>`` tokens after the prompt. Each model
4
+ call scores masked positions and reveals some of them, one block at a time:
5
+ the whole continuation (``block_length=None``) or LLaDA's semi-autoregressive
6
+ blocks, left to right. A call reveals, in one of three ways:
7
+
8
+ - ``random`` order with ``steps_per_block`` steps: an even share of the block
9
+ at uniformly random positions, the reverse of the i.i.d. masking Fugue
10
+ trains on (MDLM's sampler). Fewer steps reveal more tokens per call, each
11
+ sampled without seeing the others.
12
+ - ``random`` order with ``draft_tokens``: exact self-speculative decoding. It
13
+ samples what random order revealing one token per call would, but each call
14
+ checks up to ``draft_tokens`` tokens that the previous call drafted, with
15
+ Leviathan et al.'s (2023) rejection rule along the random order. Draft i is
16
+ checked against row i of one batched forward, which holds drafts 0..i-1.
17
+ - ``confidence`` order: each token whose sampled probability clears
18
+ ``threshold`` and at least the single most confident one (Fast-dLLM); with
19
+ ``threshold=None``, the most confident even share per step.
20
+
21
+ Confidence order lets an undertrained model copy its likeliest phrase into
22
+ loops (docs/architecture.md, "Fugue"); random order keeps it on the training
23
+ distribution.
24
+
25
+ With ``block_cache``, a call runs the whole canvas and caches everything
26
+ outside the block (Fast-dLLM's DualCache, see
27
+ ``FugueForMaskedDiffusion.refresh_block``); the next calls run only the block
28
+ until a row has revealed ``refresh_tokens`` more of it. Text outside the block
29
+ keeps its features from the last full pass in between. Rondo's samples matched
30
+ exact decoding within noise when refreshed every 8 tokens; refreshed only at
31
+ block starts, their perplexity under Monody rose by a fifth.
32
+
33
+ With ``cuda_graphs``, every call shape is captured once as a CUDA graph per
34
+ model and replayed, since a launch-bound eager forward costs several times
35
+ its GPU time.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import math
41
+ import weakref
42
+ from collections import OrderedDict
43
+ from collections.abc import Callable, Hashable
44
+ from dataclasses import dataclass
45
+ from typing import Any
46
+
47
+ import torch
48
+ import torch.nn.functional as F
49
+
50
+ from tiny_gdn.fugue import BlockCache, FugueForMaskedDiffusion
51
+
52
+ DEFAULT_BLOCK_LENGTH = 32
53
+ DEFAULT_CONFIDENCE_THRESHOLD = 0.9
54
+ DEFAULT_REFRESH_TOKENS = 8
55
+ UNMASKING_ORDERS: tuple[str, ...] = ("random", "confidence")
56
+ # Eager calls before capture, so Triton has compiled and tuned every kernel.
57
+ GRAPH_WARMUP_STEPS = 2
58
+ # Larger calls are bound by GPU work rather than launches and run eagerly.
59
+ GRAPH_TOKEN_LIMIT = 32_768
60
+ GRAPH_CACHE_SIZE = 4
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class DiffusionSampling:
65
+ new_tokens: int
66
+ # None decodes the whole continuation as one block.
67
+ block_length: int | None = DEFAULT_BLOCK_LENGTH
68
+ threshold: float | None = DEFAULT_CONFIDENCE_THRESHOLD
69
+ steps_per_block: int | None = None
70
+ order: str = "confidence"
71
+ temperature: float = 0.0
72
+ top_k: int = 0
73
+ top_p: float = 1.0
74
+ stop_at_eos: bool = True
75
+ block_cache: bool = False
76
+ refresh_tokens: int = DEFAULT_REFRESH_TOKENS
77
+ draft_tokens: int = 0
78
+ cuda_graphs: bool = False
79
+
80
+ def __post_init__(self) -> None:
81
+ if self.new_tokens <= 0:
82
+ raise ValueError("new_tokens must be positive")
83
+ if self.block_length is not None and self.block_length <= 0:
84
+ raise ValueError("block_length must be positive")
85
+ if self.order not in UNMASKING_ORDERS:
86
+ raise ValueError(f"order must be one of: {', '.join(UNMASKING_ORDERS)}")
87
+ if self.threshold is not None and not 0.0 < self.threshold <= 1.0:
88
+ raise ValueError("threshold must be in (0, 1]")
89
+ if self.threshold is not None and self.order != "confidence":
90
+ raise ValueError("a confidence threshold needs confidence order")
91
+ if self.steps_per_block is not None and self.steps_per_block <= 0:
92
+ raise ValueError("steps_per_block must be positive")
93
+ if self.draft_tokens < 0:
94
+ raise ValueError("draft_tokens cannot be negative")
95
+ if self.draft_tokens and (self.order != "random" or self.block_cache):
96
+ raise ValueError("drafts need random order without a block cache")
97
+ if self.threshold is None and self.steps_per_block is None and not self.draft_tokens:
98
+ raise ValueError("set a confidence threshold, steps_per_block or draft_tokens")
99
+ if self.temperature < 0.0:
100
+ raise ValueError("temperature cannot be negative")
101
+ if self.top_k < 0 or not 0.0 < self.top_p <= 1.0:
102
+ raise ValueError("top_k must be >= 0 and top_p in (0, 1]")
103
+ if self.refresh_tokens <= 0:
104
+ raise ValueError("refresh_tokens must be positive")
105
+
106
+ @property
107
+ def block_width(self) -> int:
108
+ return self.block_length or self.new_tokens
109
+
110
+
111
+ @dataclass(frozen=True)
112
+ class DenoisingStep:
113
+ """State after one model call: ``tokens`` is the whole canvas, masks included."""
114
+
115
+ step: int
116
+ block: int
117
+ tokens: torch.Tensor
118
+ revealed: int
119
+ remaining: int
120
+
121
+
122
+ # ------------------------------------------------------------------ sampling
123
+
124
+
125
+ def _filter_logits(logits: torch.Tensor, *, top_k: int, top_p: float) -> torch.Tensor:
126
+ if top_k > 0 and top_k < logits.shape[-1]:
127
+ kth = torch.topk(logits, top_k, dim=-1).values[..., -1:]
128
+ logits = logits.masked_fill(logits < kth, float("-inf"))
129
+ if top_p < 1.0:
130
+ sorted_logits, order = torch.sort(logits, dim=-1, descending=True)
131
+ cumulative = sorted_logits.softmax(dim=-1).cumsum(dim=-1)
132
+ drop = cumulative - sorted_logits.softmax(dim=-1) >= top_p
133
+ sorted_logits = sorted_logits.masked_fill(drop, float("-inf"))
134
+ logits = torch.full_like(logits, float("-inf")).scatter(-1, order, sorted_logits)
135
+ return logits
136
+
137
+
138
+ def _distribution(logits: torch.Tensor, sampling: DiffusionSampling) -> torch.Tensor:
139
+ """What a token is sampled from, in float64 over the last dimension; one-hot at temperature 0.
140
+
141
+ Float32 truncates the tail and lowers the effective temperature of
142
+ masked-diffusion samplers (Zheng et al., 2024).
143
+ """
144
+
145
+ if sampling.temperature == 0.0:
146
+ return F.one_hot(logits.argmax(dim=-1), logits.shape[-1]).double()
147
+ filtered = _filter_logits(logits.double() / sampling.temperature, top_k=sampling.top_k, top_p=sampling.top_p)
148
+ return filtered.softmax(dim=-1)
149
+
150
+
151
+ def _propose(
152
+ logits: torch.Tensor,
153
+ sampling: DiffusionSampling,
154
+ generator: torch.Generator | None,
155
+ ) -> tuple[torch.Tensor, torch.Tensor]:
156
+ """``(tokens, confidence)`` per row of ``[N, vocab]`` logits."""
157
+
158
+ if sampling.temperature == 0.0:
159
+ confidence, tokens = logits.float().softmax(dim=-1).max(dim=-1)
160
+ return tokens, confidence
161
+ probabilities = _distribution(logits, sampling)
162
+ tokens = torch.multinomial(probabilities, 1, generator=generator).squeeze(-1)
163
+ confidence = probabilities.gather(-1, tokens[:, None]).squeeze(-1).float()
164
+ return tokens, confidence
165
+
166
+
167
+ def _top_ranked(priority: torch.Tensor, masked: torch.Tensor, steps_left: int) -> torch.Tensor:
168
+ """An even share of each row's masked positions, highest ``priority`` first."""
169
+
170
+ take = torch.ceil(masked.sum(dim=1).float() / steps_left).long()
171
+ ranks = priority.masked_fill(~masked, -1.0).argsort(dim=1, descending=True).argsort(dim=1)
172
+ return masked & (ranks < take[:, None])
173
+
174
+
175
+ def _reveal_confident(
176
+ sampling: DiffusionSampling, masked: torch.Tensor, confidence: torch.Tensor, steps_left: int
177
+ ) -> torch.Tensor:
178
+ """Confidence order over ``[rows, width]``; ``confidence`` is -1 off the masks."""
179
+
180
+ if sampling.threshold is None:
181
+ return _top_ranked(confidence, masked, steps_left)
182
+ reveal = masked & (confidence >= sampling.threshold)
183
+ best = confidence.argmax(dim=1)
184
+ rows = torch.arange(masked.shape[0], device=masked.device)
185
+ has_masks = masked.any(dim=1)
186
+ reveal[rows[has_masks], best[has_masks]] = True
187
+ return reveal
188
+
189
+
190
+ def _random_order(
191
+ batch: int, prompt_length: int, sampling: DiffusionSampling, generator: torch.Generator | None, device: torch.device
192
+ ) -> torch.Tensor:
193
+ """Each row's continuation positions ``[batch, new_tokens]`` in a uniformly random order within each block."""
194
+
195
+ blocks = []
196
+ for start in range(0, sampling.new_tokens, sampling.block_width):
197
+ width = min(sampling.block_width, sampling.new_tokens - start)
198
+ draws = torch.rand(batch, width, generator=generator, device=device)
199
+ blocks.append(prompt_length + start + draws.argsort(dim=1))
200
+ return torch.cat(blocks, dim=1)
201
+
202
+
203
+ def _first_eos(tokens: torch.Tensor, eos_token_id: int) -> torch.Tensor:
204
+ """Each row's first EOS index in ``[rows, T]``, or ``T`` for rows without one."""
205
+
206
+ length = tokens.shape[1]
207
+ positions = torch.arange(length, device=tokens.device)
208
+ return torch.where(tokens.eq(eos_token_id), positions, length).min(dim=1).values
209
+
210
+
211
+ def _committed_eos(tokens: torch.Tensor, mask_token_id: int, eos_token_id: int) -> torch.Tensor:
212
+ """Rows of ``[rows, T]`` whose revealed prefix reaches an EOS."""
213
+
214
+ length = tokens.shape[1]
215
+ positions = torch.arange(length, device=tokens.device)
216
+ first_mask = torch.where(tokens.eq(mask_token_id), positions, length).min(dim=1).values
217
+ return _first_eos(tokens, eos_token_id) < first_mask
218
+
219
+
220
+ def _cut_order(
221
+ order: torch.Tensor, start: torch.Tensor | int, stop: torch.Tensor | int, cutoff: torch.Tensor
222
+ ) -> tuple[torch.Tensor, torch.Tensor]:
223
+ """Move each row's pending positions ``order[start:stop]`` past ``cutoff`` behind the rest.
224
+
225
+ Returns the order, the rest keeping its relative order, and how many positions each row moved.
226
+ """
227
+
228
+ index = torch.arange(order.shape[1], device=order.device)
229
+ start = torch.as_tensor(start, device=order.device).reshape(-1, 1)
230
+ stop = torch.as_tensor(stop, device=order.device).reshape(-1, 1)
231
+ pending = (index >= start) & (index < stop)
232
+ dead = pending & (order > cutoff[:, None])
233
+ moved = dead.sum(dim=1)
234
+ if not bool(moved.any()):
235
+ return order, moved
236
+ key = torch.where(index < start, 0, torch.where(pending, 1 + dead.long(), 3))
237
+ return order.gather(1, torch.sort(key, dim=1, stable=True).indices), moved
238
+
239
+
240
+ # -------------------------------------------------------------------- graphs
241
+
242
+
243
+ class _Captured:
244
+ """A function of tensors, captured once as a CUDA graph over static copies of its inputs."""
245
+
246
+ def __init__(self, function: Callable[..., Any], inputs: tuple[torch.Tensor, ...], pool: Any) -> None:
247
+ self.inputs = tuple(tensor.clone() for tensor in inputs)
248
+ device = self.inputs[0].device
249
+ current = torch.cuda.current_stream(device)
250
+ stream = torch.cuda.Stream(device=device)
251
+ stream.wait_stream(current)
252
+ with torch.cuda.stream(stream):
253
+ for _ in range(GRAPH_WARMUP_STEPS):
254
+ function(*self.inputs)
255
+ current.wait_stream(stream)
256
+ self.graph = torch.cuda.CUDAGraph()
257
+ with torch.cuda.graph(self.graph, pool=pool, capture_error_mode="thread_local"):
258
+ self.outputs = function(*self.inputs)
259
+
260
+ def __call__(self, *inputs: torch.Tensor) -> Any:
261
+ for static, tensor in zip(self.inputs, inputs, strict=True):
262
+ static.copy_(tensor)
263
+ self.graph.replay()
264
+ return self.outputs
265
+
266
+
267
+ class _GraphCache:
268
+ """One model's captured decoding graphs by call shape, least recently used dropped first."""
269
+
270
+ def __init__(self) -> None:
271
+ self.graphs: OrderedDict[Hashable, Any] = OrderedDict()
272
+
273
+ def get(self, key: Hashable, build: Callable[[], Any]) -> Any:
274
+ graph = self.graphs.pop(key, None)
275
+ if graph is None:
276
+ while len(self.graphs) >= GRAPH_CACHE_SIZE:
277
+ self.graphs.popitem(last=False)
278
+ graph = build()
279
+ self.graphs[key] = graph
280
+ return graph
281
+
282
+
283
+ # Graphs bake in the parameters' addresses, so they live and die with the model object.
284
+ _GRAPH_CACHES: weakref.WeakKeyDictionary[FugueForMaskedDiffusion, _GraphCache] = weakref.WeakKeyDictionary()
285
+
286
+
287
+ def _graphs(model: FugueForMaskedDiffusion) -> _GraphCache:
288
+ cache = _GRAPH_CACHES.get(model)
289
+ if cache is None:
290
+ cache = _GRAPH_CACHES[model] = _GraphCache()
291
+ return cache
292
+
293
+
294
+ def _graphable(sampling: DiffusionSampling, canvas: torch.Tensor) -> bool:
295
+ return sampling.cuda_graphs and canvas.is_cuda and canvas.numel() <= GRAPH_TOKEN_LIMIT
296
+
297
+
298
+ def _canvas_logits(
299
+ model: FugueForMaskedDiffusion, canvas: torch.Tensor, positions: torch.Tensor, sampling: DiffusionSampling
300
+ ) -> torch.Tensor:
301
+ """``model.canvas_logits``, replayed from a graph when ``sampling`` allows it."""
302
+
303
+ if not _graphable(sampling, canvas):
304
+ return model.canvas_logits(canvas, positions)
305
+ key = ("canvas", *canvas.shape, positions.shape[1])
306
+ graph = _graphs(model).get(key, lambda: _Captured(model.canvas_logits, (canvas, positions), None))
307
+ return graph(canvas, positions)
308
+
309
+
310
+ class _BlockGraphs:
311
+ """``refresh_block`` and ``block_logits`` for one canvas and block shape: two graphs over one cache."""
312
+
313
+ def __init__(self, model: FugueForMaskedDiffusion, canvas: torch.Tensor, positions: torch.Tensor) -> None:
314
+ pool = torch.cuda.graph_pool_handle()
315
+ self.refresh = _Captured(model.refresh_block, (canvas, positions), pool)
316
+ # Fill the cache before the block step warms up on it.
317
+ self.refresh(canvas, positions)
318
+ cache: BlockCache = self.refresh.outputs[1]
319
+ self.cache = cache
320
+ block = canvas.index_select(1, positions)
321
+ self.block = _Captured(lambda ids: model.block_logits(ids, cache), (block,), pool)
322
+
323
+
324
+ # ------------------------------------------------------------------- scorers
325
+
326
+
327
+ class _CanvasScorer:
328
+ """Logits from a forward of the whole canvas at every call."""
329
+
330
+ def __init__(self, model: FugueForMaskedDiffusion, sampling: DiffusionSampling) -> None:
331
+ self.model = model
332
+ self.sampling = sampling
333
+ self.start = 0
334
+ self.block_mask: torch.Tensor | None = None
335
+
336
+ def begin(self, canvas: torch.Tensor, start: int, stop: int) -> None:
337
+ self.start = start
338
+ self.block_mask = None
339
+ if not _graphable(self.sampling, canvas):
340
+ self.block_mask = torch.zeros_like(canvas, dtype=torch.bool)
341
+ self.block_mask[:, start:stop] = True
342
+
343
+ def __call__(self, canvas: torch.Tensor, masked: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
344
+ """Logits ``[rows, picks, vocab]`` at ``positions`` ``[rows, picks]`` of the block."""
345
+
346
+ if self.block_mask is None:
347
+ return _canvas_logits(self.model, canvas, positions, self.sampling)
348
+ # Without graphs, the packed forward scores the block, as evaluations always have.
349
+ logits = self.model(canvas, return_logits=True, logits_mask=self.block_mask).logits
350
+ logits = logits.view(canvas.shape[0], -1, logits.shape[-1])
351
+ return logits.gather(1, (positions - self.start).unsqueeze(-1).expand(-1, -1, logits.shape[-1]))
352
+
353
+
354
+ class _BlockCacheScorer:
355
+ """Logits from a full forward, then from the block alone against its cache until a refresh is due."""
356
+
357
+ def __init__(self, model: FugueForMaskedDiffusion, sampling: DiffusionSampling) -> None:
358
+ self.model = model
359
+ self.sampling = sampling
360
+ self.start = 0
361
+ self.positions: torch.Tensor | None = None
362
+ self.cache: BlockCache | None = None
363
+ self.masked_at_refresh: torch.Tensor | None = None
364
+ self.graphs: _BlockGraphs | None = None
365
+
366
+ def begin(self, canvas: torch.Tensor, start: int, stop: int) -> None:
367
+ self.start = start
368
+ self.positions = torch.arange(start, stop, device=canvas.device)
369
+ self.cache = None
370
+ self.graphs = None
371
+ if _graphable(self.sampling, canvas):
372
+ key = ("block", *canvas.shape, stop - start)
373
+ positions = self.positions
374
+ self.graphs = _graphs(self.model).get(key, lambda: _BlockGraphs(self.model, canvas, positions))
375
+
376
+ def __call__(self, canvas: torch.Tensor, masked: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
377
+ """Logits ``[rows, picks, vocab]`` at ``positions`` ``[rows, picks]`` of the block."""
378
+
379
+ if self.positions is None:
380
+ raise RuntimeError("begin a block before scoring it")
381
+ masks_left = masked.sum(dim=1)
382
+ if (
383
+ self.cache is not None
384
+ and self.masked_at_refresh is not None
385
+ and int((self.masked_at_refresh - masks_left).max()) >= self.sampling.refresh_tokens
386
+ ):
387
+ self.cache = None
388
+ if self.cache is None:
389
+ if self.graphs is None:
390
+ logits, self.cache = self.model.refresh_block(canvas, self.positions)
391
+ else:
392
+ logits, self.cache = self.graphs.refresh(canvas, self.positions)
393
+ self.masked_at_refresh = masks_left
394
+ else:
395
+ block = canvas.index_select(1, self.positions)
396
+ if self.graphs is None:
397
+ logits = self.model.block_logits(block, self.cache)
398
+ else:
399
+ logits = self.graphs.block(block)
400
+ offsets = (positions - self.start).unsqueeze(-1).expand(-1, -1, logits.shape[-1])
401
+ return logits.gather(1, offsets)
402
+
403
+
404
+ # ------------------------------------------------------------------ decoders
405
+
406
+
407
+ @torch.no_grad()
408
+ def diffusion_generate(
409
+ model: FugueForMaskedDiffusion,
410
+ prompt_ids: torch.Tensor,
411
+ sampling: DiffusionSampling,
412
+ *,
413
+ generator: torch.Generator | None = None,
414
+ eos_token_id: int | None = None,
415
+ on_step: Callable[[DenoisingStep], bool | None] | None = None,
416
+ ) -> torch.Tensor:
417
+ """Fill ``new_tokens`` masks after each ``[B, P]`` prompt row; returns ``[B, new_tokens]``.
418
+
419
+ ``on_step`` sees every model call and may return True to cancel. With
420
+ ``stop_at_eos``, positions after a row's first revealed EOS leave its
421
+ reveal order and keep their masks; the row stops once everything before
422
+ that EOS is revealed, and the run stops when every row has.
423
+ """
424
+
425
+ if prompt_ids.ndim != 2:
426
+ raise ValueError("prompt_ids must have shape [batch, prompt]")
427
+ if prompt_ids.shape[1] == 0:
428
+ raise ValueError("the prompt needs at least one token")
429
+ mask_id = model.mask_token_id
430
+ batch, prompt_length = prompt_ids.shape
431
+ total = prompt_length + sampling.new_tokens
432
+ canvas = torch.cat(
433
+ [prompt_ids, prompt_ids.new_full((batch, sampling.new_tokens), mask_id)],
434
+ dim=1,
435
+ )
436
+ if sampling.draft_tokens:
437
+ return _speculative_generate(model, canvas, prompt_length, sampling, generator, eos_token_id, on_step)
438
+ stop_at_eos = sampling.stop_at_eos and eos_token_id is not None
439
+ finished = torch.zeros(batch, dtype=torch.bool, device=canvas.device)
440
+ cutoff = torch.full((batch,), total, device=canvas.device)
441
+ scorer = _BlockCacheScorer(model, sampling) if sampling.block_cache else _CanvasScorer(model, sampling)
442
+ order = _random_order(batch, prompt_length, sampling, generator, canvas.device) if sampling.order == "random" else None
443
+ step = 0
444
+ for block, start in enumerate(range(prompt_length, total, sampling.block_width)):
445
+ stop = min(start + sampling.block_width, total)
446
+ width = stop - start
447
+ block_steps = min(sampling.steps_per_block or width, width)
448
+ scorer.begin(canvas, start, stop)
449
+ everywhere = torch.arange(start, stop, device=canvas.device).expand(batch, -1)
450
+ # Random order picks the same number of positions every call, so one graph fits the block.
451
+ share = math.ceil(width / block_steps)
452
+ if order is not None:
453
+ block_order = order[:, start - prompt_length : stop - prompt_length]
454
+ # Slots past the block's end repeat positions revealed by the first call.
455
+ block_order = torch.cat([block_order, block_order[:, :share]], dim=1)
456
+ cursor = 0
457
+ for block_step in range(block_steps if sampling.threshold is None else width):
458
+ window = canvas[:, start:stop]
459
+ masked = window.eq(mask_id) & ~finished[:, None] & (everywhere < cutoff[:, None])
460
+ remaining = int(masked.sum())
461
+ if remaining == 0:
462
+ break
463
+ steps_left = block_steps - block_step
464
+ if order is not None:
465
+ take = math.ceil((width - cursor) / steps_left)
466
+ picks = block_order[:, cursor : cursor + share]
467
+ cursor += take
468
+ live = masked.gather(1, picks - start) & (torch.arange(share, device=canvas.device) < take)
469
+ logits = scorer(canvas, masked, picks)
470
+ tokens, _ = _propose(logits.flatten(0, 1), sampling, generator)
471
+ current = canvas.gather(1, picks)
472
+ canvas.scatter_(1, picks, torch.where(live, tokens.view(batch, share), current))
473
+ revealed = int(live.sum())
474
+ else:
475
+ logits = scorer(canvas, masked, everywhere)
476
+ proposals, confidence = _propose(logits.flatten(0, 1), sampling, generator)
477
+ proposals = proposals.view(batch, width)
478
+ confidence = confidence.view(batch, width).masked_fill(~masked, -1.0)
479
+ reveal = _reveal_confident(sampling, masked, confidence, steps_left)
480
+ canvas[:, start:stop] = torch.where(reveal, proposals, window)
481
+ revealed = int(reveal.sum())
482
+ step += 1
483
+ if stop_at_eos:
484
+ cutoff = prompt_length + _first_eos(canvas[:, prompt_length:], eos_token_id)
485
+ if order is not None:
486
+ block_order, _ = _cut_order(block_order, cursor, width, cutoff)
487
+ finished |= _committed_eos(canvas[:, prompt_length:], mask_id, eos_token_id)
488
+ if on_step is not None:
489
+ cancelled = on_step(
490
+ DenoisingStep(
491
+ step=step,
492
+ block=block,
493
+ tokens=canvas[:, prompt_length:],
494
+ revealed=revealed,
495
+ remaining=remaining - revealed,
496
+ )
497
+ )
498
+ if cancelled:
499
+ return canvas[:, prompt_length:]
500
+ if stop_at_eos and bool(finished.all()):
501
+ return canvas[:, prompt_length:]
502
+ if stop_at_eos:
503
+ finished |= canvas[:, start:stop].eq(eos_token_id).any(dim=1)
504
+ if bool(finished.all()):
505
+ break
506
+ return canvas[:, prompt_length:]
507
+
508
+
509
+ def _speculative_generate(
510
+ model: FugueForMaskedDiffusion,
511
+ canvas: torch.Tensor,
512
+ prompt_length: int,
513
+ sampling: DiffusionSampling,
514
+ generator: torch.Generator | None,
515
+ eos_token_id: int | None,
516
+ on_step: Callable[[DenoisingStep], bool | None] | None,
517
+ ) -> torch.Tensor:
518
+ """Random order, one token at a time in distribution, verifying up to ``draft_tokens`` drafts per call.
519
+
520
+ Each call scores ``drafts + 1`` copies of the canvas: copy i holds drafts
521
+ 0..i-1, so its prediction at draft i's position is the conditional that
522
+ draft i is checked against, and its predictions at the next ``drafts``
523
+ positions become the next drafts if it ends up the last accepted copy.
524
+ """
525
+
526
+ batch, total = canvas.shape
527
+ device = canvas.device
528
+ mask_id = model.mask_token_id
529
+ drafts_per_call = sampling.draft_tokens
530
+ copies = drafts_per_call + 1
531
+ stop_at_eos = sampling.stop_at_eos and eos_token_id is not None
532
+ # Window slots past the end point at position 0, a prompt token that is never revealed.
533
+ order = F.pad(_random_order(batch, prompt_length, sampling, generator, device), (0, 2 * drafts_per_call + 1))
534
+ slots = torch.arange(2 * drafts_per_call + 1, device=device)
535
+ draft_slots = torch.arange(drafts_per_call, device=device)
536
+ # Copy i holds drafts j < i.
537
+ holds = torch.arange(copies, device=device)[:, None] > draft_slots[None, :]
538
+ rows = torch.arange(batch, device=device)
539
+ cursor = torch.zeros(batch, dtype=torch.long, device=device)
540
+ # order[:cursor] is revealed, order[cursor:limit] is masked and still to come.
541
+ limit = torch.full((batch,), sampling.new_tokens, dtype=torch.long, device=device)
542
+ drafts = torch.zeros(batch, drafts_per_call, dtype=torch.long, device=device)
543
+ draft_count = torch.zeros(batch, dtype=torch.long, device=device)
544
+ draft_probabilities: torch.Tensor | None = None
545
+ finished = torch.zeros(batch, dtype=torch.bool, device=device)
546
+ step = 0
547
+ while True:
548
+ remaining = (limit - cursor).masked_fill(finished, 0)
549
+ left = int(remaining.sum())
550
+ if left == 0:
551
+ break
552
+ draft_count = torch.minimum(draft_count, remaining)
553
+ window = order.gather(1, cursor[:, None] + slots)
554
+ live = draft_slots[None, :] < draft_count[:, None]
555
+ variants = canvas[:, None, :].repeat(1, copies, 1)
556
+ if drafts_per_call:
557
+ targets = window[:, None, :drafts_per_call].expand(-1, copies, -1)
558
+ placed = torch.where(
559
+ holds[None] & live[:, None, :], drafts[:, None, :].expand(-1, copies, -1), variants.gather(2, targets)
560
+ )
561
+ variants.scatter_(2, targets, placed)
562
+ picks = window.unfold(1, copies, 1)
563
+ logits = _canvas_logits(
564
+ model, variants.view(batch * copies, total), picks.reshape(batch * copies, copies), sampling
565
+ ).view(batch, copies, copies, -1)
566
+ verified = _distribution(logits[:, :, 0], sampling)
567
+ accepted = torch.zeros(batch, dtype=torch.long, device=device)
568
+ ends_at_eos = torch.zeros(batch, dtype=torch.bool, device=device)
569
+ if draft_probabilities is not None:
570
+ target = verified[:, :drafts_per_call].gather(2, drafts[..., None]).squeeze(-1)
571
+ proposal = draft_probabilities.gather(2, drafts[..., None]).squeeze(-1)
572
+ coins = torch.rand(batch, drafts_per_call, generator=generator, device=device, dtype=torch.float64)
573
+ accepted = ((coins * proposal < target) & live).long().cumprod(dim=1).sum(dim=1)
574
+ if stop_at_eos:
575
+ # An accepted EOS cuts the order the later drafts were drawn for, so the run ends with it.
576
+ accepted_eos = drafts.eq(eos_token_id) & (draft_slots[None, :] < accepted[:, None])
577
+ first_eos = torch.where(accepted_eos, draft_slots[None, :], drafts_per_call).min(dim=1).values
578
+ ends_at_eos = first_eos < accepted
579
+ accepted = torch.minimum(accepted, first_eos + 1)
580
+ following = verified[rows, accepted]
581
+ if draft_probabilities is not None:
582
+ # A rejected draft's position samples from what its target has beyond the draft.
583
+ rejected = accepted < draft_count
584
+ excess = (following - draft_probabilities[rows, accepted.clamp(max=drafts_per_call - 1)]).clamp(min=0.0)
585
+ mass = excess.sum(dim=-1, keepdim=True)
586
+ excess = torch.where(mass > 0.0, excess / mass.clamp(min=1e-300), following)
587
+ following = torch.where(rejected[:, None], excess, following)
588
+ next_token = torch.multinomial(following, 1, generator=generator).squeeze(-1)
589
+ has_next = (accepted < remaining) & ~ends_at_eos
590
+ if drafts_per_call:
591
+ kept = draft_slots[None, :] < accepted[:, None]
592
+ draft_positions = window[:, :drafts_per_call]
593
+ canvas.scatter_(1, draft_positions, torch.where(kept, drafts, canvas.gather(1, draft_positions)))
594
+ next_position = window.gather(1, accepted[:, None])
595
+ canvas.scatter_(1, next_position, torch.where(has_next[:, None], next_token[:, None], canvas.gather(1, next_position)))
596
+ revealed = accepted + has_next.long()
597
+ cursor += revealed
598
+ if drafts_per_call:
599
+ draft_probabilities = _distribution(logits[rows, accepted, 1:], sampling)
600
+ drafts = torch.multinomial(draft_probabilities.flatten(0, 1), 1, generator=generator).view(batch, -1)
601
+ draft_count = (limit - cursor).clamp(min=0, max=drafts_per_call)
602
+ if stop_at_eos:
603
+ cutoff = prompt_length + _first_eos(canvas[:, prompt_length:], eos_token_id)
604
+ order, moved = _cut_order(order, cursor, limit, cutoff)
605
+ limit -= moved
606
+ finished |= _committed_eos(canvas[:, prompt_length:], mask_id, eos_token_id)
607
+ # Drafts after an EOS or a cut were drawn for positions that no longer come next.
608
+ draft_count = draft_count.masked_fill(finished | ends_at_eos | moved.gt(0), 0)
609
+ step += 1
610
+ if on_step is not None:
611
+ count = int(revealed.sum())
612
+ cancelled = on_step(
613
+ DenoisingStep(
614
+ step=step,
615
+ block=min(int(cursor.min()), sampling.new_tokens - 1) // sampling.block_width,
616
+ tokens=canvas[:, prompt_length:],
617
+ revealed=count,
618
+ remaining=left - count,
619
+ )
620
+ )
621
+ if cancelled:
622
+ break
623
+ return canvas[:, prompt_length:]
624
+
625
+
626
+ @torch.no_grad()
627
+ def denoise_canvas(model: FugueForMaskedDiffusion, canvas: torch.Tensor, *, steps: int) -> torch.Tensor:
628
+ """Greedily fill every ``<|mask|>`` of a ``[B, T]`` canvas in ``steps`` calls, most confident first."""
629
+
630
+ mask_id = model.mask_token_id
631
+ canvas = canvas.clone()
632
+ for step in range(steps):
633
+ masked = canvas.eq(mask_id)
634
+ remaining = masked.sum(dim=1)
635
+ if int(remaining.max()) == 0:
636
+ break
637
+ logits = model(canvas, return_logits=True, logits_mask=masked).logits
638
+ confidence, proposals = logits.float().softmax(dim=-1).max(dim=-1)
639
+ scores = torch.full(canvas.shape, -1.0, device=canvas.device)
640
+ scores[masked] = confidence
641
+ filled = torch.zeros_like(canvas)
642
+ filled[masked] = proposals
643
+ take = torch.ceil(remaining.float() / (steps - step)).long()
644
+ ranks = scores.argsort(dim=1, descending=True).argsort(dim=1)
645
+ reveal = masked & (ranks < take[:, None])
646
+ canvas = torch.where(reveal, filled, canvas)
647
+ return canvas
648
+
649
+
650
+ def committed_prefix(tokens: torch.Tensor, mask_token_id: int) -> torch.Tensor:
651
+ """The revealed tokens of a ``[N]`` canvas up to its first remaining mask."""
652
+
653
+ masked = tokens.eq(mask_token_id).nonzero()
654
+ return tokens if masked.numel() == 0 else tokens[: int(masked[0])]
tiny_gdn/fused_ops.py ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fused CUDA kernels for memory-bound blocks: SiTU-GLU, the LM loss, token log-probs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ import torch.nn.functional as F
7
+ import triton
8
+ import triton.language as tl
9
+ from triton.language.extra import libdevice
10
+
11
+ from tiny_gdn.nn_common import (
12
+ linear_cross_entropy_reference,
13
+ linear_token_logprobs_reference,
14
+ situ_glu_reference,
15
+ )
16
+
17
+ BLOCK_SIZE = 1024
18
+ VOCAB_BLOCK_SIZE = 4096
19
+ LOGITS_CHUNK_BYTES = 1 << 30
20
+
21
+
22
+ @triton.jit
23
+ def _situ_glu_forward_kernel(
24
+ gate_up,
25
+ output,
26
+ total,
27
+ width,
28
+ gate_cap,
29
+ up_cap,
30
+ BLOCK: tl.constexpr,
31
+ ):
32
+ offsets = tl.program_id(0).to(tl.int64) * BLOCK + tl.arange(0, BLOCK)
33
+ mask = offsets < total
34
+ row = offsets // width
35
+ gate_offsets = offsets + row * width
36
+ gate = tl.load(gate_up + gate_offsets, mask=mask, other=0.0).to(tl.float32)
37
+ up = tl.load(gate_up + gate_offsets + width, mask=mask, other=0.0).to(tl.float32)
38
+ gated = gate_cap * libdevice.tanh(gate / gate_cap) * tl.sigmoid(gate)
39
+ result = gated * up_cap * libdevice.tanh(up / up_cap)
40
+ tl.store(output + offsets, result.to(output.dtype.element_ty), mask=mask)
41
+
42
+
43
+ @triton.jit
44
+ def _situ_glu_backward_kernel(
45
+ gate_up,
46
+ grad_output,
47
+ grad_gate_up,
48
+ total,
49
+ width,
50
+ gate_cap,
51
+ up_cap,
52
+ BLOCK: tl.constexpr,
53
+ ):
54
+ offsets = tl.program_id(0).to(tl.int64) * BLOCK + tl.arange(0, BLOCK)
55
+ mask = offsets < total
56
+ row = offsets // width
57
+ gate_offsets = offsets + row * width
58
+ gate = tl.load(gate_up + gate_offsets, mask=mask, other=0.0).to(tl.float32)
59
+ up = tl.load(gate_up + gate_offsets + width, mask=mask, other=0.0).to(tl.float32)
60
+ grad = tl.load(grad_output + offsets, mask=mask, other=0.0).to(tl.float32)
61
+ gate_tanh = libdevice.tanh(gate / gate_cap)
62
+ up_tanh = libdevice.tanh(up / up_cap)
63
+ sigmoid = tl.sigmoid(gate)
64
+ gated = gate_cap * gate_tanh * sigmoid
65
+ capped_up = up_cap * up_tanh
66
+ gate_slope = sigmoid * ((1.0 - gate_tanh * gate_tanh) + gate_cap * gate_tanh * (1.0 - sigmoid))
67
+ grad_gate = grad * capped_up * gate_slope
68
+ grad_up = grad * gated * (1.0 - up_tanh * up_tanh)
69
+ dtype = grad_gate_up.dtype.element_ty
70
+ tl.store(grad_gate_up + gate_offsets, grad_gate.to(dtype), mask=mask)
71
+ tl.store(grad_gate_up + gate_offsets + width, grad_up.to(dtype), mask=mask)
72
+
73
+
74
+ def _launch_grid(total: int) -> tuple[int]:
75
+ return (triton.cdiv(total, BLOCK_SIZE),)
76
+
77
+
78
+ class _SiTUGLUFunction(torch.autograd.Function):
79
+ @staticmethod
80
+ def forward(ctx, gate_up: torch.Tensor, gate_cap: float, up_cap: float) -> torch.Tensor:
81
+ gate_up = gate_up.contiguous()
82
+ width = gate_up.shape[-1] // 2
83
+ output = gate_up.new_empty((*gate_up.shape[:-1], width))
84
+ total = output.numel()
85
+ if total:
86
+ _situ_glu_forward_kernel[_launch_grid(total)](
87
+ gate_up, output, total, width, gate_cap, up_cap, BLOCK=BLOCK_SIZE
88
+ )
89
+ ctx.save_for_backward(gate_up)
90
+ ctx.caps = (gate_cap, up_cap)
91
+ return output
92
+
93
+ @staticmethod
94
+ def backward(ctx, grad_output: torch.Tensor) -> tuple[torch.Tensor, None, None]:
95
+ (gate_up,) = ctx.saved_tensors
96
+ gate_cap, up_cap = ctx.caps
97
+ grad_output = grad_output.contiguous()
98
+ grad_gate_up = torch.empty_like(gate_up)
99
+ width = gate_up.shape[-1] // 2
100
+ total = grad_output.numel()
101
+ if total:
102
+ _situ_glu_backward_kernel[_launch_grid(total)](
103
+ gate_up,
104
+ grad_output,
105
+ grad_gate_up,
106
+ total,
107
+ width,
108
+ gate_cap,
109
+ up_cap,
110
+ BLOCK=BLOCK_SIZE,
111
+ )
112
+ return grad_gate_up, None, None
113
+
114
+
115
+ def situ_glu(gate_up: torch.Tensor, gate_cap: float, up_cap: float) -> torch.Tensor:
116
+ """``softcap(gate) * sigmoid(gate) * softcap(up)`` over ``[..., gate | up]``.
117
+
118
+ The CUDA path evaluates in FP32 and rounds once; backward recomputes the
119
+ activations from ``gate_up`` instead of saving intermediates.
120
+ """
121
+
122
+ if gate_up.shape[-1] % 2:
123
+ raise ValueError("gate_up must have an even last dimension")
124
+ if not gate_up.is_cuda:
125
+ return situ_glu_reference(gate_up, gate_cap, up_cap)
126
+ return _SiTUGLUFunction.apply(gate_up, gate_cap, up_cap)
127
+
128
+
129
+ @triton.jit
130
+ def _cross_entropy_rows_kernel(
131
+ logits,
132
+ targets,
133
+ losses,
134
+ log_partitions,
135
+ valid_count,
136
+ row_stride,
137
+ vocab_size,
138
+ z_loss_coefficient,
139
+ ignore_index,
140
+ COMPUTE_GRAD: tl.constexpr,
141
+ BLOCK: tl.constexpr,
142
+ ):
143
+ """Per row: logsumexp and NLL; optionally overwrite the row with d(mean loss)/d(logits)."""
144
+
145
+ row = tl.program_id(0).to(tl.int64)
146
+ row_logits = logits + row * row_stride
147
+ target = tl.load(targets + row)
148
+ columns = tl.arange(0, BLOCK)
149
+ if target == ignore_index:
150
+ tl.store(losses + row, 0.0)
151
+ tl.store(log_partitions + row, 0.0)
152
+ if COMPUTE_GRAD:
153
+ for start in range(0, vocab_size, BLOCK):
154
+ offsets = start + columns
155
+ zeros = tl.zeros((BLOCK,), dtype=logits.dtype.element_ty)
156
+ tl.store(row_logits + offsets, zeros, mask=offsets < vocab_size)
157
+ else:
158
+ running_max = tl.full((), float("-inf"), tl.float32)
159
+ running_sum = tl.zeros((), dtype=tl.float32)
160
+ for start in range(0, vocab_size, BLOCK):
161
+ offsets = start + columns
162
+ values = tl.load(
163
+ row_logits + offsets, mask=offsets < vocab_size, other=float("-inf")
164
+ ).to(tl.float32)
165
+ new_max = tl.maximum(running_max, tl.max(values, axis=0))
166
+ running_sum = running_sum * tl.exp(running_max - new_max) + tl.sum(
167
+ tl.exp(values - new_max), axis=0
168
+ )
169
+ running_max = new_max
170
+ log_partition = running_max + tl.log(running_sum)
171
+ target_logit = tl.load(row_logits + target).to(tl.float32)
172
+ tl.store(losses + row, log_partition - target_logit)
173
+ tl.store(log_partitions + row, log_partition)
174
+ if COMPUTE_GRAD:
175
+ grad_scale = 1.0 / tl.load(valid_count).to(tl.float32)
176
+ softmax_scale = grad_scale * (1.0 + 2.0 * z_loss_coefficient * log_partition)
177
+ for start in range(0, vocab_size, BLOCK):
178
+ offsets = start + columns
179
+ in_vocab = offsets < vocab_size
180
+ values = tl.load(row_logits + offsets, mask=in_vocab, other=0.0).to(tl.float32)
181
+ grad = tl.exp(values - log_partition) * softmax_scale
182
+ grad = tl.where(offsets == target, grad - grad_scale, grad)
183
+ tl.store(row_logits + offsets, grad.to(logits.dtype.element_ty), mask=in_vocab)
184
+
185
+
186
+ class _LinearCrossEntropyFunction(torch.autograd.Function):
187
+ """Chunked ``states @ weight.T`` + cross-entropy + z-loss with gradients precomputed.
188
+
189
+ Each logits chunk is consumed in place, so peak extra memory is one chunk
190
+ instead of ``[tokens, vocab]`` logits plus their softmax for backward.
191
+ """
192
+
193
+ @staticmethod
194
+ def forward(
195
+ ctx,
196
+ states: torch.Tensor,
197
+ weight: torch.Tensor,
198
+ targets: torch.Tensor,
199
+ z_loss_coefficient: float,
200
+ ignore_index: int,
201
+ compute_grad: bool,
202
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
203
+ states = states.contiguous()
204
+ targets = targets.contiguous()
205
+ rows, vocab_size = states.shape[0], weight.shape[0]
206
+ valid = targets.ne(ignore_index)
207
+ valid_count = valid.sum(dtype=torch.float32)
208
+ losses = states.new_empty(rows, dtype=torch.float32)
209
+ log_partitions = states.new_empty(rows, dtype=torch.float32)
210
+ grad_states = torch.empty_like(states) if compute_grad else None
211
+ grad_weight = (
212
+ torch.zeros(weight.shape, dtype=torch.float32, device=weight.device)
213
+ if compute_grad
214
+ else None
215
+ )
216
+ chunk_rows = max(1, LOGITS_CHUNK_BYTES // (vocab_size * states.element_size()))
217
+ for start in range(0, rows, chunk_rows):
218
+ end = min(start + chunk_rows, rows)
219
+ chunk = states[start:end]
220
+ logits = chunk @ weight.t()
221
+ _cross_entropy_rows_kernel[(end - start,)](
222
+ logits,
223
+ targets[start:end],
224
+ losses[start:end],
225
+ log_partitions[start:end],
226
+ valid_count,
227
+ logits.stride(0),
228
+ vocab_size,
229
+ z_loss_coefficient,
230
+ ignore_index,
231
+ COMPUTE_GRAD=compute_grad,
232
+ BLOCK=VOCAB_BLOCK_SIZE,
233
+ num_warps=8,
234
+ )
235
+ if compute_grad:
236
+ torch.mm(logits, weight, out=grad_states[start:end])
237
+ if chunk.dtype == torch.float32:
238
+ grad_weight.addmm_(logits.t(), chunk)
239
+ else:
240
+ torch.addmm(
241
+ grad_weight, logits.t(), chunk, out_dtype=torch.float32, out=grad_weight
242
+ )
243
+ cross_entropy = losses.sum() / valid_count
244
+ z_loss = log_partitions.square().sum() / valid_count
245
+ objective = cross_entropy + z_loss_coefficient * z_loss
246
+ if compute_grad:
247
+ ctx.save_for_backward(grad_states, grad_weight)
248
+ ctx.weight_dtype = weight.dtype
249
+ ctx.mark_non_differentiable(cross_entropy, z_loss)
250
+ return objective, cross_entropy, z_loss
251
+
252
+ @staticmethod
253
+ def backward(
254
+ ctx, grad_objective: torch.Tensor, *_: torch.Tensor
255
+ ) -> tuple[torch.Tensor, torch.Tensor, None, None, None, None]:
256
+ grad_states, grad_weight = ctx.saved_tensors
257
+ scale = grad_objective.to(torch.float32)
258
+ grad_states = (grad_states * scale).to(grad_states.dtype)
259
+ grad_weight = (grad_weight * scale).to(ctx.weight_dtype)
260
+ return grad_states, grad_weight, None, None, None, None
261
+
262
+
263
+ class _WeightedLinearCrossEntropyFunction(torch.autograd.Function):
264
+ """``Σ_i w_i (CE_i + z log Z_i²)`` over chunked ``states @ weight.T``, gradients precomputed.
265
+
266
+ The row kernel runs with unit scale and each row's gradient is then
267
+ multiplied by its weight, so per-token weights cost one in-place multiply.
268
+ """
269
+
270
+ @staticmethod
271
+ def forward(
272
+ ctx,
273
+ states: torch.Tensor,
274
+ weight: torch.Tensor,
275
+ targets: torch.Tensor,
276
+ token_weights: torch.Tensor,
277
+ z_loss_coefficient: float,
278
+ compute_grad: bool,
279
+ excluded_token_id: int | None,
280
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
281
+ states = states.contiguous()
282
+ targets = targets.contiguous()
283
+ token_weights = token_weights.float().contiguous()
284
+ rows, vocab_size = states.shape[0], weight.shape[0]
285
+ unit = torch.ones((), dtype=torch.float32, device=states.device)
286
+ losses = states.new_empty(rows, dtype=torch.float32)
287
+ log_partitions = states.new_empty(rows, dtype=torch.float32)
288
+ grad_states = torch.empty_like(states) if compute_grad else None
289
+ grad_weight = (
290
+ torch.zeros(weight.shape, dtype=torch.float32, device=weight.device)
291
+ if compute_grad
292
+ else None
293
+ )
294
+ chunk_rows = max(1, LOGITS_CHUNK_BYTES // (vocab_size * states.element_size()))
295
+ for start in range(0, rows, chunk_rows):
296
+ end = min(start + chunk_rows, rows)
297
+ chunk = states[start:end]
298
+ logits = chunk @ weight.t()
299
+ if excluded_token_id is not None:
300
+ logits[:, excluded_token_id] = float("-inf")
301
+ _cross_entropy_rows_kernel[(end - start,)](
302
+ logits,
303
+ targets[start:end],
304
+ losses[start:end],
305
+ log_partitions[start:end],
306
+ unit,
307
+ logits.stride(0),
308
+ vocab_size,
309
+ z_loss_coefficient,
310
+ -100,
311
+ COMPUTE_GRAD=compute_grad,
312
+ BLOCK=VOCAB_BLOCK_SIZE,
313
+ num_warps=8,
314
+ )
315
+ if compute_grad:
316
+ logits.mul_(token_weights[start:end, None].to(logits.dtype))
317
+ torch.mm(logits, weight, out=grad_states[start:end])
318
+ if chunk.dtype == torch.float32:
319
+ grad_weight.addmm_(logits.t(), chunk)
320
+ else:
321
+ torch.addmm(
322
+ grad_weight, logits.t(), chunk, out_dtype=torch.float32, out=grad_weight
323
+ )
324
+ objective = (token_weights * (losses + z_loss_coefficient * log_partitions.square())).sum()
325
+ if compute_grad:
326
+ ctx.save_for_backward(grad_states, grad_weight)
327
+ ctx.weight_dtype = weight.dtype
328
+ ctx.mark_non_differentiable(losses, log_partitions)
329
+ return objective, losses, log_partitions
330
+
331
+ @staticmethod
332
+ def backward(
333
+ ctx, grad_objective: torch.Tensor, *_: torch.Tensor
334
+ ) -> tuple[torch.Tensor, torch.Tensor, None, None, None, None, None]:
335
+ grad_states, grad_weight = ctx.saved_tensors
336
+ scale = grad_objective.to(torch.float32)
337
+ grad_states = (grad_states * scale).to(grad_states.dtype)
338
+ grad_weight = (grad_weight * scale).to(ctx.weight_dtype)
339
+ return grad_states, grad_weight, None, None, None, None, None
340
+
341
+
342
+ def weighted_linear_cross_entropy(
343
+ states: torch.Tensor,
344
+ weight: torch.Tensor,
345
+ targets: torch.Tensor,
346
+ token_weights: torch.Tensor,
347
+ *,
348
+ z_loss_coefficient: float = 0.0,
349
+ excluded_token_id: int | None = None,
350
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
351
+ """``(Σ_i w_i (CE_i + z log Z_i²), CE [rows], log Z [rows])`` without full logits.
352
+
353
+ Every row is scored; only the weighted sum carries gradients.
354
+ ``excluded_token_id`` gets probability zero (its logit is -inf).
355
+ """
356
+
357
+ if states.ndim != 2 or targets.shape != states.shape[:1] or token_weights.shape != targets.shape:
358
+ raise ValueError("states must be [tokens, hidden], targets and token_weights [tokens]")
359
+ if not states.is_cuda:
360
+ logits = F.linear(states, weight).float()
361
+ if excluded_token_id is not None:
362
+ logits[:, excluded_token_id] = float("-inf")
363
+ log_partitions = torch.logsumexp(logits, dim=-1)
364
+ losses = log_partitions - logits.gather(1, targets[:, None]).squeeze(1)
365
+ weights = token_weights.float()
366
+ objective = (weights * (losses + z_loss_coefficient * log_partitions.square())).sum()
367
+ return objective, losses.detach(), log_partitions.detach()
368
+ compute_grad = torch.is_grad_enabled() and (states.requires_grad or weight.requires_grad)
369
+ return _WeightedLinearCrossEntropyFunction.apply(
370
+ states, weight, targets, token_weights, z_loss_coefficient, compute_grad, excluded_token_id
371
+ )
372
+
373
+
374
+ def linear_cross_entropy(
375
+ states: torch.Tensor,
376
+ weight: torch.Tensor,
377
+ targets: torch.Tensor,
378
+ *,
379
+ z_loss_coefficient: float = 0.0,
380
+ ignore_index: int = -100,
381
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
382
+ """Mean ``cross_entropy(states @ weight.T, targets)`` without full logits.
383
+
384
+ Returns ``(objective, cross_entropy, z_loss)``. Only ``objective =
385
+ cross_entropy + z_loss_coefficient * z_loss`` carries gradients; ``z_loss``
386
+ is the mean squared log-partition over non-ignored targets.
387
+ """
388
+
389
+ if states.ndim != 2 or targets.shape != states.shape[:1]:
390
+ raise ValueError("states must be [tokens, hidden] and targets [tokens]")
391
+ if not states.is_cuda:
392
+ return linear_cross_entropy_reference(
393
+ states,
394
+ weight,
395
+ targets,
396
+ z_loss_coefficient=z_loss_coefficient,
397
+ ignore_index=ignore_index,
398
+ )
399
+ compute_grad = torch.is_grad_enabled() and (states.requires_grad or weight.requires_grad)
400
+ return _LinearCrossEntropyFunction.apply(
401
+ states, weight, targets, z_loss_coefficient, ignore_index, compute_grad
402
+ )
403
+
404
+
405
+ @triton.jit
406
+ def _token_logprob_grad_rows_kernel(
407
+ logits,
408
+ targets,
409
+ log_partitions,
410
+ row_grads,
411
+ row_stride,
412
+ vocab_size,
413
+ ignore_index,
414
+ BLOCK: tl.constexpr,
415
+ ):
416
+ """Overwrite each row with ``g * (onehot(target) - softmax)``, the gradient of ``g * log p``."""
417
+
418
+ row = tl.program_id(0).to(tl.int64)
419
+ row_logits = logits + row * row_stride
420
+ target = tl.load(targets + row)
421
+ columns = tl.arange(0, BLOCK)
422
+ if target == ignore_index:
423
+ for start in range(0, vocab_size, BLOCK):
424
+ offsets = start + columns
425
+ zeros = tl.zeros((BLOCK,), dtype=logits.dtype.element_ty)
426
+ tl.store(row_logits + offsets, zeros, mask=offsets < vocab_size)
427
+ else:
428
+ scale = tl.load(row_grads + row).to(tl.float32)
429
+ log_partition = tl.load(log_partitions + row)
430
+ for start in range(0, vocab_size, BLOCK):
431
+ offsets = start + columns
432
+ in_vocab = offsets < vocab_size
433
+ values = tl.load(row_logits + offsets, mask=in_vocab, other=0.0).to(tl.float32)
434
+ grad = -scale * tl.exp(values - log_partition)
435
+ grad = tl.where(offsets == target, grad + scale, grad)
436
+ tl.store(row_logits + offsets, grad.to(logits.dtype.element_ty), mask=in_vocab)
437
+
438
+
439
+ class _LinearTokenLogprobFunction(torch.autograd.Function):
440
+ """Per-row ``log_softmax(states @ weight.T)[target]`` without keeping logits.
441
+
442
+ Preference losses weight every sequence differently, so the gradient
443
+ cannot be precomputed in forward like the mean LM loss. Forward keeps one
444
+ log-partition per row; backward recomputes each logits chunk and turns it
445
+ into the scaled softmax gradient in place.
446
+ """
447
+
448
+ @staticmethod
449
+ def forward(
450
+ ctx,
451
+ states: torch.Tensor,
452
+ weight: torch.Tensor,
453
+ targets: torch.Tensor,
454
+ ignore_index: int,
455
+ ) -> torch.Tensor:
456
+ states = states.contiguous()
457
+ targets = targets.contiguous()
458
+ rows, vocab_size = states.shape[0], weight.shape[0]
459
+ negative_logprobs = states.new_empty(rows, dtype=torch.float32)
460
+ log_partitions = states.new_empty(rows, dtype=torch.float32)
461
+ unused_count = states.new_ones((), dtype=torch.float32)
462
+ chunk_rows = max(1, LOGITS_CHUNK_BYTES // (vocab_size * states.element_size()))
463
+ for start in range(0, rows, chunk_rows):
464
+ end = min(start + chunk_rows, rows)
465
+ logits = states[start:end] @ weight.t()
466
+ _cross_entropy_rows_kernel[(end - start,)](
467
+ logits,
468
+ targets[start:end],
469
+ negative_logprobs[start:end],
470
+ log_partitions[start:end],
471
+ unused_count,
472
+ logits.stride(0),
473
+ vocab_size,
474
+ 0.0,
475
+ ignore_index,
476
+ COMPUTE_GRAD=False,
477
+ BLOCK=VOCAB_BLOCK_SIZE,
478
+ num_warps=8,
479
+ )
480
+ ctx.save_for_backward(states, weight, targets, log_partitions)
481
+ ctx.ignore_index = ignore_index
482
+ ctx.chunk_rows = chunk_rows
483
+ return negative_logprobs.neg_()
484
+
485
+ @staticmethod
486
+ def backward(
487
+ ctx, grad_logprobs: torch.Tensor
488
+ ) -> tuple[torch.Tensor | None, torch.Tensor | None, None, None]:
489
+ states, weight, targets, log_partitions = ctx.saved_tensors
490
+ row_grads = grad_logprobs.to(torch.float32).contiguous()
491
+ rows, vocab_size = states.shape[0], weight.shape[0]
492
+ grad_states = torch.empty_like(states) if ctx.needs_input_grad[0] else None
493
+ grad_weight = (
494
+ torch.zeros(weight.shape, dtype=torch.float32, device=weight.device)
495
+ if ctx.needs_input_grad[1]
496
+ else None
497
+ )
498
+ for start in range(0, rows, ctx.chunk_rows):
499
+ end = min(start + ctx.chunk_rows, rows)
500
+ chunk = states[start:end]
501
+ logits = chunk @ weight.t()
502
+ _token_logprob_grad_rows_kernel[(end - start,)](
503
+ logits,
504
+ targets[start:end],
505
+ log_partitions[start:end],
506
+ row_grads[start:end],
507
+ logits.stride(0),
508
+ vocab_size,
509
+ ctx.ignore_index,
510
+ BLOCK=VOCAB_BLOCK_SIZE,
511
+ num_warps=8,
512
+ )
513
+ if grad_states is not None:
514
+ torch.mm(logits, weight, out=grad_states[start:end])
515
+ if grad_weight is not None:
516
+ if chunk.dtype == torch.float32:
517
+ grad_weight.addmm_(logits.t(), chunk)
518
+ else:
519
+ torch.addmm(
520
+ grad_weight, logits.t(), chunk, out_dtype=torch.float32, out=grad_weight
521
+ )
522
+ return (
523
+ grad_states,
524
+ None if grad_weight is None else grad_weight.to(weight.dtype),
525
+ None,
526
+ None,
527
+ )
528
+
529
+
530
+ def linear_token_logprobs(
531
+ states: torch.Tensor,
532
+ weight: torch.Tensor,
533
+ targets: torch.Tensor,
534
+ *,
535
+ ignore_index: int = -100,
536
+ ) -> torch.Tensor:
537
+ """fp32 ``log_softmax(states @ weight.T)[row, target]`` per row without full logits.
538
+
539
+ Rows whose target is ``ignore_index`` return 0 and receive no gradient.
540
+ """
541
+
542
+ if states.ndim != 2 or targets.shape != states.shape[:1]:
543
+ raise ValueError("states must be [tokens, hidden] and targets [tokens]")
544
+ if not states.is_cuda:
545
+ return linear_token_logprobs_reference(
546
+ states, weight, targets, ignore_index=ignore_index
547
+ )
548
+ return _LinearTokenLogprobFunction.apply(states, weight, targets, ignore_index)
tiny_gdn/gated_residual_ops.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fused CUDA kernels for Senryu-2's gated residual read and write.
2
+
3
+ The read normalizes every branch, gates it through a rank-``r`` bottleneck over
4
+ all branches and averages the gated branches; the write adds one scaled update
5
+ to every branch. The bottleneck GEMMs run on cuBLAS and every elementwise or
6
+ reduction stage around them is a single Triton pass over the branches, which
7
+ replaces dozens of memory-bound PyTorch ops per read on 32k-token rows.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import torch
13
+ import torch.nn.functional as F
14
+ import triton
15
+ import triton.language as tl
16
+
17
+ GEMM_ALIGNMENT = 8
18
+ COLUMN_BLOCK = 512
19
+ GRAD_ROWS_PER_PROGRAM = 32
20
+
21
+
22
+ def _warps(block: int) -> int:
23
+ return 8 if block >= 2048 else 4
24
+
25
+
26
+ @triton.jit
27
+ def _read_normalize_kernel(
28
+ residual,
29
+ gain,
30
+ flat,
31
+ rstd,
32
+ hidden_size,
33
+ eps,
34
+ BRANCHES: tl.constexpr,
35
+ BLOCK: tl.constexpr,
36
+ ):
37
+ vector = tl.program_id(0).to(tl.int64)
38
+ branch = vector % BRANCHES
39
+ columns = tl.arange(0, BLOCK)
40
+ mask = columns < hidden_size
41
+ offsets = vector * hidden_size + columns
42
+ values = tl.load(residual + offsets, mask=mask, other=0.0).to(tl.float32)
43
+ inverse_rms = tl.rsqrt(tl.sum(values * values, axis=0) / hidden_size + eps)
44
+ scale = tl.load(gain + branch * hidden_size + columns, mask=mask, other=0.0)
45
+ tl.store(rstd + vector, inverse_rms)
46
+ tl.store(flat + offsets, (values * inverse_rms * scale).to(flat.dtype.element_ty), mask=mask)
47
+
48
+
49
+ @triton.jit
50
+ def _read_combine_kernel(
51
+ residual,
52
+ rstd,
53
+ gain,
54
+ gate_logits,
55
+ out,
56
+ hidden_size,
57
+ BRANCHES: tl.constexpr,
58
+ BLOCK: tl.constexpr,
59
+ ):
60
+ row = tl.program_id(0).to(tl.int64)
61
+ columns = tl.program_id(1) * BLOCK + tl.arange(0, BLOCK)
62
+ mask = columns < hidden_size
63
+ total = tl.zeros([BLOCK], dtype=tl.float32)
64
+ for branch in tl.static_range(BRANCHES):
65
+ vector = row * BRANCHES + branch
66
+ offsets = vector * hidden_size + columns
67
+ values = tl.load(residual + offsets, mask=mask, other=0.0).to(tl.float32)
68
+ logits = tl.load(gate_logits + offsets, mask=mask, other=0.0).to(tl.float32)
69
+ scale = tl.load(gain + branch * hidden_size + columns, mask=mask, other=0.0)
70
+ total += tl.sigmoid(logits) * (values * tl.load(rstd + vector) * scale)
71
+ tl.store(out + row * hidden_size + columns, (total / BRANCHES).to(out.dtype.element_ty), mask=mask)
72
+
73
+
74
+ @triton.jit
75
+ def _read_backward_gates_kernel(
76
+ residual,
77
+ rstd,
78
+ gain,
79
+ gate_logits,
80
+ grad_out,
81
+ flat,
82
+ grad_gate_logits,
83
+ hidden_size,
84
+ BRANCHES: tl.constexpr,
85
+ BLOCK: tl.constexpr,
86
+ ):
87
+ vector = tl.program_id(0).to(tl.int64)
88
+ row = vector // BRANCHES
89
+ branch = vector % BRANCHES
90
+ columns = tl.program_id(1) * BLOCK + tl.arange(0, BLOCK)
91
+ mask = columns < hidden_size
92
+ offsets = vector * hidden_size + columns
93
+ values = tl.load(residual + offsets, mask=mask, other=0.0).to(tl.float32)
94
+ scale = tl.load(gain + branch * hidden_size + columns, mask=mask, other=0.0)
95
+ hat = values * tl.load(rstd + vector) * scale
96
+ gates = tl.sigmoid(tl.load(gate_logits + offsets, mask=mask, other=0.0).to(tl.float32))
97
+ grad = tl.load(grad_out + row * hidden_size + columns, mask=mask, other=0.0).to(tl.float32) / BRANCHES
98
+ tl.store(flat + offsets, hat.to(flat.dtype.element_ty), mask=mask)
99
+ grad_logits = grad * hat * gates * (1.0 - gates)
100
+ tl.store(grad_gate_logits + offsets, grad_logits.to(grad_gate_logits.dtype.element_ty), mask=mask)
101
+
102
+
103
+ @triton.jit
104
+ def _read_backward_residual_kernel(
105
+ residual,
106
+ rstd,
107
+ gain,
108
+ gate_logits,
109
+ grad_out,
110
+ grad_flat,
111
+ grad_residual,
112
+ grad_gain_partial,
113
+ rows,
114
+ hidden_size,
115
+ BRANCHES: tl.constexpr,
116
+ ROWS: tl.constexpr,
117
+ BLOCK: tl.constexpr,
118
+ ):
119
+ block = tl.program_id(0).to(tl.int64)
120
+ branch = tl.program_id(1)
121
+ columns = tl.arange(0, BLOCK)
122
+ mask = columns < hidden_size
123
+ scale = tl.load(gain + branch * hidden_size + columns, mask=mask, other=0.0)
124
+ grad_gain = tl.zeros([BLOCK], dtype=tl.float32)
125
+ for index in range(ROWS):
126
+ row = block * ROWS + index
127
+ valid = row < rows
128
+ row_mask = mask & valid
129
+ vector = row * BRANCHES + branch
130
+ offsets = vector * hidden_size + columns
131
+ inverse_rms = tl.load(rstd + vector, mask=valid, other=0.0)
132
+ normalized = tl.load(residual + offsets, mask=row_mask, other=0.0).to(tl.float32) * inverse_rms
133
+ gates = tl.sigmoid(tl.load(gate_logits + offsets, mask=row_mask, other=0.0).to(tl.float32))
134
+ grad = tl.load(grad_out + row * hidden_size + columns, mask=row_mask, other=0.0).to(tl.float32)
135
+ grad_hat = gates * (grad / BRANCHES)
136
+ grad_hat += tl.load(grad_flat + offsets, mask=row_mask, other=0.0).to(tl.float32)
137
+ grad_gain += grad_hat * normalized
138
+ grad_normalized = grad_hat * scale
139
+ mean = tl.sum(grad_normalized * normalized, axis=0) / hidden_size
140
+ result = inverse_rms * (grad_normalized - normalized * mean)
141
+ tl.store(grad_residual + offsets, result.to(grad_residual.dtype.element_ty), mask=row_mask)
142
+ tl.store(grad_gain_partial + (block * BRANCHES + branch) * hidden_size + columns, grad_gain, mask=mask)
143
+
144
+
145
+ @triton.jit
146
+ def _write_forward_kernel(
147
+ residual,
148
+ scales,
149
+ update,
150
+ out,
151
+ hidden_size,
152
+ BRANCHES: tl.constexpr,
153
+ BLOCK: tl.constexpr,
154
+ ):
155
+ row = tl.program_id(0).to(tl.int64)
156
+ columns = tl.program_id(1) * BLOCK + tl.arange(0, BLOCK)
157
+ mask = columns < hidden_size
158
+ values = tl.load(update + row * hidden_size + columns, mask=mask, other=0.0).to(tl.float32)
159
+ for branch in tl.static_range(BRANCHES):
160
+ vector = row * BRANCHES + branch
161
+ offsets = vector * hidden_size + columns
162
+ scale = tl.load(scales + vector).to(tl.float32)
163
+ written = tl.load(residual + offsets, mask=mask, other=0.0).to(tl.float32) + scale * values
164
+ tl.store(out + offsets, written.to(out.dtype.element_ty), mask=mask)
165
+
166
+
167
+ @triton.jit
168
+ def _write_backward_kernel(
169
+ grad,
170
+ scales,
171
+ update,
172
+ grad_scales,
173
+ grad_update,
174
+ hidden_size,
175
+ BRANCHES: tl.constexpr,
176
+ BLOCK: tl.constexpr,
177
+ ):
178
+ row = tl.program_id(0).to(tl.int64)
179
+ columns = tl.arange(0, BLOCK)
180
+ mask = columns < hidden_size
181
+ values = tl.load(update + row * hidden_size + columns, mask=mask, other=0.0).to(tl.float32)
182
+ total = tl.zeros([BLOCK], dtype=tl.float32)
183
+ for branch in tl.static_range(BRANCHES):
184
+ vector = row * BRANCHES + branch
185
+ incoming = tl.load(grad + vector * hidden_size + columns, mask=mask, other=0.0).to(tl.float32)
186
+ total += incoming * tl.load(scales + vector).to(tl.float32)
187
+ tl.store(grad_scales + vector, tl.sum(incoming * values, axis=0).to(grad_scales.dtype.element_ty))
188
+ tl.store(grad_update + row * hidden_size + columns, total.to(grad_update.dtype.element_ty), mask=mask)
189
+
190
+
191
+ def _bottleneck_projection(down: torch.Tensor, write: torch.Tensor | None) -> torch.Tensor:
192
+ """``[down; write]`` padded with zero rows to a cuBLAS-friendly height."""
193
+
194
+ rows = down.shape[0] + (0 if write is None else write.shape[0])
195
+ padded = triton.cdiv(rows, GEMM_ALIGNMENT) * GEMM_ALIGNMENT
196
+ if write is None and padded == rows:
197
+ return down
198
+ projection = down.new_zeros(padded, down.shape[1])
199
+ projection[: down.shape[0]] = down
200
+ if write is not None:
201
+ projection[down.shape[0] : rows] = write
202
+ return projection
203
+
204
+
205
+ class _FusedGatedResidualRead(torch.autograd.Function):
206
+ """CUDA twin of ``senryu2._GatedResidualRead``: same math, rounding points and saved tensors."""
207
+
208
+ @staticmethod
209
+ def forward(ctx, residual, gamma, down, up, write, eps):
210
+ residual = residual.contiguous()
211
+ rows, branches, hidden_size = residual.shape
212
+ rank = down.shape[0]
213
+ gain = (1.0 + gamma.float()).contiguous()
214
+ flat = residual.new_empty(rows, branches * hidden_size, dtype=down.dtype)
215
+ rstd = residual.new_empty(rows, branches, dtype=torch.float32)
216
+ out = residual.new_empty(rows, hidden_size)
217
+ full = triton.next_power_of_2(hidden_size)
218
+ block = min(COLUMN_BLOCK, full)
219
+ _read_normalize_kernel[(rows * branches,)](
220
+ residual, gain, flat, rstd, hidden_size, eps, BRANCHES=branches, BLOCK=full, num_warps=_warps(full)
221
+ )
222
+ bottleneck = flat @ _bottleneck_projection(down, write).t()
223
+ hidden = bottleneck[:, :rank] / branches
224
+ gate_logits = F.silu(hidden.float()).to(down.dtype) @ up.t()
225
+ _read_combine_kernel[(rows, triton.cdiv(hidden_size, block))](
226
+ residual, rstd, gain, gate_logits, out, hidden_size, BRANCHES=branches, BLOCK=block
227
+ )
228
+ if write is None:
229
+ scales = residual.new_empty(rows, 0, dtype=torch.float32)
230
+ else:
231
+ scales = 2.0 * torch.sigmoid(bottleneck[:, rank : rank + branches].float() / branches)
232
+ ctx.save_for_backward(residual, gamma, down, up, write, rstd, hidden, scales)
233
+ scales_out = scales.to(residual.dtype, copy=True)
234
+ if write is None:
235
+ ctx.mark_non_differentiable(scales_out)
236
+ return out, scales_out
237
+
238
+ @staticmethod
239
+ def backward(ctx, grad_out, grad_scales):
240
+ residual, gamma, down, up, write, rstd, hidden, scales = ctx.saved_tensors
241
+ rows, branches, hidden_size = residual.shape
242
+ rank = down.shape[0]
243
+ dtype = down.dtype
244
+ gain = (1.0 + gamma.float()).contiguous()
245
+ grad_out = grad_out.contiguous()
246
+ full = triton.next_power_of_2(hidden_size)
247
+ block = min(COLUMN_BLOCK, full)
248
+
249
+ pre = hidden.float()
250
+ sigmoid_pre = torch.sigmoid(pre)
251
+ activated = F.silu(pre).to(dtype)
252
+ gate_logits = activated @ up.t()
253
+ flat = torch.empty_like(gate_logits)
254
+ grad_gate_logits = torch.empty_like(gate_logits)
255
+ _read_backward_gates_kernel[(rows * branches, triton.cdiv(hidden_size, block))](
256
+ residual, rstd, gain, gate_logits, grad_out, flat, grad_gate_logits, hidden_size,
257
+ BRANCHES=branches, BLOCK=block,
258
+ )
259
+ grad_up = grad_gate_logits.t() @ activated
260
+ grad_activated = (grad_gate_logits @ up).float()
261
+ del grad_gate_logits
262
+
263
+ projection = _bottleneck_projection(down, write)
264
+ grad_bottleneck = residual.new_zeros(rows, projection.shape[0], dtype=dtype)
265
+ grad_pre = grad_activated * sigmoid_pre * (1.0 + pre * (1.0 - sigmoid_pre))
266
+ grad_bottleneck[:, :rank] = (grad_pre / branches).to(dtype)
267
+ use_scales = write is not None and grad_scales is not None
268
+ if use_scales:
269
+ grad_scale_logits = grad_scales.float() * scales * (1.0 - scales / 2.0)
270
+ grad_bottleneck[:, rank : rank + branches] = (grad_scale_logits / branches).to(dtype)
271
+ grad_projection = grad_bottleneck.t() @ flat
272
+ grad_flat = grad_bottleneck @ projection
273
+ del flat
274
+
275
+ grad_residual = torch.empty_like(residual)
276
+ programs = triton.cdiv(rows, GRAD_ROWS_PER_PROGRAM)
277
+ grad_gain_partial = residual.new_empty(programs, branches, hidden_size, dtype=torch.float32)
278
+ _read_backward_residual_kernel[(programs, branches)](
279
+ residual, rstd, gain, gate_logits, grad_out, grad_flat, grad_residual, grad_gain_partial,
280
+ rows, hidden_size, BRANCHES=branches, ROWS=GRAD_ROWS_PER_PROGRAM, BLOCK=full, num_warps=_warps(full),
281
+ )
282
+ grad_write = None
283
+ if write is not None:
284
+ grad_write = (
285
+ grad_projection[rank : rank + branches].to(write.dtype) if use_scales else torch.zeros_like(write)
286
+ )
287
+ return (
288
+ grad_residual,
289
+ grad_gain_partial.sum(dim=0).to(gamma.dtype),
290
+ grad_projection[:rank].to(down.dtype),
291
+ grad_up.to(up.dtype),
292
+ grad_write,
293
+ None,
294
+ )
295
+
296
+
297
+ class _FusedGatedResidualWrite(torch.autograd.Function):
298
+ @staticmethod
299
+ def forward(ctx, residual, scales, update):
300
+ residual = residual.contiguous()
301
+ scales = scales.contiguous()
302
+ update = update.contiguous()
303
+ rows, branches, hidden_size = residual.shape
304
+ out = torch.empty_like(residual)
305
+ block = min(COLUMN_BLOCK, triton.next_power_of_2(hidden_size))
306
+ _write_forward_kernel[(rows, triton.cdiv(hidden_size, block))](
307
+ residual, scales, update, out, hidden_size, BRANCHES=branches, BLOCK=block
308
+ )
309
+ ctx.save_for_backward(scales, update)
310
+ return out
311
+
312
+ @staticmethod
313
+ def backward(ctx, grad):
314
+ scales, update = ctx.saved_tensors
315
+ grad = grad.contiguous()
316
+ rows, branches, hidden_size = grad.shape
317
+ grad_scales = torch.empty_like(scales)
318
+ grad_update = torch.empty_like(update)
319
+ full = triton.next_power_of_2(hidden_size)
320
+ _write_backward_kernel[(rows,)](
321
+ grad, scales, update, grad_scales, grad_update, hidden_size,
322
+ BRANCHES=branches, BLOCK=full, num_warps=_warps(full),
323
+ )
324
+ return grad, grad_scales, grad_update
325
+
326
+
327
+ def fused_gated_residual_read(
328
+ residual: torch.Tensor,
329
+ gamma: torch.Tensor,
330
+ down: torch.Tensor,
331
+ up: torch.Tensor,
332
+ write: torch.Tensor | None,
333
+ eps: float,
334
+ ) -> tuple[torch.Tensor, torch.Tensor]:
335
+ """``(x [N, d], scales [N, n])`` from CUDA branches ``[N, n, d]``."""
336
+
337
+ return _FusedGatedResidualRead.apply(residual, gamma, down, up, write, eps)
338
+
339
+
340
+ def fused_gated_residual_write(residual: torch.Tensor, scales: torch.Tensor, update: torch.Tensor) -> torch.Tensor:
341
+ """``residual + scales[..., None] * update[:, None]`` on CUDA, in FP32 and rounded once."""
342
+
343
+ return _FusedGatedResidualWrite.apply(residual, scales, update)
tiny_gdn/haiku_layers.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Haiku mixers: Gated MLA (NoPE), SiTU-GLU, and block Attention Residuals."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+
7
+ import torch
8
+ import torch.nn.functional as F
9
+ from torch import nn
10
+ from torch.nn.attention import sdpa_kernel
11
+
12
+ from tiny_gdn.config import TinyGDNConfig
13
+ from tiny_gdn.nn_common import RMSNorm, attention_sdpa_backends, situ_glu_reference
14
+
15
+ try:
16
+ from tiny_gdn.fused_ops import situ_glu
17
+ except ImportError:
18
+ situ_glu = situ_glu_reference
19
+
20
+
21
+ class GatedMultiheadLatentAttention(nn.Module):
22
+ """DeepSeek-style MLA with Kimi K3 NoPE and a full-rank output gate.
23
+
24
+ Queries and keys are content-only. Position is left to the KDA layers.
25
+ KV is compressed through a latent bottleneck, then expanded per head.
26
+ """
27
+
28
+ def __init__(self, config: TinyGDNConfig) -> None:
29
+ super().__init__()
30
+ self.num_heads = config.num_attention_heads
31
+ self.qk_dim = config.mla_qk_nope_head_dim
32
+ self.v_dim = config.mla_v_head_dim
33
+ self.dropout = config.attention_dropout
34
+ q_out = self.num_heads * self.qk_dim
35
+ v_out = self.num_heads * self.v_dim
36
+ kv_out = self.num_heads * (self.qk_dim + self.v_dim)
37
+
38
+ if config.mla_q_lora_rank is None:
39
+ self.q_proj: nn.Module = nn.Linear(config.hidden_size, q_out, bias=False)
40
+ else:
41
+ self.q_proj = nn.Sequential(
42
+ nn.Linear(config.hidden_size, config.mla_q_lora_rank, bias=False),
43
+ RMSNorm(config.mla_q_lora_rank, config.rms_norm_eps),
44
+ nn.Linear(config.mla_q_lora_rank, q_out, bias=False),
45
+ )
46
+ self.kv_down = nn.Linear(config.hidden_size, config.mla_kv_lora_rank, bias=False)
47
+ self.kv_norm = RMSNorm(config.mla_kv_lora_rank, config.rms_norm_eps)
48
+ self.kv_up = nn.Linear(config.mla_kv_lora_rank, kv_out, bias=False)
49
+ self.q_norm = RMSNorm(self.qk_dim, config.rms_norm_eps)
50
+ self.k_norm = RMSNorm(self.qk_dim, config.rms_norm_eps)
51
+ self.output_gate_proj = nn.Linear(config.hidden_size, v_out, bias=False)
52
+ self.o_proj = nn.Linear(v_out, config.hidden_size, bias=False)
53
+
54
+ def _attention_mask(
55
+ self,
56
+ attention_mask: torch.Tensor | None,
57
+ sequence_length: int,
58
+ device: torch.device,
59
+ ) -> torch.Tensor | None:
60
+ if attention_mask is None:
61
+ return None
62
+ if attention_mask.ndim != 2:
63
+ raise ValueError("attention_mask must have shape [batch, sequence]")
64
+ if attention_mask.shape[1] != sequence_length:
65
+ raise ValueError("attention_mask sequence length does not match input")
66
+ causal = torch.ones(
67
+ sequence_length,
68
+ sequence_length,
69
+ dtype=torch.bool,
70
+ device=device,
71
+ ).tril()
72
+ valid_keys = attention_mask[:, None, None, :].to(dtype=torch.bool, device=device)
73
+ return causal[None, None, :, :] & valid_keys
74
+
75
+ def _project_kv(self, hidden_states: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
76
+ batch_size, sequence_length, _ = hidden_states.shape
77
+ compressed = self.kv_norm(self.kv_down(hidden_states))
78
+ key_value = self.kv_up(compressed)
79
+ key, value = key_value.split(
80
+ [self.num_heads * self.qk_dim, self.num_heads * self.v_dim],
81
+ dim=-1,
82
+ )
83
+ key = self.k_norm(
84
+ key.view(batch_size, sequence_length, self.num_heads, self.qk_dim)
85
+ ).transpose(1, 2)
86
+ value = value.view(
87
+ batch_size, sequence_length, self.num_heads, self.v_dim
88
+ ).transpose(1, 2)
89
+ return key, value
90
+
91
+ def forward(
92
+ self,
93
+ hidden_states: torch.Tensor,
94
+ attention_mask: torch.Tensor | None = None,
95
+ past_key_value: tuple[torch.Tensor, torch.Tensor] | None = None,
96
+ ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor] | None]:
97
+ batch_size, sequence_length, _ = hidden_states.shape
98
+ query = self.q_proj(hidden_states)
99
+ query = self.q_norm(
100
+ query.view(batch_size, sequence_length, self.num_heads, self.qk_dim)
101
+ ).transpose(1, 2)
102
+ key, value = self._project_kv(hidden_states)
103
+ if past_key_value is not None:
104
+ key = torch.cat([past_key_value[0], key], dim=2)
105
+ value = torch.cat([past_key_value[1], value], dim=2)
106
+ present = (key, value)
107
+ past_len = 0 if past_key_value is None else past_key_value[0].shape[2]
108
+ kv_len = key.shape[2]
109
+
110
+ if attention_mask is not None and past_key_value is None:
111
+ sdpa_mask = self._attention_mask(
112
+ attention_mask, sequence_length, hidden_states.device
113
+ )
114
+ is_causal = False
115
+ elif past_key_value is not None and sequence_length == 1:
116
+ sdpa_mask = (
117
+ None
118
+ if attention_mask is None
119
+ else attention_mask[:, None, None, :].to(
120
+ dtype=torch.bool, device=hidden_states.device
121
+ )
122
+ )
123
+ is_causal = False
124
+ elif past_key_value is not None:
125
+ q_idx = torch.arange(
126
+ past_len, past_len + sequence_length, device=hidden_states.device
127
+ )[:, None]
128
+ k_idx = torch.arange(kv_len, device=hidden_states.device)[None, :]
129
+ sdpa_mask = (k_idx <= q_idx)[None, None, :, :]
130
+ is_causal = False
131
+ else:
132
+ sdpa_mask = None
133
+ is_causal = True
134
+
135
+ with sdpa_kernel(attention_sdpa_backends(query.device)):
136
+ attention_output = F.scaled_dot_product_attention(
137
+ query,
138
+ key,
139
+ value,
140
+ attn_mask=sdpa_mask,
141
+ dropout_p=self.dropout if self.training else 0.0,
142
+ is_causal=is_causal,
143
+ )
144
+ attention_output = attention_output.transpose(1, 2).reshape(
145
+ batch_size, sequence_length, -1
146
+ )
147
+ attention_output = attention_output * torch.sigmoid(
148
+ self.output_gate_proj(hidden_states)
149
+ )
150
+ return self.o_proj(attention_output), present
151
+
152
+
153
+ class SiTUGLU(nn.Module):
154
+ """Sigmoid-Tanh Unit GLU from Kimi K3.
155
+
156
+ Caps the Swish linear factor and the up-projection so the routed / deep
157
+ stack cannot explode, while matching SwiGLU near the origin.
158
+ """
159
+
160
+ def __init__(self, config: TinyGDNConfig) -> None:
161
+ super().__init__()
162
+ self.gate_up_proj = nn.Linear(
163
+ config.hidden_size,
164
+ config.intermediate_size * 2,
165
+ bias=False,
166
+ )
167
+ self.down_proj = nn.Linear(
168
+ config.intermediate_size,
169
+ config.hidden_size,
170
+ bias=False,
171
+ )
172
+ self.gate_cap = config.situ_gate_cap
173
+ self.up_cap = config.situ_up_cap
174
+
175
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
176
+ gate_up = self.gate_up_proj(hidden_states)
177
+ return self.down_proj(situ_glu(gate_up, self.gate_cap, self.up_cap))
178
+
179
+
180
+ class BlockAttentionResidual(nn.Module):
181
+ """Selective depth skip over block outputs (Kimi K3 AttnRes, block-level)."""
182
+
183
+ def __init__(self, hidden_size: int) -> None:
184
+ super().__init__()
185
+ self.query = nn.Parameter(torch.zeros(hidden_size))
186
+ self.scale = 1.0 / math.sqrt(hidden_size)
187
+
188
+ def forward(
189
+ self,
190
+ hidden_states: torch.Tensor,
191
+ memories: list[torch.Tensor],
192
+ ) -> torch.Tensor:
193
+ if not memories:
194
+ return hidden_states
195
+ stacked = torch.stack(memories, dim=2)
196
+ scores = torch.einsum("d,bsnd->bsn", self.query, stacked) * self.scale
197
+ weights = torch.softmax(scores, dim=-1)
198
+ mixed = torch.einsum("bsn,bsnd->bsd", weights, stacked)
199
+ return hidden_states + mixed
tiny_gdn/kdn/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Diagonal Kalman Delta Network (KDN) token mixer and recurrences."""
2
+
3
+ from tiny_gdn.kdn.layer import SUPPORTED_HEAD_DIMS, DiagonalKalmanDeltaNet
4
+
5
+ __all__ = ["SUPPORTED_HEAD_DIMS", "DiagonalKalmanDeltaNet"]
tiny_gdn/kdn/kernels.py ADDED
The diff for this file is too large to render. See raw diff
 
tiny_gdn/kdn/layer.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Diagonal Kalman Delta Network token mixer (arXiv:2609.07816)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Any
7
+
8
+ import torch
9
+ from einops import rearrange
10
+ from fla.layers.utils import get_layer_cache, update_layer_cache
11
+ from fla.modules import FusedRMSNormGated, ShortConvolution
12
+ from torch import nn
13
+
14
+ from tiny_gdn.kdn import recurrence
15
+ from tiny_gdn.kdn.kernels import (
16
+ _diag_kdn_frontend_precumsum_paired,
17
+ _diag_kdn_gain,
18
+ _diag_kdn_normalize,
19
+ chunk_kalman,
20
+ )
21
+
22
+ SUPPORTED_HEAD_DIMS = (64, 128)
23
+
24
+
25
+ class DiagonalKalmanDeltaNet(nn.Module):
26
+ """Delta-rule memory whose write strength is a per-channel Kalman gain.
27
+
28
+ Each head keeps a diagonal posterior covariance over its key channels:
29
+ channels that have absorbed little evidence take large corrections and
30
+ well-determined ones small corrections, replacing KDA's scalar ``beta``.
31
+ Stateless CUDA BF16 calls run the fused Triton kernels. Cached prefill,
32
+ decode, padding masks, and other dtypes use the exact PyTorch recurrences.
33
+
34
+ The cache holds ``recurrent_state = (memory [B,H,K,K], precision [B,H,K])``
35
+ in float32 plus the three short-convolution states.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ hidden_size: int,
41
+ head_dim: int,
42
+ num_heads: int,
43
+ *,
44
+ conv_size: int = 4,
45
+ norm_eps: float = 1e-5,
46
+ layer_idx: int | None = None,
47
+ ) -> None:
48
+ super().__init__()
49
+ if head_dim not in SUPPORTED_HEAD_DIMS:
50
+ raise ValueError(
51
+ f"Diagonal KDN head_dim must be one of {SUPPORTED_HEAD_DIMS}, got {head_dim}"
52
+ )
53
+ if num_heads <= 0 or hidden_size <= 0:
54
+ raise ValueError("hidden_size and num_heads must be positive")
55
+ if conv_size <= 0:
56
+ raise ValueError("conv_size must be positive")
57
+ self.hidden_size = hidden_size
58
+ self.head_dim = head_dim
59
+ self.num_heads = num_heads
60
+ self.key_dim = num_heads * head_dim
61
+ self.layer_idx = layer_idx
62
+ self.scale = head_dim**-0.5
63
+ self.info_scale = float(head_dim)
64
+
65
+ self.q_proj = nn.Linear(hidden_size, self.key_dim, bias=False)
66
+ self.k_proj = nn.Linear(hidden_size, self.key_dim, bias=False)
67
+ self.v_proj = nn.Linear(hidden_size, self.key_dim, bias=False)
68
+ self.q_conv1d, self.k_conv1d, self.v_conv1d = (
69
+ ShortConvolution(
70
+ hidden_size=self.key_dim,
71
+ kernel_size=conv_size,
72
+ bias=False,
73
+ activation="silu",
74
+ )
75
+ for _ in range(3)
76
+ )
77
+
78
+ self.f_proj = nn.Sequential(
79
+ nn.Linear(hidden_size, head_dim, bias=False),
80
+ nn.Linear(head_dim, self.key_dim, bias=False),
81
+ )
82
+ self.A_log = nn.Parameter(
83
+ torch.log(torch.empty(num_heads, dtype=torch.float32).uniform_(1, 16))
84
+ )
85
+ self.A_log._no_weight_decay = True
86
+ dt = torch.exp(
87
+ torch.rand(self.key_dim, dtype=torch.float32)
88
+ * (math.log(0.1) - math.log(0.001))
89
+ + math.log(0.001)
90
+ ).clamp(min=1e-4)
91
+ self.dt_bias = nn.Parameter(dt + torch.log(-torch.expm1(-dt)))
92
+ self.dt_bias._no_weight_decay = True
93
+
94
+ self.omega_proj = nn.Sequential(
95
+ nn.Linear(hidden_size, head_dim, bias=False),
96
+ nn.Linear(head_dim, self.key_dim, bias=True),
97
+ )
98
+ self.r_proj = nn.Linear(hidden_size, num_heads, bias=True)
99
+ self.initial_precision_param = nn.Parameter(
100
+ torch.full(
101
+ (num_heads,),
102
+ recurrence.initial_precision_logit(),
103
+ dtype=torch.float32,
104
+ )
105
+ )
106
+ self.initial_precision_param._no_weight_decay = True
107
+
108
+ self.g_proj = nn.Sequential(
109
+ nn.Linear(hidden_size, head_dim, bias=False),
110
+ nn.Linear(head_dim, self.key_dim, bias=True),
111
+ )
112
+ self.o_norm = FusedRMSNormGated(head_dim, activation="sigmoid", eps=norm_eps)
113
+ self.o_proj = nn.Linear(self.key_dim, hidden_size, bias=False)
114
+ self.reset_noise_parameters()
115
+
116
+ def reset_noise_parameters(self) -> None:
117
+ """Isotropic process noise and a neutral observation-noise bias at init.
118
+
119
+ Blanket initializers overwrite these, so callers re-run this afterwards.
120
+ """
121
+
122
+ nn.init.zeros_(self.omega_proj[1].weight)
123
+ nn.init.zeros_(self.omega_proj[1].bias)
124
+ nn.init.zeros_(self.r_proj.bias)
125
+
126
+ def _fused_supported(self, hidden_states: torch.Tensor) -> bool:
127
+ return hidden_states.is_cuda and hidden_states.dtype == torch.bfloat16
128
+
129
+ def _split_heads(self, tensor: torch.Tensor) -> torch.Tensor:
130
+ return rearrange(tensor, "... (h d) -> ... h d", d=self.head_dim).contiguous()
131
+
132
+ def _fused(
133
+ self,
134
+ q: torch.Tensor,
135
+ k: torch.Tensor,
136
+ v: torch.Tensor,
137
+ retention_raw: torch.Tensor,
138
+ omega_raw: torch.Tensor,
139
+ r_raw: torch.Tensor,
140
+ ) -> torch.Tensor:
141
+ q_memory, k_gain, k_memory = _diag_kdn_normalize(q, k)
142
+ g_cumsum, alpha, omega, r, initial_precision = (
143
+ _diag_kdn_frontend_precumsum_paired(
144
+ retention_raw,
145
+ omega_raw,
146
+ r_raw,
147
+ self.A_log.float().contiguous(),
148
+ self.dt_bias.float().view(self.num_heads, self.head_dim).contiguous(),
149
+ self.initial_precision_param.float().contiguous(),
150
+ )
151
+ )
152
+ kappa = _diag_kdn_gain(
153
+ k_gain, alpha, omega, r, initial_precision, omega_raw, r_raw
154
+ )
155
+ return chunk_kalman(q_memory, k_memory, kappa, v, g_cumsum, self.scale)
156
+
157
+ def _stateful(
158
+ self,
159
+ q: torch.Tensor,
160
+ k: torch.Tensor,
161
+ v: torch.Tensor,
162
+ retention_raw: torch.Tensor,
163
+ omega_raw: torch.Tensor,
164
+ r_raw: torch.Tensor,
165
+ state: tuple[torch.Tensor, torch.Tensor] | None,
166
+ token_mask: torch.Tensor | None,
167
+ ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
168
+ log_alpha, omega, r, initial_precision = recurrence.kalman_noise(
169
+ retention_raw,
170
+ omega_raw,
171
+ r_raw,
172
+ self.A_log,
173
+ self.dt_bias,
174
+ self.initial_precision_param,
175
+ )
176
+ q, k = recurrence.normalize_query_key(q, k)
177
+ v = v.to(q.dtype)
178
+ if token_mask is not None:
179
+ k, v, log_alpha, omega = recurrence.mask_padding(
180
+ token_mask, k, v, log_alpha, omega
181
+ )
182
+ if state is None:
183
+ memory, precision = recurrence.initial_state(
184
+ initial_precision,
185
+ batch_size=q.shape[0],
186
+ key_dim=self.head_dim,
187
+ value_dim=self.head_dim,
188
+ )
189
+ else:
190
+ memory, precision = state
191
+ if q.shape[1] == 1:
192
+ output, memory, precision = recurrence.diag_kdn_step(
193
+ q[:, 0],
194
+ k[:, 0],
195
+ v[:, 0],
196
+ log_alpha[:, 0],
197
+ omega[:, 0],
198
+ r[:, 0],
199
+ memory,
200
+ precision,
201
+ scale=self.scale,
202
+ info_scale=self.info_scale,
203
+ )
204
+ output = output[:, None]
205
+ else:
206
+ output, memory, precision = recurrence.diag_kdn_chunked(
207
+ q,
208
+ k,
209
+ v,
210
+ log_alpha,
211
+ omega,
212
+ r,
213
+ memory,
214
+ precision,
215
+ scale=self.scale,
216
+ info_scale=self.info_scale,
217
+ )
218
+ return output, (memory, precision)
219
+
220
+ def forward(
221
+ self,
222
+ hidden_states: torch.Tensor,
223
+ attention_mask: torch.Tensor | None = None,
224
+ past_key_values: Any | None = None,
225
+ use_cache: bool = False,
226
+ output_attentions: bool = False,
227
+ **kwargs: Any,
228
+ ) -> tuple[torch.Tensor, None, Any | None]:
229
+ if hidden_states.ndim != 3 or hidden_states.shape[-1] != self.hidden_size:
230
+ raise ValueError(
231
+ f"hidden_states must have shape [B, T, {self.hidden_size}]"
232
+ )
233
+ if attention_mask is not None and attention_mask.ndim != 2:
234
+ raise ValueError("attention_mask must be a [batch, sequence] padding mask")
235
+ if output_attentions:
236
+ raise ValueError("Diagonal KDN has no attention weights to return")
237
+ length = hidden_states.shape[1]
238
+ last_state = get_layer_cache(self, past_key_values)
239
+ token_mask = (
240
+ None
241
+ if attention_mask is None
242
+ else attention_mask[:, -length:].to(device=hidden_states.device, dtype=torch.bool)
243
+ )
244
+ conv_mask = (
245
+ None if token_mask is None else token_mask.to(hidden_states.dtype)
246
+ )
247
+ conv_q, conv_k, conv_v = (
248
+ (None, None, None) if last_state is None else last_state["conv_state"]
249
+ )
250
+ q, conv_q = self.q_conv1d(
251
+ x=self.q_proj(hidden_states),
252
+ mask=conv_mask,
253
+ cache=conv_q,
254
+ output_final_state=use_cache,
255
+ )
256
+ k, conv_k = self.k_conv1d(
257
+ x=self.k_proj(hidden_states),
258
+ mask=conv_mask,
259
+ cache=conv_k,
260
+ output_final_state=use_cache,
261
+ )
262
+ v, conv_v = self.v_conv1d(
263
+ x=self.v_proj(hidden_states),
264
+ mask=conv_mask,
265
+ cache=conv_v,
266
+ output_final_state=use_cache,
267
+ )
268
+ q, k, v = (self._split_heads(tensor) for tensor in (q, k, v))
269
+ retention_raw = self._split_heads(self.f_proj(hidden_states))
270
+ omega_raw = self._split_heads(self.omega_proj(hidden_states))
271
+ r_raw = self.r_proj(hidden_states).contiguous()
272
+
273
+ recurrent_state = None if last_state is None else last_state["recurrent_state"]
274
+ stateless = recurrent_state is None and token_mask is None and not use_cache
275
+ if stateless and self._fused_supported(hidden_states):
276
+ output = self._fused(q, k, v, retention_raw, omega_raw, r_raw)
277
+ else:
278
+ output, recurrent_state = self._stateful(
279
+ q, k, v, retention_raw, omega_raw, r_raw, recurrent_state, token_mask
280
+ )
281
+ update_layer_cache(
282
+ self,
283
+ past_key_values,
284
+ recurrent_state=recurrent_state,
285
+ conv_state=(conv_q, conv_k, conv_v),
286
+ offset=length,
287
+ )
288
+ output = self.o_norm(
289
+ output.to(v.dtype),
290
+ self._split_heads(self.g_proj(hidden_states)),
291
+ )
292
+ output = self.o_proj(rearrange(output, "b t h d -> b t (h d)"))
293
+ return output, None, past_key_values
tiny_gdn/kdn/recurrence.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exact PyTorch Diagonal KDN recurrences that carry state across calls.
2
+
3
+ The Triton kernels cover stateless fixed-length training. These functions
4
+ take and return ``(memory, precision)`` so prefill, decode, and padded batches
5
+ continue a sequence exactly; the sequential form is also the test oracle.
6
+
7
+ Per head, with one covariance coordinate ``p`` per key channel::
8
+
9
+ p_hat_t = alpha_t**2 * p_{t-1} + omega_t
10
+ kappa_t = p_hat_t * k_t / (r_t + sum_i p_hat_ti * k_ti**2)
11
+ 1 / p_t = 1 / p_hat_t + info_scale * k_t**2 / r_t
12
+ S_t = (I - kappa_t k_t^T) diag(alpha_t) S_{t-1} + kappa_t v_t^T
13
+ o_t = scale * S_t^T q_t
14
+
15
+ Shapes: ``q, k, log_alpha, omega`` are ``[B, T, H, K]``, ``v`` is
16
+ ``[B, T, H, V]``, ``r`` is ``[B, T, H]``, ``memory`` is ``[B, H, K, V]`` and
17
+ ``precision`` (``1 / p``) is ``[B, H, K]``. All math runs in float32.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import math
23
+
24
+ import torch
25
+ import torch.nn.functional as F
26
+
27
+ OMEGA_MIN = 0.0
28
+ R_MIN = 0.01
29
+ INITIAL_PRECISION = 1.0
30
+ INITIAL_PRECISION_FLOOR = 0.1
31
+ LOG_MEMORY_ALPHA_FLOOR = math.log(1e-6)
32
+ NORM_EPS = 1e-12
33
+ DEFAULT_CHUNK_SIZE = 64
34
+
35
+
36
+ def initial_precision_logit() -> float:
37
+ """Raw parameter value whose floored softplus equals ``INITIAL_PRECISION``."""
38
+
39
+ return math.log(math.expm1(INITIAL_PRECISION - INITIAL_PRECISION_FLOOR))
40
+
41
+
42
+ def _work(tensor: torch.Tensor) -> torch.Tensor:
43
+ return tensor.to(torch.promote_types(tensor.dtype, torch.float32))
44
+
45
+
46
+ def kalman_noise(
47
+ retention_raw: torch.Tensor,
48
+ omega_raw: torch.Tensor,
49
+ r_raw: torch.Tensor,
50
+ A_log: torch.Tensor,
51
+ dt_bias: torch.Tensor,
52
+ initial_precision_param: torch.Tensor,
53
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
54
+ """Return ``(log_alpha, omega, r, initial_precision)`` like the Triton frontend."""
55
+
56
+ heads, key_dim = retention_raw.shape[-2:]
57
+ rate = _work(A_log).exp().view(heads, 1)
58
+ log_alpha = -rate * F.softplus(
59
+ _work(retention_raw) + _work(dt_bias).view(heads, key_dim)
60
+ )
61
+ omega = OMEGA_MIN + F.softplus(_work(omega_raw))
62
+ r = R_MIN + F.softplus(_work(r_raw))
63
+ initial_precision = INITIAL_PRECISION_FLOOR + F.softplus(
64
+ _work(initial_precision_param)
65
+ )
66
+ return log_alpha, omega, r, initial_precision
67
+
68
+
69
+ def normalize_query_key(
70
+ q: torch.Tensor, k: torch.Tensor
71
+ ) -> tuple[torch.Tensor, torch.Tensor]:
72
+ return (
73
+ F.normalize(_work(q), dim=-1, eps=NORM_EPS),
74
+ F.normalize(_work(k), dim=-1, eps=NORM_EPS),
75
+ )
76
+
77
+
78
+ def initial_state(
79
+ initial_precision: torch.Tensor,
80
+ *,
81
+ batch_size: int,
82
+ key_dim: int,
83
+ value_dim: int,
84
+ ) -> tuple[torch.Tensor, torch.Tensor]:
85
+ """Zero memory and the learned per-head prior precision for new sequences."""
86
+
87
+ heads = initial_precision.shape[0]
88
+ prior = _work(initial_precision)
89
+ memory = prior.new_zeros((batch_size, heads, key_dim, value_dim))
90
+ precision = prior.view(1, heads, 1).expand(batch_size, heads, key_dim).contiguous()
91
+ return memory, precision
92
+
93
+
94
+ def mask_padding(
95
+ token_mask: torch.Tensor,
96
+ k: torch.Tensor,
97
+ v: torch.Tensor,
98
+ log_alpha: torch.Tensor,
99
+ omega: torch.Tensor,
100
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
101
+ """Turn padded positions into identity steps that leave the state untouched."""
102
+
103
+ keep = token_mask[:, :, None, None]
104
+ return (
105
+ torch.where(keep, k, 0.0),
106
+ torch.where(keep, v, 0.0),
107
+ torch.where(keep, log_alpha, 0.0),
108
+ torch.where(keep, omega, 0.0),
109
+ )
110
+
111
+
112
+ def diag_kdn_step(
113
+ q: torch.Tensor,
114
+ k: torch.Tensor,
115
+ v: torch.Tensor,
116
+ log_alpha: torch.Tensor,
117
+ omega: torch.Tensor,
118
+ r: torch.Tensor,
119
+ memory: torch.Tensor,
120
+ precision: torch.Tensor,
121
+ *,
122
+ scale: float,
123
+ info_scale: float,
124
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
125
+ """Advance one token: ``q, k, log_alpha, omega`` are ``[B, H, K]``."""
126
+
127
+ k_squared = k.square()
128
+ predicted = (2.0 * log_alpha).exp() / precision + omega
129
+ noise = r[..., None]
130
+ gain = predicted * k / (noise + (predicted * k_squared).sum(-1, keepdim=True))
131
+ precision = predicted.reciprocal() + info_scale * k_squared / noise
132
+ decayed = memory * log_alpha.clamp_min(LOG_MEMORY_ALPHA_FLOOR).exp()[..., None]
133
+ innovation = v - torch.einsum("bhk,bhkv->bhv", k, decayed)
134
+ memory = decayed + gain[..., None] * innovation[..., None, :]
135
+ output = scale * torch.einsum("bhk,bhkv->bhv", q, memory)
136
+ return output, memory, precision
137
+
138
+
139
+ def diag_kdn_recurrent(
140
+ q: torch.Tensor,
141
+ k: torch.Tensor,
142
+ v: torch.Tensor,
143
+ log_alpha: torch.Tensor,
144
+ omega: torch.Tensor,
145
+ r: torch.Tensor,
146
+ memory: torch.Tensor,
147
+ precision: torch.Tensor,
148
+ *,
149
+ scale: float,
150
+ info_scale: float,
151
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
152
+ """Token-by-token reference; returns ``(output [B,T,H,V], memory, precision)``."""
153
+
154
+ outputs = []
155
+ for token in range(q.shape[1]):
156
+ output, memory, precision = diag_kdn_step(
157
+ q[:, token],
158
+ k[:, token],
159
+ v[:, token],
160
+ log_alpha[:, token],
161
+ omega[:, token],
162
+ r[:, token],
163
+ memory,
164
+ precision,
165
+ scale=scale,
166
+ info_scale=info_scale,
167
+ )
168
+ outputs.append(output)
169
+ return torch.stack(outputs, dim=1), memory, precision
170
+
171
+
172
+ def _compose(
173
+ later: tuple[torch.Tensor, ...], earlier: tuple[torch.Tensor, ...]
174
+ ) -> tuple[torch.Tensor, ...]:
175
+ """Product ``later @ earlier`` of 2x2 maps stored as (m00, m01, m10, m11)."""
176
+
177
+ a00, a01, a10, a11 = later
178
+ b00, b01, b10, b11 = earlier
179
+ product = (
180
+ a00 * b00 + a01 * b10,
181
+ a00 * b01 + a01 * b11,
182
+ a10 * b00 + a11 * b10,
183
+ a10 * b01 + a11 * b11,
184
+ )
185
+ # Mobius maps are invariant to scaling, so renormalize to stay in range.
186
+ magnitude = torch.stack(product).abs().amax(dim=0).clamp_min(
187
+ torch.finfo(product[0].dtype).tiny
188
+ )
189
+ return tuple(entry / magnitude for entry in product)
190
+
191
+
192
+ def _inclusive_scan(maps: tuple[torch.Tensor, ...]) -> tuple[torch.Tensor, ...]:
193
+ """Hillis-Steele prefix products ``M_t @ ... @ M_1`` along dim 1."""
194
+
195
+ length = maps[0].shape[1]
196
+ offset = 1
197
+ while offset < length:
198
+ combined = _compose(
199
+ tuple(entry[:, offset:] for entry in maps),
200
+ tuple(entry[:, :-offset] for entry in maps),
201
+ )
202
+ maps = tuple(
203
+ torch.cat((entry[:, :offset], update), dim=1)
204
+ for entry, update in zip(maps, combined)
205
+ )
206
+ offset *= 2
207
+ return maps
208
+
209
+
210
+ def kalman_gain(
211
+ k: torch.Tensor,
212
+ log_alpha: torch.Tensor,
213
+ omega: torch.Tensor,
214
+ r: torch.Tensor,
215
+ precision: torch.Tensor,
216
+ *,
217
+ info_scale: float,
218
+ ) -> tuple[torch.Tensor, torch.Tensor]:
219
+ """Vector gains for every token via a parallel Mobius scan, plus final precision."""
220
+
221
+ k_squared = k.square()
222
+ noise = r[..., None]
223
+ alpha_squared = (2.0 * log_alpha).exp()
224
+ information = info_scale * k_squared / noise
225
+ maps = _inclusive_scan(
226
+ (
227
+ alpha_squared,
228
+ omega,
229
+ information * alpha_squared,
230
+ 1.0 + information * omega,
231
+ )
232
+ )
233
+ start = precision.reciprocal()[:, None]
234
+ covariance = (maps[0] * start + maps[1]) / (maps[2] * start + maps[3])
235
+ previous = torch.cat((start, covariance[:, :-1]), dim=1)
236
+ predicted = alpha_squared * previous + omega
237
+ gain = predicted * k / (noise + (predicted * k_squared).sum(-1, keepdim=True))
238
+ return gain, covariance[:, -1].reciprocal()
239
+
240
+
241
+ def chunked_memory(
242
+ q: torch.Tensor,
243
+ k: torch.Tensor,
244
+ v: torch.Tensor,
245
+ log_alpha: torch.Tensor,
246
+ gain: torch.Tensor,
247
+ memory: torch.Tensor,
248
+ *,
249
+ scale: float,
250
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
251
+ ) -> tuple[torch.Tensor, torch.Tensor]:
252
+ """Two-key delta-rule memory in WY form, one triangular solve per chunk."""
253
+
254
+ if chunk_size <= 0:
255
+ raise ValueError("chunk_size must be positive")
256
+ log_alpha = log_alpha.clamp_min(LOG_MEMORY_ALPHA_FLOOR)
257
+ length = q.shape[1]
258
+ outputs = []
259
+ for start in range(0, length, chunk_size):
260
+ stop = min(start + chunk_size, length)
261
+ size = stop - start
262
+ q_chunk, k_chunk, v_chunk, gain_chunk = (
263
+ tensor[:, start:stop].transpose(1, 2) for tensor in (q, k, v, gain)
264
+ )
265
+ cumulative = log_alpha[:, start:stop].transpose(1, 2).cumsum(dim=2)
266
+ causal = torch.ones(size, size, dtype=torch.bool, device=q.device).tril()
267
+ differences = cumulative[:, :, :, None, :] - cumulative[:, :, None, :, :]
268
+ weights = (
269
+ torch.where(causal[:, :, None], differences, -math.inf).exp()
270
+ * gain_chunk[:, :, None, :, :]
271
+ )
272
+ interaction = torch.einsum("bhtk,bhtsk->bhts", k_chunk, weights)
273
+ readout = torch.einsum("bhtk,bhtsk->bhts", q_chunk, weights)
274
+ decay = cumulative.exp()
275
+ corrected = torch.linalg.solve_triangular(
276
+ interaction,
277
+ v_chunk - (decay * k_chunk) @ memory,
278
+ upper=False,
279
+ unitriangular=True,
280
+ )
281
+ outputs.append(
282
+ (scale * ((q_chunk * decay) @ memory + readout @ corrected)).transpose(1, 2)
283
+ )
284
+ to_end = (cumulative[:, :, -1:] - cumulative).exp() * gain_chunk
285
+ memory = memory * decay[:, :, -1, :, None] + to_end.transpose(-1, -2) @ corrected
286
+ return torch.cat(outputs, dim=1), memory
287
+
288
+
289
+ def diag_kdn_chunked(
290
+ q: torch.Tensor,
291
+ k: torch.Tensor,
292
+ v: torch.Tensor,
293
+ log_alpha: torch.Tensor,
294
+ omega: torch.Tensor,
295
+ r: torch.Tensor,
296
+ memory: torch.Tensor,
297
+ precision: torch.Tensor,
298
+ *,
299
+ scale: float,
300
+ info_scale: float,
301
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
302
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
303
+ """Parallel form of :func:`diag_kdn_recurrent` with the same signature."""
304
+
305
+ gain, precision = kalman_gain(
306
+ k, log_alpha, omega, r, precision, info_scale=info_scale
307
+ )
308
+ output, memory = chunked_memory(
309
+ q, k, v, log_alpha, gain, memory, scale=scale, chunk_size=chunk_size
310
+ )
311
+ return output, memory, precision
tiny_gdn/model.py ADDED
@@ -0,0 +1,782 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import torch
9
+ import torch.nn.functional as F
10
+ from safetensors.torch import load_model, save_model
11
+ from torch import nn
12
+ from torch.nn.attention import sdpa_kernel
13
+ from torch.utils.checkpoint import checkpoint
14
+
15
+ from tiny_gdn.config import LINEAR_LAYER_TYPES, TinyGDNConfig
16
+ from tiny_gdn.haiku_layers import (
17
+ BlockAttentionResidual,
18
+ GatedMultiheadLatentAttention,
19
+ SiTUGLU,
20
+ )
21
+ from tiny_gdn.nn_common import (
22
+ RMSNorm,
23
+ attention_sdpa_backends,
24
+ linear_cross_entropy_reference,
25
+ )
26
+
27
+ try:
28
+ from tiny_gdn.fused_ops import linear_cross_entropy
29
+ except ImportError:
30
+ linear_cross_entropy = linear_cross_entropy_reference
31
+
32
+ try:
33
+ # Import the module directly — `from fla.layers import GatedDeltaNet2`
34
+ # executes layers/__init__.py and eagerly loads every attention kernel.
35
+ from fla.layers.gdn2 import GatedDeltaNet2
36
+ except ImportError as import_error:
37
+ GatedDeltaNet2 = None
38
+ FLA_IMPORT_ERROR: ImportError | None = import_error
39
+ else:
40
+ FLA_IMPORT_ERROR = None
41
+
42
+ try:
43
+ from fla.layers.kda import KimiDeltaAttention
44
+ except ImportError as import_error:
45
+ KimiDeltaAttention = None
46
+ KDA_IMPORT_ERROR: ImportError | None = import_error
47
+ else:
48
+ KDA_IMPORT_ERROR = None
49
+
50
+ try:
51
+ from tiny_gdn.kdn import DiagonalKalmanDeltaNet
52
+ except ImportError as import_error:
53
+ DiagonalKalmanDeltaNet = None
54
+ KDN_IMPORT_ERROR: ImportError | None = import_error
55
+ else:
56
+ KDN_IMPORT_ERROR = None
57
+
58
+
59
+ @dataclass
60
+ class TinyGDNOutput:
61
+ loss: torch.Tensor | None
62
+ logits: torch.Tensor | None
63
+ main_loss: torch.Tensor | None
64
+ mtp_loss: torch.Tensor | None
65
+ z_loss: torch.Tensor | None
66
+ hidden_states: torch.Tensor | None = None
67
+ past_key_values: Any | None = None
68
+
69
+
70
+ class RotaryEmbedding(nn.Module):
71
+ def __init__(self, rotary_dim: int, rope_theta: float) -> None:
72
+ super().__init__()
73
+ inverse_frequency = 1.0 / (
74
+ rope_theta
75
+ ** (
76
+ torch.arange(0, rotary_dim, 2, dtype=torch.float32)
77
+ / rotary_dim
78
+ )
79
+ )
80
+ self.rotary_dim = rotary_dim
81
+ self.register_buffer("inverse_frequency", inverse_frequency, persistent=False)
82
+
83
+ def forward(
84
+ self,
85
+ sequence_length: int,
86
+ device: torch.device,
87
+ dtype: torch.dtype,
88
+ position_offset: int = 0,
89
+ ) -> tuple[torch.Tensor, torch.Tensor]:
90
+ positions = torch.arange(
91
+ position_offset,
92
+ position_offset + sequence_length,
93
+ device=device,
94
+ dtype=torch.float32,
95
+ )
96
+ frequencies = torch.outer(positions, self.inverse_frequency.float())
97
+ embeddings = torch.cat((frequencies, frequencies), dim=-1)
98
+ return embeddings.cos().to(dtype=dtype), embeddings.sin().to(dtype=dtype)
99
+
100
+
101
+ def rotate_half(hidden_states: torch.Tensor) -> torch.Tensor:
102
+ first, second = hidden_states.chunk(2, dim=-1)
103
+ return torch.cat((-second, first), dim=-1)
104
+
105
+
106
+ def apply_rotary_embedding(
107
+ query: torch.Tensor,
108
+ key: torch.Tensor,
109
+ cosine: torch.Tensor,
110
+ sine: torch.Tensor,
111
+ rotary_dim: int,
112
+ ) -> tuple[torch.Tensor, torch.Tensor]:
113
+ cosine = cosine[None, None, :, :]
114
+ sine = sine[None, None, :, :]
115
+ query_rotary, query_pass = query[..., :rotary_dim], query[..., rotary_dim:]
116
+ key_rotary, key_pass = key[..., :rotary_dim], key[..., rotary_dim:]
117
+ query_rotary = query_rotary * cosine + rotate_half(query_rotary) * sine
118
+ key_rotary = key_rotary * cosine + rotate_half(key_rotary) * sine
119
+ return (
120
+ torch.cat((query_rotary, query_pass), dim=-1),
121
+ torch.cat((key_rotary, key_pass), dim=-1),
122
+ )
123
+
124
+
125
+ class GatedGroupedQueryAttention(nn.Module):
126
+ """QK-normalized, partially rotary GQA with a learned sigmoid output gate."""
127
+
128
+ def __init__(self, config: TinyGDNConfig) -> None:
129
+ super().__init__()
130
+ self.num_heads = config.num_attention_heads
131
+ self.num_key_value_heads = config.num_key_value_heads
132
+ self.head_dim = config.attention_head_dim
133
+ self.rotary_dim = config.rotary_dim
134
+ self.dropout = config.attention_dropout
135
+
136
+ query_size = self.num_heads * self.head_dim
137
+ key_value_size = self.num_key_value_heads * self.head_dim
138
+ self.q_gate_proj = nn.Linear(config.hidden_size, query_size * 2, bias=False)
139
+ self.k_proj = nn.Linear(config.hidden_size, key_value_size, bias=False)
140
+ self.v_proj = nn.Linear(config.hidden_size, key_value_size, bias=False)
141
+ self.o_proj = nn.Linear(query_size, config.hidden_size, bias=False)
142
+ self.q_norm = RMSNorm(self.head_dim, config.rms_norm_eps)
143
+ self.k_norm = RMSNorm(self.head_dim, config.rms_norm_eps)
144
+ self.rotary = RotaryEmbedding(self.rotary_dim, config.rope_theta)
145
+
146
+ def _attention_mask(
147
+ self,
148
+ attention_mask: torch.Tensor | None,
149
+ sequence_length: int,
150
+ device: torch.device,
151
+ ) -> torch.Tensor | None:
152
+ if attention_mask is None:
153
+ return None
154
+ if attention_mask.ndim != 2:
155
+ raise ValueError("attention_mask must have shape [batch, sequence]")
156
+ if attention_mask.shape[1] != sequence_length:
157
+ raise ValueError("attention_mask sequence length does not match input")
158
+
159
+ causal = torch.ones(
160
+ sequence_length,
161
+ sequence_length,
162
+ dtype=torch.bool,
163
+ device=device,
164
+ ).tril()
165
+ valid_keys = attention_mask[:, None, None, :].to(dtype=torch.bool, device=device)
166
+ return causal[None, None, :, :] & valid_keys
167
+
168
+ def forward(
169
+ self,
170
+ hidden_states: torch.Tensor,
171
+ attention_mask: torch.Tensor | None = None,
172
+ past_key_value: tuple[torch.Tensor, torch.Tensor] | None = None,
173
+ ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor] | None]:
174
+ batch_size, sequence_length, _ = hidden_states.shape
175
+ past_len = 0 if past_key_value is None else past_key_value[0].shape[2]
176
+ query_and_gate = self.q_gate_proj(hidden_states)
177
+ query, output_gate = query_and_gate.chunk(2, dim=-1)
178
+
179
+ query = query.view(batch_size, sequence_length, self.num_heads, self.head_dim)
180
+ key = self.k_proj(hidden_states).view(
181
+ batch_size,
182
+ sequence_length,
183
+ self.num_key_value_heads,
184
+ self.head_dim,
185
+ )
186
+ value = self.v_proj(hidden_states).view(
187
+ batch_size,
188
+ sequence_length,
189
+ self.num_key_value_heads,
190
+ self.head_dim,
191
+ )
192
+
193
+ query = self.q_norm(query).transpose(1, 2)
194
+ key = self.k_norm(key).transpose(1, 2)
195
+ value = value.transpose(1, 2)
196
+
197
+ cosine, sine = self.rotary(
198
+ sequence_length,
199
+ device=hidden_states.device,
200
+ dtype=query.dtype,
201
+ position_offset=past_len,
202
+ )
203
+ query, key = apply_rotary_embedding(
204
+ query,
205
+ key,
206
+ cosine,
207
+ sine,
208
+ rotary_dim=self.rotary_dim,
209
+ )
210
+ if past_key_value is not None:
211
+ key = torch.cat([past_key_value[0], key], dim=2)
212
+ value = torch.cat([past_key_value[1], value], dim=2)
213
+ present = (key, value)
214
+
215
+ kv_len = key.shape[2]
216
+ if attention_mask is not None and past_key_value is None:
217
+ sdpa_mask = self._attention_mask(
218
+ attention_mask,
219
+ sequence_length,
220
+ hidden_states.device,
221
+ )
222
+ is_causal = False
223
+ elif past_key_value is not None and sequence_length == 1:
224
+ # Decode step: query attends to the cached key/value prefix. Preserve
225
+ # the prefill padding mask when decoding a left-padded prompt batch.
226
+ sdpa_mask = (
227
+ None
228
+ if attention_mask is None
229
+ else attention_mask[:, None, None, :].to(
230
+ dtype=torch.bool,
231
+ device=hidden_states.device,
232
+ )
233
+ )
234
+ is_causal = False
235
+ elif past_key_value is not None:
236
+ # Prefill chunk with cache — build causal mask over kv_len.
237
+ q_idx = torch.arange(
238
+ past_len, past_len + sequence_length, device=hidden_states.device
239
+ )[:, None]
240
+ k_idx = torch.arange(kv_len, device=hidden_states.device)[None, :]
241
+ sdpa_mask = (k_idx <= q_idx)[None, None, :, :]
242
+ is_causal = False
243
+ else:
244
+ sdpa_mask = None
245
+ is_causal = True
246
+ sdpa_options = {
247
+ "attn_mask": sdpa_mask,
248
+ "dropout_p": self.dropout if self.training else 0.0,
249
+ "is_causal": is_causal,
250
+ "enable_gqa": True,
251
+ }
252
+ with sdpa_kernel(attention_sdpa_backends(query.device)):
253
+ attention_output = F.scaled_dot_product_attention(
254
+ query,
255
+ key,
256
+ value,
257
+ **sdpa_options,
258
+ )
259
+ attention_output = attention_output.transpose(1, 2).reshape(
260
+ batch_size,
261
+ sequence_length,
262
+ -1,
263
+ )
264
+ attention_output = attention_output * torch.sigmoid(output_gate)
265
+ return self.o_proj(attention_output), present
266
+
267
+
268
+ class SwiGLU(nn.Module):
269
+ def __init__(self, config: TinyGDNConfig) -> None:
270
+ super().__init__()
271
+ self.gate_up_proj = nn.Linear(
272
+ config.hidden_size,
273
+ config.intermediate_size * 2,
274
+ bias=False,
275
+ )
276
+ self.down_proj = nn.Linear(
277
+ config.intermediate_size,
278
+ config.hidden_size,
279
+ bias=False,
280
+ )
281
+
282
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
283
+ gate, up = self.gate_up_proj(hidden_states).chunk(2, dim=-1)
284
+ return self.down_proj(F.silu(gate) * up)
285
+
286
+
287
+ class TinyGDNBlock(nn.Module):
288
+ def __init__(self, config: TinyGDNConfig, layer_index: int) -> None:
289
+ super().__init__()
290
+ layer_type = config.layer_types[layer_index]
291
+ self.layer_type = layer_type
292
+ self.token_mixer_norm = RMSNorm(config.hidden_size, config.rms_norm_eps)
293
+ self.mlp_norm = RMSNorm(config.hidden_size, config.rms_norm_eps)
294
+
295
+ if layer_type == "gdn2":
296
+ if GatedDeltaNet2 is None:
297
+ raise ImportError(
298
+ "Gated DeltaNet-2 requires the pinned flash-linear-attention dependency"
299
+ ) from FLA_IMPORT_ERROR
300
+ self.token_mixer = GatedDeltaNet2(
301
+ hidden_size=config.hidden_size,
302
+ expand_v=config.linear_expand_v,
303
+ head_dim=config.linear_head_dim,
304
+ num_heads=config.linear_num_heads,
305
+ num_v_heads=config.linear_num_value_heads,
306
+ mode="chunk",
307
+ use_short_conv=True,
308
+ allow_neg_eigval=config.allow_negative_eigenvalues,
309
+ conv_size=config.linear_conv_kernel_dim,
310
+ conv_bias=False,
311
+ layer_idx=layer_index,
312
+ norm_eps=config.rms_norm_eps,
313
+ )
314
+ elif layer_type == "kda":
315
+ if KimiDeltaAttention is None:
316
+ raise ImportError(
317
+ "Kimi Delta Attention requires the pinned flash-linear-attention dependency"
318
+ ) from KDA_IMPORT_ERROR
319
+ self.token_mixer = KimiDeltaAttention(
320
+ hidden_size=config.hidden_size,
321
+ expand_v=config.linear_expand_v,
322
+ head_dim=config.linear_head_dim,
323
+ num_heads=config.linear_num_heads,
324
+ num_v_heads=config.linear_num_value_heads,
325
+ mode="chunk",
326
+ use_short_conv=True,
327
+ allow_neg_eigval=config.allow_negative_eigenvalues,
328
+ safe_gate=config.kda_safe_gate,
329
+ lower_bound=config.kda_lower_bound,
330
+ conv_size=config.linear_conv_kernel_dim,
331
+ conv_bias=False,
332
+ layer_idx=layer_index,
333
+ norm_eps=config.rms_norm_eps,
334
+ )
335
+ elif layer_type == "diag_kdn":
336
+ if DiagonalKalmanDeltaNet is None:
337
+ raise ImportError(
338
+ "Diagonal KDN requires Triton and the pinned flash-linear-attention dependency"
339
+ ) from KDN_IMPORT_ERROR
340
+ self.token_mixer = DiagonalKalmanDeltaNet(
341
+ hidden_size=config.hidden_size,
342
+ head_dim=config.linear_head_dim,
343
+ num_heads=config.linear_num_heads,
344
+ conv_size=config.linear_conv_kernel_dim,
345
+ norm_eps=config.rms_norm_eps,
346
+ layer_idx=layer_index,
347
+ )
348
+ elif layer_type == "full_attention":
349
+ self.token_mixer = GatedGroupedQueryAttention(config)
350
+ elif layer_type == "gated_mla":
351
+ self.token_mixer = GatedMultiheadLatentAttention(config)
352
+ else:
353
+ raise ValueError(f"Unsupported layer type: {layer_type}")
354
+
355
+ if config.mlp_activation == "swiglu":
356
+ self.mlp: nn.Module = SwiGLU(config)
357
+ elif config.mlp_activation == "situ_glu":
358
+ self.mlp = SiTUGLU(config)
359
+ else:
360
+ raise ValueError(f"Unsupported mlp_activation: {config.mlp_activation}")
361
+
362
+ def forward(
363
+ self,
364
+ hidden_states: torch.Tensor,
365
+ attention_mask: torch.Tensor | None = None,
366
+ *,
367
+ past_key_values: Any | None = None,
368
+ past_key_value: tuple[torch.Tensor, torch.Tensor] | None = None,
369
+ use_cache: bool = False,
370
+ ) -> tuple[torch.Tensor, Any]:
371
+ residual = hidden_states
372
+ normalized = self.token_mixer_norm(hidden_states)
373
+ present: Any = None
374
+ if self.layer_type in LINEAR_LAYER_TYPES:
375
+ mixed, _, past_key_values = self.token_mixer(
376
+ normalized,
377
+ attention_mask=attention_mask,
378
+ past_key_values=past_key_values,
379
+ use_cache=use_cache,
380
+ )
381
+ present = past_key_values
382
+ elif self.layer_type in {"full_attention", "gated_mla"}:
383
+ mixed, present = self.token_mixer(
384
+ normalized,
385
+ attention_mask=attention_mask,
386
+ past_key_value=past_key_value,
387
+ )
388
+ if not use_cache:
389
+ present = None
390
+ else:
391
+ raise ValueError(f"Unsupported layer type: {self.layer_type}")
392
+ hidden_states = residual + mixed
393
+ hidden_states = hidden_states + self.mlp(self.mlp_norm(hidden_states))
394
+ return hidden_states, present
395
+
396
+
397
+ class MultiTokenPredictionAdapter(nn.Module):
398
+ """A lightweight residual adapter for one additional prediction horizon."""
399
+
400
+ def __init__(self, config: TinyGDNConfig) -> None:
401
+ super().__init__()
402
+ self.norm = RMSNorm(config.hidden_size, config.rms_norm_eps)
403
+ self.down_proj = nn.Linear(
404
+ config.hidden_size,
405
+ config.mtp_adapter_rank,
406
+ bias=False,
407
+ )
408
+ self.up_proj = nn.Linear(
409
+ config.mtp_adapter_rank,
410
+ config.hidden_size,
411
+ bias=False,
412
+ )
413
+
414
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
415
+ adapted = self.up_proj(F.silu(self.down_proj(self.norm(hidden_states))))
416
+ return hidden_states + adapted
417
+
418
+
419
+ class TinyGDNForCausalLM(nn.Module):
420
+ def __init__(self, config: TinyGDNConfig) -> None:
421
+ super().__init__()
422
+ self.config = config
423
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
424
+ self.layers = nn.ModuleList(
425
+ TinyGDNBlock(config, layer_index)
426
+ for layer_index in range(config.num_hidden_layers)
427
+ )
428
+ self.final_norm = RMSNorm(config.hidden_size, config.rms_norm_eps)
429
+ self.mtp_adapters = nn.ModuleList(
430
+ MultiTokenPredictionAdapter(config)
431
+ for _ in range(config.mtp_num_heads)
432
+ )
433
+ self.block_attn_res = (
434
+ BlockAttentionResidual(config.hidden_size)
435
+ if config.use_block_attn_res
436
+ else None
437
+ )
438
+ self.checkpointed_layers: frozenset[int] = frozenset()
439
+
440
+ self.apply(self._initialize_module)
441
+ self._initialize_residual_projections()
442
+
443
+ def _initialize_module(self, module: nn.Module) -> None:
444
+ if isinstance(module, nn.Linear):
445
+ nn.init.normal_(
446
+ module.weight,
447
+ mean=0.0,
448
+ std=self.config.initializer_range,
449
+ )
450
+ if module.bias is not None:
451
+ nn.init.zeros_(module.bias)
452
+ elif isinstance(module, nn.Embedding):
453
+ nn.init.normal_(
454
+ module.weight,
455
+ mean=0.0,
456
+ std=self.config.initializer_range,
457
+ )
458
+ elif DiagonalKalmanDeltaNet is not None and isinstance(
459
+ module, DiagonalKalmanDeltaNet
460
+ ):
461
+ # apply() visits children first, so this runs after the Linear re-init.
462
+ module.reset_noise_parameters()
463
+
464
+ def _initialize_residual_projections(self) -> None:
465
+ residual_std = self.config.initializer_range / math.sqrt(
466
+ 2 * self.config.num_hidden_layers
467
+ )
468
+ for layer in self.layers:
469
+ nn.init.normal_(
470
+ layer.token_mixer.o_proj.weight,
471
+ mean=0.0,
472
+ std=residual_std,
473
+ )
474
+ nn.init.normal_(
475
+ layer.mlp.down_proj.weight,
476
+ mean=0.0,
477
+ std=residual_std,
478
+ )
479
+ for adapter in self.mtp_adapters:
480
+ nn.init.normal_(adapter.up_proj.weight, mean=0.0, std=residual_std)
481
+
482
+ def enable_gradient_checkpointing(
483
+ self,
484
+ enabled: bool = True,
485
+ *,
486
+ layers: int | None = None,
487
+ ) -> None:
488
+ """Recompute block activations during backward instead of storing them.
489
+
490
+ ``layers`` checkpoints only that many blocks, linear-attention blocks
491
+ first: replaying softmax attention costs quadratically in sequence
492
+ length, replaying a linear-attention block costs linearly. ``None``
493
+ checkpoints every block.
494
+ """
495
+
496
+ if not enabled:
497
+ if layers is not None:
498
+ raise ValueError("layers needs gradient checkpointing enabled")
499
+ self.checkpointed_layers = frozenset()
500
+ return
501
+ count = len(self.layers) if layers is None else layers
502
+ if not 1 <= count <= len(self.layers):
503
+ raise ValueError(f"layers must be between 1 and {len(self.layers)}")
504
+ cheapest_replay_first = sorted(
505
+ range(len(self.layers)),
506
+ key=lambda index: self.layers[index].layer_type not in LINEAR_LAYER_TYPES,
507
+ )
508
+ self.checkpointed_layers = frozenset(cheapest_replay_first[:count])
509
+
510
+ def describe_gradient_checkpointing(self) -> str:
511
+ if not self.checkpointed_layers:
512
+ return "off"
513
+ layer_types = sorted(
514
+ {self.layers[index].layer_type for index in self.checkpointed_layers}
515
+ )
516
+ return (
517
+ f"{len(self.checkpointed_layers)}/{len(self.layers)} blocks "
518
+ f"({', '.join(layer_types)})"
519
+ )
520
+
521
+ def project_to_vocabulary(self, hidden_states: torch.Tensor) -> torch.Tensor:
522
+ return F.linear(hidden_states, self.embed_tokens.weight)
523
+
524
+ def _run_layer(
525
+ self,
526
+ layer_index: int,
527
+ hidden_states: torch.Tensor,
528
+ attention_mask: torch.Tensor | None,
529
+ *,
530
+ past_key_values: Any | None = None,
531
+ past_key_value: tuple[torch.Tensor, torch.Tensor] | None = None,
532
+ use_cache: bool = False,
533
+ ) -> tuple[torch.Tensor, Any]:
534
+ layer = self.layers[layer_index]
535
+ if self.training and layer_index in self.checkpointed_layers:
536
+ hidden_states, present = checkpoint(
537
+ layer,
538
+ hidden_states,
539
+ attention_mask,
540
+ use_reentrant=False,
541
+ )
542
+ return hidden_states, present
543
+ return layer(
544
+ hidden_states,
545
+ attention_mask,
546
+ past_key_values=past_key_values,
547
+ past_key_value=past_key_value,
548
+ use_cache=use_cache,
549
+ )
550
+
551
+ def _causal_loss(
552
+ self,
553
+ hidden_states: torch.Tensor,
554
+ labels: torch.Tensor,
555
+ target_offset: int,
556
+ adapter: nn.Module | None = None,
557
+ z_loss_coefficient: float = 0.0,
558
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
559
+ """Return ``(objective, cross_entropy, z_loss)``.
560
+
561
+ Only ``objective = cross_entropy + z_loss_coefficient * z_loss`` carries
562
+ gradients; the other two are detached values for logging.
563
+ """
564
+
565
+ if target_offset < 0:
566
+ raise ValueError("target_offset cannot be negative")
567
+ if target_offset and hidden_states.shape[1] <= target_offset:
568
+ raise ValueError(
569
+ f"Sequence length must exceed target offset {target_offset}"
570
+ )
571
+ if target_offset:
572
+ prediction_states = hidden_states[:, :-target_offset, :]
573
+ targets = labels[:, target_offset:].contiguous()
574
+ else:
575
+ prediction_states = hidden_states
576
+ targets = labels.contiguous()
577
+ if adapter is not None:
578
+ prediction_states = adapter(prediction_states)
579
+ return linear_cross_entropy(
580
+ prediction_states.reshape(-1, prediction_states.shape[-1]),
581
+ self.embed_tokens.weight,
582
+ targets.reshape(-1),
583
+ z_loss_coefficient=z_loss_coefficient,
584
+ )
585
+
586
+ def forward(
587
+ self,
588
+ input_ids: torch.Tensor,
589
+ labels: torch.Tensor | None = None,
590
+ attention_mask: torch.Tensor | None = None,
591
+ past_key_values: Any | None = None,
592
+ *,
593
+ use_cache: bool = False,
594
+ return_logits: bool = True,
595
+ return_hidden_states: bool = False,
596
+ labels_are_shifted: bool = False,
597
+ include_mtp_loss: bool = True,
598
+ mtp_loss_weight: float | None = None,
599
+ z_loss_coefficient: float = 0.0,
600
+ logits_to_keep: int | None = None,
601
+ ) -> TinyGDNOutput:
602
+ if input_ids.ndim != 2:
603
+ raise ValueError("input_ids must have shape [batch, sequence]")
604
+ if input_ids.shape[1] > self.config.max_position_embeddings:
605
+ raise ValueError("Input exceeds max_position_embeddings")
606
+ if labels is not None and labels.shape != input_ids.shape:
607
+ raise ValueError("labels must have the same shape as input_ids")
608
+ if z_loss_coefficient < 0.0:
609
+ raise ValueError("z_loss_coefficient cannot be negative")
610
+ if logits_to_keep is not None and logits_to_keep <= 0:
611
+ raise ValueError("logits_to_keep must be positive")
612
+ if use_cache and labels is not None:
613
+ raise ValueError("use_cache is not supported with labels")
614
+ effective_mtp_weight = (
615
+ self.config.mtp_loss_weight
616
+ if mtp_loss_weight is None
617
+ else mtp_loss_weight
618
+ )
619
+ if not 0.0 <= effective_mtp_weight <= 1.0:
620
+ raise ValueError("mtp_loss_weight must be between zero and one")
621
+
622
+ if use_cache and past_key_values is None:
623
+ try:
624
+ from fla.models.utils import Cache as FlaCache
625
+ except ImportError as import_error:
626
+ raise ImportError(
627
+ "Cached decode requires flash-linear-attention Cache"
628
+ ) from import_error
629
+ past_key_values = {
630
+ "fla": FlaCache(),
631
+ "gqa": [None] * len(self.layers),
632
+ }
633
+ elif past_key_values is not None and not isinstance(past_key_values, dict):
634
+ raise TypeError("past_key_values must be a TinyGDN cache dict or None")
635
+
636
+ fla_cache = None if past_key_values is None else past_key_values["fla"]
637
+ gqa_cache = None if past_key_values is None else past_key_values["gqa"]
638
+
639
+ hidden_states = self.embed_tokens(input_ids)
640
+ shared_layer_indices = set(self.config.shared_layer_indices)
641
+ softmax_types = {"full_attention", "gated_mla"}
642
+ attn_memories: list[torch.Tensor] = [hidden_states]
643
+ interval = self.config.full_attention_interval
644
+ for layer_index, layer in enumerate(self.layers):
645
+ layer_gqa = None if gqa_cache is None else gqa_cache[layer_index]
646
+ hidden_states, present = self._run_layer(
647
+ layer_index,
648
+ hidden_states,
649
+ attention_mask,
650
+ past_key_values=fla_cache,
651
+ past_key_value=layer_gqa,
652
+ use_cache=use_cache,
653
+ )
654
+ if use_cache and layer.layer_type in softmax_types and gqa_cache is not None:
655
+ gqa_cache[layer_index] = present
656
+ if layer_index in shared_layer_indices:
657
+ layer_gqa = None if gqa_cache is None else gqa_cache[layer_index]
658
+ hidden_states, present = self._run_layer(
659
+ layer_index,
660
+ hidden_states,
661
+ attention_mask,
662
+ past_key_values=fla_cache,
663
+ past_key_value=layer_gqa,
664
+ use_cache=use_cache,
665
+ )
666
+ if use_cache and layer.layer_type in softmax_types and gqa_cache is not None:
667
+ gqa_cache[layer_index] = present
668
+ if (
669
+ self.block_attn_res is not None
670
+ and (layer_index + 1) % interval == 0
671
+ ):
672
+ hidden_states = self.block_attn_res(
673
+ hidden_states, [*attn_memories, hidden_states]
674
+ )
675
+ attn_memories.append(hidden_states)
676
+ hidden_states = self.final_norm(hidden_states)
677
+
678
+ main_loss = None
679
+ mtp_loss = None
680
+ z_loss = None
681
+ total_loss = None
682
+ if labels is not None:
683
+ main_target_offset = 0 if labels_are_shifted else 1
684
+ main_objective, main_loss, z_loss = self._causal_loss(
685
+ hidden_states,
686
+ labels,
687
+ target_offset=main_target_offset,
688
+ z_loss_coefficient=z_loss_coefficient,
689
+ )
690
+ if z_loss_coefficient == 0.0:
691
+ z_loss = None
692
+ if self.mtp_adapters and include_mtp_loss:
693
+ auxiliary_losses = [
694
+ self._causal_loss(
695
+ hidden_states,
696
+ labels,
697
+ target_offset=(
698
+ head_index + 1
699
+ if labels_are_shifted
700
+ else head_index + 2
701
+ ),
702
+ adapter=adapter,
703
+ )[0]
704
+ for head_index, adapter in enumerate(self.mtp_adapters)
705
+ ]
706
+ mtp_loss = torch.stack(auxiliary_losses).mean()
707
+ total_loss = main_objective + effective_mtp_weight * mtp_loss
708
+ else:
709
+ total_loss = main_objective
710
+
711
+ output_states = (
712
+ hidden_states
713
+ if logits_to_keep is None
714
+ else hidden_states[:, -logits_to_keep:, :]
715
+ )
716
+ logits = self.project_to_vocabulary(output_states) if return_logits else None
717
+ return TinyGDNOutput(
718
+ loss=total_loss,
719
+ logits=logits,
720
+ main_loss=main_loss,
721
+ mtp_loss=mtp_loss,
722
+ z_loss=z_loss,
723
+ hidden_states=hidden_states if return_hidden_states else None,
724
+ past_key_values=past_key_values if use_cache else None,
725
+ )
726
+
727
+ def parameter_report(self) -> dict[str, int]:
728
+ total = sum(parameter.numel() for parameter in self.parameters())
729
+ mtp = sum(parameter.numel() for parameter in self.mtp_adapters.parameters())
730
+ embeddings = self.embed_tokens.weight.numel()
731
+ return {
732
+ "deployable_core": total - mtp,
733
+ "training_total": total,
734
+ "embedding": embeddings,
735
+ "mtp_auxiliary": mtp,
736
+ "non_embedding_core": total - mtp - embeddings,
737
+ }
738
+
739
+ def save_checkpoint(self, output_dir: Path) -> None:
740
+ output_dir.mkdir(parents=True, exist_ok=True)
741
+ self.config.save_json(output_dir / "config.json")
742
+ save_model(self, output_dir / "model.safetensors")
743
+
744
+ @classmethod
745
+ def from_checkpoint(
746
+ cls,
747
+ checkpoint_dir: Path,
748
+ *,
749
+ device: str | torch.device = "cpu",
750
+ dtype: torch.dtype | None = None,
751
+ ) -> TinyGDNForCausalLM:
752
+ config = TinyGDNConfig.from_json(checkpoint_dir / "config.json")
753
+ model = cls(config).to(device=device, dtype=dtype)
754
+ load_model(model, checkpoint_dir / "model.safetensors", device=str(device))
755
+ return model
756
+
757
+ def extra_repr(self) -> str:
758
+ report = self.parameter_report()
759
+ return (
760
+ f"core_parameters={report['deployable_core']:,}, "
761
+ f"training_parameters={report['training_total']:,}"
762
+ )
763
+
764
+ def get_architecture_metadata(self) -> dict[str, Any]:
765
+ return {
766
+ "architecture": self.config.architecture,
767
+ "layer_types": list(self.config.layer_types),
768
+ "effective_num_layers": self.config.effective_num_layers,
769
+ "shared_layer_indices": list(self.config.shared_layer_indices),
770
+ "parameter_report": self.parameter_report(),
771
+ "features": [
772
+ "hybrid linear + softmax token mixing",
773
+ "Kimi Delta Attention or Gated DeltaNet-2",
774
+ "gated MLA or gated GQA",
775
+ "optional block Attention Residuals",
776
+ "SwiGLU or SiTU-GLU",
777
+ "QK normalization",
778
+ "zero-centered RMSNorm",
779
+ "tied input-output embeddings",
780
+ "optional multi-token prediction auxiliaries",
781
+ ],
782
+ }
tiny_gdn/ngram.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hashed n-gram addressing shared by Senryu-2's model, its host table store, and serving.
2
+
3
+ Each table row is addressed with integer ops that give the same result on CPU
4
+ and CUDA: canonical token ids (NFKC-normalized, lowercased, stripped surface
5
+ forms) are multiplied by odd 31-bit constants, XOR-combined, mixed, and reduced
6
+ modulo a distinct prime per table. Token ids stay below 2**16, so every
7
+ product stays below 2**47 and int64 never overflows.
8
+
9
+ Tables live in one concatenated ``[total_rows, embedding_dim]`` tensor; table
10
+ ``t`` owns rows ``[offset_t, offset_t + rows_t)``. The prime row counts, the
11
+ multipliers, and the canonical map are saved beside every table file and
12
+ checked on load, so a table can never be read with a different address map.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import unicodedata
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+
21
+ import numpy as np
22
+ import torch
23
+ from safetensors import safe_open
24
+ from safetensors.torch import save_file
25
+ from tokenizers import Tokenizer
26
+
27
+ from tiny_gdn.config import TinyGDNConfig
28
+
29
+ NGRAM_TABLES_FILE = "ngram_tables.safetensors"
30
+ NGRAM_TABLES_EMA_FILE = "ngram_tables_ema.safetensors"
31
+ NGRAM_OPTIMIZER_FILE = "ngram_optimizer.safetensors"
32
+
33
+ PRIME_STRIDE = 4099
34
+ HASH_SEED = 0x6B696D6F
35
+ MIX_SHIFT = 29
36
+ LOAD_CHUNK_ROWS = 1 << 20
37
+ _MASK64 = (1 << 64) - 1
38
+ _MILLER_RABIN_BASES = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37)
39
+
40
+
41
+ def _splitmix64(state: int) -> tuple[int, int]:
42
+ state = (state + 0x9E3779B97F4A7C15) & _MASK64
43
+ mixed = ((state ^ (state >> 30)) * 0xBF58476D1CE4E5B9) & _MASK64
44
+ mixed = ((mixed ^ (mixed >> 27)) * 0x94D049BB133111EB) & _MASK64
45
+ return state, mixed ^ (mixed >> 31)
46
+
47
+
48
+ def _is_prime(value: int) -> bool:
49
+ if value < 2:
50
+ return False
51
+ for base in _MILLER_RABIN_BASES:
52
+ if value % base == 0:
53
+ return value == base
54
+ odd, twos = value - 1, 0
55
+ while odd % 2 == 0:
56
+ odd //= 2
57
+ twos += 1
58
+ for base in _MILLER_RABIN_BASES:
59
+ witness = pow(base, odd, value)
60
+ if witness in (1, value - 1):
61
+ continue
62
+ for _ in range(twos - 1):
63
+ witness = witness * witness % value
64
+ if witness == value - 1:
65
+ break
66
+ else:
67
+ return False
68
+ return True
69
+
70
+
71
+ def _next_prime(value: int) -> int:
72
+ while not _is_prime(value):
73
+ value += 1
74
+ return value
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class NGramLayout:
79
+ """Where every n-gram of every order lands in the concatenated tables."""
80
+
81
+ orders: tuple[int, ...]
82
+ heads_per_order: int
83
+ table_rows: tuple[int, ...]
84
+ table_offsets: tuple[int, ...]
85
+ multipliers: tuple[tuple[int, ...], ...]
86
+ embedding_dim: int
87
+ pad_token_id: int
88
+
89
+ @classmethod
90
+ def from_config(cls, config: TinyGDNConfig) -> NGramLayout:
91
+ if not config.ngram_orders:
92
+ raise ValueError("config has no n-gram memory")
93
+ rows: list[int] = []
94
+ candidate = config.ngram_table_rows
95
+ for _ in range(config.ngram_table_count):
96
+ candidate = _next_prime(candidate + PRIME_STRIDE)
97
+ rows.append(candidate)
98
+ offsets = tuple(int(value) for value in np.cumsum([0, *rows[:-1]]))
99
+ state = HASH_SEED
100
+ multipliers: list[tuple[int, ...]] = []
101
+ max_order = max(config.ngram_orders)
102
+ for _ in range(config.ngram_table_count):
103
+ table: list[int] = []
104
+ for _ in range(max_order):
105
+ state, value = _splitmix64(state)
106
+ table.append((value >> 33) | (1 << 30) | 1)
107
+ multipliers.append(tuple(table))
108
+ return cls(
109
+ orders=tuple(config.ngram_orders),
110
+ heads_per_order=config.ngram_heads_per_order,
111
+ table_rows=tuple(rows),
112
+ table_offsets=offsets,
113
+ multipliers=tuple(multipliers),
114
+ embedding_dim=config.ngram_embedding_dim,
115
+ pad_token_id=config.bos_token_id,
116
+ )
117
+
118
+ @property
119
+ def table_count(self) -> int:
120
+ return len(self.table_rows)
121
+
122
+ @property
123
+ def total_rows(self) -> int:
124
+ return self.table_offsets[-1] + self.table_rows[-1]
125
+
126
+ @property
127
+ def max_order(self) -> int:
128
+ return max(self.orders)
129
+
130
+ @property
131
+ def memory_dim(self) -> int:
132
+ return self.table_count * self.embedding_dim
133
+
134
+ @property
135
+ def table_orders(self) -> tuple[int, ...]:
136
+ return tuple(order for order in self.orders for _ in range(self.heads_per_order))
137
+
138
+ def metadata_tensors(self) -> dict[str, torch.Tensor]:
139
+ return {
140
+ "table_rows": torch.tensor(self.table_rows, dtype=torch.int64),
141
+ "hash_multipliers": torch.tensor(self.multipliers, dtype=torch.int64),
142
+ }
143
+
144
+ def check_metadata(self, tensors: dict[str, torch.Tensor], source: Path) -> None:
145
+ for name, expected in self.metadata_tensors().items():
146
+ found = tensors.get(name)
147
+ if found is None or not torch.equal(found.cpu(), expected):
148
+ raise ValueError(f"{source} was written for a different n-gram layout ({name})")
149
+
150
+
151
+ def canonical_token_ids(tokenizer: Tokenizer, vocab_size: int) -> torch.Tensor:
152
+ """Map every token id to the smallest id sharing its normalized surface form.
153
+
154
+ Normalization is NFKC, then strip, then lowercase. Special tokens keep
155
+ their own ids, whitespace-only tokens are only NFKC-merged, and tokens
156
+ that decode to partial UTF-8 stay distinct.
157
+ """
158
+
159
+ special = set(tokenizer.get_added_tokens_decoder())
160
+ canonical = np.arange(vocab_size, dtype=np.int64)
161
+ first_by_key: dict[tuple[str, str], int] = {}
162
+ for token_id in range(min(vocab_size, tokenizer.get_vocab_size())):
163
+ if token_id in special:
164
+ continue
165
+ text = tokenizer.decode([token_id], skip_special_tokens=False)
166
+ if "\ufffd" in text:
167
+ key = ("bytes", tokenizer.id_to_token(token_id) or str(token_id))
168
+ else:
169
+ normalized = unicodedata.normalize("NFKC", text)
170
+ folded = normalized.strip().lower()
171
+ key = ("text", folded) if folded else ("space", normalized)
172
+ canonical[token_id] = first_by_key.setdefault(key, token_id)
173
+ return torch.from_numpy(canonical)
174
+
175
+
176
+ def document_distance(tokens: torch.Tensor, bos_token_id: int, row_starts: torch.Tensor | None = None) -> torch.Tensor:
177
+ """Offset of every token in a flat ``[N]`` stream from its document's first token.
178
+
179
+ Documents start at position 0, at every BOS token, and at ``row_starts``.
180
+ """
181
+
182
+ starts = tokens == bos_token_id
183
+ starts[0] = True
184
+ if row_starts is not None:
185
+ starts[row_starts] = True
186
+ positions = torch.arange(tokens.numel(), device=tokens.device)
187
+ first = torch.where(starts, positions, torch.zeros_like(positions)).cummax(dim=0).values
188
+ return positions - first
189
+
190
+
191
+ def ngram_row_indices(
192
+ canonical: torch.Tensor,
193
+ layout: NGramLayout,
194
+ *,
195
+ distance: torch.Tensor | None = None,
196
+ history: torch.Tensor | None = None,
197
+ ) -> torch.Tensor:
198
+ """Global table rows ``[..., L, tables]`` for the n-grams ending at each token.
199
+
200
+ ``canonical`` holds int64 canonical ids ``[..., L]``. Tokens before a
201
+ document start read as the pad id: with ``distance`` (same shape) a
202
+ document starts wherever the distance is zero; with ``history`` (the
203
+ ``max_order - 1`` canonical ids before position 0) the sequence continues
204
+ a cached one; with neither, position 0 starts the only document.
205
+ """
206
+
207
+ span = layout.max_order
208
+ length = canonical.shape[-1]
209
+ pad = layout.pad_token_id
210
+ if history is not None:
211
+ if history.shape[-1] != span - 1:
212
+ raise ValueError(f"history must hold {span - 1} tokens")
213
+ extended = torch.cat([history, canonical], dim=-1)
214
+ shifted = [extended[..., span - 1 - back : span - 1 - back + length] for back in range(span)]
215
+ else:
216
+ shifted = [canonical]
217
+ for back in range(1, span):
218
+ if back >= length:
219
+ previous = torch.full_like(canonical, pad)
220
+ else:
221
+ padding = canonical.new_full((*canonical.shape[:-1], back), pad)
222
+ previous = torch.cat([padding, canonical[..., :-back]], dim=-1)
223
+ if distance is not None:
224
+ previous = torch.where(distance >= back, previous, pad)
225
+ shifted.append(previous)
226
+ columns = []
227
+ for table, order in enumerate(layout.table_orders):
228
+ multipliers = layout.multipliers[table]
229
+ mixed = shifted[0] * multipliers[0]
230
+ for back in range(1, order):
231
+ mixed = mixed ^ (shifted[back] * multipliers[back])
232
+ mixed = mixed ^ (mixed >> MIX_SHIFT)
233
+ columns.append(torch.remainder(mixed, layout.table_rows[table]) + layout.table_offsets[table])
234
+ return torch.stack(columns, dim=-1)
235
+
236
+
237
+ def save_table_file(path: Path, tables: torch.Tensor, layout: NGramLayout, canonical: torch.Tensor) -> None:
238
+ if tables.shape != (layout.total_rows, layout.embedding_dim):
239
+ raise ValueError(f"tables have shape {tuple(tables.shape)}, layout needs {(layout.total_rows, layout.embedding_dim)}")
240
+ save_file(
241
+ {
242
+ "tables": tables.contiguous(),
243
+ "canonical_ids": canonical.to(dtype=torch.int64, device="cpu").contiguous(),
244
+ **layout.metadata_tensors(),
245
+ },
246
+ str(path),
247
+ )
248
+
249
+
250
+ def read_table_metadata(path: Path, layout: NGramLayout) -> torch.Tensor:
251
+ """Check ``path`` against ``layout`` and return its canonical token map."""
252
+
253
+ with safe_open(str(path), framework="pt", device="cpu") as handle:
254
+ metadata = {name: handle.get_tensor(name) for name in ("table_rows", "hash_multipliers")}
255
+ layout.check_metadata(metadata, path)
256
+ # get_tensor returns a view of a mapping of the whole file, which a kept
257
+ # view would hold open; FUSE mounts such as ntfs-3g then cannot delete it.
258
+ return handle.get_tensor("canonical_ids").clone()
259
+
260
+
261
+ def copy_rows_from_file(path: Path, name: str, destination: torch.Tensor) -> None:
262
+ """Copy tensor ``name`` of a safetensors file into ``destination`` in row chunks."""
263
+
264
+ with safe_open(str(path), framework="pt", device="cpu") as handle:
265
+ source = handle.get_slice(name)
266
+ shape = tuple(source.get_shape())
267
+ if shape != tuple(destination.shape):
268
+ raise ValueError(f"{path} holds {name} of shape {shape}, expected {tuple(destination.shape)}")
269
+ for start in range(0, shape[0], LOAD_CHUNK_ROWS):
270
+ stop = min(start + LOAD_CHUNK_ROWS, shape[0])
271
+ destination[start:stop].copy_(source[start:stop])
272
+
273
+
274
+ def load_table_file(
275
+ path: Path,
276
+ layout: NGramLayout,
277
+ *,
278
+ device: torch.device | str = "cpu",
279
+ dtype: torch.dtype = torch.float32,
280
+ out: torch.Tensor | None = None,
281
+ ) -> tuple[torch.Tensor, torch.Tensor]:
282
+ """``(tables, canonical_ids)`` from ``path``, copied in row chunks to bound host memory."""
283
+
284
+ canonical = read_table_metadata(path, layout)
285
+ tables = (
286
+ torch.empty((layout.total_rows, layout.embedding_dim), dtype=dtype, device=device)
287
+ if out is None
288
+ else out
289
+ )
290
+ copy_rows_from_file(path, "tables", tables)
291
+ return tables, canonical
tiny_gdn/nn_common.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ import torch.nn.functional as F
5
+ from torch import nn
6
+ from torch.nn.attention import SDPBackend
7
+
8
+ try:
9
+ from fla.modules.layernorm import rms_norm as fused_rms_norm
10
+ except ImportError:
11
+ fused_rms_norm = None
12
+
13
+
14
+ class RMSNorm(nn.Module):
15
+ """Zero-centered RMSNorm as used by Qwen3-Next.
16
+
17
+ On CUDA this runs FLA's single-pass Triton kernel with the ``1 + weight``
18
+ scale kept in FP32, which matches the eager FP32 math to rounding.
19
+ """
20
+
21
+ def __init__(self, hidden_size: int, eps: float) -> None:
22
+ super().__init__()
23
+ self.weight = nn.Parameter(torch.zeros(hidden_size))
24
+ self.eps = eps
25
+
26
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
27
+ scale = 1.0 + self.weight.float()
28
+ if fused_rms_norm is not None and hidden_states.is_cuda:
29
+ return fused_rms_norm(hidden_states, scale, None, eps=self.eps)
30
+ input_dtype = hidden_states.dtype
31
+ normalized = hidden_states.float()
32
+ normalized = normalized * torch.rsqrt(
33
+ normalized.square().mean(dim=-1, keepdim=True) + self.eps
34
+ )
35
+ return (normalized * scale).to(dtype=input_dtype)
36
+
37
+
38
+ def linear_cross_entropy_reference(
39
+ states: torch.Tensor,
40
+ weight: torch.Tensor,
41
+ targets: torch.Tensor,
42
+ *,
43
+ z_loss_coefficient: float = 0.0,
44
+ ignore_index: int = -100,
45
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
46
+ """Eager ``(objective, cross_entropy, z_loss)`` over materialized logits."""
47
+
48
+ logits = F.linear(states, weight)
49
+ cross_entropy = F.cross_entropy(logits, targets, ignore_index=ignore_index)
50
+ reduced_dtype = torch.promote_types(logits.dtype, torch.float32)
51
+ if z_loss_coefficient > 0.0:
52
+ # Keep logits in their training dtype for logsumexp. Casting the full
53
+ # [tokens, vocab] tensor to fp32 materializes it again in the autograd
54
+ # graph; upcast only the reduced [tokens] partition.
55
+ log_partition = torch.logsumexp(logits, dim=-1).to(reduced_dtype)
56
+ z_loss = log_partition.square()[targets.ne(ignore_index)].mean()
57
+ else:
58
+ z_loss = cross_entropy.new_zeros((), dtype=reduced_dtype)
59
+ objective = cross_entropy + z_loss_coefficient * z_loss
60
+ return objective, cross_entropy.detach(), z_loss.detach()
61
+
62
+
63
+ def linear_token_logprobs_reference(
64
+ states: torch.Tensor,
65
+ weight: torch.Tensor,
66
+ targets: torch.Tensor,
67
+ *,
68
+ ignore_index: int = -100,
69
+ ) -> torch.Tensor:
70
+ """Eager fp32 ``log_softmax(states @ weight.T)[row, target]``; ignored rows are 0."""
71
+
72
+ valid = targets.ne(ignore_index)
73
+ logprobs = F.linear(states, weight).float().log_softmax(dim=-1)
74
+ picked = logprobs.gather(1, targets.masked_fill(~valid, 0)[:, None]).squeeze(1)
75
+ return picked.masked_fill(~valid, 0.0)
76
+
77
+
78
+ def softcap(values: torch.Tensor, cap: float) -> torch.Tensor:
79
+ return cap * torch.tanh(values / cap)
80
+
81
+
82
+ def situ_glu_reference(gate_up: torch.Tensor, gate_cap: float, up_cap: float) -> torch.Tensor:
83
+ """``softcap(gate) * sigmoid(gate) * softcap(up)`` over ``[..., gate | up]``."""
84
+
85
+ gate, up = gate_up.chunk(2, dim=-1)
86
+ return softcap(gate, gate_cap) * torch.sigmoid(gate) * softcap(up, up_cap)
87
+
88
+
89
+ def attention_sdpa_backends(device: torch.device) -> list[SDPBackend]:
90
+ if device.type != "cuda":
91
+ return [SDPBackend.MATH]
92
+ return [
93
+ SDPBackend.CUDNN_ATTENTION,
94
+ SDPBackend.FLASH_ATTENTION,
95
+ SDPBackend.EFFICIENT_ATTENTION,
96
+ ]
tiny_gdn/parallax_ops.py ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Parallax attention on fla's Triton kernels, with launch settings that fit the GPU.
2
+
3
+ fla tiles every Parallax kernel 64 rows deep with two pipeline stages, sized
4
+ for Hopper's 228 KB of shared memory. Its backward key/value kernel then needs
5
+ about 105 KB per block, more than consumer Blackwell (sm_120) and Ampere allow
6
+ (99 KB), so it fails to launch there. The tile is a launch constant, so this
7
+ module launches fla's own kernels with the largest tile that fits and keeps
8
+ fla's math unchanged.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Callable
14
+ from typing import Any
15
+
16
+ import torch
17
+ import triton
18
+ import triton.language as tl
19
+ from einops import reduce
20
+ from fla.ops.parallax.decode import parallax_decode_kernel, parallax_decode_one_step
21
+ from fla.ops.parallax.parallel import (
22
+ parallel_parallax_bwd_kernel_dkv,
23
+ parallel_parallax_bwd_kernel_dqr,
24
+ parallel_parallax_bwd_kernel_preprocess,
25
+ parallel_parallax_fwd_kernel,
26
+ )
27
+ from fla.ops.utils import prepare_chunk_indices
28
+ from fla.ops.utils.op import exp2
29
+ from fla.utils import autocast_custom_bwd, autocast_custom_fwd, contiguous
30
+ from triton.runtime.errors import OutOfResources
31
+
32
+ LaunchConfig = tuple[int, int, int]
33
+ # (tile rows, warps, pipeline stages), fastest first. Measured on an RTX PRO
34
+ # 6000 (sm_120, 99 KB per block): a 32-row tile with 4 warps beats every other
35
+ # setting that launches, forward and backward.
36
+ LARGE_SHARED_MEMORY_CONFIGS: tuple[LaunchConfig, ...] = ((64, 8, 2), (32, 4, 2), (32, 4, 1), (16, 4, 1))
37
+ SMALL_SHARED_MEMORY_CONFIGS: tuple[LaunchConfig, ...] = ((32, 4, 2), (32, 4, 1), (16, 4, 1))
38
+ LARGE_SHARED_MEMORY_BYTES = 160 * 1024
39
+
40
+ _selected: dict[tuple[str, int, int], LaunchConfig] = {}
41
+
42
+
43
+ def _device_index(device: torch.device) -> int:
44
+ return torch.cuda.current_device() if device.index is None else device.index
45
+
46
+
47
+ def _candidates(device: torch.device) -> tuple[LaunchConfig, ...]:
48
+ properties = torch.cuda.get_device_properties(_device_index(device))
49
+ if properties.shared_memory_per_block_optin >= LARGE_SHARED_MEMORY_BYTES:
50
+ return LARGE_SHARED_MEMORY_CONFIGS
51
+ return SMALL_SHARED_MEMORY_CONFIGS
52
+
53
+
54
+ def _launch(name: str, device: torch.device, head_dim: int, launch: Callable[[int, int, int], Any]) -> Any:
55
+ """Run ``launch`` with the first config that fits; remember it per kernel and GPU."""
56
+
57
+ key = (name, _device_index(device), head_dim)
58
+ chosen = _selected.get(key)
59
+ candidates = (chosen,) if chosen is not None else _candidates(device)
60
+ last_error: OutOfResources | None = None
61
+ for config in candidates:
62
+ try:
63
+ result = launch(*config)
64
+ except OutOfResources as error:
65
+ last_error = error
66
+ continue
67
+ _selected[key] = config
68
+ return result
69
+ assert last_error is not None
70
+ raise last_error
71
+
72
+
73
+ def selected_launch_configs() -> dict[tuple[str, int, int], LaunchConfig]:
74
+ return dict(_selected)
75
+
76
+
77
+ def _chunk_indices(cu_seqlens: torch.Tensor | None, tile: int) -> torch.Tensor | None:
78
+ return None if cu_seqlens is None else prepare_chunk_indices(cu_seqlens, tile)
79
+
80
+
81
+ def _grid_rows(length: int, chunk_indices: torch.Tensor | None, tile: int) -> int:
82
+ return triton.cdiv(length, tile) if chunk_indices is None else len(chunk_indices)
83
+
84
+
85
+ def _forward(
86
+ q: torch.Tensor,
87
+ r: torch.Tensor,
88
+ k: torch.Tensor,
89
+ v: torch.Tensor,
90
+ scale: float,
91
+ cu_seqlens: torch.Tensor | None,
92
+ window_size_left: int,
93
+ ) -> tuple[torch.Tensor, ...]:
94
+ batch, length, query_heads, head_dim = q.shape
95
+ kv_heads = k.shape[2]
96
+ o = torch.empty_like(q)
97
+ barv = torch.empty_like(q)
98
+ d1 = torch.empty((batch, length, query_heads), device=q.device, dtype=torch.float32)
99
+ bart = torch.empty_like(d1)
100
+ m = torch.empty_like(d1)
101
+
102
+ def launch(tile: int, warps: int, stages: int) -> None:
103
+ chunk_indices = _chunk_indices(cu_seqlens, tile)
104
+ grid = (_grid_rows(length, chunk_indices, tile), batch * query_heads)
105
+ parallel_parallax_fwd_kernel[grid](
106
+ q, r, k, v, o, barv, d1, bart, m,
107
+ scale, cu_seqlens, chunk_indices, length,
108
+ HQ=query_heads, H=kv_heads, G=query_heads // kv_heads, K=head_dim,
109
+ BK=triton.next_power_of_2(head_dim),
110
+ WINDOW_SIZE_LEFT=window_size_left, BT=tile, BS=tile,
111
+ num_warps=warps, num_stages=stages,
112
+ )
113
+
114
+ _launch("forward", q.device, head_dim, launch)
115
+ return o, barv, d1, bart, m
116
+
117
+
118
+ def _backward(
119
+ saved: tuple[torch.Tensor, ...],
120
+ grad_o: torch.Tensor,
121
+ scale: float,
122
+ cu_seqlens: torch.Tensor | None,
123
+ window_size_left: int,
124
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
125
+ q, r, k, v, o, barv, d1, bart, m = saved
126
+ batch, length, query_heads, head_dim = q.shape
127
+ kv_heads = k.shape[2]
128
+ groups = query_heads // kv_heads
129
+ grad_q = torch.empty_like(q)
130
+ grad_r = torch.empty_like(r)
131
+ # dK/dV are written per query head, then summed back onto the KV heads.
132
+ grad_k_heads = torch.empty((batch, length, query_heads, head_dim), device=q.device, dtype=q.dtype)
133
+ grad_v_heads = torch.empty_like(grad_k_heads)
134
+ delta_t = torch.empty((batch, length, query_heads), device=q.device, dtype=torch.float32)
135
+ delta_b = torch.empty_like(delta_t)
136
+ block_k = triton.next_power_of_2(head_dim)
137
+
138
+ def launch(tile: int, warps: int, stages: int) -> None:
139
+ chunk_indices = _chunk_indices(cu_seqlens, tile)
140
+ grid = (_grid_rows(length, chunk_indices, tile), batch * query_heads)
141
+ parallel_parallax_bwd_kernel_preprocess[grid](
142
+ grad_o, o, barv, delta_t, delta_b,
143
+ cu_seqlens, chunk_indices, length,
144
+ HQ=query_heads, K=head_dim, BK=block_k, BT=tile,
145
+ num_warps=4, num_stages=2,
146
+ )
147
+ parallel_parallax_bwd_kernel_dqr[grid](
148
+ q, r, k, v, d1, bart, m, delta_t, delta_b, grad_o, grad_q, grad_r,
149
+ scale, cu_seqlens, chunk_indices, length,
150
+ HQ=query_heads, H=kv_heads, G=groups, K=head_dim, BK=block_k,
151
+ WINDOW_SIZE_LEFT=window_size_left, BT=tile, BS=tile,
152
+ num_warps=warps, num_stages=stages,
153
+ )
154
+ parallel_parallax_bwd_kernel_dkv[grid](
155
+ q, r, k, v, d1, bart, m, delta_t, delta_b, grad_o, grad_k_heads, grad_v_heads,
156
+ scale, cu_seqlens, chunk_indices, length,
157
+ HQ=query_heads, H=kv_heads, G=groups, K=head_dim, BK=block_k,
158
+ WINDOW_SIZE_LEFT=window_size_left, BT=tile, BS=tile,
159
+ num_warps=warps, num_stages=stages,
160
+ )
161
+
162
+ _launch("backward", q.device, head_dim, launch)
163
+ if groups == 1:
164
+ return grad_q, grad_r, grad_k_heads, grad_v_heads
165
+ grad_k = reduce(grad_k_heads, "b t (h g) d -> b t h d", g=groups, reduction="sum")
166
+ grad_v = reduce(grad_v_heads, "b t (h g) d -> b t h d", g=groups, reduction="sum")
167
+ return grad_q, grad_r, grad_k, grad_v
168
+
169
+
170
+ class _ParallaxFunction(torch.autograd.Function):
171
+ @staticmethod
172
+ @contiguous
173
+ @autocast_custom_fwd
174
+ def forward(ctx, q, r, k, v, scale, window_size_left, cu_seqlens):
175
+ o, barv, d1, bart, m = _forward(q, r, k, v, scale, cu_seqlens, window_size_left)
176
+ ctx.save_for_backward(q, r, k, v, o, barv, d1, bart, m)
177
+ ctx.scale = scale
178
+ ctx.window_size_left = window_size_left
179
+ ctx.cu_seqlens = cu_seqlens
180
+ return o
181
+
182
+ @staticmethod
183
+ @contiguous
184
+ @autocast_custom_bwd
185
+ def backward(ctx, grad_o):
186
+ saved = ctx.saved_tensors
187
+ grad_q, grad_r, grad_k, grad_v = _backward(
188
+ saved, grad_o, ctx.scale, ctx.cu_seqlens, ctx.window_size_left
189
+ )
190
+ q, r, k, v = saved[:4]
191
+ return grad_q.to(q), grad_r.to(r), grad_k.to(k), grad_v.to(v), None, None, None
192
+
193
+
194
+ def _validate(q: torch.Tensor, r: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> None:
195
+ if q.dtype not in (torch.bfloat16, torch.float16):
196
+ raise TypeError(f"Parallax needs bf16 or fp16 inputs, got {q.dtype}")
197
+ if r.shape != q.shape:
198
+ raise ValueError("r must have the shape of q")
199
+ if k.shape != v.shape or k.shape[0] != q.shape[0] or k.shape[-1] != q.shape[-1]:
200
+ raise ValueError("k and v must share q's batch and head_dim")
201
+ if q.shape[2] % k.shape[2]:
202
+ raise ValueError("query heads must be a multiple of key/value heads")
203
+
204
+
205
+ def parallax_attention(
206
+ q: torch.Tensor,
207
+ r: torch.Tensor,
208
+ k: torch.Tensor,
209
+ v: torch.Tensor,
210
+ *,
211
+ scale: float | None = None,
212
+ window_size: int | None = None,
213
+ cu_seqlens: torch.Tensor | None = None,
214
+ ) -> torch.Tensor:
215
+ """Causal Parallax over ``[batch, length, heads, head_dim]`` inputs with autograd.
216
+
217
+ ``r`` is not scaled; ``scale`` applies to ``q @ kᵀ`` only (default
218
+ ``head_dim ** -0.5``). With ``cu_seqlens`` the batch must be 1 and each
219
+ packed sequence attends only within itself.
220
+ """
221
+
222
+ _validate(q, r, k, v)
223
+ if k.shape[1] != q.shape[1]:
224
+ raise ValueError("training Parallax needs one key per query")
225
+ if cu_seqlens is not None and q.shape[0] != 1:
226
+ raise ValueError("packed sequences (cu_seqlens) need batch size 1")
227
+ if scale is None:
228
+ scale = q.shape[-1] ** -0.5
229
+ window_size_left = -1 if window_size is None else window_size
230
+ return _ParallaxFunction.apply(q, r, k, v, float(scale), window_size_left, cu_seqlens)
231
+
232
+
233
+ @torch.no_grad()
234
+ def parallax_decode_cached(
235
+ q: torch.Tensor,
236
+ r: torch.Tensor,
237
+ k: torch.Tensor,
238
+ v: torch.Tensor,
239
+ *,
240
+ scale: float | None = None,
241
+ window_size: int | None = None,
242
+ cache_start: torch.Tensor | None = None,
243
+ ) -> torch.Tensor:
244
+ """Forward-only Parallax for ``Sq`` new queries over ``Skv >= Sq`` cached keys.
245
+
246
+ Query ``i`` sits at absolute position ``Skv - Sq + i``. One query uses
247
+ fla's vector decode kernel; longer chunks use the tiled prefill kernel.
248
+ """
249
+
250
+ _validate(q, r, k, v)
251
+ batch, queries, query_heads, head_dim = q.shape
252
+ keys, kv_heads = k.shape[1], k.shape[2]
253
+ if keys < queries:
254
+ raise ValueError("the cache must hold at least as many keys as new queries")
255
+ if scale is None:
256
+ scale = head_dim ** -0.5
257
+ if queries == 1:
258
+ return parallax_decode_one_step(q, r, k, v, scale=scale, window_size=window_size, cache_start=cache_start)
259
+ window_size_left = -1 if window_size is None else window_size
260
+ q, r, k, v = (tensor.contiguous() for tensor in (q, r, k, v))
261
+ if cache_start is not None:
262
+ cache_start = cache_start.to(device=q.device, dtype=torch.int32).contiguous()
263
+ o = torch.empty_like(q)
264
+
265
+ def launch(tile: int, warps: int, stages: int) -> None:
266
+ grid = (triton.cdiv(queries, tile), batch * query_heads)
267
+ parallax_decode_kernel[grid](
268
+ q, r, k, v, o, float(scale), cache_start, queries, keys,
269
+ HQ=query_heads, H=kv_heads, G=query_heads // kv_heads, K=head_dim,
270
+ BK=triton.next_power_of_2(head_dim),
271
+ WINDOW_SIZE_LEFT=window_size_left,
272
+ USE_CACHE_START=cache_start is not None,
273
+ BT=tile, BS=tile,
274
+ num_warps=warps, num_stages=stages,
275
+ )
276
+
277
+ _launch("decode", q.device, head_dim, launch)
278
+ return o
279
+
280
+
281
+ @triton.jit(do_not_specialize=["Skv"])
282
+ def _parallax_graph_decode_kernel(
283
+ q,
284
+ r,
285
+ k,
286
+ v,
287
+ o,
288
+ scale,
289
+ cache_end,
290
+ Skv,
291
+ HQ: tl.constexpr,
292
+ H: tl.constexpr,
293
+ G: tl.constexpr,
294
+ K: tl.constexpr,
295
+ BK: tl.constexpr,
296
+ BS: tl.constexpr,
297
+ ):
298
+ """One query per row over a fixed ``Skv`` cache, stopping at that row's ``cache_end``.
299
+
300
+ ``Skv`` stays the buffer length so a CUDA graph can replay the launch while
301
+ each row's live prefix, read from ``cache_end``, changes between replays.
302
+ """
303
+
304
+ i_bh = tl.program_id(0)
305
+ i_b, i_hq = i_bh // HQ, i_bh % HQ
306
+ i_h = i_hq // G
307
+ RCP_LN2: tl.constexpr = 1.4426950216
308
+ kv_hi = tl.load(cache_end + i_b).to(tl.int32)
309
+ kv_hi = tl.minimum(tl.maximum(kv_hi, 0), Skv)
310
+
311
+ p_q = tl.make_block_ptr(q + i_bh * K, (K,), (1,), (0,), (BK,), (0,))
312
+ p_r = tl.make_block_ptr(r + i_bh * K, (K,), (1,), (0,), (BK,), (0,))
313
+ p_o = tl.make_block_ptr(o + i_bh * K, (K,), (1,), (0,), (BK,), (0,))
314
+ b_q = tl.load(p_q, boundary_check=(0,), padding_option="zero").to(tl.float32)
315
+ b_r = tl.load(p_r, boundary_check=(0,), padding_option="zero").to(tl.float32)
316
+ scale_log2 = scale * RCP_LN2
317
+ m = tl.full((1,), -float("inf"), dtype=tl.float32)
318
+ d1 = tl.zeros((1,), dtype=tl.float32)
319
+ d2 = tl.zeros((1,), dtype=tl.float32)
320
+ o1 = tl.zeros((BK,), dtype=tl.float32)
321
+ o2 = tl.zeros((BK,), dtype=tl.float32)
322
+
323
+ p_k = tl.make_block_ptr(k + (i_b * Skv * H + i_h) * K, (Skv, K), (H * K, 1), (0, 0), (BS, BK), (1, 0))
324
+ p_v = tl.make_block_ptr(v + (i_b * Skv * H + i_h) * K, (Skv, K), (H * K, 1), (0, 0), (BS, BK), (1, 0))
325
+ for i_s in range(0, tl.cdiv(kv_hi, BS) * BS, BS):
326
+ col = i_s + tl.arange(0, BS)
327
+ mask = col < kv_hi
328
+ b_k = tl.load(p_k, boundary_check=(0, 1), padding_option="zero")
329
+ b_v = tl.load(p_v, boundary_check=(0, 1), padding_option="zero")
330
+ s1 = tl.sum(b_q[None, :] * b_k, axis=1) * scale_log2
331
+ s2 = tl.sum(b_r[None, :] * b_k, axis=1)
332
+ s1 = tl.where(mask, s1, -float("inf"))
333
+ m_new = tl.maximum(m, tl.max(s1))
334
+ m_safe = tl.where(m_new == -float("inf"), 0.0, m_new)
335
+ alpha = exp2(m - m_safe)
336
+ p1 = exp2(s1 - m_safe)
337
+ p2 = p1 * s2
338
+ d1 = d1 * alpha + tl.sum(p1)
339
+ d2 = d2 * alpha + tl.sum(p2)
340
+ o1 = o1 * alpha + tl.sum(p1[:, None] * b_v, axis=0)
341
+ o2 = o2 * alpha + tl.sum(p2[:, None] * b_v, axis=0)
342
+ m = m_new
343
+ p_k = tl.advance(p_k, (BS, 0))
344
+ p_v = tl.advance(p_v, (BS, 0))
345
+
346
+ inv_d1 = tl.where(d1 > 0.0, 1.0 / d1, 0.0)
347
+ out = o1 * inv_d1 * (1.0 + d2 * inv_d1) - o2 * inv_d1
348
+ tl.store(p_o, out.to(p_o.dtype.element_ty), boundary_check=(0,))
349
+
350
+
351
+ def parallax_graph_decode(
352
+ q: torch.Tensor,
353
+ r: torch.Tensor,
354
+ k: torch.Tensor,
355
+ v: torch.Tensor,
356
+ cache_end: torch.Tensor,
357
+ *,
358
+ scale: float,
359
+ ) -> torch.Tensor:
360
+ """Single-token Parallax over ``k, v`` of ``[rows, capacity, heads, dim]``.
361
+
362
+ Row ``i`` attends to keys ``[0, cache_end[i])``. ``capacity`` is fixed, so
363
+ the launch can be captured in a CUDA graph and replayed as rows grow.
364
+ """
365
+
366
+ _validate(q, r, k, v)
367
+ batch, queries, query_heads, head_dim = q.shape
368
+ if queries != 1:
369
+ raise ValueError(f"graph decode steps one query, got {queries}")
370
+ if cache_end.shape != (batch,):
371
+ raise ValueError("cache_end must hold one length per row")
372
+ kv_heads = k.shape[2]
373
+ q, r, k, v = (tensor.contiguous() for tensor in (q, r, k, v))
374
+ cache_end = cache_end.to(device=q.device, dtype=torch.int32).contiguous()
375
+ out = torch.empty_like(q)
376
+ _parallax_graph_decode_kernel[(batch * query_heads,)](
377
+ q,
378
+ r,
379
+ k,
380
+ v,
381
+ out,
382
+ float(scale),
383
+ cache_end,
384
+ k.shape[1],
385
+ HQ=query_heads,
386
+ H=kv_heads,
387
+ G=query_heads // kv_heads,
388
+ K=head_dim,
389
+ BK=triton.next_power_of_2(head_dim),
390
+ BS=128,
391
+ num_warps=4,
392
+ num_stages=2,
393
+ )
394
+ return out
tiny_gdn/senryu2.py ADDED
@@ -0,0 +1,982 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Senryu-2: a Gated DeltaNet-2 + Parallax hybrid on a 4-branch gated residual stream.
2
+
3
+ Every sublayer reads its input from the residual branches through Qwen's
4
+ Gated Residual (per-branch RMSNorm, elementwise sigmoid gate from a low-rank
5
+ bottleneck over all branches) and writes its output back to every branch with
6
+ one data-dependent scalar per branch. A hashed n-gram memory, whose tables live
7
+ in host memory during training, is injected before block ``ngram_layer_index``
8
+ (docs/architecture.md, "Senryu-2").
9
+
10
+ Training and scoring run packed: the batch is flattened into one row and split
11
+ into documents at every BOS token, so GDN-2, Parallax, the n-gram hashes and
12
+ the n-gram convolution never look across a document boundary.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import math
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import torch
23
+ import torch.nn.functional as F
24
+ from fla.layers.gdn2 import GatedDeltaNet2
25
+ from fla.models.utils import Cache as FlaCache
26
+ from fla.modules.rotary import rotary_embedding
27
+ from safetensors.torch import save_model
28
+ from torch import nn
29
+ from torch.utils.checkpoint import checkpoint
30
+
31
+ from tiny_gdn.config import TinyGDNConfig
32
+ from tiny_gdn.gated_residual_ops import fused_gated_residual_read, fused_gated_residual_write
33
+ from tiny_gdn.haiku_layers import SiTUGLU
34
+ from tiny_gdn.model import TinyGDNOutput, linear_cross_entropy
35
+ from tiny_gdn.ngram import (
36
+ NGRAM_TABLES_EMA_FILE,
37
+ NGRAM_TABLES_FILE,
38
+ NGramLayout,
39
+ document_distance,
40
+ load_table_file,
41
+ ngram_row_indices,
42
+ )
43
+ from tiny_gdn.nn_common import RMSNorm
44
+ from tiny_gdn.parallax_ops import parallax_attention, parallax_decode_cached
45
+
46
+ GATED_RESIDUAL_CHUNK_ROWS = 4096
47
+ KV_CACHE_GRANULE = 256
48
+
49
+
50
+ def mark_muon_row_blocks(parameter: nn.Parameter, blocks: int) -> None:
51
+ """Orthogonalize ``parameter`` as ``blocks`` equal row blocks (heads, fused halves)."""
52
+
53
+ if parameter.shape[0] % blocks:
54
+ raise ValueError(f"{blocks} row blocks do not divide {parameter.shape[0]} rows")
55
+ parameter._muon_row_blocks = blocks
56
+
57
+
58
+ def mark_adamw_only(*parameters: nn.Parameter) -> None:
59
+ """Keep 2D weights that are gates or elongated low-rank maps on AdamW."""
60
+
61
+ for parameter in parameters:
62
+ parameter._adamw_only = True
63
+
64
+
65
+ def mark_gain(*parameters: nn.Parameter) -> None:
66
+ """2D per-branch norm gains and filters: AdamW without weight decay, like 1D gains."""
67
+
68
+ mark_adamw_only(*parameters)
69
+ for parameter in parameters:
70
+ parameter._no_weight_decay = True
71
+
72
+
73
+ # --------------------------------------------------------------------------- layout
74
+
75
+
76
+ @dataclass
77
+ class Senryu2Context:
78
+ """Per-forward state shared by every block (passes through activation checkpointing)."""
79
+
80
+ batch: int
81
+ length: int
82
+ cu_seqlens: torch.Tensor | None
83
+ distance: torch.Tensor | None
84
+ rotary: tuple[torch.Tensor, torch.Tensor]
85
+ position_offset: int = 0
86
+ cache: Senryu2Cache | None = None
87
+
88
+
89
+ class RotaryTables:
90
+ """Partial-RoPE cos/sin rows ``[length, rotary_dim / 2]``, built in FP32 and cached."""
91
+
92
+ def __init__(self, rotary_dim: int, theta: float) -> None:
93
+ self.rotary_dim = rotary_dim
94
+ self.theta = theta
95
+ self._cached: tuple[torch.Tensor, torch.Tensor] | None = None
96
+ self._key: tuple[torch.device, torch.dtype, bool] | None = None
97
+
98
+ def get(self, length: int, device: torch.device, dtype: torch.dtype) -> tuple[torch.Tensor, torch.Tensor]:
99
+ key = (device, dtype, torch.is_inference_mode_enabled())
100
+ cached = self._cached
101
+ if cached is None or self._key != key or cached[0].shape[0] < length:
102
+ rows = max(length, 4096 if cached is None else 2 * cached[0].shape[0])
103
+ inverse_frequency = 1.0 / (
104
+ self.theta
105
+ ** (torch.arange(0, self.rotary_dim, 2, device=device, dtype=torch.float32) / self.rotary_dim)
106
+ )
107
+ angles = torch.outer(torch.arange(rows, device=device, dtype=torch.float32), inverse_frequency)
108
+ cached = (angles.cos().to(dtype), angles.sin().to(dtype))
109
+ self._cached = cached
110
+ self._key = key
111
+ return cached
112
+
113
+
114
+ def parallax_reference(
115
+ q: torch.Tensor,
116
+ r: torch.Tensor,
117
+ k: torch.Tensor,
118
+ v: torch.Tensor,
119
+ *,
120
+ scale: float,
121
+ cu_seqlens: torch.Tensor | None = None,
122
+ ) -> torch.Tensor:
123
+ """FP32 Parallax for queries at the last ``Tq`` of ``Tk`` key positions.
124
+
125
+ ``out = O1 / d1 * (1 + d2 / d1) - O2 / d1`` with ``p1 = exp(scale q·k)``,
126
+ ``p2 = p1 * (r·k)``; ``cu_seqlens`` (batch 1, ``Tq == Tk``) masks across
127
+ packed documents.
128
+ """
129
+
130
+ batch, queries, query_heads, head_dim = q.shape
131
+ keys, kv_heads = k.shape[1], k.shape[2]
132
+ groups = query_heads // kv_heads
133
+ qf = q.float().reshape(batch, queries, kv_heads, groups, head_dim)
134
+ rf = r.float().reshape(batch, queries, kv_heads, groups, head_dim)
135
+ kf, vf = k.float(), v.float()
136
+ s1 = torch.einsum("bqhgd,bkhd->bhgqk", qf, kf) * scale
137
+ s2 = torch.einsum("bqhgd,bkhd->bhgqk", rf, kf)
138
+ query_positions = torch.arange(keys - queries, keys, device=q.device)[:, None]
139
+ key_positions = torch.arange(keys, device=q.device)[None, :]
140
+ masked = key_positions > query_positions
141
+ if cu_seqlens is not None:
142
+ segment = torch.zeros(keys, dtype=torch.long, device=q.device)
143
+ segment[cu_seqlens[1:-1].long()] = 1
144
+ segment = segment.cumsum(0)
145
+ masked = masked | (segment[keys - queries :, None] != segment[None, :])
146
+ s1 = s1.masked_fill(masked, float("-inf"))
147
+ p1 = (s1 - s1.amax(dim=-1, keepdim=True)).exp()
148
+ d1 = p1.sum(dim=-1)
149
+ p2 = p1 * s2
150
+ d2 = p2.sum(dim=-1)
151
+ o1 = torch.einsum("bhgqk,bkhd->bqhgd", p1, vf)
152
+ o2 = torch.einsum("bhgqk,bkhd->bqhgd", p2, vf)
153
+ inverse_d1 = (1.0 / d1).permute(0, 3, 1, 2)[..., None]
154
+ correction = (d2 / d1).permute(0, 3, 1, 2)[..., None]
155
+ out = o1 * inverse_d1 * (1.0 + correction) - o2 * inverse_d1
156
+ return out.reshape(batch, queries, query_heads, head_dim).to(q.dtype)
157
+
158
+
159
+ # ------------------------------------------------------------------ gated residual
160
+
161
+
162
+ class _GatedResidualRead(torch.autograd.Function):
163
+ """``x = (1/n) Σ σ(W_u SiLU(W_d vec(R̂) / n))_i ⊙ R̂_i`` and ``s = 2σ(W_w vec(R̂) / n)``.
164
+
165
+ Saves only the branches, the per-branch inverse RMS and the bottleneck
166
+ pre-activation; the normalized branches and gates are recomputed in
167
+ chunks during backward, so a 32k-token read keeps ~0.4 GB instead of ~4 GB.
168
+ CUDA runs the fused kernels in ``gated_residual_ops`` instead.
169
+ """
170
+
171
+ @staticmethod
172
+ def forward(ctx, residual, gamma, down, up, write, eps):
173
+ rows, branches, hidden_size = residual.shape
174
+ dtype = down.dtype
175
+ gain = 1.0 + gamma.float()
176
+ out = residual.new_empty(rows, hidden_size)
177
+ rstd = residual.new_empty(rows, branches, dtype=torch.float32)
178
+ hidden = residual.new_empty(rows, down.shape[0])
179
+ scales = residual.new_empty(rows, 0 if write is None else branches, dtype=torch.float32)
180
+ for start in range(0, rows, GATED_RESIDUAL_CHUNK_ROWS):
181
+ stop = min(start + GATED_RESIDUAL_CHUNK_ROWS, rows)
182
+ values = residual[start:stop].float()
183
+ chunk_rstd = torch.rsqrt(values.square().mean(dim=-1, keepdim=True) + eps)
184
+ hat = values * chunk_rstd * gain
185
+ flat = hat.to(dtype).flatten(1)
186
+ chunk_hidden = (flat @ down.t()) / branches
187
+ activated = F.silu(chunk_hidden.float()).to(dtype)
188
+ gates = torch.sigmoid((activated @ up.t()).float()).view(stop - start, branches, hidden_size)
189
+ out[start:stop] = ((gates * hat).sum(dim=1) / branches).to(out.dtype)
190
+ rstd[start:stop] = chunk_rstd.squeeze(-1)
191
+ hidden[start:stop] = chunk_hidden
192
+ if write is not None:
193
+ scales[start:stop] = 2.0 * torch.sigmoid((flat @ write.t()).float() / branches)
194
+ ctx.save_for_backward(residual, gamma, down, up, write, rstd, hidden, scales)
195
+ scales_out = scales.to(residual.dtype, copy=True)
196
+ if write is None:
197
+ ctx.mark_non_differentiable(scales_out)
198
+ return out, scales_out
199
+
200
+ @staticmethod
201
+ def backward(ctx, grad_out, grad_scales):
202
+ residual, gamma, down, up, write, rstd, hidden, scales = ctx.saved_tensors
203
+ rows, branches, hidden_size = residual.shape
204
+ dtype = down.dtype
205
+ gain = 1.0 + gamma.float()
206
+ grad_residual = torch.empty(residual.shape, dtype=residual.dtype, device=residual.device)
207
+ grad_gamma = torch.zeros(branches, hidden_size, dtype=torch.float32, device=residual.device)
208
+ grad_down = torch.zeros(down.shape, dtype=torch.float32, device=down.device)
209
+ grad_up = torch.zeros(up.shape, dtype=torch.float32, device=up.device)
210
+ grad_write = None if write is None else torch.zeros(write.shape, dtype=torch.float32, device=write.device)
211
+ use_scales = write is not None and grad_scales is not None
212
+ for start in range(0, rows, GATED_RESIDUAL_CHUNK_ROWS):
213
+ stop = min(start + GATED_RESIDUAL_CHUNK_ROWS, rows)
214
+ count = stop - start
215
+ normalized = residual[start:stop].float() * rstd[start:stop, :, None]
216
+ hat = normalized * gain
217
+ flat = hat.to(dtype).flatten(1)
218
+ pre = hidden[start:stop].float()
219
+ sigmoid_pre = torch.sigmoid(pre)
220
+ activated = (pre * sigmoid_pre).to(dtype)
221
+ gates = torch.sigmoid((activated @ up.t()).float()).view(count, branches, hidden_size)
222
+ grad_x = grad_out[start:stop].float().unsqueeze(1) / branches
223
+ grad_hat = gates * grad_x
224
+ grad_gate_logits = (grad_x * hat * gates * (1.0 - gates)).flatten(1).to(dtype)
225
+ grad_up += (grad_gate_logits.t() @ activated).float()
226
+ grad_activated = (grad_gate_logits @ up).float()
227
+ grad_pre = grad_activated * sigmoid_pre * (1.0 + pre * (1.0 - sigmoid_pre))
228
+ grad_pre = (grad_pre / branches).to(dtype)
229
+ grad_down += (grad_pre.t() @ flat).float()
230
+ grad_flat = (grad_pre @ down).float()
231
+ if use_scales:
232
+ scale_values = scales[start:stop]
233
+ grad_scale_logits = grad_scales[start:stop].float() * scale_values * (1.0 - scale_values / 2.0)
234
+ grad_scale_logits = (grad_scale_logits / branches).to(dtype)
235
+ grad_write += (grad_scale_logits.t() @ flat).float()
236
+ grad_flat += (grad_scale_logits @ write).float()
237
+ grad_hat = grad_hat + grad_flat.view(count, branches, hidden_size)
238
+ grad_gamma += (grad_hat * normalized).sum(dim=0)
239
+ grad_normalized = grad_hat * gain
240
+ grad_residual[start:stop] = (
241
+ rstd[start:stop, :, None]
242
+ * (grad_normalized - normalized * (grad_normalized * normalized).mean(dim=-1, keepdim=True))
243
+ ).to(grad_residual.dtype)
244
+ return (
245
+ grad_residual,
246
+ grad_gamma.to(gamma.dtype),
247
+ grad_down.to(down.dtype),
248
+ grad_up.to(up.dtype),
249
+ None if grad_write is None else grad_write.to(write.dtype),
250
+ None,
251
+ )
252
+
253
+
254
+ def gated_residual_read_reference(
255
+ residual: torch.Tensor,
256
+ gamma: torch.Tensor,
257
+ down: torch.Tensor,
258
+ up: torch.Tensor,
259
+ write: torch.Tensor | None,
260
+ eps: float,
261
+ ) -> tuple[torch.Tensor, torch.Tensor]:
262
+ """Eager autograd version of :class:`_GatedResidualRead`, for tests."""
263
+
264
+ rows, branches, hidden_size = residual.shape
265
+ gain = 1.0 + gamma.float()
266
+ values = residual.float()
267
+ hat = values * torch.rsqrt(values.square().mean(dim=-1, keepdim=True) + eps) * gain
268
+ flat = hat.to(down.dtype).flatten(1)
269
+ hidden = (flat @ down.t()) / branches
270
+ gates = torch.sigmoid((F.silu(hidden.float()).to(up.dtype) @ up.t()).float()).view(rows, branches, hidden_size)
271
+ out = ((gates * hat).sum(dim=1) / branches).to(residual.dtype)
272
+ if write is None:
273
+ return out, residual.new_empty(rows, 0)
274
+ scales = 2.0 * torch.sigmoid((flat @ write.t()).float() / branches)
275
+ return out, scales.to(residual.dtype)
276
+
277
+
278
+ class GatedResidual(nn.Module):
279
+ """Qwen's Gated Residual read (and write scales) over ``branches`` residual branches."""
280
+
281
+ def __init__(self, branches: int, hidden_size: int, rank: int, eps: float, *, write: bool) -> None:
282
+ super().__init__()
283
+ self.branches = branches
284
+ self.eps = eps
285
+ self.weight = nn.Parameter(torch.zeros(branches, hidden_size))
286
+ self.down_proj = nn.Linear(branches * hidden_size, rank, bias=False)
287
+ self.up_proj = nn.Linear(rank, branches * hidden_size, bias=False)
288
+ self.write_proj = nn.Linear(branches * hidden_size, branches, bias=False) if write else None
289
+ mark_gain(self.weight)
290
+ mark_adamw_only(self.down_proj.weight, self.up_proj.weight)
291
+ if self.write_proj is not None:
292
+ mark_adamw_only(self.write_proj.weight)
293
+
294
+ def read(self, residual: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
295
+ """``(x [N, d], scales [N, n])`` from branches ``[N, n, d]``."""
296
+
297
+ write = None if self.write_proj is None else self.write_proj.weight
298
+ read = fused_gated_residual_read if residual.is_cuda else _GatedResidualRead.apply
299
+ return read(residual, self.weight, self.down_proj.weight, self.up_proj.weight, write, self.eps)
300
+
301
+ @staticmethod
302
+ def write(residual: torch.Tensor, scales: torch.Tensor, update: torch.Tensor) -> torch.Tensor:
303
+ if residual.is_cuda:
304
+ return fused_gated_residual_write(residual, scales, update)
305
+ return residual + scales.unsqueeze(-1) * update.unsqueeze(-2)
306
+
307
+
308
+ # ------------------------------------------------------------------- token mixers
309
+
310
+
311
+ class ParallaxAttention(nn.Module):
312
+ """Parallax attention (softmax plus a learned first-order correction from ``r``).
313
+
314
+ Grouped query/key/value heads with per-head zero-centered RMSNorm on q, r
315
+ and k, partial RoPE on q, r and k, and a full-rank sigmoid output gate.
316
+ """
317
+
318
+ def __init__(self, config: TinyGDNConfig, layer_index: int) -> None:
319
+ super().__init__()
320
+ self.layer_index = layer_index
321
+ self.num_heads = config.num_attention_heads
322
+ self.num_kv_heads = config.num_key_value_heads
323
+ self.head_dim = config.attention_head_dim
324
+ self.scale = self.head_dim ** -0.5
325
+ query_width = self.num_heads * self.head_dim
326
+ kv_width = self.num_kv_heads * self.head_dim
327
+ hidden = config.hidden_size
328
+ self.q_proj = nn.Linear(hidden, query_width, bias=False)
329
+ self.r_proj = nn.Linear(hidden, query_width, bias=False)
330
+ self.k_proj = nn.Linear(hidden, kv_width, bias=False)
331
+ self.v_proj = nn.Linear(hidden, kv_width, bias=False)
332
+ self.g_proj = nn.Linear(hidden, query_width, bias=True)
333
+ self.o_proj = nn.Linear(query_width, hidden, bias=False)
334
+ self.q_norm = RMSNorm(self.head_dim, config.rms_norm_eps)
335
+ self.r_norm = RMSNorm(self.head_dim, config.rms_norm_eps)
336
+ self.k_norm = RMSNorm(self.head_dim, config.rms_norm_eps)
337
+ for projection in (self.q_proj, self.r_proj):
338
+ mark_muon_row_blocks(projection.weight, self.num_heads)
339
+ for projection in (self.k_proj, self.v_proj):
340
+ mark_muon_row_blocks(projection.weight, self.num_kv_heads)
341
+ mark_adamw_only(self.g_proj.weight)
342
+
343
+ def forward(self, hidden_states: torch.Tensor, context: Senryu2Context) -> torch.Tensor:
344
+ batch, length, _ = hidden_states.shape
345
+ q = self.q_norm(self.q_proj(hidden_states).view(batch, length, self.num_heads, self.head_dim))
346
+ r = self.r_norm(self.r_proj(hidden_states).view(batch, length, self.num_heads, self.head_dim))
347
+ k = self.k_norm(self.k_proj(hidden_states).view(batch, length, self.num_kv_heads, self.head_dim))
348
+ v = self.v_proj(hidden_states).view(batch, length, self.num_kv_heads, self.head_dim)
349
+ cosine, sine = context.rotary
350
+ q, r, k = (
351
+ rotary_embedding(
352
+ tensor,
353
+ cosine,
354
+ sine,
355
+ seqlen_offsets=context.position_offset,
356
+ cu_seqlens=context.cu_seqlens,
357
+ )
358
+ for tensor in (q, r, k)
359
+ )
360
+ use_kernel = q.is_cuda and q.dtype in (torch.bfloat16, torch.float16)
361
+ cache = context.cache
362
+ if cache is not None:
363
+ keys, values = cache.append_attention(self.layer_index, k, v, context.position_offset)
364
+ if not use_kernel:
365
+ out = parallax_reference(q, r, keys, values, scale=self.scale)
366
+ elif keys.shape[1] == length:
367
+ out = parallax_attention(q, r, keys, values, scale=self.scale)
368
+ else:
369
+ out = parallax_decode_cached(q, r, keys, values, scale=self.scale)
370
+ elif use_kernel:
371
+ out = parallax_attention(q, r, k, v, scale=self.scale, cu_seqlens=context.cu_seqlens)
372
+ else:
373
+ out = parallax_reference(q, r, k, v, scale=self.scale, cu_seqlens=context.cu_seqlens)
374
+ gate = torch.sigmoid(self.g_proj(hidden_states)).view(batch, length, self.num_heads, self.head_dim)
375
+ return self.o_proj((out * gate).reshape(batch, length, self.num_heads * self.head_dim))
376
+
377
+
378
+ def build_gated_deltanet2(config: TinyGDNConfig, layer_index: int) -> GatedDeltaNet2:
379
+ mixer = GatedDeltaNet2(
380
+ hidden_size=config.hidden_size,
381
+ expand_v=config.linear_expand_v,
382
+ head_dim=config.linear_head_dim,
383
+ num_heads=config.linear_num_heads,
384
+ num_v_heads=config.linear_num_value_heads,
385
+ mode="chunk",
386
+ use_short_conv=True,
387
+ allow_neg_eigval=config.allow_negative_eigenvalues,
388
+ conv_size=config.linear_conv_kernel_dim,
389
+ conv_bias=False,
390
+ layer_idx=layer_index,
391
+ norm_eps=config.rms_norm_eps,
392
+ )
393
+ if config.linear_full_rank_gate:
394
+ # The layer calls `self.g_proj(hidden_states)`, so a single full-rank
395
+ # map drops in for fla's low-rank pair.
396
+ mixer.g_proj = nn.Linear(config.hidden_size, mixer.value_dim, bias=True)
397
+ mark_adamw_only(mixer.g_proj.weight)
398
+ else:
399
+ mark_adamw_only(*(layer.weight for layer in mixer.g_proj))
400
+ mark_adamw_only(*(layer.weight for layer in mixer.f_proj))
401
+ for projection in (mixer.q_proj, mixer.k_proj, mixer.b_proj):
402
+ mark_muon_row_blocks(projection.weight, mixer.num_heads)
403
+ for projection in (mixer.v_proj, mixer.w_proj):
404
+ mark_muon_row_blocks(projection.weight, mixer.num_v_heads)
405
+ return mixer
406
+
407
+
408
+ # ------------------------------------------------------------------- n-gram memory
409
+
410
+
411
+ class BranchRMSNorm(nn.Module):
412
+ """Zero-centered RMSNorm over the last dim with one gain vector per branch."""
413
+
414
+ def __init__(self, branches: int, hidden_size: int, eps: float) -> None:
415
+ super().__init__()
416
+ self.weight = nn.Parameter(torch.zeros(branches, hidden_size))
417
+ self.eps = eps
418
+ mark_gain(self.weight)
419
+
420
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
421
+ values = hidden_states.float()
422
+ values = values * torch.rsqrt(values.square().mean(dim=-1, keepdim=True) + self.eps)
423
+ return values * (1.0 + self.weight.float())
424
+
425
+
426
+ class NGramMemory(nn.Module):
427
+ """Engram-style memory: hashed n-gram rows, gated per residual branch, then a short conv.
428
+
429
+ ``e`` concatenates one row per table. Each branch gates the shared value
430
+ ``W_V e`` with ``σ(RMSNorm(R_m) · RMSNorm(W_K,m e) / √d)``; the gated values
431
+ pass a depthwise causal convolution (dilation = largest order, zero init)
432
+ and are added to their branches.
433
+ """
434
+
435
+ def __init__(self, config: TinyGDNConfig) -> None:
436
+ super().__init__()
437
+ self.layout = NGramLayout.from_config(config)
438
+ self.branches = config.residual_branches
439
+ self.hidden_size = config.hidden_size
440
+ self.kernel = config.ngram_conv_kernel
441
+ self.dilation = self.layout.max_order
442
+ memory_dim = self.layout.memory_dim
443
+ self.value_proj = nn.Linear(memory_dim, config.hidden_size, bias=False)
444
+ self.key_proj = nn.Linear(memory_dim, self.branches * config.hidden_size, bias=False)
445
+ self.hidden_norm = BranchRMSNorm(self.branches, config.hidden_size, config.rms_norm_eps)
446
+ self.key_norm = BranchRMSNorm(self.branches, config.hidden_size, config.rms_norm_eps)
447
+ self.conv_norm = RMSNorm(config.hidden_size, config.rms_norm_eps)
448
+ self.conv_weight = nn.Parameter(torch.zeros(config.hidden_size, self.kernel))
449
+ mark_muon_row_blocks(self.key_proj.weight, self.branches)
450
+ mark_gain(self.conv_weight)
451
+ self.register_buffer("canonical_ids", torch.arange(config.vocab_size, dtype=torch.int64), persistent=False)
452
+ self.register_buffer("tables", None, persistent=False)
453
+
454
+ @property
455
+ def conv_history(self) -> int:
456
+ return (self.kernel - 1) * self.dilation
457
+
458
+ def set_canonical_ids(self, canonical: torch.Tensor) -> None:
459
+ if canonical.shape != self.canonical_ids.shape:
460
+ raise ValueError("canonical map does not match the vocabulary")
461
+ self.canonical_ids = canonical.to(device=self.canonical_ids.device, dtype=torch.int64)
462
+
463
+ def attach_tables(self, tables: torch.Tensor, canonical: torch.Tensor) -> None:
464
+ """Look rows up in ``tables [total_rows, embedding_dim]``.
465
+
466
+ The tables may stay in host memory while the model runs on the GPU:
467
+ decoding reads only ``table_count`` rows per token.
468
+ """
469
+
470
+ if tables.shape != (self.layout.total_rows, self.layout.embedding_dim):
471
+ raise ValueError(f"tables have shape {tuple(tables.shape)}")
472
+ self.set_canonical_ids(canonical)
473
+ self.tables = tables
474
+
475
+ def load_tables(self, path: Path, *, device: torch.device | str, dtype: torch.dtype) -> None:
476
+ tables, canonical = load_table_file(path, self.layout, device=device, dtype=dtype)
477
+ self.attach_tables(tables, canonical)
478
+
479
+ def lookup(
480
+ self,
481
+ input_ids: torch.Tensor,
482
+ *,
483
+ distance: torch.Tensor | None = None,
484
+ history: torch.Tensor | None = None,
485
+ ) -> torch.Tensor:
486
+ """Concatenated rows ``[..., memory_dim]`` for every position of ``input_ids``."""
487
+
488
+ if self.tables is None:
489
+ raise RuntimeError(
490
+ "Senryu-2 needs n-gram embeddings: pass ngram_embeddings or attach tables "
491
+ "with model.ngram.load_tables(...)"
492
+ )
493
+ canonical = self.canonical_ids[input_ids]
494
+ indices = ngram_row_indices(canonical, self.layout, distance=distance, history=history)
495
+ tables = self.tables
496
+ if tables.device == indices.device:
497
+ return F.embedding(indices, tables).flatten(-2)
498
+ rows = F.embedding(indices.to(tables.device), tables).flatten(-2)
499
+ return rows.to(indices.device, non_blocking=True)
500
+
501
+ def _convolve(
502
+ self,
503
+ values: torch.Tensor,
504
+ distance: torch.Tensor | None,
505
+ history: torch.Tensor | None,
506
+ ) -> tuple[torch.Tensor, torch.Tensor]:
507
+ """Causal dilated depthwise conv over ``[B, T, n, d]``; returns ``(out, new_history)``."""
508
+
509
+ batch, length = values.shape[:2]
510
+ span = self.conv_history
511
+ if history is None:
512
+ history = values.new_zeros(batch, span, self.branches, self.hidden_size)
513
+ extended = torch.cat([history, values], dim=1)
514
+ weight = self.conv_weight.to(values.dtype)
515
+ out = values * weight[:, -1]
516
+ for tap in range(1, self.kernel):
517
+ shift = tap * self.dilation
518
+ shifted = extended[:, span - shift : span - shift + length]
519
+ if distance is not None:
520
+ shifted = shifted * (distance >= shift).to(values.dtype)[..., None, None]
521
+ out = out + shifted * weight[:, -1 - tap]
522
+ return out, extended[:, -span:]
523
+
524
+ def forward(
525
+ self,
526
+ residual: torch.Tensor,
527
+ embeddings: torch.Tensor,
528
+ *,
529
+ batch: int,
530
+ distance: torch.Tensor | None,
531
+ conv_state: torch.Tensor | None = None,
532
+ ) -> tuple[torch.Tensor, torch.Tensor]:
533
+ """Branch updates ``[N, n, d]`` for branches ``[N, n, d]`` and rows ``[N, memory_dim]``."""
534
+
535
+ rows = residual.shape[0]
536
+ value = self.value_proj(embeddings)
537
+ keys = self.key_proj(embeddings).view(rows, self.branches, self.hidden_size)
538
+ similarity = (self.hidden_norm(residual) * self.key_norm(keys)).sum(dim=-1)
539
+ alpha = torch.sigmoid(similarity / math.sqrt(self.hidden_size)).to(value.dtype)
540
+ gated = alpha.unsqueeze(-1) * value.unsqueeze(1)
541
+ normalized = self.conv_norm(gated).view(batch, rows // batch, self.branches, self.hidden_size)
542
+ convolved, new_state = self._convolve(
543
+ normalized,
544
+ None if distance is None else distance.view(batch, rows // batch),
545
+ conv_state,
546
+ )
547
+ return F.silu(convolved).view(rows, self.branches, self.hidden_size) + gated, new_state
548
+
549
+
550
+ # ------------------------------------------------------------------------- blocks
551
+
552
+
553
+ class Senryu2Block(nn.Module):
554
+ def __init__(self, config: TinyGDNConfig, layer_index: int) -> None:
555
+ super().__init__()
556
+ self.layer_index = layer_index
557
+ self.layer_type = config.layer_types[layer_index]
558
+ residual = (config.residual_branches, config.hidden_size, config.residual_gate_rank, config.rms_norm_eps)
559
+ self.mixer_residual = GatedResidual(*residual, write=True)
560
+ self.mlp_residual = GatedResidual(*residual, write=True)
561
+ if self.layer_type == "gdn2":
562
+ self.token_mixer: nn.Module = build_gated_deltanet2(config, layer_index)
563
+ elif self.layer_type == "parallax":
564
+ self.token_mixer = ParallaxAttention(config, layer_index)
565
+ else:
566
+ raise ValueError(f"Senryu-2 has no {self.layer_type} layers")
567
+ self.mlp = SiTUGLU(config)
568
+ mark_muon_row_blocks(self.mlp.gate_up_proj.weight, 2)
569
+
570
+ def _mix(self, hidden_states: torch.Tensor, context: Senryu2Context) -> torch.Tensor:
571
+ batched = hidden_states.view(context.batch, context.length, -1)
572
+ if self.layer_type == "parallax":
573
+ mixed = self.token_mixer(batched, context)
574
+ else:
575
+ cache = context.cache
576
+ mixed, _, _ = self.token_mixer(
577
+ batched,
578
+ past_key_values=None if cache is None else cache.linear,
579
+ use_cache=cache is not None,
580
+ cu_seqlens=context.cu_seqlens,
581
+ )
582
+ return mixed.reshape(hidden_states.shape)
583
+
584
+ def forward(self, residual: torch.Tensor, context: Senryu2Context) -> torch.Tensor:
585
+ hidden_states, scales = self.mixer_residual.read(residual)
586
+ residual = GatedResidual.write(residual, scales, self._mix(hidden_states, context))
587
+ hidden_states, scales = self.mlp_residual.read(residual)
588
+ return GatedResidual.write(residual, scales, self.mlp(hidden_states))
589
+
590
+
591
+ # -------------------------------------------------------------------------- cache
592
+
593
+
594
+ @dataclass
595
+ class Senryu2Cache:
596
+ """Decode state: GDN-2 states, Parallax keys/values, and the n-gram tails."""
597
+
598
+ linear: Any = field(default_factory=FlaCache)
599
+ keys: dict[int, torch.Tensor] = field(default_factory=dict)
600
+ values: dict[int, torch.Tensor] = field(default_factory=dict)
601
+ length: int = 0
602
+ ngram_tokens: torch.Tensor | None = None
603
+ ngram_conv: torch.Tensor | None = None
604
+
605
+ def append_attention(
606
+ self,
607
+ layer_index: int,
608
+ keys: torch.Tensor,
609
+ values: torch.Tensor,
610
+ start: int,
611
+ ) -> tuple[torch.Tensor, torch.Tensor]:
612
+ """Write ``[B, T, H, D]`` keys/values at ``start``; return views of the whole prefix."""
613
+
614
+ stop = start + keys.shape[1]
615
+ stored_keys = self.keys.get(layer_index)
616
+ if stored_keys is None or stored_keys.shape[1] < stop:
617
+ capacity = max(stop, 0 if stored_keys is None else 2 * stored_keys.shape[1])
618
+ capacity = -(-capacity // KV_CACHE_GRANULE) * KV_CACHE_GRANULE
619
+ grown_keys = keys.new_empty(keys.shape[0], capacity, *keys.shape[2:])
620
+ grown_values = values.new_empty(values.shape[0], capacity, *values.shape[2:])
621
+ if stored_keys is not None:
622
+ grown_keys[:, :start] = stored_keys[:, :start]
623
+ grown_values[:, :start] = self.values[layer_index][:, :start]
624
+ self.keys[layer_index] = stored_keys = grown_keys
625
+ self.values[layer_index] = grown_values
626
+ stored_values = self.values[layer_index]
627
+ stored_keys[:, start:stop] = keys
628
+ stored_values[:, start:stop] = values
629
+ return stored_keys[:, :stop], stored_values[:, :stop]
630
+
631
+
632
+ # -------------------------------------------------------------------------- model
633
+
634
+
635
+ @dataclass
636
+ class _Packing:
637
+ """How a ``[B, T]`` batch maps onto one packed row of ``valid`` tokens."""
638
+
639
+ batch: int
640
+ length: int
641
+ index: torch.Tensor | None
642
+ row_starts: torch.Tensor
643
+
644
+ def pack(self, tensor: torch.Tensor) -> torch.Tensor:
645
+ flat = tensor.reshape(self.batch * self.length, *tensor.shape[2:])
646
+ return flat if self.index is None else flat.index_select(0, self.index)
647
+
648
+ def unpack(self, tensor: torch.Tensor) -> torch.Tensor:
649
+ if self.index is None:
650
+ return tensor.view(self.batch, self.length, *tensor.shape[1:])
651
+ out = tensor.new_zeros(self.batch * self.length, *tensor.shape[1:])
652
+ out.index_copy_(0, self.index, tensor)
653
+ return out.view(self.batch, self.length, *tensor.shape[1:])
654
+
655
+
656
+ class Senryu2ForCausalLM(nn.Module):
657
+ def __init__(self, config: TinyGDNConfig) -> None:
658
+ super().__init__()
659
+ if config.residual_mode != "gated_residual":
660
+ raise ValueError("Senryu2ForCausalLM needs residual_mode 'gated_residual'")
661
+ self.config = config
662
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
663
+ self.layers = nn.ModuleList(Senryu2Block(config, index) for index in range(config.num_hidden_layers))
664
+ self.final_residual = GatedResidual(
665
+ config.residual_branches,
666
+ config.hidden_size,
667
+ config.residual_gate_rank,
668
+ config.rms_norm_eps,
669
+ write=False,
670
+ )
671
+ self.ngram = NGramMemory(config) if config.ngram_orders else None
672
+ self.rotary = RotaryTables(config.rotary_dim, config.rope_theta)
673
+ self.checkpointed_layers: frozenset[int] = frozenset()
674
+ self.apply(self._initialize_module)
675
+ self._initialize_residual_projections()
676
+
677
+ # ---------------------------------------------------------------- setup
678
+ def _initialize_module(self, module: nn.Module) -> None:
679
+ if isinstance(module, nn.Linear):
680
+ nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
681
+ if module.bias is not None:
682
+ nn.init.zeros_(module.bias)
683
+ elif isinstance(module, nn.Embedding):
684
+ nn.init.normal_(module.weight, mean=0.0, std=self.config.initializer_range)
685
+
686
+ def _initialize_residual_projections(self) -> None:
687
+ residual_std = self.config.initializer_range / math.sqrt(2 * self.config.num_hidden_layers)
688
+ for layer in self.layers:
689
+ nn.init.normal_(layer.token_mixer.o_proj.weight, mean=0.0, std=residual_std)
690
+ nn.init.normal_(layer.mlp.down_proj.weight, mean=0.0, std=residual_std)
691
+
692
+ def enable_gradient_checkpointing(self, enabled: bool = True, *, layers: int | None = None) -> None:
693
+ """Recompute block activations in backward; ``layers`` picks GDN-2 blocks first."""
694
+
695
+ if not enabled:
696
+ if layers is not None:
697
+ raise ValueError("layers needs gradient checkpointing enabled")
698
+ self.checkpointed_layers = frozenset()
699
+ return
700
+ count = len(self.layers) if layers is None else layers
701
+ if not 1 <= count <= len(self.layers):
702
+ raise ValueError(f"layers must be between 1 and {len(self.layers)}")
703
+ cheapest_replay_first = sorted(
704
+ range(len(self.layers)),
705
+ key=lambda index: self.layers[index].layer_type != "gdn2",
706
+ )
707
+ self.checkpointed_layers = frozenset(cheapest_replay_first[:count])
708
+
709
+ def describe_gradient_checkpointing(self) -> str:
710
+ if not self.checkpointed_layers:
711
+ return "off"
712
+ layer_types = sorted({self.layers[index].layer_type for index in self.checkpointed_layers})
713
+ return f"{len(self.checkpointed_layers)}/{len(self.layers)} blocks ({', '.join(layer_types)})"
714
+
715
+ def project_to_vocabulary(self, hidden_states: torch.Tensor) -> torch.Tensor:
716
+ return F.linear(hidden_states, self.embed_tokens.weight)
717
+
718
+ # -------------------------------------------------------------- packing
719
+ def _packing(self, input_ids: torch.Tensor, attention_mask: torch.Tensor | None) -> _Packing:
720
+ batch, length = input_ids.shape
721
+ device = input_ids.device
722
+ if attention_mask is None or bool(attention_mask.all()):
723
+ row_starts = torch.arange(0, batch * length, length, device=device)
724
+ return _Packing(batch, length, None, row_starts)
725
+ if attention_mask.shape != input_ids.shape:
726
+ raise ValueError("attention_mask must match input_ids")
727
+ valid = attention_mask.reshape(-1).bool()
728
+ index = valid.nonzero().flatten()
729
+ lengths = attention_mask.bool().sum(dim=1)
730
+ if bool((lengths == 0).any()):
731
+ raise ValueError("every row needs at least one unmasked token")
732
+ row_starts = torch.cumsum(lengths, dim=0) - lengths
733
+ return _Packing(batch, length, index, row_starts)
734
+
735
+ def packed_layout(
736
+ self,
737
+ tokens: torch.Tensor,
738
+ row_starts: torch.Tensor | None = None,
739
+ ) -> tuple[torch.Tensor, torch.Tensor]:
740
+ """``(cu_seqlens int32, distance int64)`` for a flat token stream split at BOS."""
741
+
742
+ distance = document_distance(tokens, self.config.bos_token_id, row_starts)
743
+ starts = (distance == 0).nonzero().flatten()
744
+ cu_seqlens = torch.cat([starts, starts.new_tensor([tokens.numel()])]).to(torch.int32)
745
+ return cu_seqlens, distance
746
+
747
+ # -------------------------------------------------------------- forward
748
+ def _run_block(self, index: int, residual: torch.Tensor, context: Senryu2Context) -> torch.Tensor:
749
+ layer = self.layers[index]
750
+ if self.training and index in self.checkpointed_layers:
751
+ return checkpoint(layer, residual, context, use_reentrant=False)
752
+ return layer(residual, context)
753
+
754
+ def _inject_ngrams(
755
+ self,
756
+ residual: torch.Tensor,
757
+ embeddings: torch.Tensor,
758
+ context: Senryu2Context,
759
+ ) -> torch.Tensor:
760
+ assert self.ngram is not None
761
+ cache = context.cache
762
+ conv_state = None if cache is None else cache.ngram_conv
763
+ distance = context.distance if cache is None else None
764
+ if self.training and self.checkpointed_layers:
765
+ update, new_state = checkpoint(
766
+ self.ngram,
767
+ residual,
768
+ embeddings,
769
+ batch=context.batch,
770
+ distance=distance,
771
+ conv_state=conv_state,
772
+ use_reentrant=False,
773
+ )
774
+ else:
775
+ update, new_state = self.ngram(
776
+ residual,
777
+ embeddings,
778
+ batch=context.batch,
779
+ distance=distance,
780
+ conv_state=conv_state,
781
+ )
782
+ if cache is not None:
783
+ cache.ngram_conv = new_state
784
+ return residual + update
785
+
786
+ def _ngram_rows(
787
+ self,
788
+ input_ids: torch.Tensor,
789
+ packing: _Packing | None,
790
+ distance: torch.Tensor | None,
791
+ ngram_embeddings: torch.Tensor | None,
792
+ cache: Senryu2Cache | None,
793
+ ) -> torch.Tensor:
794
+ assert self.ngram is not None
795
+ layout = self.ngram.layout
796
+ if cache is not None:
797
+ history = cache.ngram_tokens
798
+ if history is None:
799
+ history = input_ids.new_full((input_ids.shape[0], layout.max_order - 1), layout.pad_token_id)
800
+ canonical = self.ngram.canonical_ids[input_ids]
801
+ cache.ngram_tokens = torch.cat([history, canonical], dim=1)[:, -(layout.max_order - 1) :]
802
+ if ngram_embeddings is not None:
803
+ return ngram_embeddings.reshape(-1, layout.memory_dim)
804
+ return self.ngram.lookup(input_ids, history=history).reshape(-1, layout.memory_dim)
805
+ assert packing is not None
806
+ if ngram_embeddings is not None:
807
+ if ngram_embeddings.shape != (*input_ids.shape, layout.memory_dim):
808
+ raise ValueError(
809
+ f"ngram_embeddings must have shape {(*input_ids.shape, layout.memory_dim)}"
810
+ )
811
+ return packing.pack(ngram_embeddings)
812
+ return self.ngram.lookup(packing.pack(input_ids), distance=distance)
813
+
814
+ def forward(
815
+ self,
816
+ input_ids: torch.Tensor,
817
+ labels: torch.Tensor | None = None,
818
+ attention_mask: torch.Tensor | None = None,
819
+ past_key_values: Senryu2Cache | None = None,
820
+ *,
821
+ use_cache: bool = False,
822
+ return_logits: bool = True,
823
+ return_hidden_states: bool = False,
824
+ labels_are_shifted: bool = False,
825
+ include_mtp_loss: bool = True,
826
+ mtp_loss_weight: float | None = None,
827
+ z_loss_coefficient: float = 0.0,
828
+ logits_to_keep: int | None = None,
829
+ ngram_embeddings: torch.Tensor | None = None,
830
+ ) -> TinyGDNOutput:
831
+ del include_mtp_loss, mtp_loss_weight
832
+ if input_ids.ndim != 2:
833
+ raise ValueError("input_ids must have shape [batch, sequence]")
834
+ if labels is not None and labels.shape != input_ids.shape:
835
+ raise ValueError("labels must have the same shape as input_ids")
836
+ if z_loss_coefficient < 0.0:
837
+ raise ValueError("z_loss_coefficient cannot be negative")
838
+ if logits_to_keep is not None and logits_to_keep <= 0:
839
+ raise ValueError("logits_to_keep must be positive")
840
+ if use_cache and labels is not None:
841
+ raise ValueError("use_cache is not supported with labels")
842
+ if past_key_values is not None and not isinstance(past_key_values, Senryu2Cache):
843
+ raise TypeError("past_key_values must be a Senryu2Cache or None")
844
+ cached = use_cache or past_key_values is not None
845
+ batch, length = input_ids.shape
846
+ offset = 0 if past_key_values is None else past_key_values.length
847
+ if offset + length > self.config.max_position_embeddings:
848
+ raise ValueError("Input exceeds max_position_embeddings")
849
+
850
+ embeddings = self.embed_tokens(input_ids)
851
+ device, dtype = embeddings.device, embeddings.dtype
852
+ packing: _Packing | None = None
853
+ distance: torch.Tensor | None = None
854
+ if cached:
855
+ if attention_mask is not None and not bool(attention_mask.all()):
856
+ raise ValueError("Senryu-2 decodes unpadded rows; drop the padding instead of masking it")
857
+ cache = past_key_values if past_key_values is not None else Senryu2Cache()
858
+ context = Senryu2Context(
859
+ batch=batch,
860
+ length=length,
861
+ cu_seqlens=None,
862
+ distance=None,
863
+ rotary=self.rotary.get(offset + length, device, dtype),
864
+ position_offset=offset,
865
+ cache=cache,
866
+ )
867
+ hidden = embeddings.reshape(batch * length, -1)
868
+ else:
869
+ cache = None
870
+ packing = self._packing(input_ids, attention_mask)
871
+ tokens = packing.pack(input_ids)
872
+ cu_seqlens, distance = self.packed_layout(tokens, packing.row_starts)
873
+ context = Senryu2Context(
874
+ batch=1,
875
+ length=tokens.numel(),
876
+ cu_seqlens=cu_seqlens,
877
+ distance=distance,
878
+ rotary=self.rotary.get(tokens.numel(), device, dtype),
879
+ )
880
+ hidden = packing.pack(embeddings)
881
+ residual = hidden.unsqueeze(1).expand(-1, self.config.residual_branches, -1)
882
+
883
+ for index in range(len(self.layers)):
884
+ if self.ngram is not None and index == self.config.ngram_layer_index:
885
+ rows = self._ngram_rows(input_ids, packing, distance, ngram_embeddings, cache)
886
+ residual = self._inject_ngrams(residual, rows.to(dtype), context)
887
+ residual = self._run_block(index, residual, context)
888
+ hidden_states, _ = self.final_residual.read(residual)
889
+ if cache is not None:
890
+ cache.length = offset + length
891
+
892
+ main_loss = None
893
+ z_loss = None
894
+ total_loss = None
895
+ if labels is not None:
896
+ assert packing is not None
897
+ if labels_are_shifted:
898
+ targets = labels
899
+ else:
900
+ targets = torch.cat([labels[:, 1:], labels.new_full((batch, 1), -100)], dim=1)
901
+ total_loss, main_loss, z_loss = linear_cross_entropy(
902
+ hidden_states,
903
+ self.embed_tokens.weight,
904
+ packing.pack(targets).reshape(-1),
905
+ z_loss_coefficient=z_loss_coefficient,
906
+ )
907
+ if z_loss_coefficient == 0.0:
908
+ z_loss = None
909
+
910
+ batched_states = (
911
+ hidden_states.view(batch, length, -1) if packing is None else packing.unpack(hidden_states)
912
+ )
913
+ output_states = batched_states if logits_to_keep is None else batched_states[:, -logits_to_keep:]
914
+ logits = self.project_to_vocabulary(output_states) if return_logits else None
915
+ return TinyGDNOutput(
916
+ loss=total_loss,
917
+ logits=logits,
918
+ main_loss=main_loss,
919
+ mtp_loss=None,
920
+ z_loss=z_loss,
921
+ hidden_states=batched_states if return_hidden_states else None,
922
+ past_key_values=cache if use_cache else None,
923
+ )
924
+
925
+ # ------------------------------------------------------------ reporting
926
+ def parameter_report(self) -> dict[str, int]:
927
+ total = sum(parameter.numel() for parameter in self.parameters())
928
+ embeddings = self.embed_tokens.weight.numel()
929
+ tables = 0 if self.ngram is None else self.ngram.layout.total_rows * self.ngram.layout.embedding_dim
930
+ return {
931
+ "deployable_core": total,
932
+ "training_total": total,
933
+ "embedding": embeddings,
934
+ "mtp_auxiliary": 0,
935
+ "non_embedding_core": total - embeddings,
936
+ "ngram_table": tables,
937
+ }
938
+
939
+ def save_checkpoint(self, output_dir: Path) -> None:
940
+ output_dir.mkdir(parents=True, exist_ok=True)
941
+ self.config.save_json(output_dir / "config.json")
942
+ save_model(self, output_dir / "model.safetensors")
943
+
944
+ def load_ngram_tables(
945
+ self,
946
+ checkpoint_dir: Path,
947
+ *,
948
+ weights: str,
949
+ device: str | torch.device,
950
+ dtype: torch.dtype,
951
+ ) -> None:
952
+ """Attach the checkpoint's tables that match ``weights`` ("normal" or "ema")."""
953
+
954
+ if self.ngram is None:
955
+ return
956
+ table_file = checkpoint_dir / (NGRAM_TABLES_EMA_FILE if weights == "ema" else NGRAM_TABLES_FILE)
957
+ if not table_file.is_file():
958
+ raise FileNotFoundError(f"Senryu-2 checkpoint has no n-gram tables: {table_file}")
959
+ self.ngram.load_tables(table_file, device=device, dtype=dtype)
960
+
961
+ def extra_repr(self) -> str:
962
+ report = self.parameter_report()
963
+ return f"core_parameters={report['deployable_core']:,}, ngram_table_parameters={report['ngram_table']:,}"
964
+
965
+ def get_architecture_metadata(self) -> dict[str, Any]:
966
+ return {
967
+ "architecture": self.config.architecture,
968
+ "layer_types": list(self.config.layer_types),
969
+ "effective_num_layers": self.config.effective_num_layers,
970
+ "shared_layer_indices": [],
971
+ "parameter_report": self.parameter_report(),
972
+ "features": [
973
+ "3:1 Gated DeltaNet-2 + Parallax attention hybrid",
974
+ "4-branch Gated Residual stream (gated read, per-branch write scales)",
975
+ "hashed n-gram memory (orders 2-3) with per-branch context gating",
976
+ "full-rank sigmoid output gates",
977
+ "partial RoPE, per-head QK/R normalization",
978
+ "SiTU-GLU",
979
+ "document-masked packed training",
980
+ "tied input-output embeddings",
981
+ ],
982
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_bos_token": false,
3
+ "add_eos_token": false,
4
+ "bos_token": "<|begin_of_text|>",
5
+ "eos_token": "<|end_of_text|>",
6
+ "pad_token": "<|padding|>",
7
+ "unk_token": "<|unknown|>",
8
+ "model_max_length": 2048,
9
+ "clean_up_tokenization_spaces": false,
10
+ "tokenizer_class": "PreTrainedTokenizerFast",
11
+ "chat_template": "{%- set ns = namespace(xml_tools=none, tools_emitted=false) -%}\n{%- if xml_tools is defined and xml_tools -%}\n{%- set ns.xml_tools = xml_tools -%}\n{%- elif tools is defined and tools -%}\n{%- set ns.xml_tools = tools -%}\n{%- endif -%}\n{%- set tools_preamble = 'You may call one or more functions to assist with the user query.\\nYou are provided with function signatures within <tools></tools> XML tags:\\n<tools>\\n' -%}\n{%- set tools_epilogue = '</tools>\\n\\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\\n<tool_call>\\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\\n</tool_call>' -%}\n{%- for message in messages -%}\n{%- if loop.first -%}{{- bos_token -}}{%- endif -%}\n{%- if loop.first and ns.xml_tools and message['role'] != 'system' -%}\n{{- '<|im_start|>system\\n' + tools_preamble -}}\n{%- for tool in ns.xml_tools -%}\n{%- if tool is string -%}{{- (tool | replace('<tools>', '') | replace('</tools>', '') | trim) + '\\n' -}}\n{%- else -%}{{- tool | tojson + '\\n' -}}\n{%- endif -%}\n{%- endfor -%}\n{{- tools_epilogue + '<|im_end|>\\n' -}}\n{%- set ns.tools_emitted = true -%}\n{%- endif -%}\n{%- set raw_role = message['role'] -%}\n{%- set role = 'user' if raw_role == 'tool' or raw_role == 'function' else raw_role -%}\n{%- set content_text = namespace(value='') -%}\n{%- if message['content'] is string -%}\n{%- set content_text.value = message['content'] -%}\n{%- elif message['content'] is iterable -%}\n{%- for item in message['content'] -%}\n{%- if item['type'] == 'text' -%}{%- set content_text.value = content_text.value + item['text'] -%}{%- endif -%}\n{%- endfor -%}\n{%- endif -%}\n{%- set is_tool = raw_role == 'tool' or raw_role == 'function' -%}\n{%- set prev_is_tool = loop.previtem is defined and (loop.previtem['role'] == 'tool' or loop.previtem['role'] == 'function') -%}\n{%- set next_is_tool = loop.nextitem is defined and (loop.nextitem['role'] == 'tool' or loop.nextitem['role'] == 'function') -%}\n{%- if raw_role == 'system' and not (content_text.value | trim) and not (ns.xml_tools and not ns.tools_emitted) -%}\n{%- else -%}\n{%- if is_tool and prev_is_tool -%}\n{{- '\\n' -}}\n{%- else -%}\n{{- '<|im_start|>' + role + '\\n' -}}\n{%- endif -%}\n{%- if raw_role == 'assistant' and enable_thinking is defined -%}\n{{- ('<|think|>\\n' if enable_thinking else '<|no_think|>\\n') -}}\n{%- endif -%}\n{%- if is_tool -%}\n{{- '<|tool_response|>\\n' + (content_text.value | replace('<tool_response>', '') | replace('</tool_response>', '') | trim) -}}\n{%- elif raw_role == 'system' -%}\n{{- content_text.value | replace('/system_override', '') | replace('/no_think', '') | replace('/think', '') | trim -}}\n{%- else -%}\n{{- content_text.value -}}\n{%- endif -%}\n{%- if raw_role == 'system' and ns.xml_tools and not ns.tools_emitted and '<tools>' not in content_text.value -%}\n{%- if content_text.value | trim -%}{{- '\\n\\n' -}}{%- endif -%}\n{{- tools_preamble -}}\n{%- for tool in ns.xml_tools -%}\n{%- if tool is string -%}{{- (tool | replace('<tools>', '') | replace('</tools>', '') | trim) + '\\n' -}}\n{%- else -%}{{- tool | tojson + '\\n' -}}\n{%- endif -%}\n{%- endfor -%}\n{{- tools_epilogue -}}\n{%- set ns.tools_emitted = true -%}\n{%- endif -%}\n{%- if raw_role == 'assistant' and message['tool_calls'] is defined and message['tool_calls'] -%}\n{%- if '<tool_call>' not in content_text.value -%}\n{%- for tool_call in message['tool_calls'] -%}\n{%- set fn = tool_call['function'] if tool_call['function'] is defined else tool_call -%}\n{%- if loop.first and not (content_text.value | trim) -%}\n{{- '<tool_call>\\n{\"name\": \"' + fn['name'] + '\", \"arguments\": ' -}}\n{%- else -%}\n{{- '\\n<tool_call>\\n{\"name\": \"' + fn['name'] + '\", \"arguments\": ' -}}\n{%- endif -%}\n{%- if fn['arguments'] is string -%}{{- fn['arguments'] -}}\n{%- else -%}{{- fn['arguments'] | tojson -}}\n{%- endif -%}\n{{- '}\\n</tool_call>' -}}\n{%- endfor -%}\n{%- endif -%}\n{%- endif -%}\n{%- if not (is_tool and next_is_tool) -%}\n{{- '<|im_end|>\\n' -}}\n{%- endif -%}\n{%- endif -%}\n{%- endfor -%}\n{%- if add_generation_prompt -%}\n{{- '<|im_start|>assistant\\n' -}}\n{%- if enable_thinking is defined -%}{{- ('<|think|>\\n' if enable_thinking else '<|no_think|>\\n') -}}{%- endif -%}\n{%- endif -%}",
12
+ "extra_special_tokens": [
13
+ "<|im_start|>",
14
+ "<|im_end|>",
15
+ "<|tool_call|>",
16
+ "<|tool_response|>",
17
+ "<think>",
18
+ "</think>",
19
+ "<|fim_prefix|>",
20
+ "<|fim_middle|>",
21
+ "<|fim_suffix|>",
22
+ "<|fim_pad|>",
23
+ "<|no_think|>",
24
+ "<|think|>",
25
+ "<|mask|>",
26
+ "<|reserved_009|>",
27
+ "<|reserved_010|>",
28
+ "<|reserved_011|>",
29
+ "<|reserved_012|>",
30
+ "<|reserved_013|>",
31
+ "<|reserved_014|>",
32
+ "<|reserved_015|>",
33
+ "<|reserved_016|>",
34
+ "<|reserved_017|>",
35
+ "<|reserved_018|>",
36
+ "<|reserved_019|>",
37
+ "<|reserved_020|>",
38
+ "<|reserved_021|>",
39
+ "<|reserved_022|>",
40
+ "<|reserved_023|>",
41
+ "<|reserved_024|>",
42
+ "<|reserved_025|>",
43
+ "<|reserved_026|>",
44
+ "<|reserved_027|>",
45
+ "<|reserved_028|>",
46
+ "<|reserved_029|>",
47
+ "<|reserved_030|>",
48
+ "<|reserved_031|>",
49
+ "<|reserved_032|>",
50
+ "<|reserved_033|>",
51
+ "<|reserved_034|>",
52
+ "<|reserved_035|>",
53
+ "<|reserved_036|>",
54
+ "<|reserved_037|>",
55
+ "<|reserved_038|>",
56
+ "<|reserved_039|>",
57
+ "<|reserved_040|>",
58
+ "<|reserved_041|>",
59
+ "<|reserved_042|>",
60
+ "<|reserved_043|>",
61
+ "<|reserved_044|>",
62
+ "<|reserved_045|>",
63
+ "<|reserved_046|>",
64
+ "<|reserved_047|>",
65
+ "<|reserved_048|>",
66
+ "<|reserved_049|>"
67
+ ],
68
+ "mask_token": "<|mask|>"
69
+ }
vocab.json ADDED
The diff for this file is too large to render. See raw diff