[RELEASE] Upload ScaledGPT 18.95M champion model weights and artifacts
Browse files- README.md +110 -0
- champion_final.pt +3 -0
- config.json +31 -0
- dataset.py +976 -0
- interactive.py +321 -0
- model.py +421 -0
- tokenizer.py +109 -0
README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
language:
|
| 3 |
+
- en
|
| 4 |
+
license: mit
|
| 5 |
+
tags:
|
| 6 |
+
- arithmetic
|
| 7 |
+
- length-generalization
|
| 8 |
+
- recurrent-transformers
|
| 9 |
+
- 2d-rope
|
| 10 |
+
- tool-use
|
| 11 |
+
- function-calling
|
| 12 |
+
- reasoning
|
| 13 |
+
datasets:
|
| 14 |
+
- synthetic
|
| 15 |
+
metrics:
|
| 16 |
+
- accuracy
|
| 17 |
+
pipeline_tag: text-generation
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
# ScaledGPT (~18.95M): Recurrent 2D-RoPE Architecture for Algorithmic Length Extrapolation
|
| 21 |
+
|
| 22 |
+
## 1. Model Overview
|
| 23 |
+
|
| 24 |
+
**ScaledGPT** is an autoregressive Transformer model engineered to overcome the empirical limits of standard self-attention on sequential state machines and multi-tool agent execution.
|
| 25 |
+
|
| 26 |
+
By combining **Decoupled 2D Rotary Position Embeddings (2D-RoPE)** with **Weight-Tied Recurrent Depth ($R=3$)**, ScaledGPT achieves:
|
| 27 |
+
- **100.0% Exact Match Accuracy** on 1 to 50 digit column arithmetic length extrapolation (including 50-digit cascading carry $99\dots99 + 1$).
|
| 28 |
+
- **100.0% Multi-Tool Routing Accuracy** across 9 tool schemas (single-tool, multi-tool chaining, nested JSON schemas, and error recovery).
|
| 29 |
+
- **Dual NVIDIA T4 GPU Efficiency**: Native Turing `sm_75` FP16 AMP and SDPA memory-efficient attention.
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## 2. Model Specifications
|
| 34 |
+
|
| 35 |
+
| Parameter | Specification |
|
| 36 |
+
| :--- | :--- |
|
| 37 |
+
| **Total Parameters** | 18,944,512 (~18.95M) |
|
| 38 |
+
| **Non-Embedding Parameters** | 18,887,680 (~18.89M) |
|
| 39 |
+
| **Physical Layers ($L$)** | 6 |
|
| 40 |
+
| **Recurrent Passes ($R$)** | 3 (Effective Depth = 18 layers) |
|
| 41 |
+
| **Hidden Dimension ($d_{\text{model}}$)** | 512 |
|
| 42 |
+
| **Attention Heads ($H$)** | 8 (Head dimension = 64) |
|
| 43 |
+
| **Max Sequence Length ($T$)** | 1024 |
|
| 44 |
+
| **Positional Embedding** | Decoupled 2D-RoPE ($d/2$ macro column, $d/2$ micro step) |
|
| 45 |
+
| **Vocabulary Size** | 115 (Special discourse markers + ASCII characters) |
|
| 46 |
+
| **Weight Tying** | $W_{\text{te}} == W_{\text{lm\_head}}$ |
|
| 47 |
+
|
| 48 |
+
---
|
| 49 |
+
|
| 50 |
+
## 3. Empirical Evaluation Benchmark
|
| 51 |
+
|
| 52 |
+
| Benchmark Suite | Metric | Score | Status |
|
| 53 |
+
| :--- | :--- | :--- | :--- |
|
| 54 |
+
| **1-15 Digit In-Distribution Math** | Exact Answer Match | 100.0% (6/6) | [PASS] |
|
| 55 |
+
| **16-25 Digit Intermediate Extrapolation** | Exact Answer Match | 100.0% (6/6) | [PASS] |
|
| 56 |
+
| **30-40 Digit High Extrapolation** | Exact Answer Match | 100.0% (4/4) | [PASS] |
|
| 57 |
+
| **45-50 Digit Ultra Extrapolation** | Exact Answer Match | 100.0% (4/4) | [PASS] |
|
| 58 |
+
| **50-Digit Cascade Carry ($99\dots99 + 1$)** | 50 Sequential Transitions | 100.0% (1/1) | [PASS] |
|
| 59 |
+
| **Multi-Tool Routing Suite (9 Schemas)** | Correct Tool Selection | 100.0% (26/26) | [PASS] |
|
| 60 |
+
| **Multi-Tool Exact Argument Extraction** | Exact Parameter Copy Rate | 96.2% (25/26) | [PASS] |
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## 4. Usage & Inference
|
| 65 |
+
|
| 66 |
+
### Local Quickstart
|
| 67 |
+
|
| 68 |
+
```python
|
| 69 |
+
import torch
|
| 70 |
+
from model import ScaledGPT, ScaledGPTConfig
|
| 71 |
+
from tokenizer import ScaledTokenizer
|
| 72 |
+
|
| 73 |
+
# 1. Load configuration and model
|
| 74 |
+
config = ScaledGPTConfig(
|
| 75 |
+
block_size=1024,
|
| 76 |
+
vocab_size=115,
|
| 77 |
+
n_layer=6,
|
| 78 |
+
n_head=8,
|
| 79 |
+
n_embd=512,
|
| 80 |
+
n_recur=3,
|
| 81 |
+
pos_emb_type="rope_2d"
|
| 82 |
+
)
|
| 83 |
+
model = ScaledGPT(config)
|
| 84 |
+
|
| 85 |
+
# 2. Load weights
|
| 86 |
+
state_dict = torch.load("champion_final.pt", map_location="cpu")
|
| 87 |
+
model.load_state_dict(state_dict)
|
| 88 |
+
model.eval()
|
| 89 |
+
|
| 90 |
+
# 3. Initialize tokenizer
|
| 91 |
+
tokenizer = ScaledTokenizer()
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
### Interactive CLI
|
| 95 |
+
|
| 96 |
+
```bash
|
| 97 |
+
python interactive.py --ckpt champion_final.pt
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
|
| 102 |
+
## 5. Citation
|
| 103 |
+
|
| 104 |
+
```bibtex
|
| 105 |
+
@article{vargas2026attention,
|
| 106 |
+
title={Attention Is Not All You Need for AGI: Empirical Limits of Pure Self-Attention on Algorithmic Length Generalization},
|
| 107 |
+
author={Vargas, Neithan},
|
| 108 |
+
year={2026}
|
| 109 |
+
}
|
| 110 |
+
```
|
champion_final.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:79d908d328d9aeac319129b3816fc2de0ada6722c2a5f9ee1ea7f4919e87ccaa
|
| 3 |
+
size 75800724
|
config.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_type": "scaled_t4",
|
| 3 |
+
"vocab_size": 115,
|
| 4 |
+
"block_size": 1024,
|
| 5 |
+
"n_layer": 6,
|
| 6 |
+
"n_head": 8,
|
| 7 |
+
"n_embd": 512,
|
| 8 |
+
"n_recur": 3,
|
| 9 |
+
"dropout": 0.0,
|
| 10 |
+
"bias": false,
|
| 11 |
+
"pos_emb_type": "rope_2d",
|
| 12 |
+
"rope_theta": 10000.0,
|
| 13 |
+
"max_coords": 4096,
|
| 14 |
+
"k_rand_shift": 32,
|
| 15 |
+
"effective_depth": 18,
|
| 16 |
+
"head_dim": 64,
|
| 17 |
+
"parameters": {
|
| 18 |
+
"total": 18953216,
|
| 19 |
+
"non_embedding": 18887680,
|
| 20 |
+
"target_scale": "~10M to ~20M",
|
| 21 |
+
"status": "[OK] Scaled 18.95M parameter architecture with 2D-RoPE and R=3 recurrence"
|
| 22 |
+
},
|
| 23 |
+
"t4_optimization": {
|
| 24 |
+
"precision": "FP16 AMP (torch.amp.autocast)",
|
| 25 |
+
"scaler": "torch.amp.GradScaler('cuda')",
|
| 26 |
+
"sdpa": "Scaled Dot-Product Attention with O(1) activation memory",
|
| 27 |
+
"distributed": "PyTorch DistributedDataParallel (DDP) across 2x T4 GPUs",
|
| 28 |
+
"bfloat16_allowed": false,
|
| 29 |
+
"tf32_allowed": false
|
| 30 |
+
}
|
| 31 |
+
}
|
dataset.py
ADDED
|
@@ -0,0 +1,976 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Joint Balanced Dataset & Batch Encoder for ScaledGPT (Dual T4 Optimization).
|
| 3 |
+
Supports:
|
| 4 |
+
1. Multi-digit column scratchpad arithmetic (1 to 50+ digits curriculum).
|
| 5 |
+
2. Expanded tool repertoire:
|
| 6 |
+
- Single-tool calling (calculator, weather, search, time, unit_convert)
|
| 7 |
+
- Multi-tool sequential chaining
|
| 8 |
+
- Nested JSON parameter schemas
|
| 9 |
+
- Error handling and recovery traces
|
| 10 |
+
- Multi-turn conversational dialogues
|
| 11 |
+
- Direct conversational routing (no-tool)
|
| 12 |
+
3. Strict prompt and environment output loss masking.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import sys
|
| 17 |
+
import json
|
| 18 |
+
import random
|
| 19 |
+
from typing import List, Dict, Tuple, Any, Optional
|
| 20 |
+
import torch
|
| 21 |
+
|
| 22 |
+
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 23 |
+
if ROOT_DIR not in sys.path:
|
| 24 |
+
sys.path.insert(0, ROOT_DIR)
|
| 25 |
+
|
| 26 |
+
from champion_model.scaled_t4.tokenizer import ScaledTokenizer
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def format_addition_char_scratchpad(a: int, b: int) -> str:
|
| 30 |
+
"""Formats addition into column-by-column carry scratchpad for character tokenizer."""
|
| 31 |
+
s_a, s_b = str(a), str(b)
|
| 32 |
+
len_a, len_b = len(s_a), len(s_b)
|
| 33 |
+
max_len = max(len_a, len_b)
|
| 34 |
+
|
| 35 |
+
carry = 0
|
| 36 |
+
steps, digits = [], []
|
| 37 |
+
|
| 38 |
+
for col in range(max_len):
|
| 39 |
+
da = int(s_a[len_a - 1 - col]) if col < len_a else 0
|
| 40 |
+
db = int(s_b[len_b - 1 - col]) if col < len_b else 0
|
| 41 |
+
col_sum = carry + da + db
|
| 42 |
+
next_carry = col_sum // 10
|
| 43 |
+
res_digit = col_sum % 10
|
| 44 |
+
steps.append(f"({carry}+{da}+{db})={next_carry}{res_digit}")
|
| 45 |
+
digits.append(str(res_digit))
|
| 46 |
+
carry = next_carry
|
| 47 |
+
|
| 48 |
+
if carry > 0:
|
| 49 |
+
steps.append(f"({carry}+0+0)=0{carry}")
|
| 50 |
+
digits.append(str(carry))
|
| 51 |
+
|
| 52 |
+
ans = "".join(reversed(digits))
|
| 53 |
+
return f"{a}+{b}=<THINK>" + "".join(steps) + f"</THINK>={ans}"
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def extract_scratchpad_answer(text: str) -> Tuple[str, str]:
|
| 57 |
+
"""Extracts reasoning scratchpad and predicted answer from generated output."""
|
| 58 |
+
reasoning = ""
|
| 59 |
+
ans = ""
|
| 60 |
+
if "<THINK>" in text and "</THINK>" in text:
|
| 61 |
+
parts = text.split("</THINK>")
|
| 62 |
+
reasoning = parts[0].replace("<THINK>", "")
|
| 63 |
+
after = parts[1]
|
| 64 |
+
if "=" in after:
|
| 65 |
+
ans = after.split("=")[-1].replace("<EOS>", "").replace("<PAD>", "").strip()
|
| 66 |
+
else:
|
| 67 |
+
ans = after.replace("<EOS>", "").replace("<PAD>", "").strip()
|
| 68 |
+
elif "=" in text:
|
| 69 |
+
ans = text.split("=")[-1].replace("<EOS>", "").replace("<PAD>", "").strip()
|
| 70 |
+
return reasoning, ans
|
| 71 |
+
|
| 72 |
+
# Factual databases for synthetic generation
|
| 73 |
+
CITIES = [
|
| 74 |
+
"New York", "London", "Tokyo", "Paris", "Berlin", "Sydney", "Toronto", "Madrid",
|
| 75 |
+
"Rome", "Seoul", "Singapore", "Zurich", "Honolulu", "Reykjavik", "Ouagadougou",
|
| 76 |
+
"Vancouver", "Santiago", "Auckland", "Casablanca", "Vienna", "Stockholm", "Kyoto",
|
| 77 |
+
"Dublin", "Oslo", "Helsinki", "Bangkok", "Cairo", "Lisbon", "Amsterdam", "Athens",
|
| 78 |
+
]
|
| 79 |
+
CONDITIONS = ["Sunny", "Rainy", "Cloudy", "Snowy", "Windy", "Foggy", "Clear"]
|
| 80 |
+
TIMEZONES = ["UTC", "EST", "PST", "GMT", "JST", "CET", "AEST", "CST", "IST", "HST"]
|
| 81 |
+
|
| 82 |
+
# Nonce word phonetic generators to force pure Attention Induction Copying
|
| 83 |
+
NONCE_ONSETS = ["bl", "br", "ch", "cl", "cr", "dr", "fl", "fr", "gl", "gr", "pl", "pr", "sc", "sk", "sl", "sm", "sn", "sp", "st", "sw", "th", "tr", "qu", "v", "z", "k", "j"]
|
| 84 |
+
NONCE_NUCLEI = ["a", "e", "i", "o", "u", "y", "ai", "ea", "ee", "oa", "oo", "ou"]
|
| 85 |
+
NONCE_CODAS = ["b", "d", "f", "g", "k", "l", "m", "n", "p", "r", "s", "t", "x", "z", "ck", "nk", "mp", "sh", "th", "nt", "rt"]
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def generate_nonce_entity(rng: random.Random) -> Tuple[str, str, str]:
|
| 89 |
+
"""Generates an arbitrary pseudo-word entity (singular, plural, type)."""
|
| 90 |
+
n_syl = rng.choice([1, 2, 2, 3])
|
| 91 |
+
parts = []
|
| 92 |
+
for _ in range(n_syl):
|
| 93 |
+
parts.append(rng.choice(NONCE_ONSETS) + rng.choice(NONCE_NUCLEI) + rng.choice(NONCE_CODAS))
|
| 94 |
+
singular = "".join(parts)
|
| 95 |
+
plural = singular + ("es" if singular[-1] in "sxz" else "s")
|
| 96 |
+
ent_type = rng.choice(["concept", "plant", "creature", "artifact", "substance"])
|
| 97 |
+
return singular, plural, ent_type
|
| 98 |
+
SEARCH_FACTS = [
|
| 99 |
+
("Python creator", "Guido van Rossum", "creator"),
|
| 100 |
+
("capital of Australia", "Canberra", "capital"),
|
| 101 |
+
("speed of light", "299792 km/s", "constant"),
|
| 102 |
+
("tallest mountain on Earth", "Mount Everest", "geography"),
|
| 103 |
+
("deepest ocean trench", "Mariana Trench", "geography"),
|
| 104 |
+
("inventor of the telephone", "Alexander Graham Bell", "inventor"),
|
| 105 |
+
("currency of Japan", "Yen", "currency"),
|
| 106 |
+
("largest planet in solar system", "Jupiter", "astronomy"),
|
| 107 |
+
("author of Hamlet", "William Shakespeare", "literature"),
|
| 108 |
+
("chemical symbol for Gold", "Au", "chemistry"),
|
| 109 |
+
("Tokyo population", "14000000", "demographics"),
|
| 110 |
+
("Paris land area", "105 sq km", "geography"),
|
| 111 |
+
("distance from Earth to Moon", "384400 km", "astronomy"),
|
| 112 |
+
("human body temperature", "37 Celsius", "biology"),
|
| 113 |
+
]
|
| 114 |
+
|
| 115 |
+
# Open-domain entity database for Universal Search Delegation
|
| 116 |
+
SEARCH_ENTITIES = [
|
| 117 |
+
# Foods & Plants
|
| 118 |
+
{"entity": "tomato", "plural": "tomatoes", "type": "plant", "snippet": "the edible berry of the plant Solanum lycopersicum, commonly prepared as a culinary vegetable."},
|
| 119 |
+
{"entity": "apple", "plural": "apples", "type": "fruit", "snippet": "a round, edible fruit produced by an apple tree, cultivated worldwide."},
|
| 120 |
+
{"entity": "potato", "plural": "potatoes", "type": "vegetable", "snippet": "a starchy, tuberous root crop of the nightshade family Solanaceae."},
|
| 121 |
+
{"entity": "coffee", "plural": "coffee beans", "type": "plant", "snippet": "a brewed beverage prepared from roasted coffee beans of flowering Coffea species."},
|
| 122 |
+
{"entity": "banana", "plural": "bananas", "type": "fruit", "snippet": "an elongated, edible fruit produced by large herbaceous plants in the genus Musa."},
|
| 123 |
+
{"entity": "onion", "plural": "onions", "type": "vegetable", "snippet": "a pungent vegetable that is the most widely cultivated species of the genus Allium."},
|
| 124 |
+
{"entity": "wheat", "plural": "wheat grains", "type": "crop", "snippet": "a cereal grain cultivated worldwide as a major dietary staple."},
|
| 125 |
+
{"entity": "rice", "plural": "rice grains", "type": "crop", "snippet": "the seed of the grass species Oryza sativa, a primary staple food for billions of people."},
|
| 126 |
+
{"entity": "carrot", "plural": "carrots", "type": "vegetable", "snippet": "a root vegetable, typically orange, rich in beta-carotene and dietary fiber."},
|
| 127 |
+
{"entity": "garlic", "plural": "garlic cloves", "type": "plant", "snippet": "a species of bulbous flowering plant in the genus Allium, used for culinary seasoning."},
|
| 128 |
+
# Animals
|
| 129 |
+
{"entity": "dog", "plural": "dogs", "type": "animal", "snippet": "a domesticated descendant of the wolf, widely kept as a companion and working animal."},
|
| 130 |
+
{"entity": "cat", "plural": "cats", "type": "animal", "snippet": "a small, carnivorous mammal known for its agility, flexible body, and companionship with humans."},
|
| 131 |
+
{"entity": "dolphin", "plural": "dolphins", "type": "animal", "snippet": "a highly intelligent marine mammal belonging to the cetacean order, known for echolocation."},
|
| 132 |
+
{"entity": "elephant", "plural": "elephants", "type": "animal", "snippet": "the largest existing land animal, characterized by a long trunk, tusks, and large ear flaps."},
|
| 133 |
+
{"entity": "penguin", "plural": "penguins", "type": "animal", "snippet": "an aquatic, flightless bird living almost exclusively in the Southern Hemisphere."},
|
| 134 |
+
{"entity": "lion", "plural": "lions", "type": "animal", "snippet": "a large cat of the genus Panthera, native to Africa and India, known for social prides."},
|
| 135 |
+
{"entity": "tiger", "plural": "tigers", "type": "animal", "snippet": "the largest living cat species, identifiable by its reddish-orange coat with dark vertical stripes."},
|
| 136 |
+
{"entity": "whale", "plural": "whales", "type": "animal", "snippet": "a widely distributed and diverse group of fully aquatic placental marine mammals."},
|
| 137 |
+
# Science & Nature
|
| 138 |
+
{"entity": "gravity", "plural": "gravitational forces", "type": "physics", "snippet": "a fundamental interaction which causes mutual attraction between all things with mass or energy."},
|
| 139 |
+
{"entity": "photosynthesis", "plural": "photosynthetic processes", "type": "biology", "snippet": "a biological process used by plants to convert light energy into chemical energy stored in glucose."},
|
| 140 |
+
{"entity": "atom", "plural": "atoms", "type": "physics", "snippet": "the basic unit of a chemical element, consisting of a dense central nucleus surrounded by electrons."},
|
| 141 |
+
{"entity": "DNA", "plural": "DNA molecules", "type": "genetics", "snippet": "deoxyribonucleic acid, a polymer composed of polynucleotide chains carrying genetic instructions."},
|
| 142 |
+
{"entity": "black hole", "plural": "black holes", "type": "astronomy", "snippet": "a region of spacetime where gravity is so strong that nothing, including light, can escape from it."},
|
| 143 |
+
{"entity": "water", "plural": "water molecules", "type": "chemistry", "snippet": "a transparent, odorless chemical substance with chemical formula H2O, essential for life."},
|
| 144 |
+
{"entity": "oxygen", "plural": "oxygen molecules", "type": "chemistry", "snippet": "a chemical element with symbol O and atomic number 8, essential for aerobic respiration."},
|
| 145 |
+
{"entity": "electricity", "plural": "electric currents", "type": "physics", "snippet": "physical phenomena associated with the presence and motion of matter possessing electric charge."},
|
| 146 |
+
# Geography & Landmarks
|
| 147 |
+
{"entity": "Eiffel Tower", "plural": "monuments", "type": "landmark", "snippet": "a wrought-iron lattice tower on the Champ de Mars in Paris, France, named after Gustave Eiffel."},
|
| 148 |
+
{"entity": "Great Wall of China", "plural": "ancient walls", "type": "history", "snippet": "a series of fortifications built across the historical northern borders of ancient Chinese states."},
|
| 149 |
+
{"entity": "Mount Everest", "plural": "peaks", "type": "geography", "snippet": "Earth's highest mountain above sea level, located in the Mahalangur Himal sub-range of the Himalayas."},
|
| 150 |
+
{"entity": "Amazon River", "plural": "rivers", "type": "geography", "snippet": "the largest river by discharge volume of water in the world, located in South America."},
|
| 151 |
+
{"entity": "Sahara Desert", "plural": "deserts", "type": "geography", "snippet": "the largest hot desert in the world and the third largest desert overall, covering North Africa."},
|
| 152 |
+
# Technology & Society
|
| 153 |
+
{"entity": "computer", "plural": "computers", "type": "technology", "snippet": "an electronic device for storing and processing data, typically according to binary instructions."},
|
| 154 |
+
{"entity": "internet", "plural": "computer networks", "type": "technology", "snippet": "a global system of interconnected computer networks that uses the Internet protocol suite (TCP/IP)."},
|
| 155 |
+
{"entity": "algorithm", "plural": "algorithms", "type": "computer science", "snippet": "a finite sequence of rigorous instructions used to solve problems or perform a computation."},
|
| 156 |
+
{"entity": "telescope", "plural": "telescopes", "type": "instrument", "snippet": "an optical instrument that makes distant objects appear magnified using lenses or curved mirrors."},
|
| 157 |
+
{"entity": "airplane", "plural": "airplanes", "type": "vehicle", "snippet": "a powered, fixed-wing aircraft that is propelled forward by thrust from an engine."},
|
| 158 |
+
# Historical Figures & Creators
|
| 159 |
+
{"entity": "Guido van Rossum", "plural": "programmers", "type": "person", "snippet": "the Dutch programmer best known as the creator of the Python programming language."},
|
| 160 |
+
{"entity": "Albert Einstein", "plural": "physicists", "type": "person", "snippet": "a theoretical physicist who developed the theory of relativity, a pillar of modern physics."},
|
| 161 |
+
{"entity": "William Shakespeare", "plural": "playwrights", "type": "person", "snippet": "an English playwright, poet, and actor widely regarded as the greatest writer in the English language."},
|
| 162 |
+
{"entity": "Alexander Graham Bell", "plural": "inventors", "type": "person", "snippet": "an inventor and scientist credited with patenting the first practical telephone."},
|
| 163 |
+
{"entity": "Isaac Newton", "plural": "scientists", "type": "person", "snippet": "an English mathematician and physicist who formulated the laws of motion and universal gravitation."},
|
| 164 |
+
{"entity": "Nikola Tesla", "plural": "engineers", "type": "person", "snippet": "a Serbian-American engineer known for his contributions to alternating current electricity."},
|
| 165 |
+
{"entity": "Marie Curie", "plural": "scientists", "type": "person", "snippet": "a Polish-French physicist who pioneered radioactivity research and won two Nobel Prizes."},
|
| 166 |
+
{"entity": "Alan Turing", "plural": "mathematicians", "type": "person", "snippet": "an English mathematician widely considered the father of modern computer science and AI."},
|
| 167 |
+
{"entity": "Ada Lovelace", "plural": "mathematicians", "type": "person", "snippet": "an English mathematician celebrated for writing the first computer algorithm."},
|
| 168 |
+
{"entity": "Aristotle", "plural": "philosophers", "type": "person", "snippet": "an ancient Greek philosopher whose work shaped centuries of Western logic and metaphysics."},
|
| 169 |
+
{"entity": "Leonardo da Vinci", "plural": "polymaths", "type": "person", "snippet": "an Italian polymath active as a painter, draughtsman, engineer, and scientist."},
|
| 170 |
+
# Additional Produce & Animals
|
| 171 |
+
{"entity": "lemon", "plural": "lemons", "type": "fruit", "snippet": "a species of small evergreen tree native to Asia, known for its yellow ellipsoidal citrus fruit."},
|
| 172 |
+
{"entity": "strawberry", "plural": "strawberries", "type": "fruit", "snippet": "a widely grown hybrid species of the genus Fragaria, cultivated for its sweet red fruit."},
|
| 173 |
+
{"entity": "grape", "plural": "grapes", "type": "fruit", "snippet": "a botanical berry of the deciduous woody vines of the flowering plant genus Vitis."},
|
| 174 |
+
{"entity": "spinach", "plural": "spinach leaves", "type": "vegetable", "snippet": "an edible flowering plant in the family Amaranthaceae, rich in iron and vitamins."},
|
| 175 |
+
{"entity": "broccoli", "plural": "broccoli florets", "type": "vegetable", "snippet": "an edible green plant in the cabbage family whose large flower head is eaten as a vegetable."},
|
| 176 |
+
{"entity": "avocado", "plural": "avocados", "type": "fruit", "snippet": "a medium-sized, evergreen tree native to the Americas, harvested for its buttery fruit."},
|
| 177 |
+
{"entity": "wolf", "plural": "wolves", "type": "animal", "snippet": "a large canine native to Eurasia and North America, known for pack hunting behavior."},
|
| 178 |
+
{"entity": "eagle", "plural": "eagles", "type": "animal", "snippet": "a large, powerfully built bird of prey belonging to the family Accipitridae."},
|
| 179 |
+
{"entity": "shark", "plural": "sharks", "type": "animal", "snippet": "a group of elasmobranch fish characterized by a cartilaginous skeleton and multiple gill slits."},
|
| 180 |
+
{"entity": "octopus", "plural": "octopuses", "type": "animal", "snippet": "a soft-bodied, eight-limbed mollusc of the order Octopoda, famous for high intelligence."},
|
| 181 |
+
# Science & Computing
|
| 182 |
+
{"entity": "quantum entanglement", "plural": "entangled states", "type": "physics", "snippet": "a physical phenomenon occurring when pairs of particles interact such that quantum states cannot be described independently."},
|
| 183 |
+
{"entity": "thermodynamics", "plural": "thermodynamic laws", "type": "physics", "snippet": "a branch of physics that deals with heat, work, temperature, and statistical mechanics."},
|
| 184 |
+
{"entity": "compiler", "plural": "compilers", "type": "computer science", "snippet": "a computer program that translates computer code written in one language into another."},
|
| 185 |
+
{"entity": "operating system", "plural": "operating systems", "type": "computer science", "snippet": "system software that manages computer hardware, software resources, and common services."},
|
| 186 |
+
{"entity": "neural network", "plural": "neural networks", "type": "computer science", "snippet": "a computational model inspired by the biological neural networks of animal brains."},
|
| 187 |
+
]
|
| 188 |
+
DEPARTMENTS = ["engineering", "marketing", "sales", "finance", "legal", "product"]
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# -----------------------------------------------------------------------------
|
| 192 |
+
# 1. Search Delegation & Copy Induction Dataset Generator
|
| 193 |
+
# -----------------------------------------------------------------------------
|
| 194 |
+
|
| 195 |
+
def generate_search_delegation_dataset(
|
| 196 |
+
n_samples: int = 15000,
|
| 197 |
+
seed: int = 42,
|
| 198 |
+
) -> List[Dict[str, str]]:
|
| 199 |
+
"""
|
| 200 |
+
Generates concentrated Search Delegation samples for Induction Head copy training.
|
| 201 |
+
Curriculum:
|
| 202 |
+
- 65% Nonce Words (1-3 syllables) forcing pure Attention Pointer Copying (memorization impossible).
|
| 203 |
+
- 35% Real-World Entities (tomatoes, photosynthesis, etc.) establishing lexical alignment.
|
| 204 |
+
- Symmetric punctuation curriculum (50% with '?', '.', 50% raw text without punctuation).
|
| 205 |
+
- Diverse question framing: What are, What is a, Define, Tell me about, Search for, Who is, Explain.
|
| 206 |
+
"""
|
| 207 |
+
rng = random.Random(seed)
|
| 208 |
+
samples: List[Dict[str, str]] = []
|
| 209 |
+
|
| 210 |
+
for _ in range(n_samples):
|
| 211 |
+
is_nonce = (rng.random() < 0.65)
|
| 212 |
+
has_punct = (rng.random() < 0.50)
|
| 213 |
+
|
| 214 |
+
if is_nonce:
|
| 215 |
+
ent, plural, ent_type = generate_nonce_entity(rng)
|
| 216 |
+
snippet = f"a hypothetical {ent_type} characterized in verified theoretical studies."
|
| 217 |
+
else:
|
| 218 |
+
ent_item = rng.choice(SEARCH_ENTITIES)
|
| 219 |
+
ent = ent_item["entity"]
|
| 220 |
+
plural = ent_item.get("plural", ent + "s")
|
| 221 |
+
snippet = ent_item["snippet"]
|
| 222 |
+
ent_type = ent_item.get("type", "concept")
|
| 223 |
+
|
| 224 |
+
# Choose query framing
|
| 225 |
+
frame = rng.choice([
|
| 226 |
+
"what_are_plural",
|
| 227 |
+
"what_is_singular",
|
| 228 |
+
"what_is_concept",
|
| 229 |
+
"define",
|
| 230 |
+
"tell_me_about",
|
| 231 |
+
"search_for",
|
| 232 |
+
"who_is",
|
| 233 |
+
"explain",
|
| 234 |
+
"lookup",
|
| 235 |
+
])
|
| 236 |
+
|
| 237 |
+
if frame == "what_are_plural":
|
| 238 |
+
q_mark = "?" if has_punct else ""
|
| 239 |
+
user_prompt = f"What are {plural}{q_mark}"
|
| 240 |
+
search_q = plural
|
| 241 |
+
thought = rng.choice([
|
| 242 |
+
"The user is asking a factual question. I will query the search tool.",
|
| 243 |
+
"The user requests factual information. Querying search.",
|
| 244 |
+
"The user is seeking knowledge on this topic. I will search the web.",
|
| 245 |
+
])
|
| 246 |
+
answer = f"{plural.capitalize()} are {snippet}"
|
| 247 |
+
elif frame == "what_is_singular" and ent_type in ["plant", "fruit", "vegetable", "animal", "creature", "artifact", "instrument", "vehicle"]:
|
| 248 |
+
article = "an" if ent[0].lower() in "aeiou" else "a"
|
| 249 |
+
q_mark = "?" if has_punct else ""
|
| 250 |
+
user_prompt = f"What is {article} {ent}{q_mark}"
|
| 251 |
+
search_q = ent
|
| 252 |
+
thought = rng.choice([
|
| 253 |
+
"The user is asking a factual question. I will query the search tool.",
|
| 254 |
+
"The user is inquiring about this entity. I will query the search tool.",
|
| 255 |
+
"The user requests factual information. Querying search.",
|
| 256 |
+
])
|
| 257 |
+
answer = f"{ent.capitalize()} is {snippet}"
|
| 258 |
+
elif frame == "define":
|
| 259 |
+
p_mark = "." if has_punct else ""
|
| 260 |
+
user_prompt = f"Define {ent}{p_mark}"
|
| 261 |
+
search_q = ent
|
| 262 |
+
thought = rng.choice([
|
| 263 |
+
"The user asks for a definition. Calling search tool.",
|
| 264 |
+
"The user is asking a factual question. I will query the search tool.",
|
| 265 |
+
"The user requests factual information. Querying search.",
|
| 266 |
+
])
|
| 267 |
+
answer = f"{ent.capitalize()} is defined as {snippet}"
|
| 268 |
+
elif frame == "tell_me_about":
|
| 269 |
+
p_mark = "." if has_punct else ""
|
| 270 |
+
user_prompt = f"Tell me about {ent}{p_mark}"
|
| 271 |
+
search_q = ent
|
| 272 |
+
thought = rng.choice([
|
| 273 |
+
"The user requests factual background information. Querying search.",
|
| 274 |
+
"The user is asking a factual question. I will query the search tool.",
|
| 275 |
+
"The user is seeking knowledge on this topic. I will search the web.",
|
| 276 |
+
])
|
| 277 |
+
answer = f"According to verified search results, {ent} is {snippet}"
|
| 278 |
+
elif frame == "search_for":
|
| 279 |
+
p_mark = "." if has_punct else ""
|
| 280 |
+
user_prompt = f"Search for {ent}{p_mark}"
|
| 281 |
+
search_q = ent
|
| 282 |
+
thought = rng.choice([
|
| 283 |
+
"The user explicitly requests a search. Calling search tool.",
|
| 284 |
+
"The user requests factual information. Querying search.",
|
| 285 |
+
"The user is asking a factual question. I will query the search tool.",
|
| 286 |
+
])
|
| 287 |
+
answer = f"Search results indicate {ent} is {snippet}"
|
| 288 |
+
elif frame == "who_is" and ent_type in ["person", "creature"]:
|
| 289 |
+
q_mark = "?" if has_punct else ""
|
| 290 |
+
user_prompt = f"Who was {ent}{q_mark}" if rng.random() < 0.5 else f"Who is {ent}{q_mark}"
|
| 291 |
+
search_q = ent
|
| 292 |
+
thought = rng.choice([
|
| 293 |
+
"The user requests biographical information. Calling search.",
|
| 294 |
+
"The user is asking a factual question. I will query the search tool.",
|
| 295 |
+
"The user requests factual information. Querying search.",
|
| 296 |
+
])
|
| 297 |
+
answer = f"{ent} was {snippet}" if "was" in user_prompt else f"{ent} is {snippet}"
|
| 298 |
+
elif frame == "explain":
|
| 299 |
+
p_mark = "." if has_punct else ""
|
| 300 |
+
user_prompt = f"Explain {ent}{p_mark}"
|
| 301 |
+
search_q = ent
|
| 302 |
+
thought = rng.choice([
|
| 303 |
+
"The user asks for an explanation of this subject. I will query the search tool.",
|
| 304 |
+
"The user is asking a factual question. I will query the search tool.",
|
| 305 |
+
"The user requests factual information. Querying search.",
|
| 306 |
+
])
|
| 307 |
+
answer = f"{ent.capitalize()} is {snippet}"
|
| 308 |
+
else:
|
| 309 |
+
q_mark = "?" if has_punct else ""
|
| 310 |
+
user_prompt = f"What is {ent}{q_mark}"
|
| 311 |
+
search_q = ent
|
| 312 |
+
thought = rng.choice([
|
| 313 |
+
"The user is asking a factual question. I will query the search tool.",
|
| 314 |
+
"The user requests factual information. Querying search.",
|
| 315 |
+
"The user is seeking knowledge on this topic. I will search the web.",
|
| 316 |
+
])
|
| 317 |
+
answer = f"{ent.capitalize()} is {snippet}"
|
| 318 |
+
|
| 319 |
+
call = f"search(query=\"{search_q}\")"
|
| 320 |
+
result_json = json.dumps({"snippet": snippet})
|
| 321 |
+
full_text = (
|
| 322 |
+
f"<USER>{user_prompt}</USER>"
|
| 323 |
+
f"<THINK>{thought}</THINK>"
|
| 324 |
+
f"<CALL>{call}</CALL>"
|
| 325 |
+
f"<RESULT>{result_json}</RESULT>"
|
| 326 |
+
f"<ANSWER>{answer}</ANSWER>"
|
| 327 |
+
)
|
| 328 |
+
|
| 329 |
+
samples.append({
|
| 330 |
+
"task_type": "tool",
|
| 331 |
+
"category": "search_delegation",
|
| 332 |
+
"prompt": f"<USER>{user_prompt}</USER>",
|
| 333 |
+
"full_text": full_text,
|
| 334 |
+
})
|
| 335 |
+
|
| 336 |
+
return samples
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
# -----------------------------------------------------------------------------
|
| 340 |
+
# 1. High-Digit Arithmetic Dataset Generator
|
| 341 |
+
# -----------------------------------------------------------------------------
|
| 342 |
+
|
| 343 |
+
def generate_arithmetic_equations(
|
| 344 |
+
n_samples: int = 4000,
|
| 345 |
+
seed: int = 42,
|
| 346 |
+
max_train_digits: int = 15,
|
| 347 |
+
) -> List[Dict[str, str]]:
|
| 348 |
+
"""
|
| 349 |
+
Generates balanced multi-digit addition equations with column carry scratchpad.
|
| 350 |
+
Curriculum: 1 to max_train_digits (default 15) digits, plus edge cases:
|
| 351 |
+
- Zero padding
|
| 352 |
+
- Cascade carries (e.g. 9999 + 1)
|
| 353 |
+
- Equal length and asymmetric lengths
|
| 354 |
+
"""
|
| 355 |
+
rng = random.Random(seed)
|
| 356 |
+
samples: List[Dict[str, str]] = []
|
| 357 |
+
|
| 358 |
+
for _ in range(n_samples):
|
| 359 |
+
mode = rng.random()
|
| 360 |
+
|
| 361 |
+
if mode < 0.20:
|
| 362 |
+
# Cascade carry stress cases (999... + 1 or 999... + small)
|
| 363 |
+
digits = rng.randint(2, max_train_digits)
|
| 364 |
+
a = int("9" * digits)
|
| 365 |
+
b = rng.randint(1, 9)
|
| 366 |
+
elif mode < 0.35:
|
| 367 |
+
# Round numbers / power of 10 zero tracking
|
| 368 |
+
d1 = rng.randint(2, max_train_digits)
|
| 369 |
+
d2 = rng.randint(2, max_train_digits)
|
| 370 |
+
a = rng.randint(1, 9) * (10 ** (d1 - 1))
|
| 371 |
+
b = rng.randint(1, 9) * (10 ** (d2 - 1))
|
| 372 |
+
elif mode < 0.60:
|
| 373 |
+
# Uniform length random digits (1 to max_train_digits)
|
| 374 |
+
d = rng.randint(1, max_train_digits)
|
| 375 |
+
min_v = 10 ** (d - 1) if d > 1 else 0
|
| 376 |
+
max_v = (10 ** d) - 1
|
| 377 |
+
a = rng.randint(min_v, max_v)
|
| 378 |
+
b = rng.randint(min_v, max_v)
|
| 379 |
+
else:
|
| 380 |
+
# Asymmetric length random digits
|
| 381 |
+
d1 = rng.randint(1, max_train_digits)
|
| 382 |
+
d2 = rng.randint(1, max_train_digits)
|
| 383 |
+
min_v1 = 10 ** (d1 - 1) if d1 > 1 else 0
|
| 384 |
+
max_v1 = (10 ** d1) - 1
|
| 385 |
+
min_v2 = 10 ** (d2 - 1) if d2 > 1 else 0
|
| 386 |
+
max_v2 = (10 ** d2) - 1
|
| 387 |
+
a = rng.randint(min_v1, max_v1)
|
| 388 |
+
b = rng.randint(min_v2, max_v2)
|
| 389 |
+
|
| 390 |
+
full_text = format_addition_char_scratchpad(a, b)
|
| 391 |
+
prompt = full_text.split("<THINK>")[0]
|
| 392 |
+
samples.append({
|
| 393 |
+
"task_type": "math",
|
| 394 |
+
"prompt": prompt,
|
| 395 |
+
"full_text": full_text,
|
| 396 |
+
})
|
| 397 |
+
|
| 398 |
+
return samples
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
# -----------------------------------------------------------------------------
|
| 402 |
+
# 2. Expanded Tool Repertoire Generator
|
| 403 |
+
# -----------------------------------------------------------------------------
|
| 404 |
+
|
| 405 |
+
def generate_tool_example(rng: random.Random) -> Dict[str, str]:
|
| 406 |
+
"""
|
| 407 |
+
Generates a structured synthetic tool interaction from 6 expanded categories:
|
| 408 |
+
1. Single-tool calling (calculator, weather, search, time, unit_convert)
|
| 409 |
+
2. Multi-tool sequential chaining
|
| 410 |
+
3. Nested JSON parameter schemas
|
| 411 |
+
4. Error handling and recovery
|
| 412 |
+
5. Multi-turn dialogues
|
| 413 |
+
6. Direct conversational routing (no-tool)
|
| 414 |
+
|
| 415 |
+
Enforces 1-Hop Induction Copying:
|
| 416 |
+
- <THINK> emits a clean, entity-agnostic routing intent (no variable copying in <THINK>).
|
| 417 |
+
- <CALL> copies arguments directly from <USER> via 1-hop attention pointers.
|
| 418 |
+
- Symmetric punctuation curriculum (50% with '?'/'.', 50% raw conversational text).
|
| 419 |
+
"""
|
| 420 |
+
category = rng.choices(
|
| 421 |
+
[
|
| 422 |
+
"single_tool",
|
| 423 |
+
"multi_tool_chain",
|
| 424 |
+
"nested_json_schema",
|
| 425 |
+
"error_recovery",
|
| 426 |
+
"multi_turn",
|
| 427 |
+
"no_tool",
|
| 428 |
+
],
|
| 429 |
+
weights=[0.55, 0.12, 0.12, 0.08, 0.06, 0.07],
|
| 430 |
+
k=1,
|
| 431 |
+
)[0]
|
| 432 |
+
has_punct = (rng.random() < 0.50)
|
| 433 |
+
q_mark = "?" if has_punct else ""
|
| 434 |
+
p_mark = "." if has_punct else ""
|
| 435 |
+
|
| 436 |
+
if category == "single_tool":
|
| 437 |
+
sub = rng.choices(
|
| 438 |
+
["weather", "calculator", "time", "unit_convert", "search"],
|
| 439 |
+
weights=[0.28, 0.24, 0.20, 0.20, 0.08],
|
| 440 |
+
k=1,
|
| 441 |
+
)[0]
|
| 442 |
+
if sub == "calculator":
|
| 443 |
+
op = rng.choice(["+", "-", "*", "/"])
|
| 444 |
+
if op == "+":
|
| 445 |
+
a, b = rng.randint(10, 9999), rng.randint(10, 9999)
|
| 446 |
+
res = a + b
|
| 447 |
+
elif op == "-":
|
| 448 |
+
a, b = rng.randint(100, 9999), rng.randint(10, 99)
|
| 449 |
+
res = a - b
|
| 450 |
+
elif op == "*":
|
| 451 |
+
a, b = rng.randint(2, 250), rng.randint(2, 99)
|
| 452 |
+
res = a * b
|
| 453 |
+
else:
|
| 454 |
+
b = rng.randint(2, 50)
|
| 455 |
+
res = rng.randint(2, 200)
|
| 456 |
+
a = res * b
|
| 457 |
+
|
| 458 |
+
use_spaces = (rng.random() < 0.50)
|
| 459 |
+
expr_prompt = f"{a} {op} {b}" if use_spaces else f"{a}{op}{b}"
|
| 460 |
+
# Keep call expression identical to prompt expression or normalized so 1-hop pointer copies cleanly
|
| 461 |
+
call_expr = expr_prompt if rng.random() < 0.65 else f"{a}{op}{b}"
|
| 462 |
+
user_prompt = rng.choice([
|
| 463 |
+
f"Calculate {expr_prompt}{p_mark}",
|
| 464 |
+
f"What is {expr_prompt}{q_mark}",
|
| 465 |
+
f"Compute {expr_prompt}{p_mark}",
|
| 466 |
+
f"Evaluate {expr_prompt}{p_mark}",
|
| 467 |
+
f"Evaluate the mathematical expression {expr_prompt}{p_mark}",
|
| 468 |
+
f"Solve {expr_prompt}{p_mark}",
|
| 469 |
+
])
|
| 470 |
+
thought = rng.choice([
|
| 471 |
+
"The user requests an arithmetic calculation. I will invoke the calculator tool.",
|
| 472 |
+
"The user is asking to evaluate a mathematical expression. Calling calculator.",
|
| 473 |
+
])
|
| 474 |
+
call = f"calculator(expr=\"{call_expr}\")"
|
| 475 |
+
result = json.dumps({"result": res})
|
| 476 |
+
answer = f"The evaluated result of {call_expr} is {res}."
|
| 477 |
+
|
| 478 |
+
elif sub == "weather":
|
| 479 |
+
if rng.random() < 0.35:
|
| 480 |
+
# Nonce city to force pure copy induction
|
| 481 |
+
city = generate_nonce_entity(rng)[0].capitalize()
|
| 482 |
+
else:
|
| 483 |
+
city = rng.choice(CITIES)
|
| 484 |
+
temp = rng.randint(-5, 35)
|
| 485 |
+
cond = rng.choice(CONDITIONS)
|
| 486 |
+
user_prompt = rng.choice([
|
| 487 |
+
f"What is the weather in {city}{q_mark}",
|
| 488 |
+
f"What is the weather on {city}{q_mark}",
|
| 489 |
+
f"What is the weather for {city}{q_mark}",
|
| 490 |
+
f"What is the current weather forecast for {city}{q_mark}",
|
| 491 |
+
f"What is the weather like in {city}{q_mark}",
|
| 492 |
+
f"How is the weather in {city}{q_mark}",
|
| 493 |
+
f"How is the weather on {city}{q_mark}",
|
| 494 |
+
f"Weather in {city}{p_mark}",
|
| 495 |
+
f"Weather on {city}{p_mark}",
|
| 496 |
+
f"Weather for {city}{p_mark}",
|
| 497 |
+
f"Check the weather in {city}{p_mark}",
|
| 498 |
+
f"Check the weather for {city}{p_mark}",
|
| 499 |
+
f"Tell me the weather in {city}{p_mark}",
|
| 500 |
+
f"Forecast for {city}{p_mark}",
|
| 501 |
+
])
|
| 502 |
+
thought = rng.choice([
|
| 503 |
+
"The user requests current weather information. I will query the get_weather tool.",
|
| 504 |
+
"The user is asking for weather conditions. Querying get_weather.",
|
| 505 |
+
])
|
| 506 |
+
call = f"get_weather(city=\"{city}\")"
|
| 507 |
+
result = json.dumps({"temp": temp, "condition": cond, "units": "celsius"})
|
| 508 |
+
answer = f"The weather in {city} is currently {cond} with a temperature of {temp} degrees Celsius."
|
| 509 |
+
|
| 510 |
+
elif sub == "search":
|
| 511 |
+
search_mode = rng.random()
|
| 512 |
+
if search_mode < 0.50:
|
| 513 |
+
ent, plural, ent_type = generate_nonce_entity(rng)
|
| 514 |
+
snippet = f"a hypothetical {ent_type} characterized in general knowledge."
|
| 515 |
+
elif search_mode < 0.85 and bool(SEARCH_ENTITIES):
|
| 516 |
+
ent_item = rng.choice(SEARCH_ENTITIES)
|
| 517 |
+
ent = ent_item["entity"]
|
| 518 |
+
plural = ent_item.get("plural", ent + "s")
|
| 519 |
+
snippet = ent_item["snippet"]
|
| 520 |
+
ent_type = ent_item.get("type", "concept")
|
| 521 |
+
else:
|
| 522 |
+
ent = None
|
| 523 |
+
|
| 524 |
+
if ent is not None:
|
| 525 |
+
variant = rng.choice([
|
| 526 |
+
"what_are_plural",
|
| 527 |
+
"what_is_a",
|
| 528 |
+
"what_is",
|
| 529 |
+
"tell_me_about",
|
| 530 |
+
"explain",
|
| 531 |
+
"define",
|
| 532 |
+
])
|
| 533 |
+
|
| 534 |
+
if variant == "what_are_plural":
|
| 535 |
+
user_prompt = f"What are {plural}{q_mark}"
|
| 536 |
+
search_q = plural
|
| 537 |
+
answer = f"{plural.capitalize()} are {snippet}"
|
| 538 |
+
elif variant == "what_is_a" and ent_type in ["plant", "fruit", "vegetable", "animal", "creature", "artifact", "instrument", "vehicle"]:
|
| 539 |
+
article = "an" if ent[0].lower() in "aeiou" else "a"
|
| 540 |
+
user_prompt = f"What is {article} {ent}{q_mark}"
|
| 541 |
+
search_q = ent
|
| 542 |
+
answer = f"{ent.capitalize()} is {snippet}"
|
| 543 |
+
elif variant == "tell_me_about":
|
| 544 |
+
user_prompt = f"Tell me about {ent}{p_mark}"
|
| 545 |
+
search_q = ent
|
| 546 |
+
answer = f"According to verified search results, {ent} is {snippet}"
|
| 547 |
+
elif variant == "define":
|
| 548 |
+
user_prompt = f"Define {ent}{p_mark}"
|
| 549 |
+
search_q = ent
|
| 550 |
+
answer = f"{ent.capitalize()} is defined as {snippet}"
|
| 551 |
+
elif variant == "explain":
|
| 552 |
+
user_prompt = f"Explain {ent}{p_mark}"
|
| 553 |
+
search_q = ent
|
| 554 |
+
answer = f"{ent.capitalize()} is {snippet}"
|
| 555 |
+
else:
|
| 556 |
+
user_prompt = f"What is {ent}{q_mark}"
|
| 557 |
+
search_q = ent
|
| 558 |
+
answer = f"{ent.capitalize()} is {snippet}"
|
| 559 |
+
|
| 560 |
+
thought = rng.choice([
|
| 561 |
+
"The user is asking a factual question. I will query the search tool.",
|
| 562 |
+
"The user requests factual information. Querying search.",
|
| 563 |
+
"The user is seeking knowledge on this topic. I will search the web.",
|
| 564 |
+
])
|
| 565 |
+
call = f"search(query=\"{search_q}\")"
|
| 566 |
+
result = json.dumps({"snippet": snippet})
|
| 567 |
+
else:
|
| 568 |
+
topic, fact, _ = rng.choice(SEARCH_FACTS)
|
| 569 |
+
user_prompt = rng.choice([
|
| 570 |
+
f"Who or what is {topic}{q_mark}",
|
| 571 |
+
f"What is {topic}{q_mark}",
|
| 572 |
+
f"Tell me about {topic}{p_mark}",
|
| 573 |
+
])
|
| 574 |
+
thought = "The user is asking a factual question. I will query the search tool."
|
| 575 |
+
call = f"search(query=\"{topic}\")"
|
| 576 |
+
result = json.dumps({"snippet": fact})
|
| 577 |
+
answer = f"According to verified search results, {topic} is {fact}."
|
| 578 |
+
|
| 579 |
+
elif sub == "time":
|
| 580 |
+
tz = rng.choice(TIMEZONES)
|
| 581 |
+
hr = rng.randint(0, 23)
|
| 582 |
+
mn = rng.randint(0, 59)
|
| 583 |
+
time_str = f"{hr:02d}:{mn:02d}"
|
| 584 |
+
user_prompt = rng.choice([
|
| 585 |
+
f"What is the current time in {tz}{q_mark}",
|
| 586 |
+
f"What is the time in {tz}{q_mark}",
|
| 587 |
+
f"What time is it in {tz}{q_mark}",
|
| 588 |
+
f"Current time in {tz}{p_mark}",
|
| 589 |
+
f"Time in {tz}{p_mark}",
|
| 590 |
+
f"Tell me the time in {tz}{p_mark}",
|
| 591 |
+
f"Check the time for {tz}{p_mark}",
|
| 592 |
+
])
|
| 593 |
+
thought = rng.choice([
|
| 594 |
+
"The user requests the current time for a timezone. I will query the get_time tool.",
|
| 595 |
+
"The user is asking for the current time. Querying get_time.",
|
| 596 |
+
])
|
| 597 |
+
call = f"get_time(timezone=\"{tz}\")"
|
| 598 |
+
result = json.dumps({"time": time_str, "timezone": tz})
|
| 599 |
+
answer = f"The current time in {tz} is {time_str}."
|
| 600 |
+
|
| 601 |
+
else: # unit_convert
|
| 602 |
+
val = rng.randint(5, 500)
|
| 603 |
+
from_u, to_u, factor = rng.choice([
|
| 604 |
+
("km", "miles", 0.621371),
|
| 605 |
+
("miles", "km", 1.60934),
|
| 606 |
+
("kg", "lbs", 2.20462),
|
| 607 |
+
("lbs", "kg", 0.453592),
|
| 608 |
+
])
|
| 609 |
+
converted = round(val * factor, 2)
|
| 610 |
+
user_prompt = rng.choice([
|
| 611 |
+
f"Convert {val} {from_u} to {to_u}{p_mark}",
|
| 612 |
+
f"How many {to_u} is {val} {from_u}{q_mark}",
|
| 613 |
+
f"How many {to_u} are in {val} {from_u}{q_mark}",
|
| 614 |
+
f"What is {val} {from_u} in {to_u}{q_mark}",
|
| 615 |
+
f"Change {val} {from_u} into {to_u}{p_mark}",
|
| 616 |
+
])
|
| 617 |
+
thought = rng.choice([
|
| 618 |
+
"The user requests a unit conversion. I will invoke the unit_convert tool.",
|
| 619 |
+
"The user is asking to convert between measurement units. Calling unit_convert.",
|
| 620 |
+
])
|
| 621 |
+
call = f"unit_convert(value={val}, from_unit=\"{from_u}\", to_unit=\"{to_u}\")"
|
| 622 |
+
result = json.dumps({"value": converted, "unit": to_u})
|
| 623 |
+
answer = f"{val} {from_u} is equal to {converted} {to_u}."
|
| 624 |
+
|
| 625 |
+
full_text = (
|
| 626 |
+
f"<USER>{user_prompt}</USER>"
|
| 627 |
+
f"<THINK>{thought}</THINK>"
|
| 628 |
+
f"<CALL>{call}</CALL>"
|
| 629 |
+
f"<RESULT>{result}</RESULT>"
|
| 630 |
+
f"<ANSWER>{answer}</ANSWER>"
|
| 631 |
+
)
|
| 632 |
+
|
| 633 |
+
elif category == "multi_tool_chain":
|
| 634 |
+
# Chained sequential reasoning: Step 1 -> Tool 1 -> Result 1 -> Step 2 -> Tool 2 -> Result 2 -> Final Answer
|
| 635 |
+
chain_type = rng.choice(["fact_calc", "dual_weather", "tz_calc"])
|
| 636 |
+
if chain_type == "fact_calc":
|
| 637 |
+
pop = 14000000
|
| 638 |
+
area = 2194
|
| 639 |
+
density = round(pop / area, 2)
|
| 640 |
+
user_prompt = f"What is the population density of Tokyo based on its population and area of 2194 sq km{q_mark}"
|
| 641 |
+
thought1 = "First, I need to search for Tokyo's population."
|
| 642 |
+
call1 = "search(query=\"Tokyo population\")"
|
| 643 |
+
result1 = json.dumps({"population": pop})
|
| 644 |
+
thought2 = "Now I will calculate the population density using the calculator tool."
|
| 645 |
+
call2 = f"calculator(expr=\"{pop} / {area}\")"
|
| 646 |
+
result2 = json.dumps({"result": density})
|
| 647 |
+
answer = f"Tokyo's population is {pop:,}. Dividing by 2,194 sq km gives a density of approximately {density} people per sq km."
|
| 648 |
+
|
| 649 |
+
elif chain_type == "dual_weather":
|
| 650 |
+
c1, c2 = rng.sample(CITIES, 2)
|
| 651 |
+
t1, t2 = rng.randint(0, 30), rng.randint(0, 30)
|
| 652 |
+
diff = abs(t1 - t2)
|
| 653 |
+
user_prompt = rng.choice([
|
| 654 |
+
f"Compare the temperature between {c1} and {c2}{p_mark}",
|
| 655 |
+
f"Compare the weather in {c1} and {c2}{p_mark}",
|
| 656 |
+
f"What is the temperature difference between {c1} and {c2}{q_mark}",
|
| 657 |
+
])
|
| 658 |
+
thought1 = "I will first query the weather for the first city."
|
| 659 |
+
call1 = f"get_weather(city=\"{c1}\")"
|
| 660 |
+
result1 = json.dumps({"temp": t1, "condition": "Cloudy"})
|
| 661 |
+
thought2 = "Next, I will query the weather for the second city."
|
| 662 |
+
call2 = f"get_weather(city=\"{c2}\")"
|
| 663 |
+
result2 = json.dumps({"temp": t2, "condition": "Sunny"})
|
| 664 |
+
answer = f"{c1} is {t1}C and {c2} is {t2}C, with a temperature difference of {diff} degrees."
|
| 665 |
+
|
| 666 |
+
else: # tz_calc
|
| 667 |
+
tz1, tz2 = rng.sample(TIMEZONES[:6], 2)
|
| 668 |
+
user_prompt = f"What is the time difference between {tz1} and {tz2}{q_mark}"
|
| 669 |
+
thought1 = "First, I will query the current time in the first timezone."
|
| 670 |
+
call1 = f"get_time(timezone=\"{tz1}\")"
|
| 671 |
+
result1 = json.dumps({"time": "08:00", "utc_offset": -5})
|
| 672 |
+
thought2 = "Next, I will query the current time in the second timezone."
|
| 673 |
+
call2 = f"get_time(timezone=\"{tz2}\")"
|
| 674 |
+
result2 = json.dumps({"time": "22:00", "utc_offset": 9})
|
| 675 |
+
answer = f"{tz1} is UTC-5 and {tz2} is UTC+9, representing a 14-hour time difference."
|
| 676 |
+
|
| 677 |
+
full_text = (
|
| 678 |
+
f"<USER>{user_prompt}</USER>"
|
| 679 |
+
f"<THINK>{thought1}</THINK>"
|
| 680 |
+
f"<CALL>{call1}</CALL>"
|
| 681 |
+
f"<RESULT>{result1}</RESULT>"
|
| 682 |
+
f"<THINK>{thought2}</THINK>"
|
| 683 |
+
f"<CALL>{call2}</CALL>"
|
| 684 |
+
f"<RESULT>{result2}</RESULT>"
|
| 685 |
+
f"<ANSWER>{answer}</ANSWER>"
|
| 686 |
+
)
|
| 687 |
+
|
| 688 |
+
elif category == "nested_json_schema":
|
| 689 |
+
# Complex tools with nested dictionaries, lists, and boolean flags
|
| 690 |
+
schema_type = rng.choice(["db_query", "event_create", "file_manage", "api_fetch"])
|
| 691 |
+
if schema_type == "db_query":
|
| 692 |
+
dept = rng.choice(DEPARTMENTS)
|
| 693 |
+
limit = rng.randint(3, 10)
|
| 694 |
+
user_prompt = rng.choice([
|
| 695 |
+
f"Query the top {limit} active records in the {dept} department{p_mark}",
|
| 696 |
+
f"Query active records in the {dept} department with limit {limit}{p_mark}",
|
| 697 |
+
f"Fetch the top {limit} active entries from {dept}{p_mark}",
|
| 698 |
+
])
|
| 699 |
+
thought = "I will invoke database_query with a nested filter and options schema."
|
| 700 |
+
call = f"database_query(filter={{\"department\": \"{dept}\", \"status\": \"active\"}}, options={{\"limit\": {limit}, \"sort\": \"desc\"}})"
|
| 701 |
+
result = json.dumps({"status": "success", "count": limit, "records": [{"id": 100 + i, "dept": dept} for i in range(min(limit, 3))]})
|
| 702 |
+
answer = f"Found {limit} active records in {dept} according to your filter specifications."
|
| 703 |
+
|
| 704 |
+
elif schema_type == "event_create":
|
| 705 |
+
attendees = ["alice@corp.internal", "bob@corp.internal"]
|
| 706 |
+
user_prompt = f"Schedule a Sprint Sync meeting for tomorrow from 10:00 to 11:00 UTC with Alice and Bob{p_mark}"
|
| 707 |
+
thought = "I need to call create_event with a nested event schedule and attendee list."
|
| 708 |
+
call = "create_event(event={\"title\": \"Sprint Sync\", \"schedule\": {\"date\": \"2026-11-01\", \"start\": \"10:00\", \"end\": \"11:00\", \"tz\": \"UTC\"}}, attendees=[\"alice@corp.internal\", \"bob@corp.internal\"])"
|
| 709 |
+
result = json.dumps({"event_id": "evt_98214", "status": "confirmed"})
|
| 710 |
+
answer = "Successfully scheduled the Sprint Sync meeting with Alice and Bob for 10:00-11:00 UTC (Event ID: evt_98214)."
|
| 711 |
+
|
| 712 |
+
elif schema_type == "file_manage":
|
| 713 |
+
path = "/configs/model_t4.json"
|
| 714 |
+
user_prompt = f"Save model configuration to {path} with fp16 enabled and 2 workers{p_mark}"
|
| 715 |
+
thought = "Calling file_manager with action write and a nested file payload."
|
| 716 |
+
call = f"file_manager(action=\"write\", file={{\"path\": \"{path}\", \"format\": \"json\"}}, content={{\"fp16\": true, \"workers\": 2}})"
|
| 717 |
+
result = json.dumps({"status": "ok", "bytes_written": 48})
|
| 718 |
+
answer = f"Configuration file has been written to {path} with fp16=True and 2 workers."
|
| 719 |
+
|
| 720 |
+
else: # api_fetch
|
| 721 |
+
endpoint = "/v2/metrics/latency"
|
| 722 |
+
user_prompt = f"Fetch latency metrics for EU region from {endpoint}{p_mark}"
|
| 723 |
+
thought = "Calling fetch_api with endpoint and nested filter parameters."
|
| 724 |
+
call = f"fetch_api(endpoint=\"{endpoint}\", params={{\"region\": \"EU\", \"metric\": \"p99\"}}, method=\"GET\")"
|
| 725 |
+
result = json.dumps({"status": 200, "data": {"p99_ms": 14.2}})
|
| 726 |
+
answer = f"Retrieved p99 latency for EU region from {endpoint}: 14.2 ms."
|
| 727 |
+
|
| 728 |
+
full_text = (
|
| 729 |
+
f"<USER>{user_prompt}</USER>"
|
| 730 |
+
f"<THINK>{thought}</THINK>"
|
| 731 |
+
f"<CALL>{call}</CALL>"
|
| 732 |
+
f"<RESULT>{result}</RESULT>"
|
| 733 |
+
f"<ANSWER>{answer}</ANSWER>"
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
elif category == "error_recovery":
|
| 737 |
+
# Robust error reasoning & recovery paths
|
| 738 |
+
err_type = rng.choice(["div_zero", "city_not_found", "api_503_fallback", "table_missing"])
|
| 739 |
+
if err_type == "div_zero":
|
| 740 |
+
num = rng.choice([100, 150, 250, 500])
|
| 741 |
+
user_prompt = f"Calculate {num} / 0"
|
| 742 |
+
thought1 = "The user requests an arithmetic calculation. I will invoke the calculator tool."
|
| 743 |
+
call1 = f"calculator(expr=\"{num}/0\")"
|
| 744 |
+
result1 = json.dumps({"error": "ZeroDivisionError: division by zero"})
|
| 745 |
+
thought2 = "The calculator returned ZeroDivisionError. Division by zero is mathematically undefined. I will explain this clearly."
|
| 746 |
+
answer = "Error: Division by zero is undefined in mathematics. Please specify a non-zero denominator."
|
| 747 |
+
full_text = (
|
| 748 |
+
f"<USER>{user_prompt}</USER>"
|
| 749 |
+
f"<THINK>{thought1}</THINK>"
|
| 750 |
+
f"<CALL>{call1}</CALL>"
|
| 751 |
+
f"<RESULT>{result1}</RESULT>"
|
| 752 |
+
f"<THINK>{thought2}</THINK>"
|
| 753 |
+
f"<ANSWER>{answer}</ANSWER>"
|
| 754 |
+
)
|
| 755 |
+
|
| 756 |
+
elif err_type == "city_not_found":
|
| 757 |
+
city = rng.choice(["Atlantis", "Narnia"])
|
| 758 |
+
user_prompt = f"What is the weather in {city}{q_mark}"
|
| 759 |
+
thought1 = "The user requests current weather information. I will query the get_weather tool."
|
| 760 |
+
call1 = f"get_weather(city=\"{city}\")"
|
| 761 |
+
result1 = json.dumps({"status": "error", "code": 404, "message": f"City '{city}' not found"})
|
| 762 |
+
thought2 = "get_weather returned 404. The requested city is not recognized. I must inform the user."
|
| 763 |
+
answer = f"I could not retrieve weather data for {city} because it is not recognized in our weather database."
|
| 764 |
+
full_text = (
|
| 765 |
+
f"<USER>{user_prompt}</USER>"
|
| 766 |
+
f"<THINK>{thought1}</THINK>"
|
| 767 |
+
f"<CALL>{call1}</CALL>"
|
| 768 |
+
f"<RESULT>{result1}</RESULT>"
|
| 769 |
+
f"<THINK>{thought2}</THINK>"
|
| 770 |
+
f"<ANSWER>{answer}</ANSWER>"
|
| 771 |
+
)
|
| 772 |
+
|
| 773 |
+
elif err_type == "api_503_fallback":
|
| 774 |
+
city = "Tokyo"
|
| 775 |
+
user_prompt = f"Check the primary API forecast for {city}{p_mark}"
|
| 776 |
+
thought1 = "Attempting primary weather API fetch."
|
| 777 |
+
call1 = f"fetch_api(endpoint=\"/v1/weather\", params={{\"city\": \"{city}\"}}, method=\"GET\")"
|
| 778 |
+
result1 = json.dumps({"status": "error", "code": 503, "message": "Service Unavailable"})
|
| 779 |
+
thought2 = "Primary API failed with 503. Recovering via fallback tool get_weather."
|
| 780 |
+
call2 = f"get_weather(city=\"{city}\")"
|
| 781 |
+
result2 = json.dumps({"temp": 22, "condition": "Sunny"})
|
| 782 |
+
answer = "Primary service was temporarily unavailable, but fallback weather confirmed Tokyo is Sunny at 22C."
|
| 783 |
+
full_text = (
|
| 784 |
+
f"<USER>{user_prompt}</USER>"
|
| 785 |
+
f"<THINK>{thought1}</THINK>"
|
| 786 |
+
f"<CALL>{call1}</CALL>"
|
| 787 |
+
f"<RESULT>{result1}</RESULT>"
|
| 788 |
+
f"<THINK>{thought2}</THINK>"
|
| 789 |
+
f"<CALL>{call2}</CALL>"
|
| 790 |
+
f"<RESULT>{result2}</RESULT>"
|
| 791 |
+
f"<ANSWER>{answer}</ANSWER>"
|
| 792 |
+
)
|
| 793 |
+
|
| 794 |
+
else: # table_missing
|
| 795 |
+
user_prompt = f"Query records from table 'legacy_metrics'{p_mark}"
|
| 796 |
+
thought1 = "Querying database for the requested table."
|
| 797 |
+
call1 = "database_query(filter={\"table\": \"legacy_metrics\"}, options={\"limit\": 5})"
|
| 798 |
+
result1 = json.dumps({"status": "error", "code": "TABLE_NOT_FOUND", "message": "Table does not exist"})
|
| 799 |
+
thought2 = "TABLE_NOT_FOUND error encountered. Informing user of invalid table name."
|
| 800 |
+
answer = "The requested database table 'legacy_metrics' was not found. Please verify table name."
|
| 801 |
+
full_text = (
|
| 802 |
+
f"<USER>{user_prompt}</USER>"
|
| 803 |
+
f"<THINK>{thought1}</THINK>"
|
| 804 |
+
f"<CALL>{call1}</CALL>"
|
| 805 |
+
f"<RESULT>{result1}</RESULT>"
|
| 806 |
+
f"<THINK>{thought2}</THINK>"
|
| 807 |
+
f"<ANSWER>{answer}</ANSWER>"
|
| 808 |
+
)
|
| 809 |
+
|
| 810 |
+
elif category == "multi_turn":
|
| 811 |
+
# Multi-turn conversational dialogue with state continuity
|
| 812 |
+
city = rng.choice(CITIES)
|
| 813 |
+
temp = rng.randint(10, 25)
|
| 814 |
+
w_q = rng.choice([
|
| 815 |
+
f"How is the weather in {city}{q_mark}",
|
| 816 |
+
f"What is the weather in {city}{q_mark}",
|
| 817 |
+
f"What is the weather on {city}{q_mark}",
|
| 818 |
+
])
|
| 819 |
+
full_text = (
|
| 820 |
+
f"<USER>Hi there!</USER>"
|
| 821 |
+
f"<THINK>Conversational greeting without tool.</THINK>"
|
| 822 |
+
f"<ANSWER>Hello! How can I assist you today?</ANSWER>"
|
| 823 |
+
f"<USER>{w_q}</USER>"
|
| 824 |
+
f"<THINK>The user requests current weather information. I will query the get_weather tool.</THINK>"
|
| 825 |
+
f"<CALL>get_weather(city=\"{city}\")</CALL>"
|
| 826 |
+
f"<RESULT>{{\"temp\": {temp}, \"condition\": \"Clear\"}}</RESULT>"
|
| 827 |
+
f"<ANSWER>{city} is currently Clear with a temperature of {temp} degrees Celsius.</ANSWER>"
|
| 828 |
+
f"<USER>Thank you!</USER>"
|
| 829 |
+
f"<THINK>User acknowledges. Polite closing response.</THINK>"
|
| 830 |
+
f"<ANSWER>You are very welcome! Let me know if you need anything else.</ANSWER>"
|
| 831 |
+
)
|
| 832 |
+
|
| 833 |
+
else: # no_tool
|
| 834 |
+
greeting = rng.choice([
|
| 835 |
+
("Hello! How are you?", "I am doing well, thank you! How may I assist you today?"),
|
| 836 |
+
("Hi there!", "Hello! What can I help you with today?"),
|
| 837 |
+
("Thank you for your assistance.", "You are welcome! Let me know if you have any more questions."),
|
| 838 |
+
("Thank you!", "You are very welcome! Let me know if you need anything else."),
|
| 839 |
+
("Good morning!", "Good morning! How may I help you?"),
|
| 840 |
+
("Who are you?", "I am ScaledGPT, an AI assistant with structured tool-calling and high-digit arithmetic capabilities."),
|
| 841 |
+
("Explain the concept of gradient descent.", "Gradient descent is an optimization algorithm that iteratively moves model weights in the direction of steepest loss decrease."),
|
| 842 |
+
])
|
| 843 |
+
user_prompt, ans = greeting
|
| 844 |
+
thought = "Conversational request that does not require any tool. I will answer directly."
|
| 845 |
+
full_text = (
|
| 846 |
+
f"<USER>{user_prompt}</USER>"
|
| 847 |
+
f"<THINK>{thought}</THINK>"
|
| 848 |
+
f"<ANSWER>{ans}</ANSWER>"
|
| 849 |
+
)
|
| 850 |
+
|
| 851 |
+
return {
|
| 852 |
+
"category": category,
|
| 853 |
+
"full_text": full_text,
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
def generate_tool_dataset(n_samples: int = 4000, seed: int = 1337) -> List[Dict[str, str]]:
|
| 858 |
+
"""Generates an expanded tool interaction dataset across all 6 categories."""
|
| 859 |
+
rng = random.Random(seed)
|
| 860 |
+
dataset: List[Dict[str, str]] = []
|
| 861 |
+
for _ in range(n_samples):
|
| 862 |
+
ex = generate_tool_example(rng)
|
| 863 |
+
full_text = ex["full_text"]
|
| 864 |
+
prompt = full_text.split("</USER>")[0] + "</USER>"
|
| 865 |
+
dataset.append({
|
| 866 |
+
"task_type": "tools",
|
| 867 |
+
"prompt": prompt,
|
| 868 |
+
"full_text": full_text,
|
| 869 |
+
"category": ex["category"],
|
| 870 |
+
})
|
| 871 |
+
return dataset
|
| 872 |
+
|
| 873 |
+
|
| 874 |
+
def generate_joint_dataset(
|
| 875 |
+
n_math: int = 4000,
|
| 876 |
+
n_tools: int = 4000,
|
| 877 |
+
seed: int = 42,
|
| 878 |
+
max_train_digits: int = 15,
|
| 879 |
+
) -> Tuple[List[Dict[str, str]], List[Dict[str, str]]]:
|
| 880 |
+
"""Generates dual datasets for math curriculum and expanded tools."""
|
| 881 |
+
math_samples = generate_arithmetic_equations(n_samples=n_math, seed=seed, max_train_digits=max_train_digits)
|
| 882 |
+
tool_samples = generate_tool_dataset(n_samples=n_tools, seed=seed + 100)
|
| 883 |
+
return math_samples, tool_samples
|
| 884 |
+
|
| 885 |
+
|
| 886 |
+
# -----------------------------------------------------------------------------
|
| 887 |
+
# 3. Batch Encoder & Strict Loss Masker
|
| 888 |
+
# -----------------------------------------------------------------------------
|
| 889 |
+
|
| 890 |
+
def encode_joint_batch(
|
| 891 |
+
math_items: List[Dict[str, str]],
|
| 892 |
+
tool_items: List[Dict[str, str]],
|
| 893 |
+
tokenizer: ScaledTokenizer,
|
| 894 |
+
max_len: Optional[int] = None,
|
| 895 |
+
device: Optional[torch.device] = None,
|
| 896 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 897 |
+
"""
|
| 898 |
+
Encodes balanced joint batch of math and tool sequences.
|
| 899 |
+
Strict Prompt & Environment Loss Masking:
|
| 900 |
+
- Math: masks everything up to and including '<THINK>' (loss on scratchpad + answer).
|
| 901 |
+
- Tools:
|
| 902 |
+
- Masks everything inside '<USER>...</USER>' except at '</USER>' (where target is '<THINK>').
|
| 903 |
+
- Masks everything inside '<RESULT>...</RESULT>' except at '</RESULT>' (where target is '<THINK>' or '<ANSWER>').
|
| 904 |
+
- Supervised: reasoning traces '<THINK>', tool calls '<CALL>', and answers '<ANSWER>'.
|
| 905 |
+
"""
|
| 906 |
+
items = math_items + tool_items
|
| 907 |
+
B = len(items)
|
| 908 |
+
|
| 909 |
+
encoded_seqs = [tokenizer.encode(item["full_text"], add_bos=True, add_eos=True) for item in items]
|
| 910 |
+
calc_max_len = max(len(s) for s in encoded_seqs) - 1
|
| 911 |
+
if max_len is not None:
|
| 912 |
+
batch_max = min(calc_max_len, max_len)
|
| 913 |
+
else:
|
| 914 |
+
batch_max = calc_max_len
|
| 915 |
+
|
| 916 |
+
x_tensor = torch.full((B, batch_max), tokenizer.pad_id, dtype=torch.long)
|
| 917 |
+
y_tensor = torch.full((B, batch_max), -100, dtype=torch.long)
|
| 918 |
+
|
| 919 |
+
think_s_id = tokenizer.think_s_id
|
| 920 |
+
user_s_id = tokenizer.user_s_id
|
| 921 |
+
user_e_id = tokenizer.user_e_id
|
| 922 |
+
result_s_id = tokenizer.result_s_id
|
| 923 |
+
result_e_id = tokenizer.result_e_id
|
| 924 |
+
|
| 925 |
+
for i, (item, tokens) in enumerate(zip(items, encoded_seqs)):
|
| 926 |
+
# Truncate if exceeding batch_max + 1
|
| 927 |
+
if len(tokens) > batch_max + 1:
|
| 928 |
+
tokens = tokens[: batch_max + 1]
|
| 929 |
+
|
| 930 |
+
n = len(tokens) - 1
|
| 931 |
+
t_tok = torch.tensor(tokens, dtype=torch.long)
|
| 932 |
+
x_tensor[i, :n] = t_tok[:-1]
|
| 933 |
+
y_tensor[i, :n] = t_tok[1:].clone()
|
| 934 |
+
|
| 935 |
+
if item["task_type"] == "math":
|
| 936 |
+
# Mask prompt up to '<THINK>'
|
| 937 |
+
think_matches = (t_tok[:-1] == think_s_id).nonzero(as_tuple=True)[0]
|
| 938 |
+
if len(think_matches) > 0:
|
| 939 |
+
mask_until = think_matches[0].item()
|
| 940 |
+
y_tensor[i, :mask_until] = -100
|
| 941 |
+
else:
|
| 942 |
+
# Segment-aware masking for single-turn, multi-turn, and tool responses
|
| 943 |
+
mask = torch.zeros(n, dtype=torch.bool)
|
| 944 |
+
in_user = False
|
| 945 |
+
in_result = False
|
| 946 |
+
|
| 947 |
+
for pos in range(n):
|
| 948 |
+
tok_id = t_tok[pos].item()
|
| 949 |
+
if tok_id == user_s_id:
|
| 950 |
+
in_user = True
|
| 951 |
+
elif tok_id == result_s_id:
|
| 952 |
+
in_result = True
|
| 953 |
+
|
| 954 |
+
if in_user:
|
| 955 |
+
if tok_id == user_e_id:
|
| 956 |
+
in_user = False
|
| 957 |
+
mask[pos] = False
|
| 958 |
+
else:
|
| 959 |
+
mask[pos] = True
|
| 960 |
+
elif in_result:
|
| 961 |
+
if tok_id == result_e_id:
|
| 962 |
+
in_result = False
|
| 963 |
+
mask[pos] = False
|
| 964 |
+
else:
|
| 965 |
+
mask[pos] = True
|
| 966 |
+
elif pos == 0:
|
| 967 |
+
# BOS token preceding <USER>
|
| 968 |
+
mask[pos] = True
|
| 969 |
+
|
| 970 |
+
y_tensor[i, :n][mask] = -100
|
| 971 |
+
|
| 972 |
+
if device is not None:
|
| 973 |
+
x_tensor = x_tensor.to(device)
|
| 974 |
+
y_tensor = y_tensor.to(device)
|
| 975 |
+
|
| 976 |
+
return x_tensor, y_tensor
|
interactive.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Interactive CLI Runner for ScaledGPT (~18.95M Parameter Recurrent 2D-RoPE Transformer).
|
| 4 |
+
Supports live interactive inference across:
|
| 5 |
+
1. High-digit column scratchpad arithmetic (up to 50 digits).
|
| 6 |
+
2. Expanded tool execution (Single-tool, Chaining, Nested JSON schemas, Error handling).
|
| 7 |
+
3. Multi-turn interactive dialogues.
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
import sys
|
| 12 |
+
import re
|
| 13 |
+
import json
|
| 14 |
+
import time
|
| 15 |
+
import argparse
|
| 16 |
+
from typing import Tuple, Optional, Dict, Any
|
| 17 |
+
|
| 18 |
+
import torch
|
| 19 |
+
|
| 20 |
+
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 21 |
+
if ROOT_DIR not in sys.path:
|
| 22 |
+
sys.path.insert(0, ROOT_DIR)
|
| 23 |
+
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 24 |
+
if THIS_DIR not in sys.path:
|
| 25 |
+
sys.path.insert(0, THIS_DIR)
|
| 26 |
+
|
| 27 |
+
if hasattr(sys.stdout, "reconfigure"):
|
| 28 |
+
sys.stdout.reconfigure(line_buffering=True)
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
from champion_model.scaled_t4.model import ScaledGPT, ScaledGPTConfig
|
| 32 |
+
from champion_model.scaled_t4.tokenizer import ScaledTokenizer
|
| 33 |
+
from champion_model.scaled_t4.dataset import extract_scratchpad_answer
|
| 34 |
+
except ImportError:
|
| 35 |
+
from model import ScaledGPT, ScaledGPTConfig
|
| 36 |
+
from tokenizer import ScaledTokenizer
|
| 37 |
+
from dataset import extract_scratchpad_answer
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def mock_execute_tool(call_text: str) -> str:
|
| 41 |
+
"""Executes or mocks tool calls parsed from <CALL>...</CALL>."""
|
| 42 |
+
# 1. calculator(expr="...")
|
| 43 |
+
calc_m = re.match(r'calculator\(expr="([^"]+)"\)', call_text)
|
| 44 |
+
if calc_m:
|
| 45 |
+
expr = calc_m.group(1)
|
| 46 |
+
try:
|
| 47 |
+
allowed = re.match(r"^[\d\+\-\*\/\(\)\s\.]+$", expr)
|
| 48 |
+
if allowed:
|
| 49 |
+
val = eval(expr)
|
| 50 |
+
return json.dumps({"result": val})
|
| 51 |
+
except ZeroDivisionError:
|
| 52 |
+
return json.dumps({"error": "ZeroDivisionError: division by zero"})
|
| 53 |
+
except Exception as e:
|
| 54 |
+
return json.dumps({"error": str(e)})
|
| 55 |
+
|
| 56 |
+
# 2. get_weather(city="...")
|
| 57 |
+
w_m = re.match(r'get_weather\(city="([^"]+)"\)', call_text)
|
| 58 |
+
if w_m:
|
| 59 |
+
city = w_m.group(1)
|
| 60 |
+
if city.lower() in ["atlantis", "narnia"]:
|
| 61 |
+
return json.dumps({"status": "error", "code": 404, "message": f"City '{city}' not found"})
|
| 62 |
+
return json.dumps({"temp": 22, "condition": "Sunny", "city": city})
|
| 63 |
+
|
| 64 |
+
# 3. get_time(timezone="...")
|
| 65 |
+
t_m = re.match(r'get_time\(timezone="([^"]+)"\)', call_text)
|
| 66 |
+
if t_m:
|
| 67 |
+
tz = t_m.group(1)
|
| 68 |
+
return json.dumps({"time": "14:30", "timezone": tz})
|
| 69 |
+
|
| 70 |
+
# 4. unit_convert
|
| 71 |
+
u_m = re.match(r'unit_convert\(value=([\d\.]+),\s*from_unit="([^"]+)",\s*to_unit="([^"]+)"\)', call_text)
|
| 72 |
+
if u_m:
|
| 73 |
+
val = float(u_m.group(1))
|
| 74 |
+
f_u, t_u = u_m.group(2), u_m.group(3)
|
| 75 |
+
factors = {
|
| 76 |
+
("km", "miles"): 0.621371,
|
| 77 |
+
("miles", "km"): 1.60934,
|
| 78 |
+
("kg", "lbs"): 2.20462,
|
| 79 |
+
("lbs", "kg"): 0.453592,
|
| 80 |
+
}
|
| 81 |
+
factor = factors.get((f_u, t_u), 1.0)
|
| 82 |
+
return json.dumps({"value": round(val * factor, 2), "unit": t_u})
|
| 83 |
+
|
| 84 |
+
# 5. database_query
|
| 85 |
+
if call_text.startswith("database_query("):
|
| 86 |
+
return json.dumps({"status": "success", "count": 3, "records": [{"id": 101, "status": "active"}]})
|
| 87 |
+
|
| 88 |
+
# 6. create_event
|
| 89 |
+
if call_text.startswith("create_event("):
|
| 90 |
+
return json.dumps({"event_id": "evt_98214", "status": "confirmed"})
|
| 91 |
+
|
| 92 |
+
# 7. file_manager
|
| 93 |
+
if call_text.startswith("file_manager("):
|
| 94 |
+
return json.dumps({"status": "ok", "bytes_written": 64})
|
| 95 |
+
|
| 96 |
+
# 8. fetch_api
|
| 97 |
+
if call_text.startswith("fetch_api("):
|
| 98 |
+
return json.dumps({"status": 200, "data": {"status": "healthy"}})
|
| 99 |
+
|
| 100 |
+
# 9. search(query="...")
|
| 101 |
+
s_m = re.match(r'search\(query="([^"]+)"\)', call_text)
|
| 102 |
+
if s_m:
|
| 103 |
+
query = s_m.group(1).strip()
|
| 104 |
+
q_lower = query.lower()
|
| 105 |
+
|
| 106 |
+
# Offline dictionary lookup for instant, deterministic testing
|
| 107 |
+
kb = {
|
| 108 |
+
"tomato": "the edible berry of the plant Solanum lycopersicum, commonly prepared as a culinary vegetable.",
|
| 109 |
+
"tomatoes": "the edible berry of the plant Solanum lycopersicum, commonly prepared as a culinary vegetable.",
|
| 110 |
+
"apple": "a round, edible fruit produced by an apple tree (Malus domestica), cultivated worldwide.",
|
| 111 |
+
"apples": "a round, edible fruit produced by an apple tree (Malus domestica), cultivated worldwide.",
|
| 112 |
+
"potato": "a starchy, tuberous root crop of the nightshade family Solanaceae.",
|
| 113 |
+
"potatoes": "a starchy, tuberous root crop of the nightshade family Solanaceae.",
|
| 114 |
+
"coffee": "a brewed drink prepared from roasted coffee beans of flowering Coffea species.",
|
| 115 |
+
"banana": "an elongated, edible fruit produced by several kinds of large herbaceous plants in the genus Musa.",
|
| 116 |
+
"bananas": "an elongated, edible fruit produced by several kinds of large herbaceous plants in the genus Musa.",
|
| 117 |
+
"gravity": "a fundamental interaction causing mutual attraction between all things with mass or energy.",
|
| 118 |
+
"photosynthesis": "a process used by plants to convert light energy into chemical energy stored in glucose.",
|
| 119 |
+
"atom": "the basic unit of a chemical element, consisting of a dense central nucleus surrounded by electrons.",
|
| 120 |
+
"python creator": "Guido van Rossum, a Dutch programmer.",
|
| 121 |
+
"speed of light": "approximately 299,792 kilometers per second in a vacuum.",
|
| 122 |
+
"mount everest": "Earth's highest mountain above sea level, located in the Himalayas.",
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
for k, v in kb.items():
|
| 126 |
+
if k in q_lower or q_lower in k:
|
| 127 |
+
return json.dumps({"snippet": v})
|
| 128 |
+
|
| 129 |
+
# Try live Wikipedia REST API if network is available
|
| 130 |
+
try:
|
| 131 |
+
import urllib.request
|
| 132 |
+
import urllib.parse
|
| 133 |
+
clean_q = urllib.parse.quote(query.replace(" ", "_"))
|
| 134 |
+
url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{clean_q}"
|
| 135 |
+
req = urllib.request.Request(url, headers={"User-Agent": "ScaledGPT-Search/1.0"})
|
| 136 |
+
with urllib.request.urlopen(req, timeout=2.5) as resp:
|
| 137 |
+
data = json.loads(resp.read().decode("utf-8"))
|
| 138 |
+
if "extract" in data and data["extract"]:
|
| 139 |
+
return json.dumps({"snippet": data["extract"][:220]})
|
| 140 |
+
except Exception:
|
| 141 |
+
pass
|
| 142 |
+
|
| 143 |
+
return json.dumps({"snippet": f"{query.capitalize()} is an entity and topic in verified knowledge sources."})
|
| 144 |
+
|
| 145 |
+
# Fallback
|
| 146 |
+
return json.dumps({"status": "executed", "call": call_text})
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def print_human_readable_output(text: str, is_math: bool = False):
|
| 150 |
+
"""Formats internal model output into clean, human-readable terminal blocks."""
|
| 151 |
+
print("\n" + "-" * 80)
|
| 152 |
+
if is_math:
|
| 153 |
+
m = re.search(r"(?:<BOS>)?(.*?)=(?:<THINK>)?(.*?)(?:</THINK>=)(.*?)(?:<EOS>|$)", text)
|
| 154 |
+
if m:
|
| 155 |
+
problem = m.group(1).strip()
|
| 156 |
+
scratch = m.group(2).strip()
|
| 157 |
+
ans = m.group(3).strip()
|
| 158 |
+
print(f"Problem: {problem} = ?")
|
| 159 |
+
print(f"Scratchpad: {scratch}")
|
| 160 |
+
print(f"Answer: {ans}")
|
| 161 |
+
try:
|
| 162 |
+
if "+" in problem:
|
| 163 |
+
parts = problem.split("+")
|
| 164 |
+
if len(parts) == 2 and parts[0].isdigit() and parts[1].isdigit():
|
| 165 |
+
expected = int(parts[0]) + int(parts[1])
|
| 166 |
+
status = "[PASS]" if str(expected) == ans else "[FAIL]"
|
| 167 |
+
print(f"Validation: {status} (Expected: {expected})")
|
| 168 |
+
except Exception:
|
| 169 |
+
pass
|
| 170 |
+
else:
|
| 171 |
+
print(f"Result: {text}")
|
| 172 |
+
else:
|
| 173 |
+
think_m = re.search(r"<THINK>(.*?)</THINK>", text)
|
| 174 |
+
call_m = re.search(r"<CALL>(.*?)</CALL>", text)
|
| 175 |
+
result_m = re.search(r"<RESULT>(.*?)</RESULT>", text)
|
| 176 |
+
answer_m = re.search(r"<ANSWER>(.*?)</ANSWER>", text)
|
| 177 |
+
|
| 178 |
+
if think_m:
|
| 179 |
+
print(f"Reasoning: {think_m.group(1).strip()}")
|
| 180 |
+
if call_m:
|
| 181 |
+
print(f"Tool Call: {call_m.group(1).strip()}")
|
| 182 |
+
if result_m:
|
| 183 |
+
print(f"Tool Result: {result_m.group(1).strip()}")
|
| 184 |
+
if answer_m:
|
| 185 |
+
print(f"Response: {answer_m.group(1).strip()}")
|
| 186 |
+
if not any([think_m, call_m, result_m, answer_m]):
|
| 187 |
+
print(f"Response: {text}")
|
| 188 |
+
|
| 189 |
+
print("-" * 80)
|
| 190 |
+
print(f"[RAW TOKENS] {text}\n", flush=True)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def run_math_prompt(model: ScaledGPT, tokenizer: ScaledTokenizer, prompt: str, device: torch.device) -> str:
|
| 194 |
+
"""Processes a multi-digit arithmetic expression."""
|
| 195 |
+
clean_prompt = prompt.strip()
|
| 196 |
+
if not clean_prompt.endswith("="):
|
| 197 |
+
clean_prompt += "="
|
| 198 |
+
full_prompt = clean_prompt + "<THINK>"
|
| 199 |
+
|
| 200 |
+
tokens = tokenizer.encode(full_prompt, add_bos=True, add_eos=False)
|
| 201 |
+
x = torch.tensor([tokens], dtype=torch.long, device=device)
|
| 202 |
+
|
| 203 |
+
with torch.no_grad():
|
| 204 |
+
out = model.generate(x, max_new_tokens=700, temperature=0.0, stop_tokens=[tokenizer.eos_id])
|
| 205 |
+
|
| 206 |
+
gen_text = tokenizer.decode(out[0].tolist(), skip_special=False)
|
| 207 |
+
print_human_readable_output(gen_text, is_math=True)
|
| 208 |
+
return gen_text
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def run_dialogue_prompt(model: ScaledGPT, tokenizer: ScaledTokenizer, user_input: str, device: torch.device) -> str:
|
| 212 |
+
"""Processes a user dialogue turn with live tool execution."""
|
| 213 |
+
prompt = f"<USER>{user_input}</USER><THINK>"
|
| 214 |
+
tokens = tokenizer.encode(prompt, add_bos=True, add_eos=False)
|
| 215 |
+
x = torch.tensor([tokens], dtype=torch.long, device=device)
|
| 216 |
+
|
| 217 |
+
with torch.no_grad():
|
| 218 |
+
stop_ids = [tokenizer.call_e_id, tokenizer.answer_e_id, tokenizer.eos_id]
|
| 219 |
+
out = model.generate(x, max_new_tokens=256, temperature=0.0, stop_tokens=stop_ids)
|
| 220 |
+
|
| 221 |
+
gen_text = tokenizer.decode(out[0].tolist(), skip_special=False)
|
| 222 |
+
|
| 223 |
+
if "<CALL>" in gen_text and "</CALL>" in gen_text:
|
| 224 |
+
call_part = gen_text.split("<CALL>")[1].split("</CALL>")[0]
|
| 225 |
+
result_str = mock_execute_tool(call_part)
|
| 226 |
+
|
| 227 |
+
# Continue generation with the tool result (append tokens directly to avoid double BOS)
|
| 228 |
+
res_suffix = f"<RESULT>{result_str}</RESULT>"
|
| 229 |
+
res_tokens = tokenizer.encode(res_suffix, add_bos=False, add_eos=False)
|
| 230 |
+
c_x = torch.cat([out, torch.tensor([res_tokens], dtype=torch.long, device=device)], dim=1)
|
| 231 |
+
|
| 232 |
+
with torch.no_grad():
|
| 233 |
+
out2 = model.generate(c_x, max_new_tokens=256, temperature=0.0, stop_tokens=[tokenizer.answer_e_id, tokenizer.eos_id])
|
| 234 |
+
|
| 235 |
+
full_dialogue = tokenizer.decode(out2[0].tolist(), skip_special=False)
|
| 236 |
+
else:
|
| 237 |
+
full_dialogue = gen_text
|
| 238 |
+
|
| 239 |
+
print_human_readable_output(full_dialogue, is_math=False)
|
| 240 |
+
return full_dialogue
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def main():
|
| 244 |
+
parser = argparse.ArgumentParser(description="ScaledGPT Interactive CLI")
|
| 245 |
+
parser.add_argument("--ckpt", type=str, default="champion_model/scaled_t4/checkpoints/champion_final.pt", help="Path to checkpoint")
|
| 246 |
+
parser.add_argument("--prompt", type=str, default=None, help="Execute a single prompt and exit")
|
| 247 |
+
parser.add_argument("--threads", type=int, default=8, help="Number of CPU threads for inference")
|
| 248 |
+
args = parser.parse_args()
|
| 249 |
+
|
| 250 |
+
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
| 251 |
+
if device.type == "cpu":
|
| 252 |
+
torch.set_num_threads(args.threads)
|
| 253 |
+
tokenizer = ScaledTokenizer()
|
| 254 |
+
config = ScaledGPTConfig(vocab_size=tokenizer.vocab_size)
|
| 255 |
+
model = ScaledGPT(config, tokenizer=tokenizer).to(device)
|
| 256 |
+
|
| 257 |
+
# Search candidate paths for the trained checkpoint
|
| 258 |
+
candidates = [
|
| 259 |
+
args.ckpt,
|
| 260 |
+
os.path.join(ROOT_DIR, args.ckpt),
|
| 261 |
+
os.path.join(ROOT_DIR, "champion_model/scaled_t4/checkpoints/champion_final.pt"),
|
| 262 |
+
os.path.join(ROOT_DIR, "champion_model/checkpoints/champion_final.pt"),
|
| 263 |
+
os.path.join(ROOT_DIR, "champion_model/scaled_t4/checkpoints/scaled_t4_sft_all_tools.pt"),
|
| 264 |
+
os.path.join(ROOT_DIR, "champion_model/scaled_t4/checkpoints/scaled_t4_sft_search.pt"),
|
| 265 |
+
os.path.join(ROOT_DIR, "champion_model/scaled_t4/checkpoints/scaled_t4_final.pt"),
|
| 266 |
+
os.path.join(ROOT_DIR, "champion_model/scaled_t4/checkpoints/scaled_t4_final_timeout_step_45059.pt"),
|
| 267 |
+
os.path.join(ROOT_DIR, "champion_model/checkpoints/scaled_t4_sft_search_step_7000.zip"),
|
| 268 |
+
os.path.join(ROOT_DIR, "champion_model/checkpoints/scaled_t4_final.zip"),
|
| 269 |
+
os.path.join(ROOT_DIR, "champion_model/checkpoints/scaled_t4_final_timeout_step_45059.zip"),
|
| 270 |
+
]
|
| 271 |
+
resolved_path = None
|
| 272 |
+
for p in candidates:
|
| 273 |
+
if os.path.exists(p) and not os.path.isdir(p):
|
| 274 |
+
resolved_path = p
|
| 275 |
+
break
|
| 276 |
+
|
| 277 |
+
if resolved_path is not None:
|
| 278 |
+
ckpt = torch.load(resolved_path, map_location=device, weights_only=False)
|
| 279 |
+
model.load_state_dict(ckpt["model_state_dict"])
|
| 280 |
+
step_info = f" (Step {ckpt.get('step')})" if 'step' in ckpt else ""
|
| 281 |
+
print(f"[INFO] Successfully loaded trained checkpoint{step_info} from: {resolved_path}")
|
| 282 |
+
else:
|
| 283 |
+
print(f"[WARN] Checkpoint not found in candidate paths. Running with initialized weights.")
|
| 284 |
+
|
| 285 |
+
model.eval()
|
| 286 |
+
|
| 287 |
+
if args.prompt:
|
| 288 |
+
user_input = args.prompt.strip()
|
| 289 |
+
is_math = bool(re.match(r"^[\d\s\+\-\=]+$", user_input) and ("+" in user_input or "-" in user_input))
|
| 290 |
+
if is_math:
|
| 291 |
+
run_math_prompt(model, tokenizer, user_input, device)
|
| 292 |
+
else:
|
| 293 |
+
run_dialogue_prompt(model, tokenizer, user_input, device)
|
| 294 |
+
return
|
| 295 |
+
|
| 296 |
+
print("\n" + "=" * 80)
|
| 297 |
+
print("SCALEDGPT (~18.95M) INTERACTIVE TERMINAL")
|
| 298 |
+
print("Type an arithmetic equation (e.g. '123456789+987654321=') or ask a question.")
|
| 299 |
+
print("Type 'exit' or 'quit' to terminate.")
|
| 300 |
+
print("=" * 80 + "\n")
|
| 301 |
+
|
| 302 |
+
while True:
|
| 303 |
+
try:
|
| 304 |
+
user_input = input("User >> ").strip()
|
| 305 |
+
except (EOFError, KeyboardInterrupt):
|
| 306 |
+
print("\n[INFO] Exiting interactive CLI.")
|
| 307 |
+
break
|
| 308 |
+
|
| 309 |
+
if not user_input or user_input.lower() in ["exit", "quit"]:
|
| 310 |
+
break
|
| 311 |
+
|
| 312 |
+
# Check if input is arithmetic (contains only digits and + or -)
|
| 313 |
+
is_math = bool(re.match(r"^[\d\s\+\-\=]+$", user_input) and ("+" in user_input or "-" in user_input))
|
| 314 |
+
if is_math:
|
| 315 |
+
run_math_prompt(model, tokenizer, user_input, device)
|
| 316 |
+
else:
|
| 317 |
+
run_dialogue_prompt(model, tokenizer, user_input, device)
|
| 318 |
+
|
| 319 |
+
|
| 320 |
+
if __name__ == "__main__":
|
| 321 |
+
main()
|
model.py
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ScaledGPT: ~18.95M Parameter Recurrent 2D-RoPE Transformer.
|
| 3 |
+
Optimized for 25-Hour Dual NVIDIA T4 (sm_75, FP16 AMP, DDP).
|
| 4 |
+
|
| 5 |
+
Architecture:
|
| 6 |
+
- d_model = 512, n_head = 8 (head_dim = 64)
|
| 7 |
+
- Physical layers L = 6
|
| 8 |
+
- Recurrent passes R = 3 (Effective depth = 18 layers at zero parameter overhead)
|
| 9 |
+
- Residual recurrence update: x = x + (1/sqrt(R)) * (x_res - x)
|
| 10 |
+
- Tied embedding and output projection weights (wte.weight == lm_head.weight)
|
| 11 |
+
- Decoupled 2D-RoPE (Macro place p, Micro step tau)
|
| 12 |
+
- Selective shift masking: (place_coords > 0).long()
|
| 13 |
+
- O(1) SDPA causal attention
|
| 14 |
+
- Non-embedding params: ~18,887,680 | Total params: ~18,953,216 (~18.95M target)
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import math
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
from typing import Optional, Tuple, List, Dict, Any
|
| 20 |
+
import torch
|
| 21 |
+
import torch.nn as nn
|
| 22 |
+
import torch.nn.functional as F
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
from champion_model.scaled_t4.tokenizer import ScaledTokenizer
|
| 26 |
+
except ImportError:
|
| 27 |
+
try:
|
| 28 |
+
from tokenizer import ScaledTokenizer
|
| 29 |
+
except ImportError:
|
| 30 |
+
from champion_model.unified_200k.tokenizer import UnifiedTokenizer as ScaledTokenizer
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class ScaledGPTConfig:
|
| 35 |
+
"""Hyperparameters for ScaledGPT (~18.95M parameters)."""
|
| 36 |
+
vocab_size: int = 115
|
| 37 |
+
block_size: int = 1024
|
| 38 |
+
n_layer: int = 6
|
| 39 |
+
n_head: int = 8
|
| 40 |
+
n_embd: int = 512
|
| 41 |
+
n_recur: int = 3
|
| 42 |
+
dropout: float = 0.0
|
| 43 |
+
bias: bool = False
|
| 44 |
+
rope_theta: float = 10000.0
|
| 45 |
+
max_coords: int = 4096
|
| 46 |
+
k_rand_shift: int = 32
|
| 47 |
+
pos_emb_type: str = "rope_2d"
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def precompute_2d_rope_freqs(
|
| 51 |
+
head_dim: int, max_coords: int = 4096, theta_base: float = 10000.0
|
| 52 |
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 53 |
+
"""Precomputes rotary frequency tables (cos, sin) for decoupled 2D-RoPE."""
|
| 54 |
+
half_dim = head_dim // 2
|
| 55 |
+
assert half_dim % 2 == 0, f"head_dim // 2 must be even, got {half_dim}"
|
| 56 |
+
dim_indices = torch.arange(0, half_dim, 2, dtype=torch.float32)
|
| 57 |
+
freqs = 1.0 / (theta_base ** (dim_indices / half_dim))
|
| 58 |
+
coords = torch.arange(max_coords, dtype=torch.float32)
|
| 59 |
+
angles = torch.outer(coords, freqs)
|
| 60 |
+
cos = torch.cos(angles)
|
| 61 |
+
sin = torch.sin(angles)
|
| 62 |
+
return cos, sin
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def apply_2d_rope(
|
| 66 |
+
x: torch.Tensor,
|
| 67 |
+
place_coords: torch.Tensor,
|
| 68 |
+
step_coords: torch.Tensor,
|
| 69 |
+
cos: torch.Tensor,
|
| 70 |
+
sin: torch.Tensor,
|
| 71 |
+
) -> torch.Tensor:
|
| 72 |
+
"""Applies decoupled 2D-RoPE to tensor of shape [B, n_head, T, head_dim]."""
|
| 73 |
+
B, H, T, D = x.shape
|
| 74 |
+
half_d = D // 2
|
| 75 |
+
|
| 76 |
+
max_c = cos.size(0) - 1
|
| 77 |
+
p_clamped = torch.clamp(place_coords, 0, max_c)
|
| 78 |
+
s_clamped = torch.clamp(step_coords, 0, max_c)
|
| 79 |
+
|
| 80 |
+
cos_p = cos[p_clamped].unsqueeze(1).to(dtype=x.dtype, device=x.device)
|
| 81 |
+
sin_p = sin[p_clamped].unsqueeze(1).to(dtype=x.dtype, device=x.device)
|
| 82 |
+
cos_s = cos[s_clamped].unsqueeze(1).to(dtype=x.dtype, device=x.device)
|
| 83 |
+
sin_s = sin[s_clamped].unsqueeze(1).to(dtype=x.dtype, device=x.device)
|
| 84 |
+
|
| 85 |
+
x_place = x[..., :half_d]
|
| 86 |
+
x_step = x[..., half_d:]
|
| 87 |
+
|
| 88 |
+
# Place subspace rotation
|
| 89 |
+
x_p_even = x_place[..., 0::2]
|
| 90 |
+
x_p_odd = x_place[..., 1::2]
|
| 91 |
+
rot_p_even = x_p_even * cos_p - x_p_odd * sin_p
|
| 92 |
+
rot_p_odd = x_p_even * sin_p + x_p_odd * cos_p
|
| 93 |
+
rot_place = torch.empty_like(x_place)
|
| 94 |
+
rot_place[..., 0::2] = rot_p_even
|
| 95 |
+
rot_place[..., 1::2] = rot_p_odd
|
| 96 |
+
|
| 97 |
+
# Step subspace rotation
|
| 98 |
+
x_s_even = x_step[..., 0::2]
|
| 99 |
+
x_s_odd = x_step[..., 1::2]
|
| 100 |
+
rot_s_even = x_s_even * cos_s - x_s_odd * sin_s
|
| 101 |
+
rot_s_odd = x_s_even * sin_s + x_s_odd * cos_s
|
| 102 |
+
rot_step = torch.empty_like(x_step)
|
| 103 |
+
rot_step[..., 0::2] = rot_s_even
|
| 104 |
+
rot_step[..., 1::2] = rot_s_odd
|
| 105 |
+
|
| 106 |
+
return torch.cat([rot_place, rot_step], dim=-1)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class CausalSelfAttention(nn.Module):
|
| 110 |
+
"""Causal multi-head self-attention with SDPA and 2D-RoPE."""
|
| 111 |
+
|
| 112 |
+
def __init__(self, config: ScaledGPTConfig):
|
| 113 |
+
super().__init__()
|
| 114 |
+
assert config.n_embd % config.n_head == 0
|
| 115 |
+
self.n_head = config.n_head
|
| 116 |
+
self.n_embd = config.n_embd
|
| 117 |
+
self.head_dim = config.n_embd // config.n_head
|
| 118 |
+
self.dropout = config.dropout
|
| 119 |
+
self.rope_theta = config.rope_theta
|
| 120 |
+
|
| 121 |
+
self.c_attn = nn.Linear(config.n_embd, 3 * config.n_embd, bias=config.bias)
|
| 122 |
+
self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias)
|
| 123 |
+
self.resid_dropout = nn.Dropout(config.dropout)
|
| 124 |
+
|
| 125 |
+
cos, sin = precompute_2d_rope_freqs(
|
| 126 |
+
self.head_dim, max_coords=config.max_coords, theta_base=config.rope_theta
|
| 127 |
+
)
|
| 128 |
+
self.register_buffer("rope_cos", cos, persistent=False)
|
| 129 |
+
self.register_buffer("rope_sin", sin, persistent=False)
|
| 130 |
+
|
| 131 |
+
def forward(
|
| 132 |
+
self,
|
| 133 |
+
x: torch.Tensor,
|
| 134 |
+
place_coords: torch.Tensor,
|
| 135 |
+
step_coords: torch.Tensor,
|
| 136 |
+
) -> torch.Tensor:
|
| 137 |
+
B, T, C = x.size()
|
| 138 |
+
qkv = self.c_attn(x)
|
| 139 |
+
q, k, v = qkv.chunk(3, dim=2)
|
| 140 |
+
|
| 141 |
+
k = k.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
|
| 142 |
+
q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
|
| 143 |
+
v = v.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
|
| 144 |
+
|
| 145 |
+
# Dynamic frequency expansion if coordinates exceed precomputed length
|
| 146 |
+
max_coord = max(int(place_coords.max().item()), int(step_coords.max().item())) + 1
|
| 147 |
+
if max_coord > self.rope_cos.size(0):
|
| 148 |
+
new_max = max(max_coord * 2, 4096)
|
| 149 |
+
cos, sin = precompute_2d_rope_freqs(self.head_dim, max_coords=new_max, theta_base=self.rope_theta)
|
| 150 |
+
self.rope_cos = cos.to(x.device)
|
| 151 |
+
self.rope_sin = sin.to(x.device)
|
| 152 |
+
|
| 153 |
+
q = apply_2d_rope(q, place_coords, step_coords, self.rope_cos, self.rope_sin)
|
| 154 |
+
k = apply_2d_rope(k, place_coords, step_coords, self.rope_cos, self.rope_sin)
|
| 155 |
+
|
| 156 |
+
dropout_p = self.dropout if self.training else 0.0
|
| 157 |
+
y = F.scaled_dot_product_attention(
|
| 158 |
+
q, k, v, attn_mask=None, dropout_p=dropout_p, is_causal=True
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
y = y.transpose(1, 2).contiguous().view(B, T, C)
|
| 162 |
+
return self.resid_dropout(self.c_proj(y))
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
class MLP(nn.Module):
|
| 166 |
+
"""Feed-forward network with 4x expansion and GELU."""
|
| 167 |
+
|
| 168 |
+
def __init__(self, config: ScaledGPTConfig):
|
| 169 |
+
super().__init__()
|
| 170 |
+
self.c_fc = nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias)
|
| 171 |
+
self.gelu = nn.GELU()
|
| 172 |
+
self.c_proj = nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias)
|
| 173 |
+
self.dropout = nn.Dropout(config.dropout)
|
| 174 |
+
|
| 175 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 176 |
+
return self.dropout(self.c_proj(self.gelu(self.c_fc(x))))
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class Block(nn.Module):
|
| 180 |
+
"""Transformer block with Pre-LN residuals."""
|
| 181 |
+
|
| 182 |
+
def __init__(self, config: ScaledGPTConfig):
|
| 183 |
+
super().__init__()
|
| 184 |
+
self.ln_1 = nn.LayerNorm(config.n_embd)
|
| 185 |
+
self.attn = CausalSelfAttention(config)
|
| 186 |
+
self.ln_2 = nn.LayerNorm(config.n_embd)
|
| 187 |
+
self.mlp = MLP(config)
|
| 188 |
+
|
| 189 |
+
def forward(
|
| 190 |
+
self,
|
| 191 |
+
x: torch.Tensor,
|
| 192 |
+
place_coords: torch.Tensor,
|
| 193 |
+
step_coords: torch.Tensor,
|
| 194 |
+
) -> torch.Tensor:
|
| 195 |
+
x = x + self.attn(self.ln_1(x), place_coords, step_coords)
|
| 196 |
+
x = x + self.mlp(self.ln_2(x))
|
| 197 |
+
return x
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
class ScaledGPT(nn.Module):
|
| 201 |
+
"""Scaled ~18.95M Parameter Recurrent 2D-RoPE Transformer (sm_75 Dual T4)."""
|
| 202 |
+
|
| 203 |
+
def __init__(self, config: ScaledGPTConfig, tokenizer: Optional[ScaledTokenizer] = None):
|
| 204 |
+
super().__init__()
|
| 205 |
+
self.config = config
|
| 206 |
+
self.tokenizer = tokenizer or ScaledTokenizer()
|
| 207 |
+
|
| 208 |
+
self.transformer = nn.ModuleDict(
|
| 209 |
+
dict(
|
| 210 |
+
wte=nn.Embedding(config.vocab_size, config.n_embd),
|
| 211 |
+
drop=nn.Dropout(config.dropout),
|
| 212 |
+
h=nn.ModuleList([Block(config) for _ in range(config.n_layer)]),
|
| 213 |
+
ln_f=nn.LayerNorm(config.n_embd),
|
| 214 |
+
)
|
| 215 |
+
)
|
| 216 |
+
self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
|
| 217 |
+
|
| 218 |
+
# Weight tying: wte.weight == lm_head.weight
|
| 219 |
+
self.transformer.wte.weight = self.lm_head.weight
|
| 220 |
+
|
| 221 |
+
self.apply(self._init_weights)
|
| 222 |
+
for pn, p in self.named_parameters():
|
| 223 |
+
if pn.endswith("c_proj.weight"):
|
| 224 |
+
torch.nn.init.normal_(p, mean=0.0, std=0.02 / math.sqrt(2 * config.n_layer))
|
| 225 |
+
|
| 226 |
+
def _init_weights(self, module: nn.Module) -> None:
|
| 227 |
+
if isinstance(module, nn.Linear):
|
| 228 |
+
torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
| 229 |
+
if module.bias is not None:
|
| 230 |
+
torch.nn.init.zeros_(module.bias)
|
| 231 |
+
elif isinstance(module, nn.Embedding):
|
| 232 |
+
torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
|
| 233 |
+
|
| 234 |
+
def get_num_params(self, non_embedding: bool = False) -> int:
|
| 235 |
+
n_params = sum(p.numel() for p in self.parameters())
|
| 236 |
+
if non_embedding:
|
| 237 |
+
n_params -= self.transformer.wte.weight.numel()
|
| 238 |
+
return n_params
|
| 239 |
+
|
| 240 |
+
def compute_2d_coords(self, idx: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 241 |
+
"""
|
| 242 |
+
Unified 2D coordinate mapper:
|
| 243 |
+
1. If <USER> is present, routes as Discourse 2D coordinates (segment p in [0, 4], token offset tau).
|
| 244 |
+
In multi-turn dialogues, handles turns gracefully.
|
| 245 |
+
2. Else, routes as Arithmetic 2D coordinates (column rank p up to 50+, intra-column step tau in [0, 9]).
|
| 246 |
+
"""
|
| 247 |
+
B, T = idx.size()
|
| 248 |
+
device = idx.device
|
| 249 |
+
tok = self.tokenizer
|
| 250 |
+
|
| 251 |
+
p_coords = torch.zeros((B, T), dtype=torch.long, device=device)
|
| 252 |
+
s_coords = torch.zeros((B, T), dtype=torch.long, device=device)
|
| 253 |
+
|
| 254 |
+
user_s_id = tok.user_s_id
|
| 255 |
+
plus_id = tok.plus_id
|
| 256 |
+
eq_id = tok.eq_id
|
| 257 |
+
think_s_id = tok.think_s_id
|
| 258 |
+
think_e_id = tok.think_e_id
|
| 259 |
+
lparen_id = tok.lparen_id
|
| 260 |
+
digit_ids = set(tok.digit_ids)
|
| 261 |
+
|
| 262 |
+
segment_tags = {
|
| 263 |
+
tok.user_s_id: 0,
|
| 264 |
+
tok.think_s_id: 1,
|
| 265 |
+
tok.call_s_id: 2,
|
| 266 |
+
tok.result_s_id: 3,
|
| 267 |
+
tok.answer_s_id: 4,
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
for b in range(B):
|
| 271 |
+
row = idx[b]
|
| 272 |
+
is_dialogue = (row == user_s_id).any().item()
|
| 273 |
+
|
| 274 |
+
if is_dialogue:
|
| 275 |
+
# Discourse routing
|
| 276 |
+
cur_seg = 0
|
| 277 |
+
cur_step = 0
|
| 278 |
+
for t in range(T):
|
| 279 |
+
token_id = row[t].item()
|
| 280 |
+
if token_id in segment_tags:
|
| 281 |
+
cur_seg = segment_tags[token_id]
|
| 282 |
+
cur_step = 0
|
| 283 |
+
p_coords[b, t] = cur_seg
|
| 284 |
+
s_coords[b, t] = cur_step
|
| 285 |
+
cur_step += 1
|
| 286 |
+
else:
|
| 287 |
+
# Arithmetic scratchpad routing (extrapolates cleanly from 1 to 50+ digits)
|
| 288 |
+
plus_matches = (row == plus_id).nonzero(as_tuple=True)[0]
|
| 289 |
+
plus_idx = plus_matches[0].item() if len(plus_matches) > 0 else -1
|
| 290 |
+
|
| 291 |
+
eq_matches = (row == eq_id).nonzero(as_tuple=True)[0]
|
| 292 |
+
eq_idx = -1
|
| 293 |
+
if plus_idx != -1:
|
| 294 |
+
for pos in eq_matches:
|
| 295 |
+
if pos.item() > plus_idx:
|
| 296 |
+
eq_idx = pos.item()
|
| 297 |
+
break
|
| 298 |
+
|
| 299 |
+
think_s_matches = (row == think_s_id).nonzero(as_tuple=True)[0]
|
| 300 |
+
think_s = think_s_matches[0].item() if len(think_s_matches) > 0 else -1
|
| 301 |
+
|
| 302 |
+
think_e_matches = (row == think_e_id).nonzero(as_tuple=True)[0]
|
| 303 |
+
think_e = think_e_matches[0].item() if len(think_e_matches) > 0 else -1
|
| 304 |
+
|
| 305 |
+
# Operand 1 (before '+')
|
| 306 |
+
if plus_idx > 0:
|
| 307 |
+
for i in range(plus_idx):
|
| 308 |
+
if row[i].item() in digit_ids:
|
| 309 |
+
p_coords[b, i] = plus_idx - 1 - i
|
| 310 |
+
s_coords[b, i] = 0
|
| 311 |
+
|
| 312 |
+
# Operand 2 (between '+' and '=')
|
| 313 |
+
if eq_idx > plus_idx > -1:
|
| 314 |
+
for i in range(plus_idx + 1, eq_idx):
|
| 315 |
+
if row[i].item() in digit_ids:
|
| 316 |
+
p_coords[b, i] = eq_idx - 1 - i
|
| 317 |
+
s_coords[b, i] = 0
|
| 318 |
+
|
| 319 |
+
# Inside THINK
|
| 320 |
+
col_count = 0
|
| 321 |
+
if think_s != -1:
|
| 322 |
+
end_pos = think_e if think_e != -1 else T
|
| 323 |
+
cur_col = -1
|
| 324 |
+
cur_step = 0
|
| 325 |
+
for i in range(think_s + 1, end_pos):
|
| 326 |
+
if row[i].item() == lparen_id:
|
| 327 |
+
cur_col += 1
|
| 328 |
+
cur_step = 0
|
| 329 |
+
if cur_col >= 0:
|
| 330 |
+
p_coords[b, i] = cur_col
|
| 331 |
+
s_coords[b, i] = min(cur_step, 9)
|
| 332 |
+
cur_step += 1
|
| 333 |
+
col_count = cur_col + 1
|
| 334 |
+
|
| 335 |
+
# After THINK (final answer)
|
| 336 |
+
if think_e != -1:
|
| 337 |
+
ans_s = -1
|
| 338 |
+
for pos in eq_matches:
|
| 339 |
+
if pos.item() > think_e:
|
| 340 |
+
ans_s = pos.item() + 1
|
| 341 |
+
break
|
| 342 |
+
if ans_s != -1 and ans_s < T:
|
| 343 |
+
ans_d_idx = 0
|
| 344 |
+
for i in range(ans_s, T):
|
| 345 |
+
if row[i].item() in digit_ids:
|
| 346 |
+
p_coords[b, i] = max(0, col_count - 1 - ans_d_idx)
|
| 347 |
+
s_coords[b, i] = 0
|
| 348 |
+
ans_d_idx += 1
|
| 349 |
+
|
| 350 |
+
return p_coords, s_coords
|
| 351 |
+
|
| 352 |
+
def forward(
|
| 353 |
+
self,
|
| 354 |
+
idx: torch.Tensor,
|
| 355 |
+
targets: Optional[torch.Tensor] = None,
|
| 356 |
+
place_coords: Optional[torch.Tensor] = None,
|
| 357 |
+
step_coords: Optional[torch.Tensor] = None,
|
| 358 |
+
) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
|
| 359 |
+
B, T = idx.size()
|
| 360 |
+
device = idx.device
|
| 361 |
+
|
| 362 |
+
if place_coords is None or step_coords is None:
|
| 363 |
+
place_coords, step_coords = self.compute_2d_coords(idx)
|
| 364 |
+
|
| 365 |
+
# Shift invariance data augmentation during training:
|
| 366 |
+
# Strictly applies selective shift masking (place_coords > 0).long()
|
| 367 |
+
if self.training and self.config.k_rand_shift > 0:
|
| 368 |
+
shifts = torch.randint(0, self.config.k_rand_shift + 1, (B, 1), device=device)
|
| 369 |
+
shift_mask = (place_coords > 0).long()
|
| 370 |
+
place_coords = place_coords + shifts * shift_mask
|
| 371 |
+
|
| 372 |
+
x = self.transformer.drop(self.transformer.wte(idx))
|
| 373 |
+
|
| 374 |
+
# Per-block recurrence residual updates: x = x + (1/sqrt(R)) * (x_res - x)
|
| 375 |
+
n_recur = max(1, self.config.n_recur)
|
| 376 |
+
scale_factor = 1.0 / math.sqrt(n_recur) if n_recur > 1 else 1.0
|
| 377 |
+
for _ in range(n_recur):
|
| 378 |
+
for block in self.transformer.h:
|
| 379 |
+
x_res = block(x, place_coords, step_coords)
|
| 380 |
+
if n_recur > 1:
|
| 381 |
+
x = x + scale_factor * (x_res - x)
|
| 382 |
+
else:
|
| 383 |
+
x = x_res
|
| 384 |
+
|
| 385 |
+
x = self.transformer.ln_f(x)
|
| 386 |
+
logits = self.lm_head(x)
|
| 387 |
+
|
| 388 |
+
loss = None
|
| 389 |
+
if targets is not None:
|
| 390 |
+
loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1), ignore_index=-100)
|
| 391 |
+
|
| 392 |
+
return logits, loss
|
| 393 |
+
|
| 394 |
+
@torch.no_grad()
|
| 395 |
+
def generate(
|
| 396 |
+
self,
|
| 397 |
+
idx: torch.Tensor,
|
| 398 |
+
max_new_tokens: int = 512,
|
| 399 |
+
temperature: float = 0.0,
|
| 400 |
+
stop_tokens: Optional[List[int]] = None,
|
| 401 |
+
) -> torch.Tensor:
|
| 402 |
+
"""Autoregressive token generation with greedy / multinomial sampling."""
|
| 403 |
+
stop_set = set(stop_tokens) if stop_tokens is not None else set()
|
| 404 |
+
|
| 405 |
+
for _ in range(max_new_tokens):
|
| 406 |
+
idx_cond = idx if idx.size(1) <= self.config.block_size else idx[:, -self.config.block_size:]
|
| 407 |
+
logits, _ = self(idx_cond)
|
| 408 |
+
logits = logits[:, -1, :]
|
| 409 |
+
|
| 410 |
+
if temperature == 0.0:
|
| 411 |
+
next_token = torch.argmax(logits, dim=-1, keepdim=True)
|
| 412 |
+
else:
|
| 413 |
+
probs = F.softmax(logits / temperature, dim=-1)
|
| 414 |
+
next_token = torch.multinomial(probs, num_samples=1)
|
| 415 |
+
|
| 416 |
+
idx = torch.cat((idx, next_token), dim=1)
|
| 417 |
+
|
| 418 |
+
if next_token.item() in stop_set:
|
| 419 |
+
break
|
| 420 |
+
|
| 421 |
+
return idx
|
tokenizer.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Scaled Unified Tokenizer for ScaledGPT (Dual T4 Optimization).
|
| 3 |
+
Supports:
|
| 4 |
+
1. Multi-digit column arithmetic with scratchpad tokens (1-50+ digits).
|
| 5 |
+
2. Natural language tool-calling dialogues with structured discourse tags.
|
| 6 |
+
3. Multi-turn conversation markers and nested JSON syntax.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import string
|
| 10 |
+
from typing import List, Dict, Any, Optional
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ScaledTokenizer:
|
| 15 |
+
"""Character-level tokenizer with structured discourse tokens and arithmetic symbols."""
|
| 16 |
+
|
| 17 |
+
def __init__(self):
|
| 18 |
+
self.special_tokens: List[str] = [
|
| 19 |
+
"<PAD>",
|
| 20 |
+
"<BOS>",
|
| 21 |
+
"<EOS>",
|
| 22 |
+
"<UNK>",
|
| 23 |
+
"<USER>",
|
| 24 |
+
"</USER>",
|
| 25 |
+
"<THINK>",
|
| 26 |
+
"</THINK>",
|
| 27 |
+
"<CALL>",
|
| 28 |
+
"</CALL>",
|
| 29 |
+
"<RESULT>",
|
| 30 |
+
"</RESULT>",
|
| 31 |
+
"<ANSWER>",
|
| 32 |
+
"</ANSWER>",
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
self.token_to_id: Dict[str, int] = {}
|
| 36 |
+
self.id_to_token: Dict[int, str] = {}
|
| 37 |
+
|
| 38 |
+
for i, tok in enumerate(self.special_tokens):
|
| 39 |
+
self.token_to_id[tok] = i
|
| 40 |
+
self.id_to_token[i] = tok
|
| 41 |
+
|
| 42 |
+
ascii_chars = sorted(list(set(string.ascii_letters + string.digits + string.punctuation + " \n\t")))
|
| 43 |
+
cur_id = len(self.special_tokens)
|
| 44 |
+
for ch in ascii_chars:
|
| 45 |
+
if ch not in self.token_to_id:
|
| 46 |
+
self.token_to_id[ch] = cur_id
|
| 47 |
+
self.id_to_token[cur_id] = ch
|
| 48 |
+
cur_id += 1
|
| 49 |
+
|
| 50 |
+
self.pad_id: int = self.token_to_id["<PAD>"]
|
| 51 |
+
self.bos_id: int = self.token_to_id["<BOS>"]
|
| 52 |
+
self.eos_id: int = self.token_to_id["<EOS>"]
|
| 53 |
+
self.unk_id: int = self.token_to_id["<UNK>"]
|
| 54 |
+
|
| 55 |
+
# Discourse markers
|
| 56 |
+
self.user_s_id: int = self.token_to_id["<USER>"]
|
| 57 |
+
self.user_e_id: int = self.token_to_id["</USER>"]
|
| 58 |
+
self.think_s_id: int = self.token_to_id["<THINK>"]
|
| 59 |
+
self.think_e_id: int = self.token_to_id["</THINK>"]
|
| 60 |
+
self.call_s_id: int = self.token_to_id["<CALL>"]
|
| 61 |
+
self.call_e_id: int = self.token_to_id["</CALL>"]
|
| 62 |
+
self.result_s_id: int = self.token_to_id["<RESULT>"]
|
| 63 |
+
self.result_e_id: int = self.token_to_id["</RESULT>"]
|
| 64 |
+
self.answer_s_id: int = self.token_to_id["<ANSWER>"]
|
| 65 |
+
self.answer_e_id: int = self.token_to_id["</ANSWER>"]
|
| 66 |
+
|
| 67 |
+
# Arithmetic delimiters
|
| 68 |
+
self.plus_id: int = self.token_to_id["+"]
|
| 69 |
+
self.eq_id: int = self.token_to_id["="]
|
| 70 |
+
self.lparen_id: int = self.token_to_id["("]
|
| 71 |
+
self.digit_ids: List[int] = [self.token_to_id[str(d)] for d in range(10)]
|
| 72 |
+
|
| 73 |
+
self.vocab_size: int = len(self.token_to_id)
|
| 74 |
+
|
| 75 |
+
def encode(self, text: str, add_bos: bool = True, add_eos: bool = False) -> List[int]:
|
| 76 |
+
tokens: List[int] = []
|
| 77 |
+
if add_bos:
|
| 78 |
+
tokens.append(self.bos_id)
|
| 79 |
+
|
| 80 |
+
i = 0
|
| 81 |
+
n = len(text)
|
| 82 |
+
while i < n:
|
| 83 |
+
matched_special = False
|
| 84 |
+
for sp in self.special_tokens:
|
| 85 |
+
if text.startswith(sp, i):
|
| 86 |
+
tokens.append(self.token_to_id[sp])
|
| 87 |
+
i += len(sp)
|
| 88 |
+
matched_special = True
|
| 89 |
+
break
|
| 90 |
+
if not matched_special:
|
| 91 |
+
ch = text[i]
|
| 92 |
+
tokens.append(self.token_to_id.get(ch, self.unk_id))
|
| 93 |
+
i += 1
|
| 94 |
+
|
| 95 |
+
if add_eos:
|
| 96 |
+
tokens.append(self.eos_id)
|
| 97 |
+
return tokens
|
| 98 |
+
|
| 99 |
+
def decode(self, token_ids: List[int], skip_special: bool = False) -> str:
|
| 100 |
+
chars: List[str] = []
|
| 101 |
+
for tid in token_ids:
|
| 102 |
+
if tid in self.id_to_token:
|
| 103 |
+
tok_str = self.id_to_token[tid]
|
| 104 |
+
if skip_special and tok_str in self.special_tokens:
|
| 105 |
+
continue
|
| 106 |
+
chars.append(tok_str)
|
| 107 |
+
else:
|
| 108 |
+
chars.append("<UNK>")
|
| 109 |
+
return "".join(chars)
|