# Scaling Run v5: TinyStories 多遍(5.33 ep) + dim768/18L(同 v4) + 8 卡 DDP bf16 — Design Document

## Goal

v4 把模型放大到 dim768/18L（core 127.43M），训了 ~720.9M token（~1.54 epoch），val 一路降到末步仍在降
= 仍**欠拟合**。当时留下一个明确问题：**TinyStories 这本语料对 127M core 模型，到底是不是数据上限？**

v5 是为回答这个问题专门设计的**对照实验**：

1. **架构完全冻结 = v4**（dim 768 / 24 heads × 32 head_dim / 18 layers / SwiGLU ffn 2048，core 127.43M，
   总 204.63M）。**一个权重维度都不改**——这样 v4→v5 的 val 差异**只能归因于「更多数据」这一个变量**。
2. **数据放大到接近饱和**：v5 训 **~2.49B token = ~5.33 epoch**（v4 才 1.54 ep），同一份全量 TinyStories
   token 流多跑 3.5×，看 val 还能不能继续降。
3. **bf16（T12/KI-2）找回甜点区**：v4 是 bf16 的触发点——dim768 fp32 在 32GB 显存里 per-rank batch 32
   （global 256）OOM，被迫降到 per-rank 16（global 128）。v5 上 bf16 混合精度（fp32 master），激活减半
   → **找回 per-rank 32 / global 256 的甜点区**，同时吞吐从 v4 的 ~145K 升到 ~217K tok/s。
4. 训完存 registry + 导出 xserv 验证可服务，给出**相比 v4 的提升**与**数据天花板结论**。

> **这一版的工程意义**：v5 是 xtrain scaling 阶梯上第一个**有意不放大模型**的版本——它不是为了「更低的 val」，
> 而是为了**测准 TinyStories 在 dim768 的数据天花板**。结论（见下）很干脆：**3.5× 数据只换来 ~5% 的 val
> 改善，且末段 val 走平**——同尺寸模型在 TinyStories 上已近饱和，v6 该换轴（更大模型 / 更广语料），而**不是**
> 继续榨 TinyStories 的 epoch。同时 v5 兑现了 v4 留的 bf16 杠杆（KI-2）：bf16 找回 global 256 甜点区，
> 8 卡稳态 ~217K tok/s。

## 数据

| 项 | v4 | v5 |
|----|----|----|
| 来源 | TinyStories **全量 train**（复用 v1 缓存）| 同 |
| token 数（语料）| 468,260,367 | 同 |
| **训练消费 token** | ~720.9M（22000 步 × 32768）| **~2.49B**（38000 步 × 65536）|
| epoch 占比 | ~1.54 | **~5.33**（×3.5）|
| tokenizer | gpt2 BPE（vocab 50257）| 同 |
| 缓存 | `data/tinystories-train.txt.u16.bin`（u16）| **直接复用** |
| held-out val | 全量末尾 1,000,000 token | **同一 1M token**（与 v0–v4 完全相同的保留集，公平对比）|

**复用缓存**：`Corpus::load_cached` 读 `<corpus>.u16.bin`，启动即载入 467.26M train token（末尾 1M 留 val）。
held-out val 仍是全量末尾 1M token（`split_tail`），与 v0–v4 同一保留集——**v0–v5 的 val loss 直接可比**。

**这一版数据设计的核心**：v4 才 1.54 epoch，v5 把同一份语料喂到 **5.33 epoch**（×3.5）。如果 TinyStories
对 127M core 还有数据空间，val 该继续显著下降；如果已近饱和，val 会迅速放缓、走平。**v5 就是来读这个信号的。**

## 架构

v5 = **与 v4 字节级同构的** tiny Qwen3（RoPE + RMSNorm + per-head QK-norm + SwiGLU + 独立 lm_head，MHA）。
**刻意一个维度都不改**，让「更多数据」成为唯一被测变量。

| 维度 | v4 | v5 |
|------|----|----|
| dim（= heads·head_dim）| 768 | **768（同）** |
| n_layers | 18 | **18（同）** |
| n_heads | 24 | **24（同）** |
| head_dim | 32 | 32（同）|
| ffn_hidden（SwiGLU）| 2048 | 2048（同）|
| vocab | 50257 | 50257（同）|
| **core 参数** | 127,432,704（≈127.43M）| **127,432,704（同）** |
| embed + lm_head（2×vocab×dim）| 77,194,752（≈77.19M）| 77,194,752（同）|
| **总参数** | 204,627,456（≈204.63M）| **204,627,456（同）** |

**为什么不放大模型**：scaling 实验里「数据」和「容量」两个变量若同时动，val 的变化无法归因。v4→v5 把容量冻死、
只动数据，得到的 val 差就**纯粹是数据的边际收益**——这是判断「TinyStories 是否到数据天花板」的唯一干净办法。
config.json 与 v4 完全一致（导出的 201 tensors 形状一字不差）。

## 训练器：8 卡 DDP bf16（T12 混合精度，fp32 master）

v4 用 8 卡 DDP **fp32**，被 dim768 的显存压到 per-rank batch 16（global 128）。v5 切 **bf16 混合精度**：

- **fp32 master 权重 + AdamW/clip/DDP 全部保持 fp32**（数值安全），只把 linears 走
  `cublasGemmEx`（16BF 输入 / fp32 accum）、激活存 bf16；norm/softmax/rope/CE 仍 fp32。新增 cast autodiff op
  桥接（fwd 降精度 / bwd 升精度）→ 优化器零改动。无 loss scaling（T12 实测 dim768 不需要）。
- **激活减半 → 找回甜点区**：bf16 把 dim768 的 per-rank batch 重新撑到 **32（global 256）**，正是 v4 因 fp32
  OOM 失去的甜点区。同时吞吐 **~145K（v4 fp32）→ ~217K tok/s（v5 bf16，×1.5）**。
- **8 卡 thread-per-GPU**：all-reduce device 梯度取均值后各 rank 本地 GpuAdamW step，跨 rank 参数 bit-identical
  （T8/T11 已验证）。

全程稳态 **~217,000 tok/s**、wall-clock **~3.2h** 训完 2.49B token。bf16 收敛全程对住 fp32（T12 已做
150 步 3.984 vs 3.988 对拍），v5 的 train/val 曲线平滑无异常。

## 超参

| 项 | 值 | 备注 |
|----|----|----|
| optimizer | 手写 AdamW（GPU 端 step）| wd=0.1，β/eps 用 xtrain-optim 默认 |
| LR schedule | 线性 warmup → cosine decay | max_lr **6e-4** → min_lr **6e-5**（同 v1–v4）|
| warmup | ~1900 步（lr 在 ~step 1900 达峰 6.00e-4，cosine 衰减到末步 6e-5）| |
| grad clip | global-norm 1.0 | gnorm 全程平稳（warmup 后 ~0.4 起持续下降）|
| steps | **38000** | ~3.2h @ 8 卡 |
| global batch | **256**（per-rank 32 × world 8）| **bf16 找回的甜点区**（v4 fp32 只能 128）|
| seq_len | **256** | 同 v2–v4 |
| tokens/step | 256×256 = 65536 | 总训练 token ≈ **2.49B**（~5.33 epoch）|
| world size | **8**（RTX 5090，sm_120）| |
| 精度 | **bf16 混合精度**（fp32 master）| T12/KI-2；导出 xserv 同样 BF16 |

**算力**：dash5 8× RTX 5090，全程 ~217,000 tok/s（step 50 起即稳态），wall-clock ≈ **3.2 小时**。

## 结果

- **train loss**：start **11.0675** → end **1.0588**（全程平滑下降）
- **best val loss（held-out 1M token，step 34999）**：**1.1102**
- **final val loss（step 37999）**：**1.1131**
- val loss 曲线（每 ~2000 步抽样）：

| step | 499 | 1999 | 3999 | 5999 | 7999 | 9999 | 13999 | 17999 | 21999 | 25999 | 29999 | 33999 | **34999** | 37999 |
|------|-----|------|------|------|------|------|-------|-------|-------|-------|-------|-------|-------|-------|
| val  | 2.6838 | 1.6033 | 1.4132 | 1.3317 | 1.2980 | 1.2596 | 1.2194 | 1.1846 | 1.1575 | 1.1374 | 1.1217 | 1.1151 | **1.1102** | 1.1131 |

### ⚠️ 数据天花板：末段走平

v5 的 val 在**末段明显走平**——这是 v4 单调下降曲线上看不到的新行为：

| step | 34999 | 35499 | 35999 | 36499 | 36999 | 37499 | 37999 |
|------|-------|-------|-------|-------|-------|-------|-------|
| val  | **1.1102 (best)** | 1.1126 | 1.1131 | 1.1135 | 1.1119 | 1.1143 | 1.1131 |

best（1.1102）出现在 step 34999，之后 3000 步 val 在 **1.1102–1.1143 的 ~0.004 带内来回抖动、不再单调下降**。
对比 v4 的 val 一路降到末步仍在降（欠拟合）——**v5 已经把 TinyStories 这本语料学到平台期**。

## 相比 v4 的提升 —— 以及数据天花板分析

**完整 val 阶梯（各版各自 best val，同一 1M token 保留集）**：

| 模型 | core 参数 | 训练 token | epoch | **best val** | 相比上一版 |
|------|-----------|-----------|-------|--------------|-----------|
| v0-baseline | 41K | ~0.72M | — | 3.8050 | — |
| v1 | 8.39M | ~5.1M | — | 2.5847 | ↓1.22 |
| v2 | 28.32M | ~36.9M | — | 1.7055 | ↓0.88 |
| v3 | 67.13M | ~245.8M | ~0.53 | 1.3027 | ↓0.40 |
| v4 | 127.43M | ~720.9M | ~1.54 | 1.1690 | ↓0.13 |
| **v5** | **127.43M（同 v4）** | **~2.49B（×3.5）** | **~5.33** | **1.1102** | **↓0.06（仅 ~5%）** |

**这是本版的核心发现**。把它和前几档对比：v2→v3 数据 ×6.7（val ↓0.40），v3→v4 数据 ×2.9 + 模型 ×1.9
（val ↓0.13）。而 **v4→v5：模型不变、数据 ×3.5，val 只 ↓0.06（~5%）**。结合末段走平：

> **结论：在 dim768 / 127M-core 这个尺寸，TinyStories 已接近数据饱和。**
> 同一份语料从 1.54 epoch 喂到 5.33 epoch（×3.5），val 仅改善 ~5% 且末段走平——**「更多 TinyStories token」
> 这条杠杆已经基本榨干**。这不是模型欠拟合（v4 那种末步仍降），而是**语料的信息量对这个尺寸的模型已学尽**。

**下一档（v6）的杠杆，按收益排序**：

1. **换轴：更大模型 或 更广语料**——这两条才是 v5 之后真正能继续降 val 的方向。
2. **不该做**：继续加 TinyStories 的 epoch。v5 已经证明 6+ epoch 的边际收益薄到不值得算力。

### 并排采样（greedy 40 tok，xserv 服务，同 prompt，v4 vs v5）

| prompt | v4 | v5 |
|--------|----|----|
| `Once upon a time` | …a little girl named Lily. She loved to play outside in the **sunshine. One day, she saw a big, scary dog. She was scared and ran away.** | …a little girl named Lily. She loved to play outside in the **park. One day, she saw a big, white cloud in the sky. It looked like a fluffy pillow.** |
| `One day` | One day, a little girl named Lily went to the park with her mom. She saw a big tree with a swing. Lily wanted to play on the swing, but she was **too small. She asked her** | One day, a little girl named Lily went to the park with her mom. She saw a big tree with a swing. Lily wanted to play on the swing, but she was **too small to reach it.** |
| `The little` | The little girl was so happy that she had found the perfect **place to hide. She stayed there for a long time, until it was time to go home. She said goodbye to the tree** | The little girl was so happy that she had found the perfect **thing to replace the broken one. She thanked her mom and they both went home with a smile on their faces.** |

**结论**：v4 和 v5 都写出完整、连贯、有收束的 TinyStories——两者**质量在同一水平**，差异是**情节走向的细微不同**
（"scary dog → ran away" vs "white cloud → fluffy pillow"），**而非可感知的质量提升**。这与 val 仅 ↓0.06 完全一致：
**同尺寸模型多看 3.5× 数据，采样质量已无肉眼可见的提升**——这是数据天花板在生成侧的直接体现。

## xserv 验证

导出 HF Qwen3 safetensors（命名映射 + 2D 权重转置 [in,out]→[out,in] + BF16，见 T9 `docs/08`，
**201 tensors** = 18 层 × 11 + embed + norm + lm_head，config.json 与 v4 一字不差），存入 registry 后用
`xserv-cli` 加载并贪心生成：

```
$ xserv-cli ~/projects/tiny-models/v5-tinystories-dim768 --max-tokens 40
Model: qwen3, layers=18, hidden=768, heads=24/24 kv, vocab=50257
Loaded 201 tensors
xserv> Once upon a time, there was a little girl named Lily. She loved to play outside in the park.
       One day, she saw a big, white cloud in the sky. It looked like a fluffy pillow.
xserv> One day, a little girl named Lily went to the park with her mom. She saw a big tree with a
       swing. Lily wanted to play on the swing, but she was too small to reach it.
xserv> The little girl was so happy that she had found the perfect thing to replace the broken one.
       She thanked her mom and they both went home with a smile on their faces.
```

**token-match**：xserv（BF16）对 xtrain 自身贪心，**3 个 prompt 全部逐 token 完全一致**（40 tok 内零分叉）。
v5 **训练即 bf16**（fp32 master），权重本就在 bf16 数值域里收敛，导出 BF16 给 xserv 后两侧贪心匹配更紧（v4
是 fp32 训练 → BF16 导出，已 3/3 一致；v5 同样 3/3 且数值路径更一致）。闭环成立。

## v6 提案

v5 给出了明确的数据天花板结论，v6 该**换轴**。两条候选：

- **A. 更大模型（dim 1024+）**：v5 证明 TinyStories 对 127M core 已饱和，但**更大的模型也许能从同一语料里
  榨出更多**（容量上限尚未触顶）。注意 dim 越大，embed/lm_head 占比越摊薄（dim768 时 77.19M / 204.63M ≈ 38%，
  dim1024 会降到 ~34%）→ **KI-4（大词表占比）的压力反而变小**。但若只换更大模型、仍喂 TinyStories，很可能
  很快又撞上「这本语料的信息上限」——TinyStories 本身的内容多样性有限。
- **B. 更广语料（FineWeb-edu 等通用高质语料）+ 可能换 tokenizer（KI-4）**：v5 的天花板是**语料**的天花板，
  不是模型的。换更丰富的语料能**抬高数据本身的信息量上限**，让更大的模型有东西可学。配合换更小/更贴合的
  tokenizer（KI-4）可进一步降 embed/lm_head 浪费。

**我的判断：B 解锁的空间更大。** v5 的核心教训是「瓶颈在语料而非容量」——只放大模型（A）大概率撞上同一本
TinyStories 的信息天花板，收益有限；换更广语料（B）才是抬高天花板本身。理想的 v6 = **A+B 同时**（更大模型
吃更广语料），但若只能选一个，先 **B（广化语料）**。

**KI-3（激活重计算）是否需要**：若 v6 走 A（dim1024+ 更大模型），激活显存会显著上升，**bf16 已经省了一半**
（v5 验证），但更大 batch/更长 seq 下仍可能吃紧 → **届时 KI-3（激活重计算）才成为下一个显存杠杆**（T12 文档
已把它列为「bf16 之后的下一个显存杠杆」）。若 v6 只走 B（同 dim768 换语料），现有 bf16 + 缓存分配器够用，
**KI-3 暂不需要**。即：**KI-3 的触发条件 = v6 放大到 dim1024+**，与 A 路线绑定。
