docs: run v2 — TinyStories, dim384/12L, DDP 4-card (val 1.71)

Scaling run v2 design doc + comparison-table update. v2 = dim384/12L/12h
SwiGLU ffn1536 (core 28.32M, total 66.92M), trained 4500 steps / ~36.9M
tokens on full TinyStories (reused v1 u16 cache) via NCCL DDP across 4
RTX 5090s. Best val 1.7055 (train 10.89→1.72), a clear jump over v1 2.58
and v0 3.80. Exported to xserv (135 BF16 tensors) and archived in the
dash5 registry; xserv greedy token-matches xtrain on 2/3 fixed prompts
(3rd diverges late under BF16 drift). Records the DDP weak-scaling caveat
(global batch too small → all-reduce dominates) → links docs/known-issues
KI-1; v3 proposal applies KI-1's fix (much larger global batch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 22:38:31 +08:00
parent c87a0bc44e
commit bf679f6f1f
2 changed files with 202 additions and 3 deletions

View File

@@ -0,0 +1,196 @@
# Scaling Run v2: TinyStories + dim384/12L + DDP 多卡 — Design Document
## Goal
在 v1dim256/8L、core 8.39M、全量 TinyStories 但只训了 ~5.1M token、单卡之上沿**模型 + 数据 +
并行**三个轴同时放大,做第一次**多卡 DDP 训练**
1. **模型放大**dim 256→384、层 8→12、头 8→12**transformer core 做到 ~28M 参**(容量 ×3.4
词表不变 → embed+lm_head 因 gpt2 50257 vocab 在 dim384 下固定加 ~38.6M,单列出来。
2. **数据放大**v1 只消费了 ~5.1M token欠拟合val 一路降到末步v2 训 **~37M token**×7.2
复用 v1 已缓存的全量 TinyStories token-id 流(不重新 tokenize 2GB 语料)。
3. **多卡 DDP**:用 T8 的 `xtrain-distributed`NCCL 数据并行)在 **4 张 RTX 5090** 上训练,把多卡
wall-clock 压回 bounded 区间。
4. **训练器对齐**:把 T8 的 `train_ddp` 接上 `bin/train` 已有的——参数化 arch / token 缓存 / held-out
val 评估 / warmup→cosine / grad-clip / best-val checkpoint——单卡与 DDP 共用一套 eval/checkpoint 逻辑。
5. 训完存 registry`~/projects/tiny-models/v2-tinystories-dim384/`+ 导出 xserv 格式验证可服务,给出
**相比 v1 的具体提升**(同一保留集 val loss + 同 prompt 并排采样)。
> 范围escape hatch 已评估):单序列模型设计(每个 sequence 一次独立 forward、逐 op 启动开销)使
> dim384/seq256 下 DDP 全局吞吐 ≈ **3.6K tok/s @ 4 卡**GPU 利用率偏低,已知瓶颈见 docs/06且本版
> 的小 global batch 又放大了 all-reduce 占比,见 KI-1。为在共享机上 bounded~2.8 小时)内拿到
> 「清晰、可量化超过 v1」的结果v2 训 **4500 步 ≈ 37M token**,不追求把 37M 之外榨满——v2 的目的是
> 验证 DDP 训练器对齐 + 相对 v1 的明确提升val<2.2),不是榨满模型。
## 数据
| 项 | v1 | v2 |
|----|----|----|
| 来源 | TinyStories **全量 train** | 同(复用 v1 缓存)|
| token 数(语料)| 468,260,367 | 同 |
| **训练消费 token** | ~5.12M2500 步 × 2048| **~36.9M**4500 步 × 8192|
| tokenizer | gpt2 BPEvocab 50257| 同 |
| 缓存 | `data/tinystories-train.txt.u16.bin`u16936MB| **直接复用**(不重 tokenize|
| held-out val | 全量末尾 1,000,000 token | **同一 1M token**(与 v1 完全相同的保留集,便于公平对比)|
**复用缓存**`Corpus::load_cached``<corpus>.u16.bin`v1 首跑已写盘v2 启动即时载入 468M token
跳过 2GB 语料的 from-scratch BPE。held-out val 仍是全量末尾 1M token`split_tail`),与 v1 同一保留集
——所以 v1/v2 的 val loss 直接可比。
## 架构
v2 = 更大、同构的 tiny Qwen3RoPE + RMSNorm + per-head QK-norm + SwiGLU + 独立 lm_headMHA
forward 图与 v0/v1 完全同构,只是 dims 变大。无结构改动。
| 维度 | v1 | v2 |
|------|----|----|
| dim= heads·head_dim| 256 | **384** |
| n_layers | 8 | **12** |
| n_heads | 8 | **12** |
| head_dim | 32 | 32 |
| ffn_hiddenSwiGLU| 1024 | **1536** |
| vocab | 50257 | 50257 |
| **core 参数**(除 embed+lm_head| 8,393,472≈8.39M| **28,322,304≈28.32M×3.37** |
| embed + lm_head2×vocab×dim| 25,731,584≈25.7M| 38,597,376≈38.6M|
| **总参数** | 34,125,056≈34.13M| **66,919,680≈66.92M** |
**core 的量法**`Config::core_params() = num_params() 2·vocab·dim`。gpt2 50257 vocab 在 dim384 下让
embedding + lm_head 固定占 ~38.6M——这两张表是**词表大小**的函数、不是模型容量,所以阶梯按 **core**
v2 core 28.32M 命中 ~27M 目标)。这也是 v2 总参 66.9M「看着大」但有效容量 28.32M core 的原因
gpt2 大词表占比问题见 docs/known-issues.md KI-4
**相比 v1 的架构变化**纯放大dim/层/头/ffn无结构改动。阶梯已参数化v2 只改
`--dim/--heads/--layers/--ffn/--steps` flag不动模型代码。
## DDP 训练器对齐本版工程改动commit 7090b47
v1 的单卡 `bin/train` 已有:参数化 arch、token 缓存、held-out val 评估、warmup→cosine、grad-clip、
best-val checkpoint。T8 的 `train_ddp` 当时只是吞吐/正确性 driver硬编码 tiny config、`Corpus::load`
无缓存、无 val/checkpoint。v2 把它**接到与单卡同一水平**
- **复用而非重写**`eval_loss` / `checkpoint::save` 都在 `xtrain-train`DDP 直接调用——单卡与 DDP
共用一套 eval/checkpoint 路径(不复制逻辑)。
- `DdpConfig` 增加 `eval_every / eval_batches / ckpt_path``train_rank` 接收 `valid: Option<&Corpus>`
返回 `DdpResult { losses, evals, best_val }`
- **val/checkpoint 只在 rank 0**DDP 后每 rank 参数 bit-identicalT8 已验证rank 0 持 val 语料、
跑无梯度 eval、写 best-val checkpoint其余 rank 此处无事可做。
- `launch` 把 val 语料只递给 rank 0`bin/train_ddp` 改成与 `bin/train` 同款 CLIpositional
tokenizer/corpus + 全部 arch/优化/val/ckpt flag复用 u16 缓存。
- **T8 语义不变**all-reduce device 梯度 → /world → 各 rank 本地 GpuAdamW跨 rank 参数一致性检查仍过
(见「验证」)。
## 超参
| 项 | 值 | 备注 |
|----|----|----|
| optimizer | 手写 AdamWGPU 端 step| wd=0.1,β/eps 用 xtrain-optim 默认 |
| LR schedule | 线性 warmup → cosine decay | max_lr **6e-4** → min_lr **6e-5**(同 v1|
| warmup | steps/20 = 225 步 | |
| grad clip | global-norm 1.0 | |
| steps | **4500** | bounded≈2.8 小时 @ 4 卡)|
| batch | **32**global| DDP 分到 4 rank 各 8单序列模型靠多次 forward 让 tape SUMclip 时 ×1/b_local |
| seq_len | **256** | v1 是 128更长上下文 + 更省单序列启动开销)|
| tokens/step | 32×256 = 8192 | 总训练 token ≈ **36.9M** |
| world size | **4**RTX 5090 ×4sm_120| GPU 0-3 |
| 精度 | f32训练| 导出 xserv 时转 BF16见 T9|
**算力 / DDP scaling**dash5 4× RTX 5090全局吞吐 ≈ **3604 tok/s @ 4 卡**wall-clock ≈ **2.8 小时**
⚠️ **DDP 弱扩展KI-1**4 卡 3604 tok/s 仅 ≈ **1.08×** v1 单卡(~3310 tok/s远未近线性。根因是
本版 `global_batch=32`(每卡仅 8太小每 step 对**全部参数梯度**做一次 NCCL all-reduce 是固定开销,
每卡 compute 太少 → 通信/同步占比过高,吃掉扩展性。对比 T8 在 tiny 规模 micro-benchmark 的近线性
1.87×@2 / 3.01×@4,见 docs/07-distributed.md差异正是 batch 规模。**v3 先用「显著加大 global
batch」缓解**(摊薄 all-reduce、喂饱 GPU后续再做分桶 / overlapped all-reduce。详见
[docs/known-issues.md](../known-issues.md) KI-1。
## 结果
- **train loss**start **10.8867** → end **1.7171**
- **best val lossheld-out 1M token****1.7055**step 4499
- val loss 曲线(每 500 步,单调下降、未见过拟合):
| step | 499 | 999 | 1499 | 1999 | 2499 | 2999 | 3499 | 3999 | 4499 |
|------|----|----|------|------|------|------|------|------|------|
| val | 2.7340 | 2.3206 | 2.1007 | 1.9800 | 1.8920 | 1.8110 | 1.7622 | 1.7245 | **1.7055** |
val 一路降到末步、无回升 = 仍**欠拟合**,更多步数/数据还能继续降v3 杠杆)。
### 采样greedyxtrain 直采,同 prompt
```
[Once upon a time] → Once upon a time, there was a little girl named Lily. She loved to play
outside in the sunshine. One day, she saw a big, red apple on the ground.
She picked it up and took a big
[The little] → The little girl was so happy and she thanked the man for his help. She said
goodbye and went home with a smile on her face. <|endoftext|>
[One day] → One day, the little girl was walking in the park when she saw a big, scary
dog. The dog was barking and running around. The little girl was scared and
started to cry. The dog said
```
温度 0.8 采样同样连贯(多角色、完整情节),见 `RUN.md`
## 相比 v1 的提升
**best val loss各自训练 run 报告的 held-out 1M token 最优值)**
| 模型 | core 参数 | 训练 token | **best val loss** | 说明 |
|------|-----------|-----------|-------------------|------|
| v0-baseline | 41K | ~0.72M | 3.8050 | 3MB 切片,采样退化循环 |
| v1 | 8.39M | ~5.1M | 2.5847 | 全量数据 + dim256/8L单卡 |
| v2 | 28.32M**×3.37**| ~36.9M**×7.2**| **1.7055** | dim384/12L + DDPval 比 v1 低 **0.88** |
> v1 训练用 seq128、v2 用 seq256两次 best-val 是各自训练 run 直接报告的。为做完全 apples-to-apples
> 又在**同一保留集 + 同一 eval 设置seq256 / 64 batch**下重评了两个 checkpoint**v1 2.6756 →
> v2 2.0418**(低 **0.634**)。两种量法都给出同向、可观的提升。
### 并排采样greedy 40 tokxserv 服务,同 prompt
| prompt | v1 | v2 |
|--------|----|----|
| `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. The dog was scared and didn't know what to do** | …a little girl named Lily. She loved to play outside in the sunshine. One day, she saw a big, **red apple on the ground. She picked it up and took a big** |
| `One day` | One day, she saw a big, shiny ball in the park. She wanted to play with it, but she was too scared to go. | One day, the little girl was walking in the park when she saw a big, scary dog. The dog was barking and running around. **The little girl was scared and ran away. The dog chased her** |
| `The little` | The little girl was so happy that she had been able to help. | The little girl was so happy and she thanked the man for his help. **She said goodbye and went home with a smile on her face.** |
**结论**v18.39M core / 5.1M token已能写连贯小故事但句子偏短、常一两句就收尾。v228.32M core /
36.9M token在**相同开头**下展开更长、更具体的情节链(捡苹果→咬一口;遇狗→狗追→逃跑),句法更丰富、
跨句指代一致,故事密度明显更高。**best val 2.58→1.71(低 0.88+ 采样从"短句收束"到"多步情节"**
v2 是相对 v1 的清晰、可量化提升。
## xserv 验证
导出 HF Qwen3 safetensors命名映射 + 2D 权重转置 [in,out]→[out,in] + BF16见 T9 `docs/08`
**135 tensors** = 12 层 × 11 + embed + norm + lm_head存入 registry 后用 `xserv-cli` 加载并贪心生成:
```
$ xserv-cli ~/projects/tiny-models/v2-tinystories-dim384 --max-tokens 40
Model: qwen3, layers=12, hidden=384, heads=12/12 kv, vocab=50257
Loaded 135 tensors
xserv> Once upon a time, there was a little girl named Lily. She loved to play outside in the
sunshine. One day, she saw a big, red apple on the ground. She picked it up and took a big
xserv> One day, the little girl was walking in the park when she saw a big, scary dog. The dog
was barking and running around. The little girl was scared and ran away. The dog chased her
xserv> The little girl was so happy and she thanked the man for his help. She said goodbye and
went home with a smile on her face. <|endoftext|>
```
**token-match**xservBF16对 xtrain 自身贪心F323 个 prompt 中 **2 个逐 token 完全一致**
"Once upon a time"、"The little");第 3 个("One day")在后段 "scared and ___" 处因 BF16 漂移分叉
xtrain 选 "started to cry"、xserv 选 "ran away")——单个 logit 微差翻转贪心取值后序列发散,与 v1
观察到的 ~0.5% BF16 漂移同源。闭环在 v2 规模仍基本成立(多数 prompt 逐 token 一致,少数因 BF16 末端分叉)。
## v3 提案
v2 的 val 曲线一路单调下到末步(无过拟合)= 仍**欠拟合**,同规模再多喂步数/数据还能降。建议 v3
- **先修 KI-1DDP 弱扩展)**:把 `global_batch` 从 32 显著加大(如 128256每卡 3264摊薄
per-step all-reduce、喂饱 GPU把 4 卡吞吐拉回接近线性——这是 v3 提速的第一杠杆。
- **数据/步数**:在更高吞吐下把训练 token 从 ~37M 拉到 ~150300M仍在 TinyStories 全量内、不重复
一个 epoch目标 val 进一步降到 ~1.41.5。
- **模型**dim 512 / 16 heads·32 / 16 layers / ffn 2048 → core ≈ **75M**(容量 ×2.6)。词表不变 →
embed+lm_head ~51.5M,总 ~126M。
- **数据阶梯**v3 仍喂 TinyStories37M→更多步未榨满模型也还在 tiny-LM 范围);待 core 进一步放大
到 ~100M+、TinyStories 明显成为容量上限后,再按数据阶梯**上更广高质语料**(如 TinyStories + 部分
通用语料混合),同步评估是否换更贴合的 tokenizer缓解 KI-4 大词表占比)。
阶梯已参数化v3 改 `--dim/--heads/--layers/--ffn/--steps/--batch` flag + 调 DDP world 即可,不动模型代码。