File size: 6,862 Bytes
db7b729 dde517d db7b729 dde517d db7b729 dde517d db7b729 dde517d e26e649 dde517d db7b729 dde517d db7b729 dde517d db7b729 dde517d db7b729 dde517d 4f44f20 dde517d db7b729 4f44f20 dde517d db7b729 dde517d db7b729 dde517d db7b729 dde517d db7b729 dde517d db7b729 8414bb4 dde517d 8414bb4 dde517d db7b729 dde517d db7b729 dde517d f994e7a dde517d 4f44f20 dde517d f994e7a dde517d 8414bb4 dde517d db7b729 99b71c2 dde517d db7b729 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | ---
language:
- en
license: mit
library_name: pytorch
pipeline_tag: text-generation
tags:
- arithmetic
- length-generalization
- character-level
- rope
- 2d-rope
- tool-use
datasets:
- synthetic
metrics:
- accuracy
---
# CBT-18.95M
**A 19M-parameter character-level Transformer that adds numbers longer than any it was trained on.**
CBT (Character-Based Transformer) gives every token a two-dimensional position: *which column* it belongs to and *which step* inside that column. With that layout, a model trained on sums of up to 25 digits keeps adding correctly at 26, 40 and 55 digits. An identical twin with ordinary 1D positions gets every one of those wrong.
The same model also does simple tool calling (weather, search, calculator, unit conversion, ...) in a small chat format.
[Paper (PDF)](paper/CBT-18.95M.pdf) · [Paper (Markdown)](paper/CBT-18.95M.md) · [Code on GitHub](https://github.com/NeithanT/CBT)
---
## Try it
```bash
pip install -U huggingface_hub
hf download Neithanvarvas/CBT --local-dir CBT && cd CBT
pip install -r requirements.txt
python chat.py
```
`git clone https://huggingface.co/Neithanvarvas/CBT` also works; without git-lfs the weights are fetched on first run.
```text
> 123456789+987654321
scratchpad : (0+9+1)=10(1+8+2)=11(1+7+3)=11(1+6+4)=11 ... (1+0+0)=01
answer : 1111111110 [PASS]
> What is the weather in Tokyo?
think : The user requests current weather information. I will query the get_weather tool.
call : get_weather(city="Tokyo")
result : {"temp": 22, "condition": "Sunny", "city": "Tokyo"}
answer : The weather in Tokyo is currently Sunny with a temperature of 22 degrees Celsius.
```
Runs on a CPU; no GPU needed. Pass `--online` to let the search tool query Wikipedia, or `--raw` to see the token stream.
### From Python
```python
from cbt import load, add, chat
model, tok = load() # uses this folder, or downloads from the Hub if run elsewhere
add(model, tok, 31415926535897932384626433832795, 27182818284590452353602874713527)["answer"]
# '58598744820488384738229308546322'
chat(model, tok, "Convert 10 km to miles")["answer"]
```
`chat` runs one user turn and executes at most one tool call. Tools are offline stand-ins in [`cbt/tools.py`](cbt/tools.py); pass `tool_executor=your_function` to connect real ones.
### Compare with the 1D-RoPE twin
The control model from the results below ships in [`twin_1d/`](twin_1d/). Give both the same long sum:
```python
twin, _ = load(variant="twin_1d")
a, b = 31415926535897932384626433832795, 27182818284590452353602874713527 # 32 digits, unseen
add(model, tok, a, b)["correct"] # True
add(twin, tok, a, b)["correct"] # False
```
From the terminal: `python chat.py --variant twin_1d`.
---
## Results
Greedy decoding, 30 random problems per length. Training covered sums of 1 to 25 digits.
### Addition: CBT vs. its 1D-RoPE twin
The twin has the same architecture, data and number of training steps. Only the position encoding differs. Its weights are in [`twin_1d/`](twin_1d/).
| Operand digits | Seen in training? | CBT (2D) | 1D twin |
| :--- | :---: | :---: | :---: |
| 4, 8, 12, 14, 16 | yes | 150 / 150 | 149 / 150 |
| 20, 25 | yes | 60 / 60 | 57 / 60 |
| 26 | no | **30 / 30** | 0 / 30 |
| 30, 35, 40, 45, 50, 55 | no | **180 / 180** | 0 / 180 |
| 60 | no | 25 / 30 | 0 / 30 |
| 70 | no | 19 / 30 | 0 / 30 |
| **All unseen (26-70)** | no | **254 / 270** | **0 / 270** |
### Tool use: exact tool and arguments
| Test set | Prompts | CBT (2D) | 1D twin |
| :--- | :---: | :---: | :---: |
| Rephrased requests | 26 | 26 | 25 |
| Out-of-distribution entities | 18 | 18 | 18 |
| Entities never seen in training | 30 | 30 | 30 |
Both models do equally well here, so tool use comes from the training data, not from the 2D layout. The fine-tuning data was broadened after an earlier model failed the first two sets, so the unseen-entity set is the fairer test.
All numbers come from single training runs with small test sets. Raw per-problem results are in [`evaluation/`](evaluation/); limitations are discussed in the paper.
---
## How it works
Each token gets a coordinate (p, tau):
- **Arithmetic:** p is the decimal place (units, tens, ...) and tau is the step within that column of the scratchpad.
- **Chat:** p is the dialogue segment (user, think, call, result, answer) and tau is the character position inside it.
Half of each attention head is rotated by p and half by tau (Decoupled 2D-RoPE). Aligning digits of the same place becomes one fixed relation, "same p", which does not change as numbers get longer, so the model learns it once. During training, p is randomly shifted by up to 45, so the model has seen place values up to 70.
| | |
| :--- | :--- |
| Parameters | 18.94M (18.89M non-embedding) |
| Layers | 6, each applied 3 times with shared weights (effective depth 18) |
| Width / heads | 512 / 8 (head dim 64) |
| Context | 1024 characters |
| Vocabulary | 111: 97 ASCII characters (letters, digits, punctuation, space, tab, newline) + 14 control tags |
| Positions | Decoupled 2D-RoPE, coordinate shift k = 45 |
| Training | Pretraining, then several supervised fine-tuning rounds (23,927 steps in the last) |
### Prompt formats
```text
Addition: <BOS>123+456=<THINK>(0+3+6)=09(0+2+5)=07(0+1+4)=05</THINK>=579<EOS>
Chat: <BOS><USER>question</USER><THINK>...</THINK><CALL>tool(arg="...")</CALL><RESULT>{json}</RESULT><ANSWER>...</ANSWER>
```
Scratchpad steps read `(carry + digit_a + digit_b) = carry_out digit`, from the units column upward.
---
## Limitations
- Addition of non-negative integers is the only arithmetic it was trained on. Accuracy drops beyond 55 digits, and the 1024-character context caps sums at roughly 75 digits.
- Replies can drift into garbled text after the tool result (for example, "What is the weather in Paris?" ends with misspelled filler).
- The chat ability is narrow: it was trained on synthetic tool-calling dialogues with a fixed set of tools. It is not a general assistant and has little world knowledge.
- Character-level, English-only, ASCII-only. Other characters become `<UNK>`.
---
## Files
```text
README.md this card
config.json model hyperparameters
model.safetensors weights
twin_1d/ the matched 1D-RoPE control (config.json, model.safetensors)
cbt/ model, tokenizer, tool stubs, load/add/chat helpers
chat.py terminal demo
requirements.txt
paper/ the paper (PDF and Markdown)
evaluation/ per-problem results behind the tables above
```
## Citation
```bibtex
@article{vargas2026cbt,
title = {From Digits to Characters: Two-Dimensional Rotary Coordinates for Length Generalization in a Compact Character-Level Transformer},
author = {Vargas Vargas, Neithan},
year = {2026}
}
```
|