Kye Gomez commited on
Commit ·
12f6c5b
1
Parent(s): 5ffb897
[docs][readme-badges][add pypi twitter github badges to
Browse filesreadme][improvement][init-sort][sort imports alphabetically in
init][improvement][version-bump][bump version to
0.3.0][feat][tokenizer-class][add MythosTokenizer to init
exports][feat][test-tokenizer][add tokenizer test suite with printed output]
- README.md +27 -0
- open_mythos/__init__.py +10 -10
- open_mythos/moda.py +1134 -0
- pyproject.toml +1 -1
- test_main.py +0 -7
- tests/test_tokenizer.py +75 -0
README.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
| 1 |
# OpenMythos
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
> **Disclaimer:** OpenMythos is an independent, community-driven theoretical reconstruction based solely on publicly available research and speculation. It is not affiliated with, endorsed by, or connected to Anthropic or any of their proprietary systems.
|
| 4 |
|
| 5 |
OpenMythos is an open-source, theoretical implementation of the Claude Mythos model. It implements a Recurrent-Depth Transformer (RDT) with three stages: **Prelude** (transformer blocks), a looped **Recurrent Block** (up to `max_loop_iters`), and a final **Coda**. Attention is switchable between MLA and GQA, and the feed-forward uses a sparse MoE with routed and shared experts ideal for exploring compute-adaptive, depth-variable reasoning.
|
|
|
|
| 1 |
# OpenMythos
|
| 2 |
|
| 3 |
+
<p align="center">
|
| 4 |
+
<a href="https://pypi.org/project/open-mythos/" target="_blank">
|
| 5 |
+
<picture>
|
| 6 |
+
<source srcset="https://img.shields.io/pypi/v/open-mythos?style=for-the-badge&color=3670A0" media="(prefers-color-scheme: dark)">
|
| 7 |
+
<img alt="Version" src="https://img.shields.io/pypi/v/open-mythos?style=for-the-badge&color=3670A0">
|
| 8 |
+
</picture>
|
| 9 |
+
</a>
|
| 10 |
+
<a href="https://pypi.org/project/open-mythos/" target="_blank">
|
| 11 |
+
<picture>
|
| 12 |
+
<source srcset="https://img.shields.io/pypi/dm/open-mythos?style=for-the-badge&color=3670A0" media="(prefers-color-scheme: dark)">
|
| 13 |
+
<img alt="Downloads" src="https://img.shields.io/pypi/dm/open-mythos?style=for-the-badge&color=3670A0">
|
| 14 |
+
</picture>
|
| 15 |
+
</a>
|
| 16 |
+
<a href="https://twitter.com/kyegomezb/">
|
| 17 |
+
<picture>
|
| 18 |
+
<source srcset="https://img.shields.io/badge/Twitter-Follow-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white" media="(prefers-color-scheme: dark)">
|
| 19 |
+
<img src="https://img.shields.io/badge/Twitter-Follow-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white" alt="Twitter">
|
| 20 |
+
</picture>
|
| 21 |
+
</a>
|
| 22 |
+
<a href="https://github.com/kyegomez/OpenMythos" target="_blank">
|
| 23 |
+
<picture>
|
| 24 |
+
<source srcset="https://img.shields.io/badge/GitHub-Repo-171515?style=for-the-badge&logo=github&logoColor=white" media="(prefers-color-scheme: dark)">
|
| 25 |
+
<img src="https://img.shields.io/badge/GitHub-Repo-171515?style=for-the-badge&logo=github&logoColor=white" alt="GitHub">
|
| 26 |
+
</picture>
|
| 27 |
+
</a>
|
| 28 |
+
</p>
|
| 29 |
+
|
| 30 |
> **Disclaimer:** OpenMythos is an independent, community-driven theoretical reconstruction based solely on publicly available research and speculation. It is not affiliated with, endorsed by, or connected to Anthropic or any of their proprietary systems.
|
| 31 |
|
| 32 |
OpenMythos is an open-source, theoretical implementation of the Claude Mythos model. It implements a Recurrent-Depth Transformer (RDT) with three stages: **Prelude** (transformer blocks), a looped **Recurrent Block** (up to `max_loop_iters`), and a final **Coda**. Attention is switchable between MLA and GQA, and the feed-forward uses a sparse MoE with routed and shared experts ideal for exploring compute-adaptive, depth-variable reasoning.
|
open_mythos/__init__.py
CHANGED
|
@@ -1,29 +1,29 @@
|
|
| 1 |
from open_mythos.main import (
|
| 2 |
-
|
| 3 |
-
RMSNorm,
|
| 4 |
-
GQAttention,
|
| 5 |
-
MLAttention,
|
| 6 |
Expert,
|
| 7 |
-
|
| 8 |
LoRAAdapter,
|
| 9 |
-
TransformerBlock,
|
| 10 |
LTIInjection,
|
| 11 |
-
|
| 12 |
-
|
|
|
|
| 13 |
OpenMythos,
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
apply_rope,
|
| 16 |
loop_index_embedding,
|
|
|
|
| 17 |
)
|
| 18 |
from open_mythos.tokenizer import MythosTokenizer
|
| 19 |
from open_mythos.variants import (
|
| 20 |
mythos_1b,
|
|
|
|
| 21 |
mythos_3b,
|
| 22 |
mythos_10b,
|
| 23 |
mythos_50b,
|
| 24 |
mythos_100b,
|
| 25 |
mythos_500b,
|
| 26 |
-
mythos_1t,
|
| 27 |
)
|
| 28 |
|
| 29 |
__all__ = [
|
|
|
|
| 1 |
from open_mythos.main import (
|
| 2 |
+
ACTHalting,
|
|
|
|
|
|
|
|
|
|
| 3 |
Expert,
|
| 4 |
+
GQAttention,
|
| 5 |
LoRAAdapter,
|
|
|
|
| 6 |
LTIInjection,
|
| 7 |
+
MLAttention,
|
| 8 |
+
MoEFFN,
|
| 9 |
+
MythosConfig,
|
| 10 |
OpenMythos,
|
| 11 |
+
RecurrentBlock,
|
| 12 |
+
RMSNorm,
|
| 13 |
+
TransformerBlock,
|
| 14 |
apply_rope,
|
| 15 |
loop_index_embedding,
|
| 16 |
+
precompute_rope_freqs,
|
| 17 |
)
|
| 18 |
from open_mythos.tokenizer import MythosTokenizer
|
| 19 |
from open_mythos.variants import (
|
| 20 |
mythos_1b,
|
| 21 |
+
mythos_1t,
|
| 22 |
mythos_3b,
|
| 23 |
mythos_10b,
|
| 24 |
mythos_50b,
|
| 25 |
mythos_100b,
|
| 26 |
mythos_500b,
|
|
|
|
| 27 |
)
|
| 28 |
|
| 29 |
__all__ = [
|
open_mythos/moda.py
ADDED
|
@@ -0,0 +1,1134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Mixture-of-Depths Attention (MoDA) + DeepSeek Mixture-of-Experts FFN
|
| 3 |
+
======================================================================
|
| 4 |
+
Paper (attention): "Mixture-of-Depths Attention" arXiv 2603.15619
|
| 5 |
+
Paper (MoE): "DeepSeekMoE: Towards Ultimate Expert Specialization
|
| 6 |
+
in Mixture-of-Experts Language Models" arXiv 2401.06066
|
| 7 |
+
Reference impl (V3): https://github.com/deepseek-ai/DeepSeek-V3
|
| 8 |
+
|
| 9 |
+
Architecture
|
| 10 |
+
------------
|
| 11 |
+
This file fuses two independent architectural improvements:
|
| 12 |
+
|
| 13 |
+
1. **MoDA** — each attention head jointly attends to current-layer sequence
|
| 14 |
+
KV pairs (causal) *and* depth KV pairs from all preceding layers at the
|
| 15 |
+
same token position, under a single softmax.
|
| 16 |
+
|
| 17 |
+
2. **DeepSeek MoE** (replaces the dense SwiGLU FFN in every block):
|
| 18 |
+
* K_s *shared experts* — always activated, capture common knowledge.
|
| 19 |
+
* N_r *routed experts* — sparse; top-K activated per token.
|
| 20 |
+
* Fine-grained expert segmentation: each expert has a small hidden dim
|
| 21 |
+
(≈ dense_hidden / m) so that activating more experts keeps FLOPs
|
| 22 |
+
constant while improving specialisation.
|
| 23 |
+
* Expert-level balance loss prevents routing collapse.
|
| 24 |
+
|
| 25 |
+
Gate routing (faithful to DeepSeek-V3 model.py)
|
| 26 |
+
------------------------------------------------
|
| 27 |
+
scores = softmax(x W^T) # or sigmoid for V3 style
|
| 28 |
+
original = scores # saved for weight computation
|
| 29 |
+
[optional] scores += bias # V3 aux-loss-free routing
|
| 30 |
+
[optional] group-limited masking # V3 device-group routing
|
| 31 |
+
indices = topk(scores, K)
|
| 32 |
+
weights = original[indices] # un-biased original scores
|
| 33 |
+
[sigmoid] weights /= sum(weights) # re-normalise for sigmoid gating
|
| 34 |
+
weights *= route_scale
|
| 35 |
+
|
| 36 |
+
Balance loss (DeepSeekMoE §3.3, used when training without V3 bias routing)
|
| 37 |
+
L_ExpBal = Σ_i f_i · P_i
|
| 38 |
+
f_i = (N_r / (K · T)) · #{tokens routing to i} (normalised frequency)
|
| 39 |
+
P_i = (1/T) Σ_t s_{i,t} (mean soft gate score)
|
| 40 |
+
|
| 41 |
+
Memory note
|
| 42 |
+
-----------
|
| 43 |
+
MoDA's unified attention has O(T·L) combined KV length. For long sequences
|
| 44 |
+
use the Triton kernel from https://github.com/hustvl/MoDA.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
from __future__ import annotations
|
| 48 |
+
|
| 49 |
+
import math
|
| 50 |
+
from dataclasses import dataclass
|
| 51 |
+
from typing import List, Optional, Tuple
|
| 52 |
+
|
| 53 |
+
import torch
|
| 54 |
+
import torch.nn as nn
|
| 55 |
+
import torch.nn.functional as F
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@dataclass
|
| 59 |
+
class MoDAConfig:
|
| 60 |
+
"""Configuration for a MoDA + DeepSeek-MoE decoder-only language model.
|
| 61 |
+
|
| 62 |
+
Attention (MoDA)
|
| 63 |
+
----------------
|
| 64 |
+
vocab_size: Vocabulary size.
|
| 65 |
+
d_model: Hidden dimension (must equal n_heads_q * head_dim).
|
| 66 |
+
n_layers: Number of transformer layers.
|
| 67 |
+
n_heads_q: Query heads.
|
| 68 |
+
n_heads_kv: Key/value heads for GQA (must divide n_heads_q).
|
| 69 |
+
head_dim: Per-head dimension (usually d_model // n_heads_q).
|
| 70 |
+
max_seq_len: Maximum sequence length for the RoPE cache.
|
| 71 |
+
rope_base: RoPE frequency base.
|
| 72 |
+
attn_dropout: Attention dropout (0 for inference).
|
| 73 |
+
norm_eps: RMSNorm epsilon.
|
| 74 |
+
|
| 75 |
+
MoE FFN (DeepSeekMoE / DeepSeek-V3 style)
|
| 76 |
+
------------------------------------------
|
| 77 |
+
n_shared_experts: Always-active shared experts (K_s). Capture common
|
| 78 |
+
knowledge; excluded from routing and balance loss.
|
| 79 |
+
n_routed_experts: Total pool of routed experts (N_r).
|
| 80 |
+
n_activated_experts: Top-K selected from routed experts per token (K').
|
| 81 |
+
expert_hidden_dim: Per-expert intermediate dimension.
|
| 82 |
+
Set to dense_ffn_hidden / m where m is the
|
| 83 |
+
fine-grained segmentation factor so that total
|
| 84 |
+
activated FLOPs match a dense FFN:
|
| 85 |
+
(n_shared + n_activated) × expert_hidden ≈ dense_hidden
|
| 86 |
+
moe_balance_alpha: Weight of the expert-level balance loss. Set to
|
| 87 |
+
0.0 to disable (e.g. when using V3 bias routing).
|
| 88 |
+
moe_score_func: "softmax" (DeepSeekMoE / V2) or "sigmoid" (V3).
|
| 89 |
+
moe_n_groups: Number of expert groups for group-limited routing
|
| 90 |
+
(V3 uses 8; set 1 to disable, default).
|
| 91 |
+
moe_topk_groups: Number of groups a token may route to
|
| 92 |
+
(V3 uses 3; set 1 to disable, default).
|
| 93 |
+
moe_route_scale: Scalar multiplied onto the selected gate weights
|
| 94 |
+
after normalisation (V3 uses 2.5446; default 1.0).
|
| 95 |
+
|
| 96 |
+
Defaults approximate the DeepSeekMoE 2B configuration scaled to
|
| 97 |
+
d_model = 2048, keeping per-token FLOPs equal to a dense SwiGLU with
|
| 98 |
+
hidden_dim = 5 632 (≈ 8/3 × 2048):
|
| 99 |
+
(n_shared + n_activated) × expert_hidden = (2+6) × 704 = 5 632.
|
| 100 |
+
"""
|
| 101 |
+
|
| 102 |
+
# ---- Transformer / MoDA ----
|
| 103 |
+
vocab_size: int = 32_000
|
| 104 |
+
d_model: int = 2048
|
| 105 |
+
n_layers: int = 24
|
| 106 |
+
n_heads_q: int = 16
|
| 107 |
+
n_heads_kv: int = 8
|
| 108 |
+
head_dim: int = 128
|
| 109 |
+
max_seq_len: int = 4_096
|
| 110 |
+
rope_base: float = 10_000.0
|
| 111 |
+
attn_dropout: float = 0.0
|
| 112 |
+
norm_eps: float = 1e-6
|
| 113 |
+
|
| 114 |
+
# ---- DeepSeek MoE FFN ----
|
| 115 |
+
n_shared_experts: int = 2 # K_s
|
| 116 |
+
n_routed_experts: int = 64 # N_r
|
| 117 |
+
n_activated_experts: int = 6 # K' top-K from routed pool
|
| 118 |
+
expert_hidden_dim: int = 704 # per-expert intermediate dim
|
| 119 |
+
moe_balance_alpha: float = 0.001 # balance-loss weight (0 = disabled)
|
| 120 |
+
moe_score_func: str = "softmax" # "softmax" | "sigmoid"
|
| 121 |
+
moe_n_groups: int = 1 # expert groups (1 = no grouping)
|
| 122 |
+
moe_topk_groups: int = 1 # groups to route into (1 = no limit)
|
| 123 |
+
moe_route_scale: float = 1.0 # gate-weight scale factor
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# ---------------------------------------------------------------------------
|
| 127 |
+
# Primitives
|
| 128 |
+
# ---------------------------------------------------------------------------
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class RMSNorm(nn.Module):
|
| 132 |
+
"""Root Mean Square Layer Normalization (no bias, no mean subtraction)."""
|
| 133 |
+
|
| 134 |
+
def __init__(self, dim: int, eps: float = 1e-6) -> None:
|
| 135 |
+
"""Create an RMSNorm layer.
|
| 136 |
+
|
| 137 |
+
Args:
|
| 138 |
+
dim: Feature dimension to normalise over (the last axis of input).
|
| 139 |
+
eps: Stability constant added before the reciprocal square-root to
|
| 140 |
+
prevent division by zero when the RMS is near zero.
|
| 141 |
+
"""
|
| 142 |
+
super().__init__()
|
| 143 |
+
self.eps = eps
|
| 144 |
+
self.weight = nn.Parameter(torch.ones(dim))
|
| 145 |
+
|
| 146 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 147 |
+
"""Normalise *x* by its root-mean-square and apply a learnable scale.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
x: Input tensor of arbitrary shape ``[..., dim]``.
|
| 151 |
+
|
| 152 |
+
Returns:
|
| 153 |
+
Normalised tensor, same shape as *x*.
|
| 154 |
+
"""
|
| 155 |
+
rms = x.pow(2).mean(-1, keepdim=True).add(self.eps).rsqrt()
|
| 156 |
+
return x * rms * self.weight
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class RotaryEmbedding(nn.Module):
|
| 160 |
+
"""Rotary Position Embedding (RoPE) with lazy cache extension.
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
dim: Per-head dimension (head_dim).
|
| 164 |
+
max_seq_len: Initial cache size.
|
| 165 |
+
base: Frequency base (default 10 000).
|
| 166 |
+
"""
|
| 167 |
+
|
| 168 |
+
def __init__(
|
| 169 |
+
self, dim: int, max_seq_len: int = 8_192, base: float = 10_000.0
|
| 170 |
+
) -> None:
|
| 171 |
+
"""Initialise RoPE and pre-compute the cos/sin cache.
|
| 172 |
+
|
| 173 |
+
Args:
|
| 174 |
+
dim: Per-head dimension. Must be even.
|
| 175 |
+
max_seq_len: Number of positions to cache on construction. The
|
| 176 |
+
cache doubles automatically when a longer sequence
|
| 177 |
+
is encountered.
|
| 178 |
+
base: Frequency base θ. Higher values slow the rotation
|
| 179 |
+
rate, extending effective context length.
|
| 180 |
+
"""
|
| 181 |
+
super().__init__()
|
| 182 |
+
self.dim = dim
|
| 183 |
+
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
|
| 184 |
+
self.register_buffer("inv_freq", inv_freq, persistent=False)
|
| 185 |
+
self._build_cache(max_seq_len)
|
| 186 |
+
|
| 187 |
+
def _build_cache(self, seq_len: int) -> None:
|
| 188 |
+
"""Pre-compute and register ``_cos`` / ``_sin`` buffers up to *seq_len*.
|
| 189 |
+
|
| 190 |
+
Called once at init and again (doubling capacity) whenever ``forward``
|
| 191 |
+
is asked for a sequence longer than the current cache.
|
| 192 |
+
|
| 193 |
+
Args:
|
| 194 |
+
seq_len: Number of positions to pre-compute.
|
| 195 |
+
"""
|
| 196 |
+
t = torch.arange(
|
| 197 |
+
seq_len, device=self.inv_freq.device, dtype=self.inv_freq.dtype
|
| 198 |
+
)
|
| 199 |
+
freqs = torch.einsum("i,j->ij", t, self.inv_freq) # [T, dim/2]
|
| 200 |
+
emb = torch.cat([freqs, freqs], dim=-1) # [T, dim]
|
| 201 |
+
self.register_buffer("_cos", emb.cos()[None, None], persistent=False)
|
| 202 |
+
self.register_buffer("_sin", emb.sin()[None, None], persistent=False)
|
| 203 |
+
|
| 204 |
+
def forward(self, seq_len: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 205 |
+
"""Return cached (cos, sin) tables for the first *seq_len* positions.
|
| 206 |
+
|
| 207 |
+
Args:
|
| 208 |
+
seq_len: Number of positions required.
|
| 209 |
+
|
| 210 |
+
Returns:
|
| 211 |
+
Tuple of ``(cos, sin)``, each shaped ``[1, 1, seq_len, dim]``,
|
| 212 |
+
ready to broadcast with ``[B, H, T, dim]`` query / key tensors.
|
| 213 |
+
"""
|
| 214 |
+
if seq_len > self._cos.shape[2]:
|
| 215 |
+
self._build_cache(seq_len * 2)
|
| 216 |
+
return self._cos[:, :, :seq_len], self._sin[:, :, :seq_len]
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
|
| 220 |
+
"""Return *x* with its last dimension split and swapped with negation.
|
| 221 |
+
|
| 222 |
+
Given ``x = [x₁, x₂]`` (each half of the last dim), returns
|
| 223 |
+
``[-x₂, x₁]``. Combined with the cos/sin multiply in
|
| 224 |
+
:func:`apply_rotary_emb` this implements the 2-D rotation matrix
|
| 225 |
+
that defines RoPE.
|
| 226 |
+
|
| 227 |
+
Args:
|
| 228 |
+
x: Tensor with an even-sized last dimension.
|
| 229 |
+
|
| 230 |
+
Returns:
|
| 231 |
+
Rotated tensor with the same shape as *x*.
|
| 232 |
+
"""
|
| 233 |
+
half = x.shape[-1] // 2
|
| 234 |
+
return torch.cat([-x[..., half:], x[..., :half]], dim=-1)
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
def apply_rotary_emb(
|
| 238 |
+
x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
|
| 239 |
+
) -> torch.Tensor:
|
| 240 |
+
"""Apply Rotary Position Embeddings in-place to query or key tensors.
|
| 241 |
+
|
| 242 |
+
Implements ``x_rot = x * cos + rotate_half(x) * sin``, which is
|
| 243 |
+
equivalent to multiplying each consecutive pair of dimensions by a
|
| 244 |
+
2-D rotation matrix whose angle depends on the sequence position and
|
| 245 |
+
the dimension's frequency.
|
| 246 |
+
|
| 247 |
+
Args:
|
| 248 |
+
x: Query or key tensor, shape ``[B, H, T, d]``.
|
| 249 |
+
cos: Pre-computed cosines, shape ``[1, 1, T, d]``.
|
| 250 |
+
sin: Pre-computed sines, shape ``[1, 1, T, d]``.
|
| 251 |
+
|
| 252 |
+
Returns:
|
| 253 |
+
Rotated tensor with the same shape and dtype as *x*.
|
| 254 |
+
"""
|
| 255 |
+
return x * cos + _rotate_half(x) * sin
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
# ---------------------------------------------------------------------------
|
| 259 |
+
# DeepSeek MoE FFN
|
| 260 |
+
# ---------------------------------------------------------------------------
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
class DeepSeekExpert(nn.Module):
|
| 264 |
+
"""Single fine-grained SwiGLU expert.
|
| 265 |
+
|
| 266 |
+
Faithful to DeepSeek-V3 ``Expert``:
|
| 267 |
+
output = w2( SiLU(w1(x)) ⊙ w3(x) )
|
| 268 |
+
|
| 269 |
+
where w1 is the gate projection, w3 the up-projection, w2 the
|
| 270 |
+
down-projection — identical to a SwiGLU FFN at smaller hidden dim.
|
| 271 |
+
|
| 272 |
+
Args:
|
| 273 |
+
d_model: Input / output dimension.
|
| 274 |
+
hidden_dim: Expert intermediate dimension (≪ dense FFN hidden_dim).
|
| 275 |
+
"""
|
| 276 |
+
|
| 277 |
+
def __init__(self, d_model: int, hidden_dim: int) -> None:
|
| 278 |
+
"""Create a single fine-grained SwiGLU expert.
|
| 279 |
+
|
| 280 |
+
Args:
|
| 281 |
+
d_model: Token hidden dimension (input and output size).
|
| 282 |
+
hidden_dim: Expert intermediate dimension. Typically much
|
| 283 |
+
smaller than the dense FFN hidden dim — set to
|
| 284 |
+
``dense_hidden / m`` where *m* is the fine-grained
|
| 285 |
+
segmentation factor so total activated FLOPs match
|
| 286 |
+
the dense baseline.
|
| 287 |
+
"""
|
| 288 |
+
super().__init__()
|
| 289 |
+
self.w1 = nn.Linear(d_model, hidden_dim, bias=False) # gate
|
| 290 |
+
self.w3 = nn.Linear(d_model, hidden_dim, bias=False) # up
|
| 291 |
+
self.w2 = nn.Linear(hidden_dim, d_model, bias=False) # down
|
| 292 |
+
|
| 293 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 294 |
+
"""Compute ``w2( SiLU(w1(x)) ⊙ w3(x) )``.
|
| 295 |
+
|
| 296 |
+
Args:
|
| 297 |
+
x: Token features assigned to this expert, shape
|
| 298 |
+
``[num_assigned_tokens, d_model]``.
|
| 299 |
+
|
| 300 |
+
Returns:
|
| 301 |
+
Expert output, shape ``[num_assigned_tokens, d_model]``.
|
| 302 |
+
"""
|
| 303 |
+
return self.w2(F.silu(self.w1(x)) * self.w3(x))
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
class DeepSeekGate(nn.Module):
|
| 307 |
+
"""Token-to-expert routing gate.
|
| 308 |
+
|
| 309 |
+
Faithful to DeepSeek-V3 ``Gate`` (minus distributed sharding).
|
| 310 |
+
|
| 311 |
+
Routing algorithm
|
| 312 |
+
~~~~~~~~~~~~~~~~~
|
| 313 |
+
1. ``scores = softmax(x W^T)`` or ``sigmoid(x W^T)``
|
| 314 |
+
2. ``original_scores = scores`` (saved — will be used for gate weights)
|
| 315 |
+
3. [optional] ``scores += bias`` (V3 aux-loss-free bias routing)
|
| 316 |
+
4. [optional] Group-limited masking:
|
| 317 |
+
- reshape scores → [T, n_groups, experts_per_group]
|
| 318 |
+
- keep only top-``topk_groups`` groups per token
|
| 319 |
+
- mask the rest to −∞
|
| 320 |
+
5. ``indices = topk(scores, K')`` (routing decision)
|
| 321 |
+
6. ``weights = original_scores[indices]`` (un-biased weights)
|
| 322 |
+
7. [sigmoid only] ``weights /= sum(weights)`` (re-normalise)
|
| 323 |
+
8. ``weights *= route_scale``
|
| 324 |
+
|
| 325 |
+
The ``original_scores`` (full distribution, before bias/masking) are also
|
| 326 |
+
returned so the MoE layer can compute the expert-level balance loss.
|
| 327 |
+
|
| 328 |
+
Args:
|
| 329 |
+
d_model: Token hidden dimension.
|
| 330 |
+
n_routed_experts: Total routed expert pool size (N_r).
|
| 331 |
+
n_activated: Top-K experts to select (K').
|
| 332 |
+
score_func: ``"softmax"`` or ``"sigmoid"``.
|
| 333 |
+
n_groups: Number of expert groups (1 = disabled).
|
| 334 |
+
topk_groups: Groups a token may route to (1 = disabled).
|
| 335 |
+
route_scale: Scalar applied to final gate weights.
|
| 336 |
+
use_bias: If True, add a learnable per-expert bias used only
|
| 337 |
+
for the routing decision (V3 aux-loss-free scheme).
|
| 338 |
+
"""
|
| 339 |
+
|
| 340 |
+
def __init__(
|
| 341 |
+
self,
|
| 342 |
+
d_model: int,
|
| 343 |
+
n_routed_experts: int,
|
| 344 |
+
n_activated: int,
|
| 345 |
+
score_func: str = "softmax",
|
| 346 |
+
n_groups: int = 1,
|
| 347 |
+
topk_groups: int = 1,
|
| 348 |
+
route_scale: float = 1.0,
|
| 349 |
+
use_bias: bool = False,
|
| 350 |
+
) -> None:
|
| 351 |
+
"""Create the routing gate.
|
| 352 |
+
|
| 353 |
+
Args:
|
| 354 |
+
d_model: Token hidden dimension.
|
| 355 |
+
n_routed_experts: Total number of routed experts in the pool (N_r).
|
| 356 |
+
n_activated: How many experts to select per token (K').
|
| 357 |
+
score_func: Affinity function — ``"softmax"`` (original
|
| 358 |
+
DeepSeekMoE / V2) or ``"sigmoid"`` (V3).
|
| 359 |
+
n_groups: Number of expert groups for device-limited
|
| 360 |
+
routing. Set to 1 to disable (default).
|
| 361 |
+
topk_groups: Number of groups each token may route into.
|
| 362 |
+
Set to 1 to disable (default).
|
| 363 |
+
route_scale: Scalar multiplied onto gate weights after
|
| 364 |
+
optional sigmoid normalisation (V3 uses 2.5446;
|
| 365 |
+
default 1.0 leaves weights unchanged).
|
| 366 |
+
use_bias: If ``True``, initialise a learnable per-expert
|
| 367 |
+
float32 bias added to routing scores only (not
|
| 368 |
+
gate weights). Enables the V3 aux-loss-free
|
| 369 |
+
load-balancing scheme where the bias is adjusted
|
| 370 |
+
outside the optimizer to track expert loads.
|
| 371 |
+
"""
|
| 372 |
+
super().__init__()
|
| 373 |
+
self.n_routed_experts = n_routed_experts
|
| 374 |
+
self.n_activated = n_activated
|
| 375 |
+
self.score_func = score_func
|
| 376 |
+
self.n_groups = n_groups
|
| 377 |
+
self.topk_groups = topk_groups
|
| 378 |
+
self.route_scale = route_scale
|
| 379 |
+
|
| 380 |
+
# Gating projection: [N_r, D]
|
| 381 |
+
self.weight = nn.Parameter(torch.empty(n_routed_experts, d_model))
|
| 382 |
+
nn.init.kaiming_uniform_(self.weight, a=math.sqrt(5))
|
| 383 |
+
|
| 384 |
+
# Optional per-expert routing bias (V3 aux-loss-free load balancing).
|
| 385 |
+
# Updated outside the optimizer by monitoring expert loads — not trained
|
| 386 |
+
# through the balance loss. Initialised to zero.
|
| 387 |
+
self.bias: Optional[nn.Parameter] = (
|
| 388 |
+
nn.Parameter(torch.zeros(n_routed_experts, dtype=torch.float32))
|
| 389 |
+
if use_bias
|
| 390 |
+
else None
|
| 391 |
+
)
|
| 392 |
+
|
| 393 |
+
def forward(
|
| 394 |
+
self, x: torch.Tensor
|
| 395 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 396 |
+
"""Compute routing weights and expert indices.
|
| 397 |
+
|
| 398 |
+
Args:
|
| 399 |
+
x: ``[num_tokens, d_model]`` (flattened B × T).
|
| 400 |
+
|
| 401 |
+
Returns:
|
| 402 |
+
weights: ``[num_tokens, K']`` gate weights (dtype = x.dtype).
|
| 403 |
+
indices: ``[num_tokens, K']`` selected expert indices (int64).
|
| 404 |
+
original_scores: ``[num_tokens, N_r]`` full soft scores (float32),
|
| 405 |
+
used by :class:`DeepSeekMoE` for the balance loss.
|
| 406 |
+
"""
|
| 407 |
+
# Affinity logits
|
| 408 |
+
logits = F.linear(x, self.weight) # [T, N_r]
|
| 409 |
+
|
| 410 |
+
if self.score_func == "softmax":
|
| 411 |
+
scores = logits.softmax(dim=-1, dtype=torch.float32)
|
| 412 |
+
else: # sigmoid (V3)
|
| 413 |
+
scores = logits.sigmoid().to(torch.float32)
|
| 414 |
+
|
| 415 |
+
original_scores = scores # un-biased; used for weights + balance loss
|
| 416 |
+
|
| 417 |
+
# Routing scores (may differ from original_scores if bias is active)
|
| 418 |
+
routing = scores
|
| 419 |
+
if self.bias is not None:
|
| 420 |
+
routing = routing + self.bias
|
| 421 |
+
|
| 422 |
+
# Group-limited routing (V3 device-group constraint)
|
| 423 |
+
if self.n_groups > 1:
|
| 424 |
+
# [T, n_groups, experts_per_group]
|
| 425 |
+
g = routing.view(x.size(0), self.n_groups, -1)
|
| 426 |
+
if self.bias is None:
|
| 427 |
+
group_scores = g.amax(dim=-1) # [T, G]
|
| 428 |
+
else:
|
| 429 |
+
# Top-2 sum per group (V3 heuristic)
|
| 430 |
+
group_scores = g.topk(2, dim=-1)[0].sum(dim=-1)
|
| 431 |
+
_, top_groups = group_scores.topk(self.topk_groups, dim=-1) # [T, topk_g]
|
| 432 |
+
mask = torch.ones(
|
| 433 |
+
x.size(0), self.n_groups, dtype=torch.bool, device=x.device
|
| 434 |
+
).scatter_(
|
| 435 |
+
1, top_groups, False
|
| 436 |
+
) # True = masked out
|
| 437 |
+
routing = g.masked_fill(mask.unsqueeze(-1), float("-inf")).flatten(1)
|
| 438 |
+
|
| 439 |
+
# Top-K selection (on routing scores which may include bias / group mask)
|
| 440 |
+
_, indices = routing.topk(self.n_activated, dim=-1) # [T, K']
|
| 441 |
+
|
| 442 |
+
# Gate weights from original (un-biased) scores
|
| 443 |
+
weights = original_scores.gather(1, indices) # [T, K']
|
| 444 |
+
|
| 445 |
+
if self.score_func == "sigmoid":
|
| 446 |
+
weights = weights / weights.sum(dim=-1, keepdim=True).clamp(min=1e-9)
|
| 447 |
+
|
| 448 |
+
weights = (weights * self.route_scale).to(x.dtype)
|
| 449 |
+
return weights, indices, original_scores
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
class DeepSeekMoE(nn.Module):
|
| 453 |
+
"""DeepSeek Mixture-of-Experts layer — drop-in replacement for a dense FFN.
|
| 454 |
+
|
| 455 |
+
Combines shared experts (always active) and routed experts (sparse top-K)
|
| 456 |
+
exactly as in DeepSeek-V3 ``MoE``, adapted for single-device training
|
| 457 |
+
(no ColumnParallel/RowParallel, no all_reduce).
|
| 458 |
+
|
| 459 |
+
Forward pass
|
| 460 |
+
~~~~~~~~~~~~
|
| 461 |
+
::
|
| 462 |
+
|
| 463 |
+
x_flat = x.view(-1, D) # [B*T, D]
|
| 464 |
+
|
| 465 |
+
# Shared path (always executed)
|
| 466 |
+
z = shared_experts(x_flat) # [B*T, D]
|
| 467 |
+
|
| 468 |
+
# Routed path (sparse)
|
| 469 |
+
weights, indices, scores = gate(x_flat) # [B*T, K'], [B*T, K'], [B*T, N_r]
|
| 470 |
+
y = zeros_like(x_flat)
|
| 471 |
+
for each expert i:
|
| 472 |
+
toks = tokens that selected expert i
|
| 473 |
+
y[toks] += experts[i](x_flat[toks]) * weights[toks, rank_of_i]
|
| 474 |
+
|
| 475 |
+
output = (y + z).view(B, T, D)
|
| 476 |
+
|
| 477 |
+
# Training: expert-level balance loss (DeepSeekMoE §3.3)
|
| 478 |
+
L_ExpBal = Σ_i f_i · P_i
|
| 479 |
+
f_i = (N_r / (K' · T)) · #{tokens → expert i}
|
| 480 |
+
P_i = mean_t(scores_{t,i})
|
| 481 |
+
|
| 482 |
+
Args:
|
| 483 |
+
cfg: :class:`MoDAConfig` instance.
|
| 484 |
+
"""
|
| 485 |
+
|
| 486 |
+
def __init__(self, cfg: MoDAConfig) -> None:
|
| 487 |
+
"""Build the MoE layer from a :class:`MoDAConfig`.
|
| 488 |
+
|
| 489 |
+
Constructs:
|
| 490 |
+
* ``shared_experts`` — one dense SwiGLU FFN with hidden dimension
|
| 491 |
+
``n_shared_experts × expert_hidden_dim``.
|
| 492 |
+
* ``gate`` — :class:`DeepSeekGate` for top-K routing.
|
| 493 |
+
* ``experts`` — ``nn.ModuleList`` of ``n_routed_experts``
|
| 494 |
+
:class:`DeepSeekExpert` instances, each with ``expert_hidden_dim``
|
| 495 |
+
intermediate units.
|
| 496 |
+
|
| 497 |
+
Args:
|
| 498 |
+
cfg: Model configuration. The relevant fields are
|
| 499 |
+
``n_shared_experts``, ``n_routed_experts``,
|
| 500 |
+
``n_activated_experts``, ``expert_hidden_dim``,
|
| 501 |
+
``moe_balance_alpha``, ``moe_score_func``,
|
| 502 |
+
``moe_n_groups``, ``moe_topk_groups``, and
|
| 503 |
+
``moe_route_scale``.
|
| 504 |
+
"""
|
| 505 |
+
super().__init__()
|
| 506 |
+
self.d_model = cfg.d_model
|
| 507 |
+
self.n_routed_experts = cfg.n_routed_experts
|
| 508 |
+
self.n_activated_experts = cfg.n_activated_experts
|
| 509 |
+
self.moe_balance_alpha = cfg.moe_balance_alpha
|
| 510 |
+
|
| 511 |
+
# Shared experts: single dense SwiGLU with hidden = K_s × expert_hidden
|
| 512 |
+
# (matches DeepSeek-V3's ``MLP(dim, n_shared_experts * moe_inter_dim)``)
|
| 513 |
+
shared_hidden = cfg.n_shared_experts * cfg.expert_hidden_dim
|
| 514 |
+
self.shared_experts = _SharedFFN(cfg.d_model, shared_hidden)
|
| 515 |
+
|
| 516 |
+
# Routing gate
|
| 517 |
+
self.gate = DeepSeekGate(
|
| 518 |
+
d_model=cfg.d_model,
|
| 519 |
+
n_routed_experts=cfg.n_routed_experts,
|
| 520 |
+
n_activated=cfg.n_activated_experts,
|
| 521 |
+
score_func=cfg.moe_score_func,
|
| 522 |
+
n_groups=cfg.moe_n_groups,
|
| 523 |
+
topk_groups=cfg.moe_topk_groups,
|
| 524 |
+
route_scale=cfg.moe_route_scale,
|
| 525 |
+
use_bias=False,
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
# Routed experts pool
|
| 529 |
+
self.experts = nn.ModuleList(
|
| 530 |
+
[
|
| 531 |
+
DeepSeekExpert(cfg.d_model, cfg.expert_hidden_dim)
|
| 532 |
+
for _ in range(cfg.n_routed_experts)
|
| 533 |
+
]
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
|
| 537 |
+
"""Run the MoE layer.
|
| 538 |
+
|
| 539 |
+
Args:
|
| 540 |
+
x: ``[B, T, D]`` hidden states.
|
| 541 |
+
|
| 542 |
+
Returns:
|
| 543 |
+
output: ``[B, T, D]`` updated hidden states.
|
| 544 |
+
balance_loss: Scalar expert-level balance loss (during training),
|
| 545 |
+
or ``None`` during inference.
|
| 546 |
+
"""
|
| 547 |
+
shape = x.shape
|
| 548 |
+
x_flat = x.view(-1, self.d_model) # [T_tot, D]
|
| 549 |
+
n_tokens = x_flat.size(0)
|
| 550 |
+
|
| 551 |
+
# ---- Shared experts (all tokens) ---------------------------------
|
| 552 |
+
z = self.shared_experts(x_flat) # [T_tot, D]
|
| 553 |
+
|
| 554 |
+
# ---- Routed experts (sparse) -------------------------------------
|
| 555 |
+
weights, indices, scores = self.gate(x_flat)
|
| 556 |
+
# weights: [T_tot, K'], indices: [T_tot, K'], scores: [T_tot, N_r]
|
| 557 |
+
|
| 558 |
+
y = torch.zeros_like(x_flat)
|
| 559 |
+
|
| 560 |
+
# Dispatch: for each expert compute on its assigned tokens
|
| 561 |
+
# (token-major loop matches DeepSeek-V3's reference implementation)
|
| 562 |
+
counts = torch.bincount(indices.flatten(), minlength=self.n_routed_experts)
|
| 563 |
+
for i, expert in enumerate(self.experts):
|
| 564 |
+
if counts[i].item() == 0:
|
| 565 |
+
continue
|
| 566 |
+
tok_idx, rank_in_k = torch.where(
|
| 567 |
+
indices == i
|
| 568 |
+
) # which tokens & which K slot
|
| 569 |
+
y[tok_idx] += expert(x_flat[tok_idx]) * weights[tok_idx, rank_in_k, None]
|
| 570 |
+
|
| 571 |
+
output = (y + z).view(shape)
|
| 572 |
+
|
| 573 |
+
# ---- Expert-level balance loss (DeepSeekMoE §3.3) ----------------
|
| 574 |
+
balance_loss: Optional[torch.Tensor] = None
|
| 575 |
+
if self.training and self.moe_balance_alpha > 0.0:
|
| 576 |
+
balance_loss = self._balance_loss(indices, scores, n_tokens)
|
| 577 |
+
|
| 578 |
+
return output, balance_loss
|
| 579 |
+
|
| 580 |
+
def _balance_loss(
|
| 581 |
+
self,
|
| 582 |
+
indices: torch.Tensor, # [T, K'] int64
|
| 583 |
+
scores: torch.Tensor, # [T, N_r] float32 (full distribution)
|
| 584 |
+
n_tokens: int,
|
| 585 |
+
) -> torch.Tensor:
|
| 586 |
+
"""Compute the expert-level balance loss (DeepSeekMoE §3.3).
|
| 587 |
+
|
| 588 |
+
Penalises routing imbalance by encouraging the model to spread tokens
|
| 589 |
+
evenly across experts. Only the soft-score term ``P_i`` receives a
|
| 590 |
+
gradient; the hard-count term ``f_i`` is non-differentiable and acts
|
| 591 |
+
as a fixed weighting coefficient.
|
| 592 |
+
|
| 593 |
+
::
|
| 594 |
+
|
| 595 |
+
f_i = (N_r / (K' × T)) × #{tokens routed to expert i}
|
| 596 |
+
P_i = (1/T) Σ_t scores[t, i]
|
| 597 |
+
L = Σ_i f_i · P_i
|
| 598 |
+
|
| 599 |
+
For perfect balance ``f_i = 1`` for all *i* and ``L = Σ P_i = 1``
|
| 600 |
+
(softmax) or some constant (sigmoid). Overloaded experts produce
|
| 601 |
+
large ``f_i``, pushing their mean score ``P_i`` up via the gradient
|
| 602 |
+
and thereby attracting more tokens — stabilising load over training.
|
| 603 |
+
|
| 604 |
+
Args:
|
| 605 |
+
indices: ``[T, K']`` int64 — expert indices selected per token.
|
| 606 |
+
scores: ``[T, N_r]`` float32 — full soft distribution from the
|
| 607 |
+
gate (before top-K selection), used for ``P_i``.
|
| 608 |
+
n_tokens: Total number of tokens in the batch (``B × T``).
|
| 609 |
+
|
| 610 |
+
Returns:
|
| 611 |
+
Scalar balance loss tensor.
|
| 612 |
+
"""
|
| 613 |
+
Nr, K = self.n_routed_experts, self.n_activated_experts
|
| 614 |
+
|
| 615 |
+
# Routing counts per expert (non-differentiable)
|
| 616 |
+
counts = torch.zeros(Nr, dtype=torch.float32, device=indices.device)
|
| 617 |
+
counts.scatter_add_(
|
| 618 |
+
0,
|
| 619 |
+
indices.flatten(),
|
| 620 |
+
torch.ones(indices.numel(), dtype=torch.float32, device=indices.device),
|
| 621 |
+
)
|
| 622 |
+
f = counts * (Nr / (K * n_tokens)) # normalised frequency [N_r]
|
| 623 |
+
|
| 624 |
+
# Mean soft gate score per expert (differentiable through softmax/sigmoid)
|
| 625 |
+
P = scores.mean(dim=0) # [N_r]
|
| 626 |
+
|
| 627 |
+
# f is derived from hard top-K → no gradient; gradient flows through P only
|
| 628 |
+
return (f * P).sum()
|
| 629 |
+
|
| 630 |
+
|
| 631 |
+
class _SharedFFN(nn.Module):
|
| 632 |
+
"""Dense SwiGLU FFN used for the always-active shared experts.
|
| 633 |
+
|
| 634 |
+
Mirrors :class:`DeepSeekExpert` but is a single larger MLP whose
|
| 635 |
+
``hidden_dim`` equals ``n_shared_experts × expert_hidden_dim``. This
|
| 636 |
+
matches DeepSeek-V3's ``MLP(dim, n_shared_experts * moe_inter_dim)``.
|
| 637 |
+
|
| 638 |
+
Not part of the public API — instantiated only by :class:`DeepSeekMoE`.
|
| 639 |
+
"""
|
| 640 |
+
|
| 641 |
+
def __init__(self, d_model: int, hidden_dim: int) -> None:
|
| 642 |
+
"""Create the shared-expert FFN.
|
| 643 |
+
|
| 644 |
+
Args:
|
| 645 |
+
d_model: Token hidden dimension (input and output).
|
| 646 |
+
hidden_dim: Combined intermediate size for all shared experts
|
| 647 |
+
(``n_shared_experts × expert_hidden_dim``).
|
| 648 |
+
"""
|
| 649 |
+
super().__init__()
|
| 650 |
+
self.w1 = nn.Linear(d_model, hidden_dim, bias=False)
|
| 651 |
+
self.w3 = nn.Linear(d_model, hidden_dim, bias=False)
|
| 652 |
+
self.w2 = nn.Linear(hidden_dim, d_model, bias=False)
|
| 653 |
+
|
| 654 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 655 |
+
"""Apply the shared SwiGLU FFN to every token.
|
| 656 |
+
|
| 657 |
+
Args:
|
| 658 |
+
x: Flattened token features, shape ``[B*T, d_model]``.
|
| 659 |
+
|
| 660 |
+
Returns:
|
| 661 |
+
Transformed features, shape ``[B*T, d_model]``.
|
| 662 |
+
"""
|
| 663 |
+
return self.w2(F.silu(self.w1(x)) * self.w3(x))
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
# ---------------------------------------------------------------------------
|
| 667 |
+
# MoDA Attention (unchanged from base file)
|
| 668 |
+
# ---------------------------------------------------------------------------
|
| 669 |
+
|
| 670 |
+
|
| 671 |
+
class MoDAAttention(nn.Module):
|
| 672 |
+
"""Mixture-of-Depths Attention — read side.
|
| 673 |
+
|
| 674 |
+
Each query jointly attends (single softmax) to:
|
| 675 |
+
* Sequence KVs at the current layer (causal GQA).
|
| 676 |
+
* Depth KVs from all preceding layers at the *same* token position.
|
| 677 |
+
|
| 678 |
+
Depth cache entries are written externally by :class:`MoDABlock` from
|
| 679 |
+
the full block output X_l^out (after the MoE FFN).
|
| 680 |
+
|
| 681 |
+
Args:
|
| 682 |
+
cfg: :class:`MoDAConfig` instance.
|
| 683 |
+
"""
|
| 684 |
+
|
| 685 |
+
def __init__(self, cfg: MoDAConfig) -> None:
|
| 686 |
+
"""Build the MoDA attention module.
|
| 687 |
+
|
| 688 |
+
Creates four projection matrices (Q, K, V, O) sized for GQA and
|
| 689 |
+
stores the attention scale and dropout rate.
|
| 690 |
+
|
| 691 |
+
Args:
|
| 692 |
+
cfg: Model configuration. Must satisfy
|
| 693 |
+
``n_heads_q % n_heads_kv == 0`` (GQA requirement).
|
| 694 |
+
|
| 695 |
+
Raises:
|
| 696 |
+
ValueError: If ``n_heads_q`` is not divisible by ``n_heads_kv``.
|
| 697 |
+
"""
|
| 698 |
+
super().__init__()
|
| 699 |
+
if cfg.n_heads_q % cfg.n_heads_kv != 0:
|
| 700 |
+
raise ValueError(
|
| 701 |
+
f"n_heads_q ({cfg.n_heads_q}) must be divisible by "
|
| 702 |
+
f"n_heads_kv ({cfg.n_heads_kv}) for GQA."
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
self.n_heads_q = cfg.n_heads_q
|
| 706 |
+
self.n_heads_kv = cfg.n_heads_kv
|
| 707 |
+
self.head_dim = cfg.head_dim
|
| 708 |
+
self.gqa_group = cfg.n_heads_q // cfg.n_heads_kv
|
| 709 |
+
self.scale = cfg.head_dim**-0.5
|
| 710 |
+
self.dropout = cfg.attn_dropout
|
| 711 |
+
|
| 712 |
+
inner_q = cfg.n_heads_q * cfg.head_dim
|
| 713 |
+
inner_kv = cfg.n_heads_kv * cfg.head_dim
|
| 714 |
+
|
| 715 |
+
self.q_proj = nn.Linear(cfg.d_model, inner_q, bias=False)
|
| 716 |
+
self.k_proj = nn.Linear(cfg.d_model, inner_kv, bias=False)
|
| 717 |
+
self.v_proj = nn.Linear(cfg.d_model, inner_kv, bias=False)
|
| 718 |
+
self.o_proj = nn.Linear(inner_q, cfg.d_model, bias=False)
|
| 719 |
+
|
| 720 |
+
def _expand_kv(self, kv: torch.Tensor) -> torch.Tensor:
|
| 721 |
+
"""Repeat KV heads along dim 1 to match the number of query heads.
|
| 722 |
+
|
| 723 |
+
With GQA group size G, each KV head is shared by G query heads.
|
| 724 |
+
``repeat_interleave(G, dim=1)`` produces the correct interleaved
|
| 725 |
+
expansion so that query head ``h`` is paired with KV head ``h // G``.
|
| 726 |
+
|
| 727 |
+
Args:
|
| 728 |
+
kv: Key or value tensor whose dim 1 is the KV-head axis.
|
| 729 |
+
Supported shapes: ``[B, Hk, T, d]`` (sequence) and
|
| 730 |
+
``[B, Hk, T, L, d]`` (depth stack).
|
| 731 |
+
|
| 732 |
+
Returns:
|
| 733 |
+
Tensor with dim 1 expanded from ``Hk`` to ``Hq = Hk × G``.
|
| 734 |
+
Returns *kv* unchanged when ``gqa_group == 1``.
|
| 735 |
+
"""
|
| 736 |
+
if self.gqa_group == 1:
|
| 737 |
+
return kv
|
| 738 |
+
return kv.repeat_interleave(self.gqa_group, dim=1)
|
| 739 |
+
|
| 740 |
+
def forward(
|
| 741 |
+
self,
|
| 742 |
+
x: torch.Tensor,
|
| 743 |
+
depth_k_cache: List[torch.Tensor],
|
| 744 |
+
depth_v_cache: List[torch.Tensor],
|
| 745 |
+
cos: torch.Tensor,
|
| 746 |
+
sin: torch.Tensor,
|
| 747 |
+
) -> torch.Tensor:
|
| 748 |
+
"""Compute MoDA attention output.
|
| 749 |
+
|
| 750 |
+
Args:
|
| 751 |
+
x: ``[B, T, D]`` input hidden states.
|
| 752 |
+
depth_k_cache: ``L`` tensors each ``[B, Hk, T, d]`` — depth keys.
|
| 753 |
+
depth_v_cache: Matching depth values.
|
| 754 |
+
cos/sin: RoPE tables ``[1, 1, T, d]``.
|
| 755 |
+
|
| 756 |
+
Returns:
|
| 757 |
+
``[B, T, D]`` output hidden states.
|
| 758 |
+
"""
|
| 759 |
+
B, T, D = x.shape
|
| 760 |
+
Hq, Hk, d = self.n_heads_q, self.n_heads_kv, self.head_dim
|
| 761 |
+
|
| 762 |
+
Q = self.q_proj(x).view(B, T, Hq, d).transpose(1, 2)
|
| 763 |
+
K = self.k_proj(x).view(B, T, Hk, d).transpose(1, 2)
|
| 764 |
+
V = self.v_proj(x).view(B, T, Hk, d).transpose(1, 2)
|
| 765 |
+
|
| 766 |
+
Q = apply_rotary_emb(Q, cos, sin)
|
| 767 |
+
K = apply_rotary_emb(K, cos, sin)
|
| 768 |
+
|
| 769 |
+
K_e = self._expand_kv(K)
|
| 770 |
+
V_e = self._expand_kv(V)
|
| 771 |
+
|
| 772 |
+
L = len(depth_k_cache)
|
| 773 |
+
|
| 774 |
+
if L == 0:
|
| 775 |
+
out = F.scaled_dot_product_attention(
|
| 776 |
+
Q,
|
| 777 |
+
K_e,
|
| 778 |
+
V_e,
|
| 779 |
+
is_causal=True,
|
| 780 |
+
dropout_p=self.dropout if self.training else 0.0,
|
| 781 |
+
scale=self.scale,
|
| 782 |
+
)
|
| 783 |
+
else:
|
| 784 |
+
# Sequence logits [B, Hq, T, T] with causal mask
|
| 785 |
+
seq_logits = torch.matmul(Q, K_e.transpose(-2, -1)) * self.scale
|
| 786 |
+
causal_mask = torch.triu(
|
| 787 |
+
torch.full((T, T), float("-inf"), device=x.device, dtype=Q.dtype),
|
| 788 |
+
diagonal=1,
|
| 789 |
+
)
|
| 790 |
+
seq_logits = seq_logits + causal_mask
|
| 791 |
+
|
| 792 |
+
# Depth KVs: [B, Hk, L, T, d] → [B, Hk, T, L, d]
|
| 793 |
+
K_depth = torch.stack(depth_k_cache, dim=2).permute(0, 1, 3, 2, 4)
|
| 794 |
+
V_depth = torch.stack(depth_v_cache, dim=2).permute(0, 1, 3, 2, 4)
|
| 795 |
+
K_depth_e = self._expand_kv(K_depth)
|
| 796 |
+
V_depth_e = self._expand_kv(V_depth)
|
| 797 |
+
|
| 798 |
+
# Depth logits [B, Hq, T, L]
|
| 799 |
+
depth_logits = torch.einsum("bhid,bhild->bhil", Q, K_depth_e) * self.scale
|
| 800 |
+
|
| 801 |
+
# Unified softmax over T + L positions
|
| 802 |
+
combined = torch.cat([seq_logits, depth_logits], dim=-1)
|
| 803 |
+
weights = F.softmax(combined, dim=-1)
|
| 804 |
+
if self.training and self.dropout > 0.0:
|
| 805 |
+
weights = F.dropout(weights, p=self.dropout)
|
| 806 |
+
|
| 807 |
+
seq_contrib = torch.matmul(weights[:, :, :, :T], V_e)
|
| 808 |
+
depth_contrib = torch.einsum(
|
| 809 |
+
"bhil,bhild->bhid", weights[:, :, :, T:], V_depth_e
|
| 810 |
+
)
|
| 811 |
+
out = seq_contrib + depth_contrib
|
| 812 |
+
|
| 813 |
+
out = out.transpose(1, 2).reshape(B, T, Hq * d)
|
| 814 |
+
return self.o_proj(out)
|
| 815 |
+
|
| 816 |
+
|
| 817 |
+
# ---------------------------------------------------------------------------
|
| 818 |
+
# MoDA Transformer Block
|
| 819 |
+
# ---------------------------------------------------------------------------
|
| 820 |
+
|
| 821 |
+
|
| 822 |
+
class MoDABlock(nn.Module):
|
| 823 |
+
"""Single MoDA + DeepSeek-MoE transformer block.
|
| 824 |
+
|
| 825 |
+
Structure (post-norm, per MoDA paper recommendation):
|
| 826 |
+
|
| 827 |
+
.. code-block::
|
| 828 |
+
|
| 829 |
+
x ──► Attention ──► + ──► RMSNorm ──► x_mid
|
| 830 |
+
x ↑ (residual)
|
| 831 |
+
x_mid ──► MoE ──► + ──► RMSNorm ──► x_out
|
| 832 |
+
x_mid ↑ (residual)
|
| 833 |
+
x_out ──► W_K^W ──► k_write } appended to MoDA depth KV cache
|
| 834 |
+
└─► W_V^W ──► v_write } by MoDAModel for the next layer
|
| 835 |
+
|
| 836 |
+
The MoE layer also returns an optional expert-level balance loss scalar
|
| 837 |
+
which is propagated up to :class:`MoDAModel` for inclusion in the total
|
| 838 |
+
training loss.
|
| 839 |
+
|
| 840 |
+
Args:
|
| 841 |
+
cfg: :class:`MoDAConfig` instance.
|
| 842 |
+
"""
|
| 843 |
+
|
| 844 |
+
def __init__(self, cfg: MoDAConfig) -> None:
|
| 845 |
+
"""Build one MoDA + MoE transformer block.
|
| 846 |
+
|
| 847 |
+
Constructs and wires together:
|
| 848 |
+
* ``attn`` — :class:`MoDAAttention` (depth-aware GQA).
|
| 849 |
+
* ``moe`` — :class:`DeepSeekMoE` (shared + routed experts).
|
| 850 |
+
* ``norm_attn`` / ``norm_ffn`` — post-sublayer :class:`RMSNorm`.
|
| 851 |
+
* ``k_write`` / ``v_write`` — depth-cache write projections
|
| 852 |
+
``D → n_heads_kv × head_dim``.
|
| 853 |
+
|
| 854 |
+
Args:
|
| 855 |
+
cfg: Model configuration.
|
| 856 |
+
"""
|
| 857 |
+
super().__init__()
|
| 858 |
+
inner_kv = cfg.n_heads_kv * cfg.head_dim
|
| 859 |
+
|
| 860 |
+
self.attn = MoDAAttention(cfg)
|
| 861 |
+
self.moe = DeepSeekMoE(cfg)
|
| 862 |
+
self.norm_attn = RMSNorm(cfg.d_model, cfg.norm_eps)
|
| 863 |
+
self.norm_ffn = RMSNorm(cfg.d_model, cfg.norm_eps)
|
| 864 |
+
|
| 865 |
+
# MoDA depth-cache write projections: K_l = X_l^out W_K^W, V_l = X_l^out W_V^W
|
| 866 |
+
self.k_write = nn.Linear(cfg.d_model, inner_kv, bias=False)
|
| 867 |
+
self.v_write = nn.Linear(cfg.d_model, inner_kv, bias=False)
|
| 868 |
+
|
| 869 |
+
self._n_heads_kv = cfg.n_heads_kv
|
| 870 |
+
self._head_dim = cfg.head_dim
|
| 871 |
+
|
| 872 |
+
def forward(
|
| 873 |
+
self,
|
| 874 |
+
x: torch.Tensor,
|
| 875 |
+
depth_k_cache: List[torch.Tensor],
|
| 876 |
+
depth_v_cache: List[torch.Tensor],
|
| 877 |
+
cos: torch.Tensor,
|
| 878 |
+
sin: torch.Tensor,
|
| 879 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, Optional[torch.Tensor]]:
|
| 880 |
+
"""Run one MoDA + MoE transformer block.
|
| 881 |
+
|
| 882 |
+
Args:
|
| 883 |
+
x: ``[B, T, D]`` input hidden states.
|
| 884 |
+
depth_k_cache: Depth keys from all preceding layers, each ``[B, Hk, T, d]``.
|
| 885 |
+
depth_v_cache: Matching depth values.
|
| 886 |
+
cos/sin: RoPE tables ``[1, 1, T, d]``.
|
| 887 |
+
|
| 888 |
+
Returns:
|
| 889 |
+
x_out: ``[B, T, D]`` updated hidden states.
|
| 890 |
+
k_write: ``[B, Hk, T, d]`` depth cache key for this layer.
|
| 891 |
+
v_write: ``[B, Hk, T, d]`` depth cache value for this layer.
|
| 892 |
+
balance_loss: Scalar expert-level balance loss, or ``None`` at inference.
|
| 893 |
+
"""
|
| 894 |
+
B, T, _ = x.shape
|
| 895 |
+
|
| 896 |
+
# Post-norm attention sub-layer
|
| 897 |
+
x = self.norm_attn(x + self.attn(x, depth_k_cache, depth_v_cache, cos, sin))
|
| 898 |
+
|
| 899 |
+
# Post-norm MoE sub-layer
|
| 900 |
+
moe_out, balance_loss = self.moe(x)
|
| 901 |
+
x = self.norm_ffn(x + moe_out)
|
| 902 |
+
|
| 903 |
+
# Depth write projections from X_l^out (full block output, after MoE)
|
| 904 |
+
k_write = (
|
| 905 |
+
self.k_write(x).view(B, T, self._n_heads_kv, self._head_dim).transpose(1, 2)
|
| 906 |
+
)
|
| 907 |
+
v_write = (
|
| 908 |
+
self.v_write(x).view(B, T, self._n_heads_kv, self._head_dim).transpose(1, 2)
|
| 909 |
+
)
|
| 910 |
+
|
| 911 |
+
# RoPE on k_write for positional consistency during future depth reads
|
| 912 |
+
k_write = apply_rotary_emb(k_write, cos, sin)
|
| 913 |
+
|
| 914 |
+
return x, k_write, v_write, balance_loss
|
| 915 |
+
|
| 916 |
+
|
| 917 |
+
# ---------------------------------------------------------------------------
|
| 918 |
+
# Full MoDA + MoE Language Model
|
| 919 |
+
# ---------------------------------------------------------------------------
|
| 920 |
+
|
| 921 |
+
|
| 922 |
+
class MoDAModel(nn.Module):
|
| 923 |
+
"""Decoder-only LM with Mixture-of-Depths Attention and DeepSeek MoE FFN.
|
| 924 |
+
|
| 925 |
+
Loss = LM cross-entropy + moe_balance_alpha × mean(per-layer balance losses)
|
| 926 |
+
|
| 927 |
+
The depth KV cache is a local list inside :meth:`forward` — never stored
|
| 928 |
+
on ``self``, so autograd is clean across independent forward calls.
|
| 929 |
+
|
| 930 |
+
Args:
|
| 931 |
+
cfg: :class:`MoDAConfig` specifying the full model.
|
| 932 |
+
"""
|
| 933 |
+
|
| 934 |
+
def __init__(self, cfg: MoDAConfig) -> None:
|
| 935 |
+
"""Build the full MoDA + MoE language model.
|
| 936 |
+
|
| 937 |
+
Constructs the token embedding, RoPE, all transformer blocks, a final
|
| 938 |
+
RMSNorm, and the language-model head. The embedding and LM-head
|
| 939 |
+
weights are tied so they share the same parameter.
|
| 940 |
+
|
| 941 |
+
Args:
|
| 942 |
+
cfg: :class:`MoDAConfig` that fully specifies the model.
|
| 943 |
+
"""
|
| 944 |
+
super().__init__()
|
| 945 |
+
self.cfg = cfg
|
| 946 |
+
|
| 947 |
+
self.embed = nn.Embedding(cfg.vocab_size, cfg.d_model)
|
| 948 |
+
self.rope = RotaryEmbedding(cfg.head_dim, cfg.max_seq_len, cfg.rope_base)
|
| 949 |
+
self.blocks = nn.ModuleList([MoDABlock(cfg) for _ in range(cfg.n_layers)])
|
| 950 |
+
self.norm_out = RMSNorm(cfg.d_model, cfg.norm_eps)
|
| 951 |
+
self.lm_head = nn.Linear(cfg.d_model, cfg.vocab_size, bias=False)
|
| 952 |
+
|
| 953 |
+
self.lm_head.weight = self.embed.weight # weight tying
|
| 954 |
+
|
| 955 |
+
self._init_weights()
|
| 956 |
+
|
| 957 |
+
def _init_weights(self) -> None:
|
| 958 |
+
"""Apply GPT-style weight initialisation to every sub-module.
|
| 959 |
+
|
| 960 |
+
* :class:`nn.Linear` and :class:`nn.Embedding` weights are drawn from
|
| 961 |
+
``Normal(0, 0.02)`` — the standard initialisation used by GPT-2 and
|
| 962 |
+
most subsequent transformer implementations.
|
| 963 |
+
* :class:`DeepSeekGate` weight matrices are re-initialised with
|
| 964 |
+
``kaiming_uniform`` (fan-in) to match the default ``nn.Linear``
|
| 965 |
+
init and avoid the Normal distribution being too narrow for a matrix
|
| 966 |
+
used without a subsequent non-linearity.
|
| 967 |
+
|
| 968 |
+
Called automatically at the end of :meth:`__init__`.
|
| 969 |
+
"""
|
| 970 |
+
for m in self.modules():
|
| 971 |
+
if isinstance(m, (nn.Linear, nn.Embedding)):
|
| 972 |
+
nn.init.normal_(m.weight, mean=0.0, std=0.02)
|
| 973 |
+
elif isinstance(m, DeepSeekGate):
|
| 974 |
+
nn.init.kaiming_uniform_(m.weight, a=math.sqrt(5))
|
| 975 |
+
|
| 976 |
+
def forward(
|
| 977 |
+
self,
|
| 978 |
+
input_ids: torch.Tensor,
|
| 979 |
+
labels: Optional[torch.Tensor] = None,
|
| 980 |
+
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
|
| 981 |
+
"""Run the full MoDA + MoE language model.
|
| 982 |
+
|
| 983 |
+
Args:
|
| 984 |
+
input_ids: ``[B, T]`` token indices.
|
| 985 |
+
labels: ``[B, T]`` targets for LM loss; -100 positions ignored.
|
| 986 |
+
|
| 987 |
+
Returns:
|
| 988 |
+
logits: ``[B, T, vocab_size]``.
|
| 989 |
+
loss: ``lm_loss + balance_loss`` if labels provided, else ``None``.
|
| 990 |
+
"""
|
| 991 |
+
B, T = input_ids.shape
|
| 992 |
+
if T > self.cfg.max_seq_len:
|
| 993 |
+
raise ValueError(
|
| 994 |
+
f"Sequence length {T} exceeds max_seq_len={self.cfg.max_seq_len}."
|
| 995 |
+
)
|
| 996 |
+
|
| 997 |
+
x = self.embed(input_ids)
|
| 998 |
+
cos, sin = self.rope(T)
|
| 999 |
+
|
| 1000 |
+
depth_k_cache: List[torch.Tensor] = []
|
| 1001 |
+
depth_v_cache: List[torch.Tensor] = []
|
| 1002 |
+
balance_losses: List[torch.Tensor] = []
|
| 1003 |
+
|
| 1004 |
+
for block in self.blocks:
|
| 1005 |
+
x, k_write, v_write, bal = block(x, depth_k_cache, depth_v_cache, cos, sin)
|
| 1006 |
+
depth_k_cache.append(k_write)
|
| 1007 |
+
depth_v_cache.append(v_write)
|
| 1008 |
+
if bal is not None:
|
| 1009 |
+
balance_losses.append(bal)
|
| 1010 |
+
|
| 1011 |
+
x = self.norm_out(x)
|
| 1012 |
+
logits = self.lm_head(x)
|
| 1013 |
+
|
| 1014 |
+
loss: Optional[torch.Tensor] = None
|
| 1015 |
+
if labels is not None:
|
| 1016 |
+
lm_loss = F.cross_entropy(
|
| 1017 |
+
logits.view(-1, self.cfg.vocab_size),
|
| 1018 |
+
labels.view(-1),
|
| 1019 |
+
ignore_index=-100,
|
| 1020 |
+
)
|
| 1021 |
+
if balance_losses and self.cfg.moe_balance_alpha > 0.0:
|
| 1022 |
+
avg_balance = torch.stack(balance_losses).mean()
|
| 1023 |
+
loss = lm_loss + self.cfg.moe_balance_alpha * avg_balance
|
| 1024 |
+
else:
|
| 1025 |
+
loss = lm_loss
|
| 1026 |
+
|
| 1027 |
+
return logits, loss
|
| 1028 |
+
|
| 1029 |
+
def num_parameters(self, trainable_only: bool = False) -> int:
|
| 1030 |
+
"""Count the total number of scalar parameters in the model.
|
| 1031 |
+
|
| 1032 |
+
Args:
|
| 1033 |
+
trainable_only: If ``True``, count only parameters with
|
| 1034 |
+
``requires_grad=True``, excluding frozen layers.
|
| 1035 |
+
|
| 1036 |
+
Returns:
|
| 1037 |
+
Integer parameter count.
|
| 1038 |
+
"""
|
| 1039 |
+
params = (
|
| 1040 |
+
self.parameters()
|
| 1041 |
+
if not trainable_only
|
| 1042 |
+
else (p for p in self.parameters() if p.requires_grad)
|
| 1043 |
+
)
|
| 1044 |
+
return sum(p.numel() for p in params)
|
| 1045 |
+
|
| 1046 |
+
def extra_repr(self) -> str:
|
| 1047 |
+
"""Return a one-line summary string shown inside ``repr(model)``.
|
| 1048 |
+
|
| 1049 |
+
Displayed by PyTorch's ``__repr__`` directly after the class name,
|
| 1050 |
+
before the sub-module tree.
|
| 1051 |
+
|
| 1052 |
+
Returns:
|
| 1053 |
+
Human-readable string listing key model dimensions and the total
|
| 1054 |
+
parameter count.
|
| 1055 |
+
"""
|
| 1056 |
+
c = self.cfg
|
| 1057 |
+
return (
|
| 1058 |
+
f"vocab={c.vocab_size}, d_model={c.d_model}, layers={c.n_layers}, "
|
| 1059 |
+
f"heads={c.n_heads_q}/{c.n_heads_kv} (GQA), "
|
| 1060 |
+
f"experts=shared×{c.n_shared_experts}+routed×{c.n_routed_experts}"
|
| 1061 |
+
f"(top-{c.n_activated_experts}), "
|
| 1062 |
+
f"params={self.num_parameters():,}"
|
| 1063 |
+
)
|
| 1064 |
+
|
| 1065 |
+
|
| 1066 |
+
# # ---------------------------------------------------------------------------
|
| 1067 |
+
# # Smoke test
|
| 1068 |
+
# # ---------------------------------------------------------------------------
|
| 1069 |
+
|
| 1070 |
+
# if __name__ == "__main__":
|
| 1071 |
+
# torch.manual_seed(42)
|
| 1072 |
+
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 1073 |
+
# print(f"Device: {device}")
|
| 1074 |
+
|
| 1075 |
+
# # Tiny config: 4 layers, 8 routed experts, top-2
|
| 1076 |
+
# cfg = MoDAConfig(
|
| 1077 |
+
# vocab_size=512,
|
| 1078 |
+
# d_model=128,
|
| 1079 |
+
# n_layers=4,
|
| 1080 |
+
# n_heads_q=4,
|
| 1081 |
+
# n_heads_kv=2,
|
| 1082 |
+
# head_dim=32,
|
| 1083 |
+
# max_seq_len=64,
|
| 1084 |
+
# # MoE: 2 shared + 8 routed, activate top-2
|
| 1085 |
+
# # (2+2)*64 = 256 ≈ equivalent to dense SwiGLU hidden~256
|
| 1086 |
+
# n_shared_experts=2,
|
| 1087 |
+
# n_routed_experts=8,
|
| 1088 |
+
# n_activated_experts=2,
|
| 1089 |
+
# expert_hidden_dim=64,
|
| 1090 |
+
# moe_balance_alpha=0.01,
|
| 1091 |
+
# moe_score_func="softmax",
|
| 1092 |
+
# )
|
| 1093 |
+
|
| 1094 |
+
# model = MoDAModel(cfg).to(device)
|
| 1095 |
+
# print(f"Parameters: {model.num_parameters():,}")
|
| 1096 |
+
# print(model)
|
| 1097 |
+
|
| 1098 |
+
# B, T = 2, 32
|
| 1099 |
+
# input_ids = torch.randint(0, cfg.vocab_size, (B, T), device=device)
|
| 1100 |
+
# labels = torch.randint(0, cfg.vocab_size, (B, T), device=device)
|
| 1101 |
+
|
| 1102 |
+
# logits, loss = model(input_ids, labels)
|
| 1103 |
+
# assert logits.shape == (B, T, cfg.vocab_size)
|
| 1104 |
+
# print(f"Logits shape : {logits.shape}")
|
| 1105 |
+
# print(f"Loss (LM + balance): {loss.item():.4f}")
|
| 1106 |
+
|
| 1107 |
+
# loss.backward()
|
| 1108 |
+
|
| 1109 |
+
# # Verify gradients
|
| 1110 |
+
# last_writes = {
|
| 1111 |
+
# f"blocks.{cfg.n_layers - 1}.k_write.weight",
|
| 1112 |
+
# f"blocks.{cfg.n_layers - 1}.v_write.weight",
|
| 1113 |
+
# }
|
| 1114 |
+
# missing = [
|
| 1115 |
+
# name
|
| 1116 |
+
# for name, p in model.named_parameters()
|
| 1117 |
+
# if p.grad is None and name not in last_writes
|
| 1118 |
+
# ]
|
| 1119 |
+
# if missing:
|
| 1120 |
+
# print(f"WARNING — unexpected missing gradients: {missing}")
|
| 1121 |
+
# else:
|
| 1122 |
+
# print("All parameters received gradients (excluding last-block writes).")
|
| 1123 |
+
|
| 1124 |
+
# # Spot-check: MoE gate weights must receive gradients (through balance loss P_i)
|
| 1125 |
+
# gate0_grad = model.blocks[0].moe.gate.weight.grad
|
| 1126 |
+
# assert gate0_grad is not None, "blocks[0].moe.gate.weight has no gradient!"
|
| 1127 |
+
# print(f"blocks[0].moe.gate.weight grad norm : {gate0_grad.norm().item():.6f}")
|
| 1128 |
+
|
| 1129 |
+
# # Spot-check: depth write projections gradient flows from layer ≥ 1 depth reads
|
| 1130 |
+
# k0_grad = model.blocks[0].k_write.weight.grad
|
| 1131 |
+
# assert k0_grad is not None, "blocks[0].k_write.weight has no gradient!"
|
| 1132 |
+
# print(f"blocks[0].k_write.weight grad norm : {k0_grad.norm().item():.6f}")
|
| 1133 |
+
|
| 1134 |
+
# print("Smoke test passed.")
|
pyproject.toml
CHANGED
|
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
|
|
| 5 |
|
| 6 |
[tool.poetry]
|
| 7 |
name = "open-mythos"
|
| 8 |
-
version = "0.
|
| 9 |
description = "OpenMythos — open-source theoretical reconstruction of the Claude Mythos Recurrent-Depth Transformer architecture"
|
| 10 |
license = "MIT"
|
| 11 |
authors = ["Kye Gomez <kye@swarms.world>"]
|
|
|
|
| 5 |
|
| 6 |
[tool.poetry]
|
| 7 |
name = "open-mythos"
|
| 8 |
+
version = "0.3.0"
|
| 9 |
description = "OpenMythos — open-source theoretical reconstruction of the Claude Mythos Recurrent-Depth Transformer architecture"
|
| 10 |
license = "MIT"
|
| 11 |
authors = ["Kye Gomez <kye@swarms.world>"]
|
test_main.py
CHANGED
|
@@ -1,10 +1,3 @@
|
|
| 1 |
-
"""
|
| 2 |
-
pytest suite for OpenMythos (second.py).
|
| 3 |
-
Tests every major class and feature: shapes, correctness invariants, and
|
| 4 |
-
architecture-specific properties (LTI stability, ACT halting, depth extrapolation,
|
| 5 |
-
KV cache consistency, GQA vs MLA swap).
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
import torch
|
| 9 |
import pytest
|
| 10 |
from open_mythos.main import (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import torch
|
| 2 |
import pytest
|
| 3 |
from open_mythos.main import (
|
tests/test_tokenizer.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pytest
|
| 2 |
+
from open_mythos.tokenizer import MythosTokenizer
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
@pytest.fixture(scope="module")
|
| 6 |
+
def tokenizer():
|
| 7 |
+
tok = MythosTokenizer()
|
| 8 |
+
print(f"\nLoaded tokenizer: {tok.tokenizer.name_or_path}")
|
| 9 |
+
return tok
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_loads(tokenizer):
|
| 13 |
+
assert tokenizer is not None
|
| 14 |
+
print(f"Tokenizer: {tokenizer}")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def test_vocab_size(tokenizer):
|
| 18 |
+
size = tokenizer.vocab_size
|
| 19 |
+
print(f"Vocab size: {size:,}")
|
| 20 |
+
assert size > 0
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def test_encode_returns_list_of_ints(tokenizer):
|
| 24 |
+
ids = tokenizer.encode("Hello, world!")
|
| 25 |
+
print(f"encode('Hello, world!') → {ids}")
|
| 26 |
+
assert isinstance(ids, list)
|
| 27 |
+
assert all(isinstance(i, int) for i in ids)
|
| 28 |
+
assert len(ids) > 0
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_encode_empty_string(tokenizer):
|
| 32 |
+
ids = tokenizer.encode("")
|
| 33 |
+
print(f"encode('') → {ids}")
|
| 34 |
+
assert isinstance(ids, list)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def test_decode_returns_string(tokenizer):
|
| 38 |
+
ids = tokenizer.encode("Hello, world!")
|
| 39 |
+
text = tokenizer.decode(ids)
|
| 40 |
+
print(f"decode({ids}) → '{text}'")
|
| 41 |
+
assert isinstance(text, str)
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_roundtrip(tokenizer):
|
| 45 |
+
original = "The quick brown fox jumps over the lazy dog."
|
| 46 |
+
ids = tokenizer.encode(original)
|
| 47 |
+
recovered = tokenizer.decode(ids)
|
| 48 |
+
print(f"original: '{original}'")
|
| 49 |
+
print(f"token ids: {ids}")
|
| 50 |
+
print(f"recovered: '{recovered}'")
|
| 51 |
+
assert original in recovered or recovered in original
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def test_encode_long_text(tokenizer):
|
| 55 |
+
text = "OpenMythos is a recurrent depth transformer. " * 100
|
| 56 |
+
ids = tokenizer.encode(text)
|
| 57 |
+
print(f"Long text ({len(text)} chars) → {len(ids)} tokens")
|
| 58 |
+
assert len(ids) > 100
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def test_custom_model_id():
|
| 62 |
+
tok = MythosTokenizer(model_id="openai/gpt-oss-20b")
|
| 63 |
+
print(f"Custom model_id vocab size: {tok.vocab_size:,}")
|
| 64 |
+
assert tok.vocab_size > 0
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def test_vocab_size_consistent(tokenizer):
|
| 68 |
+
outer = tokenizer.vocab_size
|
| 69 |
+
inner = tokenizer.tokenizer.vocab_size
|
| 70 |
+
print(f"vocab_size property: {outer:,} | inner tokenizer.vocab_size: {inner:,}")
|
| 71 |
+
assert outer == inner
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if __name__ == "__main__":
|
| 75 |
+
pytest.main([__file__, "--verbose", "-s"])
|