Upload 4 files
Browse files- README.md +162 -3
- Sabaki-Dense.pt +3 -0
- infer.py +670 -0
- tokenizer.json +0 -0
README.md
CHANGED
|
@@ -1,3 +1,162 @@
|
|
| 1 |
-
---
|
| 2 |
-
license: cc-by-nc-4.0
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-nc-4.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
pipeline_tag: text-generation
|
| 6 |
+
tags:
|
| 7 |
+
- causal-lm
|
| 8 |
+
- dense
|
| 9 |
+
- recurrent
|
| 10 |
+
- custom-architecture
|
| 11 |
+
- preview
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# Sabaki Preview
|
| 15 |
+
|
| 16 |
+
Sabaki is an experimental decoder-only language model implemented in PyTorch. It combines recurrent Kimi Delta Attention (KDA), periodic gated rotary attention, and dense SiTU-GLU feed-forward layers.
|
| 17 |
+
|
| 18 |
+
This post-trained model is a research preview for local experiments, evaluation, and further post-training. Instruction following and factual accuracy can be uneven.
|
| 19 |
+
|
| 20 |
+
## At a glance
|
| 21 |
+
|
| 22 |
+
| Property | Value |
|
| 23 |
+
| --- | --- |
|
| 24 |
+
| Model parameters, excluding embeddings | **124,128,144 (124.1M)** |
|
| 25 |
+
| Active parameters per token, excluding embeddings | **124,128,144 (124.1M)**; dense feed-forward layers throughout |
|
| 26 |
+
| Tied embedding matrix, counted separately | 37,748,736 parameters; shared by token input and vocabulary output |
|
| 27 |
+
| Layers | 16: 12 recurrent KDA layers and 4 global-attention layers |
|
| 28 |
+
| Hidden size | 768 |
|
| 29 |
+
| Heads | 12 heads of width 64 |
|
| 30 |
+
| Feed-forward layers | Dense SiTU-GLU, intermediate width 2,048 in every block |
|
| 31 |
+
| Vocabulary | 49,152-token lossless byte-level BPE |
|
| 32 |
+
| Configured context | 16,384 tokens |
|
| 33 |
+
| Implementation | Custom PyTorch model with FP32 weights; BF16 training compute |
|
| 34 |
+
| License | CC BY-NC 4.0 |
|
| 35 |
+
|
| 36 |
+
The 16,384-token context is a configuration limit, not a measured long-context capability. The pretraining continuation used 2,048-token sequences, and the post-training checkpoints used 4,096-token sequences. Reliable behavior beyond those training lengths has not been established.
|
| 37 |
+
|
| 38 |
+
## Model design
|
| 39 |
+
|
| 40 |
+
The model repeats three KDA blocks followed by one gated rotary-attention block:
|
| 41 |
+
|
| 42 |
+
| Blocks (one-based) | Sequence mixer | Feed-forward path |
|
| 43 |
+
| --- | --- | --- |
|
| 44 |
+
| 1-3, 5-7, 9-11, 13-15 | Kimi Delta Attention | Dense SiTU-GLU, width 2,048 |
|
| 45 |
+
| 4, 8, 12, 16 | Gated causal multi-head attention with RoPE | Dense SiTU-GLU, width 2,048 |
|
| 46 |
+
|
| 47 |
+
Each block applies RMSNorm before its sequence mixer and before its feed-forward layer, with an ordinary residual addition after each. A final RMSNorm precedes the tied vocabulary projection. RMSNorm epsilon is `1e-6`.
|
| 48 |
+
|
| 49 |
+
### Recurrent KDA layers
|
| 50 |
+
|
| 51 |
+
KDA projects queries, keys, and values at the full 768-channel width and applies separate causal depthwise convolutions with kernel size 4. Queries and keys are normalized before the delta-rule recurrence. A rank-64 projection produces input-dependent per-channel decay, and a sigmoid write gate controls state updates. Each head maintains a 64 x 64 recurrent matrix, plus short convolution histories. The recurrent output is normalized, multiplied by a learned sigmoid output gate, and projected back to the residual stream.
|
| 52 |
+
|
| 53 |
+
These layers carry fixed-size state as the sequence grows. The four global-attention layers still retain a conventional KV cache, so the complete model does not have a constant-size cache.
|
| 54 |
+
|
| 55 |
+
### Gated rotary attention
|
| 56 |
+
|
| 57 |
+
The global layers use full-width Q, K, and V projections with 12 query/key/value heads of width 64. RoPE is applied to Q and K with base `10,000`, followed by causal scaled dot-product attention. A learned sigmoid gate modulates the merged heads before the output projection. These are standard full-head attention layers, not latent-compressed MLA.
|
| 58 |
+
|
| 59 |
+
### Dense SiTU-GLU feed-forward layers
|
| 60 |
+
|
| 61 |
+
Every block projects the normalized hidden state into two 2,048-wide branches, applies the bounded SiTU gate and up activations, multiplies them, and projects back to width 768. With gate preactivation `g` and up preactivation `u`, the activation is:
|
| 62 |
+
|
| 63 |
+
```text
|
| 64 |
+
(4 * tanh(g / 4) * sigmoid(g)) * (25 * tanh(u / 25))
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
There are no routed experts, shared experts, routers, or expert-balancing biases. The dense checkpoints are incompatible with the older MoE architecture.
|
| 68 |
+
|
| 69 |
+
| Component | Parameters, excluding embeddings |
|
| 70 |
+
| --- | ---: |
|
| 71 |
+
| 12 KDA mixers, including their internal normalization | 36,808,848 |
|
| 72 |
+
| 4 gated rotary-attention mixers | 11,796,480 |
|
| 73 |
+
| 16 dense feed-forward layers | 75,497,472 |
|
| 74 |
+
| Block and final RMSNorm scales | 25,344 |
|
| 75 |
+
| **Total** | **124,128,144** |
|
| 76 |
+
|
| 77 |
+
## Training
|
| 78 |
+
|
| 79 |
+
| Stage | Training tokens |
|
| 80 |
+
| --- | --- |
|
| 81 |
+
| Pretraining | 16B tokens |
|
| 82 |
+
| Supervised fine-tuning (SFT) | About 200M assistant-target tokens |
|
| 83 |
+
| Further grounded-chat SFT | About 165K assistant-target tokens |
|
| 84 |
+
|
| 85 |
+
The completed checkpoints contain FP32 model tensors and no optimizer state. Training used BF16 compute.
|
| 86 |
+
|
| 87 |
+
## Running with PyTorch
|
| 88 |
+
|
| 89 |
+
Use Sabaki's custom PyTorch implementation with the matching tokenizer and trained weights. Generation runs the model in evaluation mode with gradients disabled, processes the prompt once, and then uses cached recurrent states and attention keys/values to generate subsequent tokens.
|
| 90 |
+
|
| 91 |
+
The prompt serializer uses dedicated control-token IDs. A single-turn prompt has this structure:
|
| 92 |
+
|
| 93 |
+
```text
|
| 94 |
+
<|bos|><|user|>Explain why the sky appears blue.<|user_end|><|assistant|>
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
Assistant answers use `<|final|>...<|final_end|><|assistant_end|>`, with `<|eos|>` closing a training episode. Use the tokenizer's control-token serialization to construct this prompt. Plain `USER:` / `ASSISTANT:` labels are not the trained protocol.
|
| 98 |
+
|
| 99 |
+
The PyTorch generation defaults are temperature `0.2`, top-p `0.95`, and a maximum of `128` new tokens. Generation stops at `<|final_end|>` or `<|eos|>`, or when the token budget is exhausted. Keep the prompt and generated continuation within the configured context length.
|
| 100 |
+
|
| 101 |
+
Always set a generation limit.
|
| 102 |
+
|
| 103 |
+
## Evaluation
|
| 104 |
+
|
| 105 |
+
These scores describe **Sabaki after the 200M-token SFT stage**. The later grounded chat stage has a separate evaluation below.
|
| 106 |
+
|
| 107 |
+
| Benchmark | Questions | Zero-shot normalized accuracy |
|
| 108 |
+
| --- | ---: | ---: |
|
| 109 |
+
| HellaSwag | 10,042 | 36.85% |
|
| 110 |
+
| ARC-Easy | 2,376 | 47.05% |
|
| 111 |
+
| ARC-Challenge | 1,172 | 28.75% |
|
| 112 |
+
| PIQA | 1,838 | 65.83% |
|
| 113 |
+
| ArithMark-3 | 1,000 | 36.00% |
|
| 114 |
+
|
| 115 |
+
These are local evaluation results, not a verified leaderboard submission.
|
| 116 |
+
|
| 117 |
+
The later grounded-chat checkpoint has a separate assistant-token evaluation: held-out NLL 2.4488 and perplexity 11.5746 across 50 conversations and 3,703 assistant tokens. This does not replace a five-benchmark evaluation of those weights.
|
| 118 |
+
|
| 119 |
+
### Intelligence Index comparison
|
| 120 |
+
|
| 121 |
+
Sabaki scores **20.33** on the Open SLM Leaderboard's Intelligence Index (INT), using the full benchmark evaluation of the 200M token SFT stage. The index adjusts each benchmark for chance performance, averages ARC-Easy and ARC-Challenge into a single ARC component, and weights HellaSwag, ARC, and PIQA at 1.0 each and ArithMark-3 at 0.65.
|
| 122 |
+
|
| 123 |
+
The following comparison uses the [Open SLM Leaderboard](https://huggingface.co/spaces/AxiomicLabs/Open_SLM_Leaderboard) scores and formula checked on 12 October 2026. Higher is better.
|
| 124 |
+
|
| 125 |
+
| Model | INT score |
|
| 126 |
+
| --- | ---: |
|
| 127 |
+
| GPT-X3-150M | 33.05 |
|
| 128 |
+
| SmolLM2-135M | 27.13 |
|
| 129 |
+
| MobileLLM-R1-140M-base | 24.64 |
|
| 130 |
+
| GPT-X2-125M | 23.36 |
|
| 131 |
+
| JugnuLM-110M-R2+ | 20.39 |
|
| 132 |
+
| **Sabaki** | **20.33** |
|
| 133 |
+
| Speck2-140M | 20.02 |
|
| 134 |
+
| GPT-X-125M | 19.94 |
|
| 135 |
+
| GTM-v2-chat | 15.36 |
|
| 136 |
+
| OPT-125M | 13.80 |
|
| 137 |
+
| GPT-2 | 13.58 |
|
| 138 |
+
|
| 139 |
+
Sabaki is numerically close to JugnuLM-110M-R2+, slightly ahead of Speck2-140M and GPT-X-125M, and above GPT-2 and OPT-125M. It trails SmolLM2-135M by 6.80 INT points. Small differences should not be interpreted as established performance advantages.
|
| 140 |
+
|
| 141 |
+
By score alone, Sabaki would sit 21st among the 210 existing entries with all five benchmark scores checked on that date. This is an unofficial comparison using local PyTorch results; it does not establish leaderboard eligibility or a verified placement. The later grounded-chat checkpoint has no recorded full INT evaluation.
|
| 142 |
+
|
| 143 |
+
## Limitations
|
| 144 |
+
|
| 145 |
+
This preview can produce incorrect facts, invented citations, repetitive text, and unreliable instructions. Languages other than English have not been evaluated separately. Protocol tokens for tools and other structured outputs do not establish reliable tool-use capabilities. The model has not received comprehensive safety evaluation or preference optimization.
|
| 146 |
+
|
| 147 |
+
## License
|
| 148 |
+
|
| 149 |
+
The model weights are available under the **Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0)** license. Attribution is required, and commercial use is not permitted. Software retains its own license.
|
| 150 |
+
|
| 151 |
+
## Citation
|
| 152 |
+
|
| 153 |
+
If this preview helps your research, please cite the model repository. A formal citation can be added with a full release.
|
| 154 |
+
|
| 155 |
+
```bibtex
|
| 156 |
+
@software{sabaki_preview_2026,
|
| 157 |
+
author = {{Local-Axiom-AI}},
|
| 158 |
+
title = {Sabaki Preview},
|
| 159 |
+
year = {2026},
|
| 160 |
+
note = {A compact hybrid recurrent and gated rotary-attention language model with dense SiTU feed-forward layers}
|
| 161 |
+
}
|
| 162 |
+
```
|
Sabaki-Dense.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:95791057dbd96359f86cae6ece6091f587083fdb35de41a590777a2d485f6815
|
| 3 |
+
size 647584403
|
infer.py
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Standalone Sabaki Dense inference: Python 3.11+ and PyTorch 2.8+.
|
| 2 |
+
|
| 3 |
+
Keep this script, a regular Sabaki Dense checkpoint, and tokenizer.json together.
|
| 4 |
+
No repository package, Transformers, Triton, or Flash Linear Attention required.
|
| 5 |
+
|
| 6 |
+
Examples:
|
| 7 |
+
python infer.py --checkpoint chat-grounded-500.pt --prompt "Why is the sky blue?"
|
| 8 |
+
python infer.py --checkpoint unc-chat.pt --prompt "Hello!" --device cpu
|
| 9 |
+
python infer.py --checkpoint dense-pretrain-sixday.pt --raw --prompt "The sky is"
|
| 10 |
+
|
| 11 |
+
This reference implementation favors portability over optimized prefill speed.
|
| 12 |
+
Checkpoint format identifiers retain their original spelling for compatibility.
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
from dataclasses import asdict, dataclass
|
| 18 |
+
import heapq
|
| 19 |
+
import json
|
| 20 |
+
import math
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
import sys
|
| 23 |
+
from typing import Any, Iterable, Sequence
|
| 24 |
+
|
| 25 |
+
import torch
|
| 26 |
+
from torch import Tensor, nn
|
| 27 |
+
import torch.nn.functional as F
|
| 28 |
+
|
| 29 |
+
@dataclass
|
| 30 |
+
class SabakiConfig:
|
| 31 |
+
"""Architecture configuration.
|
| 32 |
+
|
| 33 |
+
``canonical()`` uses dense FFNs and head-width rotary attention. ``tiny()`` keeps
|
| 34 |
+
the same block types and dataflow but is intended for tests and examples.
|
| 35 |
+
"""
|
| 36 |
+
|
| 37 |
+
architecture_version: str = "kurokiba-dense-v1"
|
| 38 |
+
vocab_size: int = 49_152
|
| 39 |
+
d_model: int = 768
|
| 40 |
+
num_layers: int = 16
|
| 41 |
+
num_heads: int = 12
|
| 42 |
+
head_dim: int = 64
|
| 43 |
+
context_length: int = 16_384
|
| 44 |
+
conv_kernel_size: int = 4
|
| 45 |
+
kda_chunk_size: int = 64
|
| 46 |
+
decay_rank: int = 64
|
| 47 |
+
decay_min_log: float = -5.0
|
| 48 |
+
rope_theta: float = 10_000.0
|
| 49 |
+
dense_hidden_dim: int = 2_048
|
| 50 |
+
rms_norm_eps: float = 1e-6
|
| 51 |
+
situ_gate_cap: float = 4.0
|
| 52 |
+
situ_up_cap: float = 25.0
|
| 53 |
+
initializer_std: float = 0.02
|
| 54 |
+
init_seed: int = 20_260_729
|
| 55 |
+
|
| 56 |
+
def __post_init__(self) -> None:
|
| 57 |
+
if self.architecture_version != "kurokiba-dense-v1":
|
| 58 |
+
raise ValueError("unsupported architecture; initialize a new kurokiba-dense-v1 model")
|
| 59 |
+
if self.d_model != self.num_heads * self.head_dim:
|
| 60 |
+
raise ValueError("d_model must equal num_heads * head_dim")
|
| 61 |
+
if min(self.vocab_size, self.d_model, self.num_heads, self.decay_rank, self.conv_kernel_size) < 1:
|
| 62 |
+
raise ValueError("model, vocabulary, convolution, and decay dimensions must be positive")
|
| 63 |
+
if self.num_layers < 1:
|
| 64 |
+
raise ValueError("num_layers must be positive")
|
| 65 |
+
if self.head_dim < 2 or self.head_dim % 2:
|
| 66 |
+
raise ValueError("head_dim must be positive and even for RoPE")
|
| 67 |
+
if not math.isfinite(self.rope_theta) or self.rope_theta <= 1.0:
|
| 68 |
+
raise ValueError("rope_theta must be finite and greater than one")
|
| 69 |
+
if self.dense_hidden_dim < 1:
|
| 70 |
+
raise ValueError("dense_hidden_dim must be positive")
|
| 71 |
+
if self.context_length < 1:
|
| 72 |
+
raise ValueError("context_length must be positive")
|
| 73 |
+
if self.kda_chunk_size < 1:
|
| 74 |
+
raise ValueError("kda_chunk_size must be positive")
|
| 75 |
+
|
| 76 |
+
@classmethod
|
| 77 |
+
def canonical(cls) -> "SabakiConfig":
|
| 78 |
+
return cls()
|
| 79 |
+
|
| 80 |
+
@classmethod
|
| 81 |
+
def tiny(cls) -> "SabakiConfig":
|
| 82 |
+
return cls(
|
| 83 |
+
vocab_size=512,
|
| 84 |
+
d_model=64,
|
| 85 |
+
num_layers=4,
|
| 86 |
+
num_heads=4,
|
| 87 |
+
head_dim=16,
|
| 88 |
+
context_length=256,
|
| 89 |
+
conv_kernel_size=4,
|
| 90 |
+
kda_chunk_size=8,
|
| 91 |
+
decay_rank=16,
|
| 92 |
+
dense_hidden_dim=128,
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
@property
|
| 96 |
+
def attention_types(self) -> tuple[str, ...]:
|
| 97 |
+
# Every fourth layer is global rotary attention; the last layer is always global.
|
| 98 |
+
return tuple(
|
| 99 |
+
"attention" if (index + 1) % 4 == 0 or index == self.num_layers - 1 else "kda"
|
| 100 |
+
for index in range(self.num_layers)
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
@property
|
| 104 |
+
def residual_std(self) -> float:
|
| 105 |
+
return self.initializer_std / (2 * self.num_layers) ** 0.5
|
| 106 |
+
|
| 107 |
+
def to_dict(self) -> dict[str, Any]:
|
| 108 |
+
return asdict(self)
|
| 109 |
+
|
| 110 |
+
@classmethod
|
| 111 |
+
def from_dict(cls, values: dict[str, Any]) -> "SabakiConfig":
|
| 112 |
+
if values.get("architecture_version") != "kurokiba-dense-v1" or "attnres_group_size" in values:
|
| 113 |
+
raise ValueError("MoE/legacy config is incompatible with dense-v1; initialize new dense weights")
|
| 114 |
+
return cls(**values)
|
| 115 |
+
|
| 116 |
+
def save(self, path: str | Path) -> None:
|
| 117 |
+
Path(path).write_text(
|
| 118 |
+
json.dumps(self.to_dict(), indent=2, sort_keys=True) + "\n",
|
| 119 |
+
encoding="utf-8",
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
@classmethod
|
| 123 |
+
def load(cls, path: str | Path) -> "SabakiConfig":
|
| 124 |
+
return cls.from_dict(json.loads(Path(path).read_text(encoding="utf-8")))
|
| 125 |
+
|
| 126 |
+
def parameter_count_formula(self) -> dict[str, int]:
|
| 127 |
+
"""Return structural parameter counts without allocating the model."""
|
| 128 |
+
d = self.d_model
|
| 129 |
+
h = self.num_heads
|
| 130 |
+
hd = self.head_dim
|
| 131 |
+
|
| 132 |
+
kda_per_layer = (
|
| 133 |
+
3 * d * d
|
| 134 |
+
+ 3 * d * self.conv_kernel_size
|
| 135 |
+
+ self.decay_rank * d
|
| 136 |
+
+ d * self.decay_rank
|
| 137 |
+
+ h * hd
|
| 138 |
+
+ h
|
| 139 |
+
+ h * d
|
| 140 |
+
+ h * hd
|
| 141 |
+
+ 2 * d * d
|
| 142 |
+
)
|
| 143 |
+
attention_per_layer = 5 * d * d # fused QKV, output, output gate
|
| 144 |
+
dense = self.num_layers * 3 * d * self.dense_hidden_dim
|
| 145 |
+
kda_layers = self.attention_types.count("kda")
|
| 146 |
+
attention_layers = self.attention_types.count("attention")
|
| 147 |
+
norms = (2 * self.num_layers + 1) * d
|
| 148 |
+
embedding = self.vocab_size * d
|
| 149 |
+
non_embedding = kda_layers * kda_per_layer + attention_layers * attention_per_layer + dense + norms
|
| 150 |
+
return {
|
| 151 |
+
"embedding": embedding,
|
| 152 |
+
"kda": kda_layers * kda_per_layer,
|
| 153 |
+
"attention": attention_layers * attention_per_layer,
|
| 154 |
+
"dense_ffn": dense,
|
| 155 |
+
"module_and_final_norms": norms,
|
| 156 |
+
"trainable": embedding + non_embedding,
|
| 157 |
+
"persistent": embedding + non_embedding,
|
| 158 |
+
"active_non_embedding": non_embedding,
|
| 159 |
+
"active_with_embedding": embedding + non_embedding,
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
CONTROL_TOKENS: tuple[str, ...] = (
|
| 164 |
+
"<|pad|>",
|
| 165 |
+
"<|bos|>",
|
| 166 |
+
"<|eos|>",
|
| 167 |
+
"<|system|>",
|
| 168 |
+
"<|system_end|>",
|
| 169 |
+
"<|developer|>",
|
| 170 |
+
"<|developer_end|>",
|
| 171 |
+
"<|user|>",
|
| 172 |
+
"<|user_end|>",
|
| 173 |
+
"<|assistant|>",
|
| 174 |
+
"<|assistant_end|>",
|
| 175 |
+
"<|tool_schema|>",
|
| 176 |
+
"<|tool_schema_end|>",
|
| 177 |
+
"<|tool_call|>",
|
| 178 |
+
"<|tool_call_end|>",
|
| 179 |
+
"<|parallel_tool_calls|>",
|
| 180 |
+
"<|parallel_tool_calls_end|>",
|
| 181 |
+
"<|tool_result|>",
|
| 182 |
+
"<|tool_result_end|>",
|
| 183 |
+
"<|tool_error|>",
|
| 184 |
+
"<|tool_error_end|>",
|
| 185 |
+
"<|final|>",
|
| 186 |
+
"<|final_end|>",
|
| 187 |
+
"<|memory|>",
|
| 188 |
+
"<|memory_end|>",
|
| 189 |
+
"<|confirmation_required|>",
|
| 190 |
+
"<|confirmation_required_end|>",
|
| 191 |
+
"<|confirmation_granted|>",
|
| 192 |
+
"<|confirmation_denied|>",
|
| 193 |
+
"<|citation|>",
|
| 194 |
+
"<|citation_end|>",
|
| 195 |
+
"<|observation|>",
|
| 196 |
+
"<|observation_end|>",
|
| 197 |
+
"<|sep|>",
|
| 198 |
+
"<|json|>",
|
| 199 |
+
"<|json_end|>",
|
| 200 |
+
"<|cancelled|>",
|
| 201 |
+
"<|truncated|>",
|
| 202 |
+
"<|unknown|>",
|
| 203 |
+
"<|tool_denied|>",
|
| 204 |
+
"<|tool_denied_end|>",
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
RESERVED_TOKEN_COUNT = 256
|
| 208 |
+
BYTE_TOKEN_OFFSET = 256
|
| 209 |
+
BASE_VOCAB_SIZE = RESERVED_TOKEN_COUNT + 256
|
| 210 |
+
CANONICAL_VOCAB_SIZE = 49_152
|
| 211 |
+
CANONICAL_MERGE_COUNT = CANONICAL_VOCAB_SIZE - BASE_VOCAB_SIZE
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
class ByteBPETokenizer:
|
| 215 |
+
"""Lossless UTF-8 byte BPE with an isolated reserved-token namespace.
|
| 216 |
+
|
| 217 |
+
Ordinary ``encode`` never recognizes control-token-looking text. Trusted
|
| 218 |
+
callers must insert controls explicitly with ``control_id`` or
|
| 219 |
+
``encode_parts``.
|
| 220 |
+
"""
|
| 221 |
+
|
| 222 |
+
def __init__(
|
| 223 |
+
self,
|
| 224 |
+
merges: Sequence[tuple[int, int]] = (),
|
| 225 |
+
*,
|
| 226 |
+
vocab_size: int = CANONICAL_VOCAB_SIZE,
|
| 227 |
+
) -> None:
|
| 228 |
+
if vocab_size < BASE_VOCAB_SIZE + len(merges):
|
| 229 |
+
raise ValueError("vocab_size is too small for the supplied merges")
|
| 230 |
+
self.vocab_size = vocab_size
|
| 231 |
+
self.merges = tuple((int(left), int(right)) for left, right in merges)
|
| 232 |
+
self._merge_rank: dict[tuple[int, int], int] = {}
|
| 233 |
+
self._merge_id: dict[tuple[int, int], int] = {}
|
| 234 |
+
self._symbols: dict[int, bytes] = {
|
| 235 |
+
BYTE_TOKEN_OFFSET + value: bytes((value,)) for value in range(256)
|
| 236 |
+
}
|
| 237 |
+
for rank, pair in enumerate(self.merges):
|
| 238 |
+
token_id = BASE_VOCAB_SIZE + rank
|
| 239 |
+
if pair in self._merge_rank:
|
| 240 |
+
raise ValueError(f"duplicate merge pair at rank {rank}: {pair}")
|
| 241 |
+
if pair[0] not in self._symbols or pair[1] not in self._symbols:
|
| 242 |
+
raise ValueError(f"merge {pair} references an unknown token")
|
| 243 |
+
self._merge_rank[pair] = rank
|
| 244 |
+
self._merge_id[pair] = token_id
|
| 245 |
+
self._symbols[token_id] = self._symbols[pair[0]] + self._symbols[pair[1]]
|
| 246 |
+
self._control_to_id = {
|
| 247 |
+
token: index for index, token in enumerate(CONTROL_TOKENS)
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
@property
|
| 251 |
+
def trained_vocab_size(self) -> int:
|
| 252 |
+
return BASE_VOCAB_SIZE + len(self.merges)
|
| 253 |
+
|
| 254 |
+
def control_id(self, token: str) -> int:
|
| 255 |
+
try:
|
| 256 |
+
return self._control_to_id[token]
|
| 257 |
+
except KeyError as exc:
|
| 258 |
+
raise KeyError(f"unknown control token: {token}") from exc
|
| 259 |
+
|
| 260 |
+
def encode_bytes(self, value: bytes) -> list[int]:
|
| 261 |
+
tokens = [BYTE_TOKEN_OFFSET + byte for byte in value]
|
| 262 |
+
if len(tokens) < 2 or not self._merge_rank:
|
| 263 |
+
return tokens
|
| 264 |
+
|
| 265 |
+
length = len(tokens)
|
| 266 |
+
previous = [index - 1 for index in range(length)]
|
| 267 |
+
following = [index + 1 for index in range(length)]
|
| 268 |
+
following[-1] = -1
|
| 269 |
+
alive = [True] * length
|
| 270 |
+
candidates: list[tuple[int, int, int, int]] = []
|
| 271 |
+
|
| 272 |
+
def add_candidate(left_index: int) -> None:
|
| 273 |
+
if left_index < 0 or not alive[left_index]:
|
| 274 |
+
return
|
| 275 |
+
right_index = following[left_index]
|
| 276 |
+
if right_index < 0 or not alive[right_index]:
|
| 277 |
+
return
|
| 278 |
+
pair = (tokens[left_index], tokens[right_index])
|
| 279 |
+
rank = self._merge_rank.get(pair)
|
| 280 |
+
if rank is not None:
|
| 281 |
+
heapq.heappush(
|
| 282 |
+
candidates,
|
| 283 |
+
(rank, left_index, pair[0], pair[1]),
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
for index in range(length - 1):
|
| 287 |
+
add_candidate(index)
|
| 288 |
+
|
| 289 |
+
while candidates:
|
| 290 |
+
rank, left_index, expected_left, expected_right = heapq.heappop(
|
| 291 |
+
candidates
|
| 292 |
+
)
|
| 293 |
+
if not alive[left_index] or tokens[left_index] != expected_left:
|
| 294 |
+
continue
|
| 295 |
+
right_index = following[left_index]
|
| 296 |
+
if (
|
| 297 |
+
right_index < 0
|
| 298 |
+
or not alive[right_index]
|
| 299 |
+
or tokens[right_index] != expected_right
|
| 300 |
+
):
|
| 301 |
+
continue
|
| 302 |
+
|
| 303 |
+
tokens[left_index] = BASE_VOCAB_SIZE + rank
|
| 304 |
+
alive[right_index] = False
|
| 305 |
+
next_index = following[right_index]
|
| 306 |
+
following[left_index] = next_index
|
| 307 |
+
if next_index >= 0:
|
| 308 |
+
previous[next_index] = left_index
|
| 309 |
+
add_candidate(previous[left_index])
|
| 310 |
+
add_candidate(left_index)
|
| 311 |
+
|
| 312 |
+
encoded: list[int] = []
|
| 313 |
+
index = 0
|
| 314 |
+
while index >= 0:
|
| 315 |
+
if alive[index]:
|
| 316 |
+
encoded.append(tokens[index])
|
| 317 |
+
index = following[index]
|
| 318 |
+
return encoded
|
| 319 |
+
|
| 320 |
+
def encode(self, text: str) -> list[int]:
|
| 321 |
+
return self.encode_bytes(text.encode("utf-8"))
|
| 322 |
+
|
| 323 |
+
def encode_parts(self, parts: Iterable[str | tuple[str, str]]) -> list[int]:
|
| 324 |
+
"""Encode trusted controls and ordinary text without token injection.
|
| 325 |
+
|
| 326 |
+
A plain string is always ordinary text. A ``("control", token)`` tuple
|
| 327 |
+
explicitly inserts one reserved token.
|
| 328 |
+
"""
|
| 329 |
+
token_ids: list[int] = []
|
| 330 |
+
for part in parts:
|
| 331 |
+
if isinstance(part, str):
|
| 332 |
+
token_ids.extend(self.encode(part))
|
| 333 |
+
elif len(part) == 2 and part[0] == "control":
|
| 334 |
+
token_ids.append(self.control_id(part[1]))
|
| 335 |
+
else:
|
| 336 |
+
raise ValueError(f"invalid tokenizer part: {part!r}")
|
| 337 |
+
return token_ids
|
| 338 |
+
|
| 339 |
+
def decode_bytes(self, token_ids: Iterable[int], *, skip_control: bool = False) -> bytes:
|
| 340 |
+
pieces: list[bytes] = []
|
| 341 |
+
for token_id in token_ids:
|
| 342 |
+
token_id = int(token_id)
|
| 343 |
+
if 0 <= token_id < RESERVED_TOKEN_COUNT:
|
| 344 |
+
if skip_control:
|
| 345 |
+
continue
|
| 346 |
+
if token_id >= len(CONTROL_TOKENS):
|
| 347 |
+
raise ValueError(f"reserved token {token_id} has no display string")
|
| 348 |
+
pieces.append(CONTROL_TOKENS[token_id].encode("ascii"))
|
| 349 |
+
else:
|
| 350 |
+
try:
|
| 351 |
+
pieces.append(self._symbols[token_id])
|
| 352 |
+
except KeyError as exc:
|
| 353 |
+
raise ValueError(
|
| 354 |
+
f"token {token_id} is not defined by this tokenizer"
|
| 355 |
+
) from exc
|
| 356 |
+
return b"".join(pieces)
|
| 357 |
+
|
| 358 |
+
def decode(
|
| 359 |
+
self,
|
| 360 |
+
token_ids: Iterable[int],
|
| 361 |
+
*,
|
| 362 |
+
errors: str = "strict",
|
| 363 |
+
skip_control: bool = False,
|
| 364 |
+
) -> str:
|
| 365 |
+
return self.decode_bytes(token_ids, skip_control=skip_control).decode(
|
| 366 |
+
"utf-8", errors=errors
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
def to_dict(self) -> dict[str, object]:
|
| 370 |
+
return {
|
| 371 |
+
"version": 1,
|
| 372 |
+
"type": "lossless_byte_bpe",
|
| 373 |
+
"vocab_size": self.vocab_size,
|
| 374 |
+
"reserved_token_count": RESERVED_TOKEN_COUNT,
|
| 375 |
+
"control_tokens": list(CONTROL_TOKENS),
|
| 376 |
+
"merges": [list(pair) for pair in self.merges],
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
def save(self, path: str | Path) -> None:
|
| 380 |
+
Path(path).write_text(
|
| 381 |
+
json.dumps(self.to_dict(), ensure_ascii=False, indent=2) + "\n",
|
| 382 |
+
encoding="utf-8",
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
@classmethod
|
| 386 |
+
def load(cls, path: str | Path) -> "ByteBPETokenizer":
|
| 387 |
+
payload = json.loads(Path(path).read_text(encoding="utf-8"))
|
| 388 |
+
if payload.get("reserved_token_count") != RESERVED_TOKEN_COUNT:
|
| 389 |
+
raise ValueError("tokenizer uses an incompatible reserved-token range")
|
| 390 |
+
if tuple(payload.get("control_tokens", ())) != CONTROL_TOKENS:
|
| 391 |
+
raise ValueError("tokenizer control-token map is incompatible")
|
| 392 |
+
return cls(
|
| 393 |
+
[tuple(pair) for pair in payload["merges"]],
|
| 394 |
+
vocab_size=int(payload["vocab_size"]),
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
|
| 398 |
+
class RMSNorm(nn.Module):
|
| 399 |
+
def __init__(self, shape, eps):
|
| 400 |
+
super().__init__()
|
| 401 |
+
self.weight = nn.Parameter(torch.ones(shape))
|
| 402 |
+
self.eps = eps
|
| 403 |
+
|
| 404 |
+
def forward(self, x):
|
| 405 |
+
return F.rms_norm(x, self.weight.shape, self.weight, self.eps)
|
| 406 |
+
|
| 407 |
+
|
| 408 |
+
class CausalDepthwiseConv(nn.Module):
|
| 409 |
+
def __init__(self, width, kernel):
|
| 410 |
+
super().__init__()
|
| 411 |
+
self.weight = nn.Parameter(torch.empty(width, kernel))
|
| 412 |
+
|
| 413 |
+
def forward(self, x, history):
|
| 414 |
+
history_length = history.shape[-1]
|
| 415 |
+
extended = torch.cat((history.flip(-1).transpose(1, 2), x), dim=1)
|
| 416 |
+
result = x * self.weight[:, 0]
|
| 417 |
+
for tap in range(1, self.weight.shape[1]):
|
| 418 |
+
start = history_length - tap
|
| 419 |
+
result = result + extended[:, start:start + x.shape[1]] * self.weight[:, tap]
|
| 420 |
+
newest = torch.cat((x.flip(1).transpose(1, 2), history), dim=-1)
|
| 421 |
+
return result, newest[..., :history_length]
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
class KDA(nn.Module):
|
| 425 |
+
def __init__(self, cfg):
|
| 426 |
+
super().__init__()
|
| 427 |
+
self.config = cfg
|
| 428 |
+
d, h, hd = cfg.d_model, cfg.num_heads, cfg.head_dim
|
| 429 |
+
self.q_proj = nn.Linear(d, d, bias=False)
|
| 430 |
+
self.k_proj = nn.Linear(d, d, bias=False)
|
| 431 |
+
self.v_proj = nn.Linear(d, d, bias=False)
|
| 432 |
+
self.q_conv = CausalDepthwiseConv(d, cfg.conv_kernel_size)
|
| 433 |
+
self.k_conv = CausalDepthwiseConv(d, cfg.conv_kernel_size)
|
| 434 |
+
self.v_conv = CausalDepthwiseConv(d, cfg.conv_kernel_size)
|
| 435 |
+
self.alpha_down = nn.Linear(d, cfg.decay_rank, bias=False)
|
| 436 |
+
self.alpha_up = nn.Linear(cfg.decay_rank, d, bias=False)
|
| 437 |
+
self.alpha_bias = nn.Parameter(torch.empty(h, hd))
|
| 438 |
+
self.alpha_log_scale = nn.Parameter(torch.empty(h))
|
| 439 |
+
self.beta_proj = nn.Linear(d, h, bias=False)
|
| 440 |
+
self.head_norm = RMSNorm((h, hd), cfg.rms_norm_eps)
|
| 441 |
+
self.output_gate = nn.Linear(d, d, bias=False)
|
| 442 |
+
self.out_proj = nn.Linear(d, d, bias=False)
|
| 443 |
+
|
| 444 |
+
def forward(self, x, state=None):
|
| 445 |
+
cfg = self.config
|
| 446 |
+
batch, length, _ = x.shape
|
| 447 |
+
if state is None:
|
| 448 |
+
recurrent = torch.zeros(batch, cfg.num_heads, cfg.head_dim, cfg.head_dim,
|
| 449 |
+
dtype=torch.float32, device=x.device)
|
| 450 |
+
histories = [x.new_zeros(batch, cfg.d_model, cfg.conv_kernel_size - 1) for _ in range(3)]
|
| 451 |
+
else:
|
| 452 |
+
recurrent = state[0].float()
|
| 453 |
+
histories = [item.to(dtype=x.dtype) for item in state[1:]]
|
| 454 |
+
mixed, new_histories = [], []
|
| 455 |
+
for projection, convolution, history in zip(
|
| 456 |
+
(self.q_proj, self.k_proj, self.v_proj),
|
| 457 |
+
(self.q_conv, self.k_conv, self.v_conv), histories
|
| 458 |
+
):
|
| 459 |
+
value, history = convolution(projection(x), history)
|
| 460 |
+
mixed.append(F.silu(value).view(batch, length, cfg.num_heads, cfg.head_dim).float())
|
| 461 |
+
new_histories.append(history)
|
| 462 |
+
q, k, v = mixed
|
| 463 |
+
q = q / torch.sqrt(q.square().sum(-1, keepdim=True) + cfg.rms_norm_eps)
|
| 464 |
+
k = k / torch.sqrt(k.square().sum(-1, keepdim=True) + cfg.rms_norm_eps)
|
| 465 |
+
decay = self.alpha_up(self.alpha_down(x)) + self.alpha_bias.flatten()
|
| 466 |
+
z = decay.view_as(q).float()
|
| 467 |
+
scale = self.alpha_log_scale.float().exp().view(1, 1, -1, 1)
|
| 468 |
+
alpha = (cfg.decay_min_log * torch.sigmoid(scale * z)).exp()
|
| 469 |
+
beta = self.beta_proj(x).sigmoid().float()
|
| 470 |
+
outputs = []
|
| 471 |
+
for index in range(length):
|
| 472 |
+
decayed = alpha[:, index].unsqueeze(-1) * recurrent
|
| 473 |
+
prediction = torch.matmul(decayed.transpose(-2, -1), k[:, index].unsqueeze(-1)).squeeze(-1)
|
| 474 |
+
error = v[:, index] - prediction
|
| 475 |
+
recurrent = decayed + beta[:, index, :, None, None] * (k[:, index].unsqueeze(-1) * error.unsqueeze(-2))
|
| 476 |
+
outputs.append(torch.matmul(recurrent.transpose(-2, -1), q[:, index].unsqueeze(-1)).squeeze(-1))
|
| 477 |
+
output = self.head_norm(torch.stack(outputs, dim=1).to(x.dtype)).flatten(2)
|
| 478 |
+
result = self.out_proj(self.output_gate(x).sigmoid() * output)
|
| 479 |
+
return result, (recurrent.to(x.dtype), *new_histories)
|
| 480 |
+
|
| 481 |
+
|
| 482 |
+
class RotaryAttention(nn.Module):
|
| 483 |
+
def __init__(self, cfg):
|
| 484 |
+
super().__init__()
|
| 485 |
+
self.config = cfg
|
| 486 |
+
self.qkv_proj = nn.Linear(cfg.d_model, 3 * cfg.d_model, bias=False)
|
| 487 |
+
self.out_proj = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
|
| 488 |
+
self.output_gate = nn.Linear(cfg.d_model, cfg.d_model, bias=False)
|
| 489 |
+
self._rope_tables = None
|
| 490 |
+
|
| 491 |
+
def rotary(self, x, offset):
|
| 492 |
+
cfg = self.config
|
| 493 |
+
if self._rope_tables is None or self._rope_tables[0].device != x.device:
|
| 494 |
+
frequencies = cfg.rope_theta ** (-torch.arange(0, cfg.head_dim, 2, device=x.device, dtype=torch.float32) / cfg.head_dim)
|
| 495 |
+
angles = torch.arange(cfg.context_length, device=x.device, dtype=torch.float32)[:, None] * frequencies[None, :]
|
| 496 |
+
self._rope_tables = angles.cos(), angles.sin()
|
| 497 |
+
cos, sin = (table[offset:offset + x.shape[2]][None, None] for table in self._rope_tables)
|
| 498 |
+
even, odd = x.float()[..., 0::2], x.float()[..., 1::2]
|
| 499 |
+
return torch.stack((even * cos - odd * sin, even * sin + odd * cos), dim=-1).flatten(-2).to(x.dtype)
|
| 500 |
+
|
| 501 |
+
def forward(self, x, state=None):
|
| 502 |
+
cfg = self.config
|
| 503 |
+
batch, length, _ = x.shape
|
| 504 |
+
q, k, v = self.qkv_proj(x).view(batch, length, 3, cfg.num_heads, cfg.head_dim).unbind(2)
|
| 505 |
+
q, k, v = (item.transpose(1, 2) for item in (q, k, v))
|
| 506 |
+
offset = 0 if state is None else state[0].shape[2]
|
| 507 |
+
q, k = self.rotary(q, offset), self.rotary(k, offset)
|
| 508 |
+
if state is not None:
|
| 509 |
+
k, v = torch.cat((state[0], k), dim=2), torch.cat((state[1], v), dim=2)
|
| 510 |
+
if offset and length > 1:
|
| 511 |
+
output = torch.cat([
|
| 512 |
+
F.scaled_dot_product_attention(q[:, :, i:i+1], k[:, :, :offset+i+1], v[:, :, :offset+i+1], dropout_p=0.0)
|
| 513 |
+
for i in range(length)
|
| 514 |
+
], dim=2)
|
| 515 |
+
else:
|
| 516 |
+
output = F.scaled_dot_product_attention(q, k, v, dropout_p=0.0, is_causal=offset == 0)
|
| 517 |
+
output = output.transpose(1, 2).reshape(batch, length, cfg.d_model)
|
| 518 |
+
return self.out_proj(self.output_gate(x).sigmoid() * output), (k, v)
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
class SiTUGLU(nn.Module):
|
| 522 |
+
def __init__(self, cfg):
|
| 523 |
+
super().__init__()
|
| 524 |
+
self.gate_up_proj = nn.Linear(cfg.d_model, 2 * cfg.dense_hidden_dim, bias=False)
|
| 525 |
+
self.out_proj = nn.Linear(cfg.dense_hidden_dim, cfg.d_model, bias=False)
|
| 526 |
+
self.gate_cap, self.up_cap = cfg.situ_gate_cap, cfg.situ_up_cap
|
| 527 |
+
|
| 528 |
+
def forward(self, x):
|
| 529 |
+
gate_pre, up_pre = self.gate_up_proj(x).chunk(2, dim=-1)
|
| 530 |
+
gate = self.gate_cap * torch.tanh(gate_pre / self.gate_cap)
|
| 531 |
+
gate = gate * torch.sigmoid(gate_pre)
|
| 532 |
+
up = self.up_cap * torch.tanh(up_pre / self.up_cap)
|
| 533 |
+
return self.out_proj(gate * up)
|
| 534 |
+
|
| 535 |
+
|
| 536 |
+
class Block(nn.Module):
|
| 537 |
+
def __init__(self, cfg, kind):
|
| 538 |
+
super().__init__()
|
| 539 |
+
self.attn_norm = RMSNorm(cfg.d_model, cfg.rms_norm_eps)
|
| 540 |
+
self.ffn_norm = RMSNorm(cfg.d_model, cfg.rms_norm_eps)
|
| 541 |
+
self.attn = KDA(cfg) if kind == "kda" else RotaryAttention(cfg)
|
| 542 |
+
self.ffn = SiTUGLU(cfg)
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
@dataclass
|
| 546 |
+
class Cache:
|
| 547 |
+
layers: list
|
| 548 |
+
length: int
|
| 549 |
+
|
| 550 |
+
|
| 551 |
+
class Sabaki(nn.Module):
|
| 552 |
+
def __init__(self, cfg):
|
| 553 |
+
super().__init__()
|
| 554 |
+
self.config = cfg
|
| 555 |
+
self.token_embedding = nn.Embedding(cfg.vocab_size, cfg.d_model)
|
| 556 |
+
self.blocks = nn.ModuleList(Block(cfg, kind) for kind in cfg.attention_types)
|
| 557 |
+
self.final_norm = RMSNorm(cfg.d_model, cfg.rms_norm_eps)
|
| 558 |
+
|
| 559 |
+
def forward(self, ids, cache=None, *, last_logits_only=True):
|
| 560 |
+
offset = 0 if cache is None else cache.length
|
| 561 |
+
if ids.ndim != 2 or ids.shape[1] < 1:
|
| 562 |
+
raise ValueError("Expected nonempty [batch, sequence] token IDs")
|
| 563 |
+
if offset + ids.shape[1] > self.config.context_length:
|
| 564 |
+
raise ValueError("Prompt and continuation exceed the context limit")
|
| 565 |
+
hidden = self.token_embedding(ids)
|
| 566 |
+
states = []
|
| 567 |
+
for index, block in enumerate(self.blocks):
|
| 568 |
+
attention, state = block.attn(block.attn_norm(hidden), None if cache is None else cache.layers[index])
|
| 569 |
+
hidden = hidden + attention
|
| 570 |
+
hidden = hidden + block.ffn(block.ffn_norm(hidden))
|
| 571 |
+
states.append(state)
|
| 572 |
+
hidden = self.final_norm(hidden)
|
| 573 |
+
logits = F.linear(hidden[:, -1:] if last_logits_only else hidden, self.token_embedding.weight)
|
| 574 |
+
return logits, Cache(states, offset + ids.shape[1])
|
| 575 |
+
|
| 576 |
+
|
| 577 |
+
def load_model(path, device, dtype):
|
| 578 |
+
payload = torch.load(path, map_location="cpu", weights_only=True, mmap=True)
|
| 579 |
+
if payload.get("format") != "kurokiba-dense-v1":
|
| 580 |
+
raise ValueError("Expected a regular Sabaki Dense checkpoint; Sabaki Max and MoE are unsupported")
|
| 581 |
+
cfg = SabakiConfig.from_dict(payload["config"])
|
| 582 |
+
with torch.device("meta"):
|
| 583 |
+
model = Sabaki(cfg)
|
| 584 |
+
model.load_state_dict(payload["model"], strict=True, assign=True)
|
| 585 |
+
return model.to(device=device, dtype=dtype).eval()
|
| 586 |
+
|
| 587 |
+
|
| 588 |
+
def sample(logits, temperature, top_p):
|
| 589 |
+
if temperature == 0:
|
| 590 |
+
return logits.argmax(-1)
|
| 591 |
+
probabilities = (logits.float() / temperature).softmax(-1)
|
| 592 |
+
if top_p < 1:
|
| 593 |
+
probabilities, indices = probabilities.sort(descending=True, dim=-1)
|
| 594 |
+
remove = probabilities.cumsum(-1) - probabilities >= top_p
|
| 595 |
+
probabilities = probabilities.masked_fill(remove, 0)
|
| 596 |
+
probabilities = probabilities / probabilities.sum(-1, keepdim=True)
|
| 597 |
+
return indices.gather(-1, torch.multinomial(probabilities, 1)).squeeze(-1)
|
| 598 |
+
return torch.multinomial(probabilities, 1).squeeze(-1)
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
def main():
|
| 602 |
+
directory = Path(__file__).resolve().parent
|
| 603 |
+
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 604 |
+
parser.add_argument("--checkpoint", type=Path, default=directory / "chat-grounded-500.pt")
|
| 605 |
+
parser.add_argument("--tokenizer", type=Path, default=directory / "tokenizer.json")
|
| 606 |
+
parser.add_argument("--prompt", required=True)
|
| 607 |
+
parser.add_argument("--system")
|
| 608 |
+
parser.add_argument("--raw", action="store_true", help="plain continuation, without chat role/control tokens")
|
| 609 |
+
parser.add_argument("--device", default="auto", help="auto, cpu, cuda, or cuda:N; honors CUDA_VISIBLE_DEVICES")
|
| 610 |
+
parser.add_argument("--dtype", choices=("auto", "float32", "bfloat16", "float16"), default="auto")
|
| 611 |
+
parser.add_argument("--max-new-tokens", type=int, default=128)
|
| 612 |
+
parser.add_argument("--temperature", type=float, default=0.2, help="0 selects greedy decoding")
|
| 613 |
+
parser.add_argument("--top-p", type=float, default=0.95)
|
| 614 |
+
parser.add_argument("--seed", type=int, default=20260729)
|
| 615 |
+
parser.add_argument("--threads", type=int, default=4)
|
| 616 |
+
args = parser.parse_args()
|
| 617 |
+
if args.max_new_tokens < 1 or args.threads < 1:
|
| 618 |
+
parser.error("max-new-tokens and threads must be positive")
|
| 619 |
+
if not math.isfinite(args.temperature) or args.temperature < 0 or not 0 < args.top_p <= 1:
|
| 620 |
+
parser.error("temperature must be finite and nonnegative; top-p must be in (0, 1]")
|
| 621 |
+
if args.raw and args.system is not None:
|
| 622 |
+
parser.error("--system cannot be combined with --raw")
|
| 623 |
+
for path in (args.checkpoint, args.tokenizer):
|
| 624 |
+
if not path.is_file():
|
| 625 |
+
parser.error(f"Missing file: {path}")
|
| 626 |
+
torch.set_num_threads(args.threads)
|
| 627 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") if args.device == "auto" else torch.device(args.device)
|
| 628 |
+
if device.type not in {"cpu", "cuda"}:
|
| 629 |
+
parser.error("Supported devices are cpu and cuda[:N]")
|
| 630 |
+
if device.type == "cuda":
|
| 631 |
+
if not torch.cuda.is_available():
|
| 632 |
+
parser.error("CUDA is unavailable; use --device cpu")
|
| 633 |
+
torch.cuda.set_device(device)
|
| 634 |
+
dtype_name = args.dtype
|
| 635 |
+
if dtype_name == "auto":
|
| 636 |
+
dtype_name = "bfloat16" if device.type == "cuda" and torch.cuda.is_bf16_supported() else "float32"
|
| 637 |
+
model = load_model(args.checkpoint, device, getattr(torch, dtype_name))
|
| 638 |
+
tokenizer = ByteBPETokenizer.load(args.tokenizer)
|
| 639 |
+
if tokenizer.vocab_size != model.config.vocab_size or tokenizer.trained_vocab_size != tokenizer.vocab_size:
|
| 640 |
+
parser.error("A fully defined matching tokenizer is required")
|
| 641 |
+
if args.raw:
|
| 642 |
+
prompt = tokenizer.encode(args.prompt)
|
| 643 |
+
else:
|
| 644 |
+
parts = [("control", "<|bos|>")]
|
| 645 |
+
if args.system is not None:
|
| 646 |
+
parts += [("control", "<|system|>"), args.system, ("control", "<|system_end|>")]
|
| 647 |
+
parts += [("control", "<|user|>"), args.prompt, ("control", "<|user_end|>"), ("control", "<|assistant|>")]
|
| 648 |
+
prompt = tokenizer.encode_parts(parts)
|
| 649 |
+
if not prompt or len(prompt) >= model.config.context_length:
|
| 650 |
+
parser.error("Prompt must be nonempty and leave space for generation within the context limit")
|
| 651 |
+
print(f"Sabaki | {device} | {dtype_name} | prompt: {len(prompt)} tokens", file=sys.stderr)
|
| 652 |
+
limit = min(args.max_new_tokens, model.config.context_length - len(prompt))
|
| 653 |
+
stop = {tokenizer.control_id(token) for token in ("<|final_end|>", "<|assistant_end|>", "<|eos|>")}
|
| 654 |
+
torch.manual_seed(args.seed)
|
| 655 |
+
generated = []
|
| 656 |
+
with torch.inference_mode():
|
| 657 |
+
logits, cache = model(torch.tensor([prompt], device=device))
|
| 658 |
+
for index in range(limit):
|
| 659 |
+
token = int(sample(logits[:, -1], args.temperature, args.top_p).item())
|
| 660 |
+
generated.append(token)
|
| 661 |
+
if token in stop or index + 1 == limit:
|
| 662 |
+
break
|
| 663 |
+
logits, cache = model(torch.tensor([[token]], device=device), cache)
|
| 664 |
+
if not args.raw and tokenizer.control_id("<|final|>") in generated:
|
| 665 |
+
generated = generated[generated.index(tokenizer.control_id("<|final|>")) + 1:]
|
| 666 |
+
print(tokenizer.decode(generated, errors="replace", skip_control=True))
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
if __name__ == "__main__":
|
| 670 |
+
main()
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|