Monody-Exp: monody-pass-7 EMA weights
Browse files- README.md +50 -0
- config.json +80 -0
- evaluation.json +160 -0
- inference.py +302 -0
- merges.txt +0 -0
- model.safetensors +3 -0
- requirements.txt +11 -0
- special_tokens_map.json +63 -0
- stage.json +11 -0
- tiny_gdn/__init__.py +20 -0
- tiny_gdn/config.py +488 -0
- tiny_gdn/detokenize.py +131 -0
- tiny_gdn/fugue.py +1012 -0
- tiny_gdn/fugue_sampling.py +654 -0
- tiny_gdn/fused_ops.py +548 -0
- tiny_gdn/gated_residual_ops.py +343 -0
- tiny_gdn/haiku_layers.py +199 -0
- tiny_gdn/kdn/__init__.py +5 -0
- tiny_gdn/kdn/kernels.py +0 -0
- tiny_gdn/kdn/layer.py +293 -0
- tiny_gdn/kdn/recurrence.py +311 -0
- tiny_gdn/model.py +782 -0
- tiny_gdn/ngram.py +291 -0
- tiny_gdn/nn_common.py +96 -0
- tiny_gdn/parallax_ops.py +394 -0
- tiny_gdn/senryu2.py +982 -0
- tokenizer.json +0 -0
- tokenizer_config.json +69 -0
- vocab.json +0 -0
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
|
|
|