nkthebass commited on
Commit
4d49814
·
verified ·
1 Parent(s): b224971

TinyBrainBot-3M-Loop-v2: 3M-Loop continued on general math (final checkpoint)

Browse files
README.md ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ library_name: transformers
6
+ pipeline_tag: text-generation
7
+ tags:
8
+ - tinybrainbot
9
+ - small-language-model
10
+ - from-scratch
11
+ - recurrent-depth
12
+ - looped-transformer
13
+ - arithmetic
14
+ - custom_code
15
+ - base
16
+ datasets:
17
+ - HuggingFaceFW/fineweb-edu
18
+ - HuggingFaceTB/cosmopedia
19
+ - roneneldan/TinyStories
20
+ - wikimedia/wikipedia
21
+ model-index:
22
+ - name: tinybrainbot-3m-loop-v2
23
+ results:
24
+ - task:
25
+ type: text-generation
26
+ dataset:
27
+ type: allenai/ai2_arc
28
+ name: ARC-Easy
29
+ config: ARC-Easy
30
+ split: test
31
+ metrics:
32
+ - type: acc_norm
33
+ value: 33.46
34
+ name: acc_norm (0-shot)
35
+ - task:
36
+ type: text-generation
37
+ dataset:
38
+ type: allenai/ai2_arc
39
+ name: ARC-Challenge
40
+ config: ARC-Challenge
41
+ split: test
42
+ metrics:
43
+ - type: acc_norm
44
+ value: 22.78
45
+ name: acc_norm (0-shot)
46
+ - task:
47
+ type: text-generation
48
+ dataset:
49
+ type: Rowan/hellaswag
50
+ name: HellaSwag
51
+ split: validation
52
+ metrics:
53
+ - type: acc_norm
54
+ value: 27.17
55
+ name: acc_norm (0-shot)
56
+ - task:
57
+ type: text-generation
58
+ dataset:
59
+ type: ybisk/piqa
60
+ name: PIQA
61
+ split: validation
62
+ metrics:
63
+ - type: acc_norm
64
+ value: 54.79
65
+ name: acc_norm (0-shot)
66
+ - task:
67
+ type: text-generation
68
+ dataset:
69
+ type: AxiomicLabs/Arithmark-3.0
70
+ name: ArithMark-3.0
71
+ split: train
72
+ metrics:
73
+ - type: acc_norm
74
+ value: 90.70
75
+ name: acc_norm (0-shot)
76
+ ---
77
+
78
+ # TinyBrainBot-3M-Loop-v2
79
+
80
+ **A 2.96M-parameter math-first language model that reuses its layers.** Four transformer blocks are run three times in a loop, so every token passes through 12 layers of computation while only 4 layers of weights are stored. Trained from scratch on 6.0B tokens. Compared with [TinyBrainBot-3M-Loop](https://huggingface.co/nkthebass/tinybrainbot-3m-loop), its arithmetic carries over to bare expressions, order of operations and numbers written in words, not only word-problem stories.
81
+
82
+ | Benchmark (0-shot, full set) | acc_norm |
83
+ |---|--:|
84
+ | **Open SLM Leaderboard Intelligence Index** | **20.15** |
85
+ |   *(raw acc)* | *19.91* |
86
+ | HellaSwag | 27.17 |
87
+ |   *(raw acc)* | *27.01* |
88
+ | ARC-Easy | 33.46 |
89
+ |   *(raw acc)* | *33.59* |
90
+ | ARC-Challenge | 22.78 |
91
+ |   *(raw acc)* | *18.17* |
92
+ | PIQA | 54.79 |
93
+ |   *(raw acc)* | *55.98* |
94
+ | ArithMark-3.0 | 90.7 |
95
+ |   *(raw acc)* | *90.6* |
96
+
97
+ lm-eval 0.4.13 in float32 for HellaSwag, ARC and PIQA; the official `bencharithmark-3.py` (float32) for ArithMark-3.0.
98
+
99
+ ## Arithmetic beyond one benchmark
100
+
101
+ Every column below is a zero-shot, log-likelihood multiple-choice math test, all scored by us with the official `bencharithmark-3.py` scoring in float32, so every model is measured the same way (numbers can differ slightly from leaderboard listings).
102
+
103
+ | Model | Params | ArithMark-3 | AM3, numbers in words | AM3 as bare equations | ArithMark-2 | ArithMark-1 | Held-out stories |
104
+ |---|--:|--:|--:|--:|--:|--:|--:|
105
+ | **TinyBrainBot-3M-Loop-v2** | 2.96M | **90.7** | **55.0** | **96.8** | **83.8** | 36.1 | **80.1** |
106
+ | TinyBrainBot-3M-Loop | 2.96M | 89.3 | 27.3 | 88.2 | 58.1 | 29.4 | 81.0 |
107
+ | Inferno-Exp-1.7 | 394M | 85.9 | 59.1 | 86.9 | 59.9 | 47.9 | 74.0 |
108
+ | Pascaline-3M | 3M | 69.2 | 27.6 | 99.7 | 66.5 | 36.2 | 43.0 |
109
+ | SmolLM2-135M | 135M | 38.8 | 34.2 | 38.6 | 33.2 | 33.1 | 34.7 |
110
+ | GPT-S3-3M | 2.97M | 38.9 | 25.8 | 36.2 | 31.0 | 31.1 | 34.6 |
111
+
112
+ - **Numbers in words** and **bare equations** are ArithMark-3's own problems with only the surface changed.
113
+ - **ArithMark-2** uses its official metric (raw log-likelihood); its official `benchmark_arithmark-2.0.py` gives the same results.
114
+ - **Held-out stories:** about 1,600 ArithMark-style problems written by Gemma-3-27B and Llama-3.3-70B, models that wrote none of our training data, with every number and answer set by Python.
115
+ - **ArithMark-1:** its answer key mostly evaluates expressions strictly left to right (e.g. `14 * -17 + -1 * -18 = 4302`), while ArithMark-2 and ordinary arithmetic use operator precedence (that expression is -220). This model follows precedence, so it scores lower on ArithMark-1's longer expressions (62% on its two-operand stage, falling with each extra operator).
116
+
117
+ ## How it works
118
+
119
+ - **Recurrent depth.** 4 shared blocks (width 256, 4 attention heads of 64 with 2 KV heads, SwiGLU 560, RoPE, RMSNorm) are applied 3 times. Each pass adds a learned loop embedding and a gated copy of the input embedding. Training sampled 2 to 4 loops per step, and you can change the count at inference (`model.model.eval_loops = 4`); 3 is best.
120
+ - **Factorized embedding.** The 4,104-token vocabulary uses 96-dimensional embeddings projected up to 256 (and down again before the tied output layer), which frees about 630k parameters for the 4th block.
121
+ - **Numbers.** A units-first digit tokenizer writes every number as a length token plus its digits, smallest place first, and a digit-place embedding tells the model which place each digit is in.
122
+
123
+ ## Training
124
+
125
+ **Stage 1 (4.46B tokens):** the TinyBrainBot-3M-Loop run up to the start of its learning-rate decay.
126
+
127
+ | Part | Share | What |
128
+ |---|--:|---|
129
+ | Q&A and facts | 25% | our distilled Q&A, short facts, explanations |
130
+ | Arithmetic | 20% | word problems: about 150k different story wordings written by ling-3.0-flash and Mistral-Small-3.2, each checked by solving it, with every number and answer filled in by Python; plus our own relation (more/fewer than) and story generators |
131
+ | Synthetic textbooks | 15% | Cosmopedia (Stanford, OpenStax) |
132
+ | Stories | 15% | TinyStories |
133
+ | Commonsense | 10% | Cosmopedia WikiHow, our everyday-commonsense prose |
134
+ | Web | 10% | FineWeb-Edu |
135
+ | Reference | 5% | Wikipedia |
136
+
137
+ **Stage 2 (1.5B tokens, math-first):** constant learning rate for 1.0B tokens, then a decay to zero over the last 0.5B.
138
+
139
+ | Part | Share | What |
140
+ |---|--:|---|
141
+ | Story arithmetic | 25% | the same verified wordings, with a quarter of the problems' numbers written in words |
142
+ | Bare arithmetic | 25% | our generator: expressions with order of operations, parentheses, negative numbers and 2-5 operands, in about 19 forms ("43 + 19 = 62", "What is ...?", "Compute ...:") |
143
+ | Number words | 8% | words to digits and back, arithmetic in words, counting, comparisons |
144
+ | Relations and stories | 12% | our relation and story generators |
145
+ | Q&A and facts | 20% | as in stage 1 |
146
+ | Cosmopedia, FineWeb-Edu | 10% | as in stage 1 |
147
+
148
+ No benchmark training or test data was used, and none of our generators copy any benchmark's problems, code or sentences.
149
+
150
+ ## Use
151
+
152
+ ```python
153
+ from transformers import AutoModelForCausalLM, AutoTokenizer
154
+ repo = "nkthebass/tinybrainbot-3m-loop-v2"
155
+ tok = AutoTokenizer.from_pretrained(repo, trust_remote_code=True)
156
+ model = AutoModelForCausalLM.from_pretrained(repo, trust_remote_code=True)
157
+ ids = tok("Mia had 46 stickers. She gave 18 to her brother, so now Mia has", return_tensors="pt").input_ids
158
+ out = model.generate(ids, max_new_tokens=8, do_sample=False)
159
+ print(tok.decode(out[0][ids.shape[1]:]))
160
+ ```
161
+
162
+ `trust_remote_code=True` is required (custom architecture and tokenizer). There is no KV cache: generation recomputes the prefix, which is fast at this size. Padded batches work on either side (the model honors `attention_mask`). There is no GGUF, because llama.cpp does not support the looped architecture.
163
+
164
+ To reproduce the scores:
165
+ ```
166
+ lm_eval --model hf --model_args pretrained=nkthebass/tinybrainbot-3m-loop-v2,dtype=float32,trust_remote_code=True --tasks arc_easy,arc_challenge,hellaswag,piqa --num_fewshot 0
167
+ python bencharithmark-3.py --model nkthebass/tinybrainbot-3m-loop-v2 --dtype float32
168
+ ```
169
+
170
+ ## Limitations
171
+
172
+ - A 3M base model: it writes simple English and makes up facts. It is not a chat model.
173
+ - Arithmetic is strongest on short problems; long, chatty setups and long left-to-right chains lower its accuracy.
174
+ - 1,024-token context.
config.json ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "TBBLoopForCausalLM"
4
+ ],
5
+ "model_type": "tbbloop",
6
+ "auto_map": {
7
+ "AutoConfig": "configuration_tbbloop.TBBLoopConfig",
8
+ "AutoModelForCausalLM": "modeling_tbbloop.TBBLoopForCausalLM"
9
+ },
10
+ "native": {
11
+ "vocab_size": 4104,
12
+ "hidden_size": 256,
13
+ "n_layers": 3,
14
+ "n_heads": 4,
15
+ "head_dim": 64,
16
+ "n_kv_heads": 2,
17
+ "ffn_size": 560,
18
+ "context_length": 1024,
19
+ "rope_theta": 10000.0,
20
+ "rms_norm_eps": 1e-05,
21
+ "tie_embeddings": true,
22
+ "init_std": 0.02,
23
+ "dropout": 0.0,
24
+ "gradient_checkpointing": false,
25
+ "loop_unique": 4,
26
+ "n_loops": 3,
27
+ "loop_train_min": 2,
28
+ "loop_train_max": 4,
29
+ "digit_place": true,
30
+ "digit_lo": 24,
31
+ "digit_hi": 33,
32
+ "place_vocab": 16,
33
+ "embed_rank": 96
34
+ },
35
+ "eval_loops": 0,
36
+ "vocab_size": 4104,
37
+ "hidden_size": 256,
38
+ "num_hidden_layers": 4,
39
+ "max_position_embeddings": 1024,
40
+ "tie_word_embeddings": true,
41
+ "bos_token_id": 0,
42
+ "eos_token_id": 0,
43
+ "torch_dtype": "float32",
44
+ "use_cache": false
45
+ }
configuration_tbbloop.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Config for TinyBrainBot recurrent-depth models (fields mirror the native TinyBrainBotConfig)."""
2
+ from transformers import PretrainedConfig
3
+
4
+
5
+ class TBBLoopConfig(PretrainedConfig):
6
+ model_type = "tbbloop"
7
+
8
+ def __init__(self, native=None, eval_loops=0, tie_word_embeddings=True, **kwargs):
9
+ self.native = native or {}
10
+ self.eval_loops = eval_loops
11
+ super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs)
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0c14a8e8c807fe53e39dabe1a86e09b1d790822f4e80efe35bf250e67aa9ba93
3
+ size 11861372
modeling_tbbloop.py ADDED
@@ -0,0 +1,415 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TinyBrainBot recurrent-depth model for transformers (trust_remote_code=True).
2
+
3
+ Below the marker is the exact architecture source the model was trained with. The wrapper builds it from the
4
+ config, so weights load unchanged. Generation recomputes the prefix each step (no KV cache): fine at this size.
5
+ """
6
+ from __future__ import annotations
7
+ # ======================= native architecture (verbatim) =======================
8
+ """TinyBrainBot reference architecture.
9
+
10
+ Matches the blueprint spec:
11
+ - decoder-only transformer
12
+ - 8 layers, hidden 1536, 12 heads (head_dim = 128), FFN 6144
13
+ - RoPE positional encoding
14
+ - RMSNorm (pre-norm)
15
+ - SwiGLU feed-forward
16
+ - 36k Unigram vocab
17
+ - ctx 2048
18
+
19
+ The defaults here produce a ~300M parameter model. Tests use smaller overrides.
20
+ """
21
+
22
+ from dataclasses import dataclass, field, asdict
23
+ from typing import Optional
24
+
25
+ import math
26
+
27
+ import torch
28
+ import torch.nn as nn
29
+ import torch.nn.functional as F
30
+ from torch.utils.checkpoint import checkpoint as _ckpt
31
+
32
+
33
+ @dataclass
34
+ class TinyBrainBotConfig:
35
+ # Blueprint defaults
36
+ vocab_size: int = 36_000
37
+ hidden_size: int = 1536
38
+ n_layers: int = 8
39
+ n_heads: int = 12
40
+ head_dim: int = 128
41
+ n_kv_heads: Optional[int] = None # None => MHA (== n_heads); a smaller divisor => GQA
42
+ ffn_size: int = 6144
43
+ context_length: int = 2048
44
+ rope_theta: float = 10_000.0
45
+ rms_norm_eps: float = 1e-5
46
+ tie_embeddings: bool = True
47
+ init_std: float = 0.02
48
+ dropout: float = 0.0
49
+ gradient_checkpointing: bool = False
50
+ # recurrent depth (opt-in): store `loop_unique` blocks and run them `n_loops` times. 0 = ordinary stack.
51
+ loop_unique: int = 0
52
+ n_loops: int = 1
53
+ loop_train_min: int = 0 # training samples the loop count from [min, max] per step (0 = n_loops)
54
+ loop_train_max: int = 0
55
+ # digit-place embedding (opt-in): each digit token gets its index in its digit run (1 = first digit; with
56
+ # units-first numbers that is the units digit). Digit token ids are digit_lo..digit_hi.
57
+ digit_place: bool = False
58
+ digit_lo: int = -1
59
+ digit_hi: int = -1
60
+ place_vocab: int = 16
61
+ # factorized embedding (opt-in): a vocab x embed_rank table, projected up to hidden_size on the way in and down
62
+ # to embed_rank before the (tied) output layer. 0 = ordinary vocab x hidden_size embedding.
63
+ embed_rank: int = 0
64
+
65
+ @property
66
+ def attention_dim(self) -> int:
67
+ return self.n_heads * self.head_dim
68
+
69
+ @property
70
+ def num_kv_heads(self) -> int:
71
+ """KV head count — equals n_heads for MHA, fewer for GQA."""
72
+ return self.n_kv_heads if self.n_kv_heads is not None else self.n_heads
73
+
74
+ @property
75
+ def kv_dim(self) -> int:
76
+ return self.num_kv_heads * self.head_dim
77
+
78
+ def summary(self) -> dict:
79
+ return {
80
+ **asdict(self),
81
+ "attention_dim": self.attention_dim,
82
+ }
83
+
84
+
85
+ # ----------------------------------------------------------------------------
86
+ # Building blocks
87
+ # ----------------------------------------------------------------------------
88
+
89
+ class RMSNorm(nn.Module):
90
+ def __init__(self, dim: int, eps: float = 1e-5):
91
+ super().__init__()
92
+ self.eps = eps
93
+ self.weight = nn.Parameter(torch.ones(dim))
94
+
95
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
96
+ # computed in float32 for stability, cast back to input dtype
97
+ orig_dtype = x.dtype
98
+ x = x.float()
99
+ variance = x.pow(2).mean(dim=-1, keepdim=True)
100
+ x = x * torch.rsqrt(variance + self.eps)
101
+ return (self.weight * x).to(orig_dtype)
102
+
103
+
104
+ def _precompute_rope_cache(dim: int, max_len: int, theta: float, device, dtype):
105
+ # standard RoPE frequencies
106
+ inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2, device=device).float() / dim))
107
+ t = torch.arange(max_len, device=device).float()
108
+ freqs = torch.outer(t, inv_freq) # [max_len, dim/2]
109
+ cos = freqs.cos().to(dtype)
110
+ sin = freqs.sin().to(dtype)
111
+ return cos, sin
112
+
113
+
114
+ def _apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
115
+ # x: [B, H, T, D]; cos/sin: [T, D/2], or [B, T, D/2] for per-row positions (padded batches)
116
+ x1, x2 = x[..., ::2], x[..., 1::2]
117
+ if cos.dim() == 3:
118
+ cos_b, sin_b = cos.unsqueeze(1), sin.unsqueeze(1)
119
+ else:
120
+ # broadcast cos/sin over [B, H, T, D/2]
121
+ cos_b = cos.unsqueeze(0).unsqueeze(0)
122
+ sin_b = sin.unsqueeze(0).unsqueeze(0)
123
+ y1 = x1 * cos_b - x2 * sin_b
124
+ y2 = x1 * sin_b + x2 * cos_b
125
+ out = torch.stack([y1, y2], dim=-1).flatten(-2)
126
+ return out
127
+
128
+
129
+ def _repeat_kv(x: torch.Tensor, n_rep: int) -> torch.Tensor:
130
+ """Expand KV heads to match query heads for GQA. x: [B, n_kv, T, head_dim]."""
131
+ if n_rep == 1:
132
+ return x
133
+ B, n_kv, T, hd = x.shape
134
+ return x[:, :, None, :, :].expand(B, n_kv, n_rep, T, hd).reshape(B, n_kv * n_rep, T, hd)
135
+
136
+
137
+ class CausalSelfAttention(nn.Module):
138
+ def __init__(self, cfg: TinyBrainBotConfig):
139
+ super().__init__()
140
+ self.cfg = cfg
141
+ nkv = cfg.num_kv_heads
142
+ assert cfg.n_heads % nkv == 0, "n_heads must be divisible by n_kv_heads"
143
+ # Combined Q/K/V. With MHA (nkv == n_heads) this is 3*attention_dim — the exact
144
+ # shape older checkpoints stored, so they load unchanged. With GQA it shrinks K/V.
145
+ self.qkv = nn.Linear(cfg.hidden_size, (cfg.n_heads + 2 * nkv) * cfg.head_dim, bias=False)
146
+ self.out_proj = nn.Linear(cfg.attention_dim, cfg.hidden_size, bias=False)
147
+ self.scale = 1.0 / math.sqrt(cfg.head_dim)
148
+ self._q_size = cfg.n_heads * cfg.head_dim
149
+ self._kv_size = nkv * cfg.head_dim
150
+ self._n_rep = cfg.n_heads // nkv
151
+
152
+ def forward(
153
+ self,
154
+ x: torch.Tensor,
155
+ cos: torch.Tensor,
156
+ sin: torch.Tensor,
157
+ past_kv: Optional[tuple[torch.Tensor, torch.Tensor]] = None,
158
+ attn_mask: Optional[torch.Tensor] = None,
159
+ ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
160
+ B, T, _ = x.shape
161
+ nkv = self.cfg.num_kv_heads
162
+ q, k, v = self.qkv(x).split([self._q_size, self._kv_size, self._kv_size], dim=-1)
163
+ q = q.view(B, T, self.cfg.n_heads, self.cfg.head_dim).transpose(1, 2)
164
+ k = k.view(B, T, nkv, self.cfg.head_dim).transpose(1, 2)
165
+ v = v.view(B, T, nkv, self.cfg.head_dim).transpose(1, 2)
166
+
167
+ if cos.dim() == 3: # per-row positions, already [B, T, D/2]
168
+ q = _apply_rope(q, cos, sin)
169
+ k = _apply_rope(k, cos, sin)
170
+ else:
171
+ offset = past_kv[0].shape[-2] if past_kv is not None else 0
172
+ q = _apply_rope(q, cos[offset:offset + T], sin[offset:offset + T])
173
+ k = _apply_rope(k, cos[offset:offset + T], sin[offset:offset + T])
174
+
175
+ if past_kv is not None:
176
+ k = torch.cat([past_kv[0], k], dim=-2)
177
+ v = torch.cat([past_kv[1], v], dim=-2)
178
+ new_kv = (k, v) # cache holds the SMALL (n_kv) heads — the whole point of GQA
179
+
180
+ # Expand KV to n_heads for attention (no-op under MHA).
181
+ k = _repeat_kv(k, self._n_rep)
182
+ v = _repeat_kv(v, self._n_rep)
183
+
184
+ if attn_mask is not None: # padded batch: causal AND not-a-pad-key, [B, 1, T, T] bool
185
+ attn = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
186
+ else:
187
+ is_causal = T > 1
188
+ attn = F.scaled_dot_product_attention(q, k, v, is_causal=is_causal)
189
+ attn = attn.transpose(1, 2).contiguous().view(B, T, self.cfg.attention_dim)
190
+ return self.out_proj(attn), new_kv
191
+
192
+
193
+ class SwiGLU(nn.Module):
194
+ """Gated SiLU feed-forward (as in LLaMA / Mistral)."""
195
+
196
+ def __init__(self, cfg: TinyBrainBotConfig):
197
+ super().__init__()
198
+ self.gate = nn.Linear(cfg.hidden_size, cfg.ffn_size, bias=False)
199
+ self.up = nn.Linear(cfg.hidden_size, cfg.ffn_size, bias=False)
200
+ self.down = nn.Linear(cfg.ffn_size, cfg.hidden_size, bias=False)
201
+
202
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
203
+ return self.down(F.silu(self.gate(x)) * self.up(x))
204
+
205
+
206
+ class DecoderBlock(nn.Module):
207
+ def __init__(self, cfg: TinyBrainBotConfig):
208
+ super().__init__()
209
+ self.attn_norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
210
+ self.attn = CausalSelfAttention(cfg)
211
+ self.ffn_norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
212
+ self.ffn = SwiGLU(cfg)
213
+
214
+ def forward(
215
+ self,
216
+ x: torch.Tensor,
217
+ cos: torch.Tensor,
218
+ sin: torch.Tensor,
219
+ past_kv: Optional[tuple[torch.Tensor, torch.Tensor]] = None,
220
+ attn_mask: Optional[torch.Tensor] = None,
221
+ ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor]]:
222
+ attn_out, new_kv = self.attn(self.attn_norm(x), cos, sin, past_kv, attn_mask)
223
+ x = x + attn_out
224
+ x = x + self.ffn(self.ffn_norm(x))
225
+ return x, new_kv
226
+
227
+ def forward_no_cache(self, x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
228
+ out, _ = self.forward(x, cos, sin, None)
229
+ return out
230
+
231
+
232
+ class TinyBrainBotModel(nn.Module):
233
+ def __init__(self, cfg: TinyBrainBotConfig):
234
+ super().__init__()
235
+ self.cfg = cfg
236
+ r = cfg.embed_rank if cfg.embed_rank > 0 else cfg.hidden_size
237
+ self.embed = nn.Embedding(cfg.vocab_size, r)
238
+ self.embed_up = nn.Linear(r, cfg.hidden_size, bias=False) if cfg.embed_rank > 0 else None
239
+ self.head_down = nn.Linear(cfg.hidden_size, r, bias=False) if cfg.embed_rank > 0 else None
240
+ n_blocks = cfg.loop_unique if cfg.loop_unique > 0 else cfg.n_layers
241
+ self.blocks = nn.ModuleList([DecoderBlock(cfg) for _ in range(n_blocks)])
242
+ # recurrent depth: a learned vector per loop pass, and a gate re-injecting the input embedding each pass
243
+ # (named *_norm so the optimiser puts it in the no-weight-decay group)
244
+ self.loop_emb = nn.Embedding(32, cfg.hidden_size) if cfg.loop_unique > 0 else None
245
+ self.reinject_norm = nn.Parameter(torch.tensor(0.5)) if cfg.loop_unique > 0 else None
246
+ self.place = nn.Embedding(cfg.place_vocab, cfg.hidden_size) if cfg.digit_place else None
247
+ self.train_loops: Optional[int] = None # set per step by the trainer (same on every rank)
248
+ self.eval_loops: Optional[int] = None # override for evaluation ("think longer")
249
+ self.final_norm = RMSNorm(cfg.hidden_size, cfg.rms_norm_eps)
250
+ self.lm_head = nn.Linear(r, cfg.vocab_size, bias=False)
251
+ if cfg.tie_embeddings:
252
+ self.lm_head.weight = self.embed.weight
253
+
254
+ self._rope_cache: Optional[tuple[torch.Tensor, torch.Tensor]] = None
255
+ self._rope_device: Optional[torch.device] = None
256
+ self._rope_dtype: Optional[torch.dtype] = None
257
+
258
+ self.apply(self._init_weights)
259
+ if self.embed_up is not None:
260
+ # keep the residual stream at the usual scale: up-projection ~ 1/sqrt(rank)
261
+ nn.init.normal_(self.embed_up.weight, std=r ** -0.5)
262
+ nn.init.normal_(self.head_down.weight, std=cfg.hidden_size ** -0.5)
263
+
264
+ def _init_weights(self, m: nn.Module) -> None:
265
+ if isinstance(m, nn.Linear):
266
+ nn.init.normal_(m.weight, std=self.cfg.init_std)
267
+ if m.bias is not None:
268
+ nn.init.zeros_(m.bias)
269
+ elif isinstance(m, nn.Embedding):
270
+ nn.init.normal_(m.weight, std=self.cfg.init_std)
271
+
272
+ def _place_ids(self, ids: torch.Tensor) -> torch.Tensor:
273
+ d = (ids >= self.cfg.digit_lo) & (ids <= self.cfg.digit_hi)
274
+ idx = torch.arange(ids.shape[1], device=ids.device).expand_as(ids)
275
+ last_nd = torch.cummax(torch.where(d, torch.full_like(idx, -1), idx), dim=1).values
276
+ return torch.where(d, (idx - last_nd).clamp(max=self.cfg.place_vocab - 1), torch.zeros_like(idx))
277
+
278
+ def loops_now(self) -> int:
279
+ if self.training and self.train_loops is not None:
280
+ return self.train_loops
281
+ if not self.training and self.eval_loops is not None:
282
+ return self.eval_loops
283
+ return self.cfg.n_loops
284
+
285
+ def _ensure_rope(self, seq_len: int, device: torch.device, dtype: torch.dtype) -> tuple[torch.Tensor, torch.Tensor]:
286
+ need_rebuild = (
287
+ self._rope_cache is None
288
+ or self._rope_device != device
289
+ or self._rope_dtype != dtype
290
+ or self._rope_cache[0].shape[0] < seq_len
291
+ )
292
+ if need_rebuild:
293
+ target_len = max(seq_len, self.cfg.context_length)
294
+ cos, sin = _precompute_rope_cache(self.cfg.head_dim, target_len, self.cfg.rope_theta, device, dtype)
295
+ self._rope_cache = (cos, sin)
296
+ self._rope_device = device
297
+ self._rope_dtype = dtype
298
+ return self._rope_cache
299
+
300
+ def forward(
301
+ self,
302
+ input_ids: torch.Tensor,
303
+ labels: Optional[torch.Tensor] = None,
304
+ past_kv: Optional[list[tuple[torch.Tensor, torch.Tensor]]] = None,
305
+ use_cache: bool = False,
306
+ attention_mask: Optional[torch.Tensor] = None,
307
+ ) -> dict:
308
+ B, T = input_ids.shape
309
+ past_len = past_kv[0][0].shape[-2] if past_kv is not None else 0
310
+ if T + past_len > self.cfg.context_length:
311
+ raise ValueError(f"sequence length {T + past_len} exceeds context_length {self.cfg.context_length}")
312
+ x = self.embed(input_ids)
313
+ if self.embed_up is not None:
314
+ x = self.embed_up(x)
315
+ if self.place is not None:
316
+ x = x + self.place(self._place_ids(input_ids))
317
+ cos, sin = self._ensure_rope(T + past_len, x.device, x.dtype)
318
+ amask = None
319
+ if attention_mask is not None and not bool(attention_mask.all()):
320
+ # padded batch (left or right): RoPE positions count from each row's first real token, pad keys are
321
+ # masked out, and pad query rows attend to themselves only (no NaN). Unpadded input takes the old path.
322
+ if past_kv is not None or use_cache:
323
+ raise ValueError("attention_mask with padding is not supported together with a KV cache")
324
+ am = attention_mask.to(device=x.device, dtype=torch.bool)
325
+ pos = (am.long().cumsum(-1) - 1).clamp(min=0)
326
+ cos, sin = cos[pos], sin[pos]
327
+ eye = torch.eye(T, dtype=torch.bool, device=x.device)
328
+ amask = (eye.tril() | torch.ones(T, T, dtype=torch.bool, device=x.device).tril()) & am[:, None, None, :]
329
+ amask = amask | eye[None, None]
330
+ use_ckpt = self.cfg.gradient_checkpointing and self.training
331
+ new_past: list[tuple[torch.Tensor, torch.Tensor]] = []
332
+ if self.cfg.loop_unique > 0:
333
+ if past_kv is not None or use_cache:
334
+ raise ValueError("recurrent-depth models do not use a KV cache (recompute the prefix)")
335
+ x0 = x
336
+ for k in range(self.loops_now()):
337
+ x = x + self.loop_emb.weight[k].to(x.dtype)
338
+ if k > 0:
339
+ x = x + self.reinject_norm.to(x.dtype) * x0
340
+ for block in self.blocks:
341
+ x = _ckpt(block.forward_no_cache, x, cos, sin, use_reentrant=False) if use_ckpt else block(x, cos, sin, None, amask)[0]
342
+ for i, block in enumerate(self.blocks if self.cfg.loop_unique == 0 else []):
343
+ layer_past = past_kv[i] if past_kv is not None else None
344
+ if use_ckpt:
345
+ x = _ckpt(block.forward_no_cache, x, cos, sin, use_reentrant=False)
346
+ else:
347
+ x, kv = block(x, cos, sin, layer_past, amask)
348
+ if use_cache:
349
+ new_past.append(kv)
350
+ x = self.final_norm(x)
351
+ logits = self.lm_head(self.head_down(x) if self.head_down is not None else x)
352
+
353
+ out = {"logits": logits}
354
+ if use_cache and not use_ckpt:
355
+ out["past_kv"] = new_past
356
+ if labels is not None:
357
+ # standard next-token loss, labels shifted upstream
358
+ # unsqueeze(0) makes loss 1-D so torch.nn.DataParallel can
359
+ # gather per-device losses along dim 0 (noop on single GPU).
360
+ if (labels != -100).any():
361
+ loss = F.cross_entropy(
362
+ logits.view(-1, self.cfg.vocab_size),
363
+ labels.view(-1),
364
+ ignore_index=-100,
365
+ )
366
+ else:
367
+ # SFT edge case: a packed window can land entirely in masked
368
+ # (user/system) tokens, leaving no supervised targets. cross_entropy
369
+ # would then divide by zero and return NaN, poisoning training.
370
+ # Emit a graph-connected zero loss for these batches instead.
371
+ loss = logits.sum() * 0.0
372
+ out["loss"] = loss.unsqueeze(0)
373
+ return out
374
+
375
+ def num_parameters(self) -> int:
376
+ return sum(p.numel() for p in self.parameters())
377
+
378
+ # ======================= transformers wrapper =======================
379
+ from transformers import PreTrainedModel, GenerationMixin
380
+ from transformers.modeling_outputs import CausalLMOutput
381
+ from .configuration_tbbloop import TBBLoopConfig
382
+
383
+
384
+ class TBBLoopForCausalLM(PreTrainedModel, GenerationMixin):
385
+ config_class = TBBLoopConfig
386
+ _tied_weights_keys = ["model.lm_head.weight"]
387
+ _supports_cache_class = False
388
+
389
+ def __init__(self, config):
390
+ super().__init__(config)
391
+ self.model = TinyBrainBotModel(TinyBrainBotConfig(**config.native))
392
+ if getattr(config, "eval_loops", 0):
393
+ self.model.eval_loops = int(config.eval_loops)
394
+
395
+ def _init_weights(self, module):
396
+ pass # weights always come from the checkpoint
397
+
398
+ def get_input_embeddings(self):
399
+ return self.model.embed
400
+
401
+ def set_input_embeddings(self, value):
402
+ self.model.embed = value
403
+
404
+ def get_output_embeddings(self):
405
+ return self.model.lm_head
406
+
407
+ def forward(self, input_ids=None, attention_mask=None, labels=None, **kwargs):
408
+ logits = self.model(input_ids, attention_mask=attention_mask)["logits"]
409
+ loss = None
410
+ if labels is not None:
411
+ loss = F.cross_entropy(logits[:, :-1].reshape(-1, logits.shape[-1]).float(), labels[:, 1:].reshape(-1), ignore_index=-100)
412
+ return CausalLMOutput(loss=loss, logits=logits)
413
+
414
+ def prepare_inputs_for_generation(self, input_ids, **kwargs):
415
+ return {"input_ids": input_ids[:, -self.model.cfg.context_length:]}
special_tokens_map.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ "<|user|>",
4
+ "<|assistant|>",
5
+ "<|system|>",
6
+ "<|end|>",
7
+ "<think>",
8
+ "</think>",
9
+ "<|sep|>"
10
+ ],
11
+ "bos_token": "<|endoftext|>",
12
+ "eos_token": "<|endoftext|>",
13
+ "pad_token": "<|pad|>"
14
+ }
tbb_meta.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "byte-level BPE (HF tokenizers)",
3
+ "vocab_size": 4096,
4
+ "special_tokens": [
5
+ "<|endoftext|>",
6
+ "<|user|>",
7
+ "<|assistant|>",
8
+ "<|system|>",
9
+ "<|end|>",
10
+ "<think>",
11
+ "</think>",
12
+ "<|sep|>",
13
+ "<|pad|>"
14
+ ],
15
+ "digits": "split individually",
16
+ "normalization": "none",
17
+ "sample_chars": 400000000,
18
+ "sample_shares": {
19
+ "qa-plain": 0.14100000000000001,
20
+ "qa-distill": 0.055,
21
+ "facts-plain": 0.041,
22
+ "capability-distill": 0.0165,
23
+ "reasoning-distill": 0.006500000000000001,
24
+ "math-direct-3m": 0.19999999999999998,
25
+ "cosmopedia-wikihow": 0.0757,
26
+ "commonsense-distill": 0.0092,
27
+ "procedural-anneal": 0.006900000000000001,
28
+ "everyday-activity": 0.0045000000000000005,
29
+ "social-situations": 0.0045000000000000005,
30
+ "everyday-reasoning": 0.00135,
31
+ "ds-prose": 0.00135,
32
+ "cosmopedia-stanford": 0.12300000000000001,
33
+ "cosmopedia-openstax": 0.027000000000000003,
34
+ "tiny-stories-v2": 0.14150000000000001,
35
+ "fineweb-edu-10bt": 0.09,
36
+ "wiki-dense": 0.055
37
+ }
38
+ }
tokenization_tbbuf.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """TinyBrainBot units-first number tokenizer (byte-level BPE + length-prefixed, reversed digit runs).
2
+
3
+ Every run of 1-8 digits is written as a length token and then its digits units first:
4
+ "62" -> "<len2>26" "1234" -> "<len4>4321"
5
+ so a model emits the units digit first and carries flow forward in time. Digit runs touching ":" (clock
6
+ times like 3:15) and runs longer than 8 digits are left unchanged. The text is rewritten on encode and
7
+ restored on decode, so callers see ordinary numbers. The rewrite is idempotent (an already-rewritten run
8
+ is not rewritten again), so it is safe on every encode path.
9
+
10
+ The idea (units-first digit tokens; a length prefix) follows Atom2.7m and Pascaline-3M. This is our own
11
+ implementation.
12
+ """
13
+ import re
14
+
15
+ from transformers import PreTrainedTokenizerFast
16
+
17
+ MAX_LEN = 8
18
+ # a digit run: not part of "<lenN>", not right after a "<lenN>" tag, not touching another digit or ":"
19
+ _RUN = re.compile(r"(?<!<len)(?<!<len\d>)(?<![\d:])(\d{1,%d})(?![\d:])" % MAX_LEN)
20
+ _TAG = re.compile(r"<len([1-%d])>(\d+)" % MAX_LEN)
21
+
22
+
23
+ def encode_numbers(text):
24
+ if not isinstance(text, str):
25
+ return text
26
+ return _RUN.sub(lambda m: "<len%d>%s" % (len(m.group(1)), m.group(1)[::-1]), text)
27
+
28
+
29
+ def decode_numbers(text):
30
+ def back(m):
31
+ n = int(m.group(1)); d = m.group(2)
32
+ return d[:n][::-1] + d[n:]
33
+ return _TAG.sub(back, text)
34
+
35
+
36
+ def _map(x):
37
+ if isinstance(x, str):
38
+ return encode_numbers(x)
39
+ if isinstance(x, tuple):
40
+ return tuple(_map(v) for v in x)
41
+ if isinstance(x, list):
42
+ return [_map(v) for v in x]
43
+ return x
44
+
45
+
46
+ class TBBUnitsFirstTokenizerFast(PreTrainedTokenizerFast):
47
+ def _batch_encode_plus(self, batch_text_or_text_pairs, *args, **kwargs):
48
+ if not kwargs.get("is_split_into_words", False):
49
+ batch_text_or_text_pairs = _map(batch_text_or_text_pairs)
50
+ return super()._batch_encode_plus(batch_text_or_text_pairs, *args, **kwargs)
51
+
52
+ def _encode_plus(self, text, text_pair=None, *args, **kwargs):
53
+ if not kwargs.get("is_split_into_words", False):
54
+ text, text_pair = _map(text), _map(text_pair)
55
+ return super()._encode_plus(text, text_pair, *args, **kwargs)
56
+
57
+ def tokenize(self, text, *args, **kwargs):
58
+ return super().tokenize(encode_numbers(text), *args, **kwargs)
59
+
60
+ def _decode(self, token_ids, *args, **kwargs):
61
+ return decode_numbers(super()._decode(token_ids, *args, **kwargs))
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "<|endoftext|>",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "1": {
12
+ "content": "<|user|>",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "2": {
20
+ "content": "<|assistant|>",
21
+ "lstrip": false,
22
+ "normalized": false,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "3": {
28
+ "content": "<|system|>",
29
+ "lstrip": false,
30
+ "normalized": false,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": true
34
+ },
35
+ "4": {
36
+ "content": "<|end|>",
37
+ "lstrip": false,
38
+ "normalized": false,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": true
42
+ },
43
+ "5": {
44
+ "content": "<think>",
45
+ "lstrip": false,
46
+ "normalized": false,
47
+ "rstrip": false,
48
+ "single_word": false,
49
+ "special": true
50
+ },
51
+ "6": {
52
+ "content": "</think>",
53
+ "lstrip": false,
54
+ "normalized": false,
55
+ "rstrip": false,
56
+ "single_word": false,
57
+ "special": true
58
+ },
59
+ "7": {
60
+ "content": "<|sep|>",
61
+ "lstrip": false,
62
+ "normalized": false,
63
+ "rstrip": false,
64
+ "single_word": false,
65
+ "special": true
66
+ },
67
+ "8": {
68
+ "content": "<|pad|>",
69
+ "lstrip": false,
70
+ "normalized": false,
71
+ "rstrip": false,
72
+ "single_word": false,
73
+ "special": true
74
+ },
75
+ "4096": {
76
+ "content": "<len1>",
77
+ "lstrip": false,
78
+ "normalized": false,
79
+ "rstrip": false,
80
+ "single_word": false,
81
+ "special": false
82
+ },
83
+ "4097": {
84
+ "content": "<len2>",
85
+ "lstrip": false,
86
+ "normalized": false,
87
+ "rstrip": false,
88
+ "single_word": false,
89
+ "special": false
90
+ },
91
+ "4098": {
92
+ "content": "<len3>",
93
+ "lstrip": false,
94
+ "normalized": false,
95
+ "rstrip": false,
96
+ "single_word": false,
97
+ "special": false
98
+ },
99
+ "4099": {
100
+ "content": "<len4>",
101
+ "lstrip": false,
102
+ "normalized": false,
103
+ "rstrip": false,
104
+ "single_word": false,
105
+ "special": false
106
+ },
107
+ "4100": {
108
+ "content": "<len5>",
109
+ "lstrip": false,
110
+ "normalized": false,
111
+ "rstrip": false,
112
+ "single_word": false,
113
+ "special": false
114
+ },
115
+ "4101": {
116
+ "content": "<len6>",
117
+ "lstrip": false,
118
+ "normalized": false,
119
+ "rstrip": false,
120
+ "single_word": false,
121
+ "special": false
122
+ },
123
+ "4102": {
124
+ "content": "<len7>",
125
+ "lstrip": false,
126
+ "normalized": false,
127
+ "rstrip": false,
128
+ "single_word": false,
129
+ "special": false
130
+ },
131
+ "4103": {
132
+ "content": "<len8>",
133
+ "lstrip": false,
134
+ "normalized": false,
135
+ "rstrip": false,
136
+ "single_word": false,
137
+ "special": false
138
+ }
139
+ },
140
+ "additional_special_tokens": [
141
+ "<|user|>",
142
+ "<|assistant|>",
143
+ "<|system|>",
144
+ "<|end|>",
145
+ "<think>",
146
+ "</think>",
147
+ "<|sep|>"
148
+ ],
149
+ "bos_token": "<|endoftext|>",
150
+ "clean_up_tokenization_spaces": false,
151
+ "eos_token": "<|endoftext|>",
152
+ "extra_special_tokens": {},
153
+ "model_max_length": 1024,
154
+ "pad_token": "<|pad|>",
155
+ "tokenizer_class": "TBBUnitsFirstTokenizerFast",
156
+ "unk_token": null,
157
+ "auto_map": {
158
+ "AutoTokenizer": [
159
+ "tokenization_tbbuf.TBBUnitsFirstTokenizerFast",
160
+ "tokenization_tbbuf.TBBUnitsFirstTokenizerFast"
161
+ ]
162
+ },
163
+ "model_input_names": [
164
+ "input_ids",
165
+ "attention_mask"
166
+ ]
167
+ }