docs: Phase T18 — dropout design (device RNG + mask)

Counter-based (stateless) RNG → Bernoulli(keep=1-p) mask, inverted 1/(1-p)
scaling at train, identity at eval. New autodiff `dropout` op (fwd generates +
applies mask, bwd applies the SAME cached mask). Wired at the two residual-path
sites (attn / ffn outputs); attention-probs dropout deliberately skipped (fused
SDPA doesn't materialise probs). Documents the RNG choice, per-site deterministic
seed (so T13 recompute reproduces the same mask), train/eval switch, p=0
bit-identity, and the acceptance gates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 00:05:08 +08:00
parent 31cc2bf745
commit 6b8c1e4e0f

155
docs/17-dropout.md Normal file
View File

@@ -0,0 +1,155 @@
# Phase T18: Dropoutdevice RNG + mask— Design Document
## Goal
在已有的 tape autograd 引擎T4+ tiny transformerT5之上**手写一个 dropout 算子**
训练时按 Bernoulli(keep = 1p) 生成一个 0/1 mask丢弃的元素置 0、保留的元素按
**inverted dropout**`1/(1p)`让训练期望与推理一致推理eval时 dropout 是**恒等**。
新增一个 autodiff `dropout` 节点:**前向生成并施加 mask反向施加同一个 mask**。
接到模型的标准位置residual 之前的 attention / MLP 子块输出attention-probs dropout 不做,见下)。
通过 `Config.dropout` / `--dropout` 暴露 `p`**默认 `p=0`**。
明确范围T18 只做这些):
1. 一个 device 端 **counter-based RNG**Philox 风格的 bit-mix`(seed, 元素下标)` 无状态地产出
每元素的 Bernoulli 抽样 → 0/1 mask保留=1丢弃=0同 seed **逐位可复现**
2. 一个 `dropout` autodiff 节点fwd 生成 mask + 施加 inverted scalingbwd 用**缓存的同一 mask**)。
3. 模型里加 **training / eval 开关**train 走 dropout、eval/采样/导出走恒等。
4. `p``Config.dropout` 落地,`bin/train``--dropout` flag。
明确**不做**attention-probssoftmax 后dropout——本项目 attention 是**一个 fused batched SDPA 算子**
`ops::attention`softmax 在 kernel 内部不物化 probs 给外部施加 mask在其上插 dropout 要么改 fused kernel、
要么退回组合路径,**不值当**且偏离「标准 residual/ffn dropout」这条主线。文档明确记下「只做 residual-path dropout」。
## Module Layout
```
csrc/ops/dropout.cu # 新counter-based RNG mask 生成 + 施加 (fwd) / 反向施加同 mask
# fp32 + bf16 两条activation 流可能是 bf16对齐 cast.cu 风格)
crates/xtrain-cuda/
├── build.rs # 新增 dropout.cu
└── src/ffi.rs # 新增 launch_dropout_{f32,bf16} 声明no_cuda 门控)
crates/xtrain-tensor/
└── src/tensor.rs # 新增 Tensor::dropout_mask_apply(p, seed) -> (out, mask)
# Tensor::dropout_apply_mask(&mask) -> outbwd 用)
crates/xtrain-autodiff/
├── src/ops.rs # 新增节点 dropout(x, p, seed)p==0 提前返回 x.clone(),零节点)
└── tests/autograd.rs # 新增:固定 seed grad-checkmask 跨 ± 扰动固定)+ 期望保持数值检查
crates/xtrain-model/
├── src/config.rs # Config 加 dropout: f32默认 0
├── src/model.rs # train/eval 开关Cell<bool>+ 在 attn/ffn 子块输出接 dropout
│ # per-site 确定性 seed与 checkpoint recompute 兼容)
└── tests/dropout.rs # 新增p=0 逐位一致 / eval 恒等 / 期望保持 / p>0 小训练收敛
crates/xtrain-train/src/bin/train.rs # --dropout flag → Config.dropout训练 model.train()sample 前 model.eval()
```
为什么 RNG/mask 落在 `tensor.rs`(而非引擎):和 `scale`/`silu` 一样是一个 device kernel 的薄封装;
autodiff 层只负责把它包成带 backward 的 `Var` 节点(对齐 T4 既有分层)。
## Key Design Decisions
### RNGcounter-basedPhilox 风格),无状态、可复现、与重计算兼容
mask[i] 只由 `(seed, i)` 决定,**不读取任何可变 RNG 状态**
```
key = seed XOR (i * 0x9E3779B97F4A7C15) // golden-ratio 常数打散下标
h = splitmix64(key) // 几轮 bit-mixxorshift+乘法)
u = (h >> 40) as f32 / 2^24 // [0,1) 均匀
keep = u >= p // Bernoulli(keep = 1p)
out[i] = keep ? x[i] * (1/(1p)) : 0
```
选 counter-based 而非「per-step 推进一个全局 LCG 状态」的关键原因 = **激活重计算T13**
checkpoint 的 segment 在 backward 时会**重跑一遍 forward**`segment_fn` 再执行)。
若 dropout 用「调用时推进的可变状态」,重跑会拿到**不同的 mask** → 梯度与前向用的 mask 不一致 → 错。
counter-based + **每个 dropout 站点一个确定性 seed**(见下)保证:重跑同 seed → **同 mask**
重计算依旧逐位一致T13 的硬闸门不被 dropout 破坏)。
> 复现性:同一 `(seed, p, shape)` 下 mask 逐位确定fp32/bf16 mask 判定都在 fp32 里算 `u`bf16 仅存/取
> activation所以两精度的 mask **同分布**drop 与否由 fp32 `u` 决定,不受 bf16 舍入影响)。
### 每个 dropout 站点的确定性 seed兼容 checkpoint 重算)
模型持有一个 `base_seed``Cell<u64>`,每个训练 step 自增一次 → 每步换 mask`block_forward`
收到 `block_seed = base_seed XOR layer_index`,块内两处 dropout 再各 XOR 一个站点常量
attn=0xA77, ffn=0xF7N派生出**该站点的 seed**。这些都是**纯函数**(只看 `base_seed + layer_index +
站点常量`,无可变推进),所以:
- 同一 step 内不同站点 mask 不同seed 不同);
- checkpoint 重算 `block_forward` 时,`block_seed` 由捕获的 `base_seed`/`layer_index` 重新算出 → **同 seed → 同 mask**
- 跨 step mask 变化(`base_seed` 每步 +1
`base_seed` 的自增放在**训练入口**`loss_batched` 训练态调用时 advance 一次。eval/`forward`/采样
**不 advance、不插 dropout**(恒等)。
### train / eval 开关
`TinyTransformer` 加一个 `Cell<bool> training`(默认 **false** = eval安全未显式开训练就不丢弃
- `model.train()` / `model.eval()` 切换builder 风格 `with_training(bool)` 也提供,给测试)。
- `forward_batched` 里:`p > 0 && training` 才在 attn/ffn 子块输出插 `ops::dropout`;否则**完全不建 dropout 节点**。
- 因此 **`p == 0`** 或 **eval** → forward 图与改动前**逐字节相同**`ops::dropout``p==0` 时也提前
`return x.clone()`,双保险)→ 满足「p=0 与无 dropout 逐位一致」回归闸门。
训练 loop`train`)开 `model.train()``eval_loss` / `generate` / 导出 `forward` 走 eval恒等——
导出的模型权重不含任何 dropoutxserv 闭环不受影响。
### dropout 接在哪wiring
接**两处 residual-path dropout**(标准 Pre-LN transformer 位置,对齐 GPT/LLaMA 训练实践):
```
h = h + dropout( attention(rms_norm(h)) ) # attn 子块输出,残差前
h = h + dropout( swiglu_mlp(rms_norm(h)) ) # ffn 子块输出,残差前
```
**不做** attention-probs dropout理由见 Goalfused SDPA 不物化 probs。embedding dropout 也不做(非必需)。
### dropout 节点的 backward为什么 grad-check 成立)
```
fwd: out = x ⊙ mask ⊙ (1/(1p)) # mask 由 seed 生成,缓存进 backward 闭包
bwd: dx = d ⊙ mask ⊙ (1/(1p)) # 用同一个缓存 mask
```
dropout 在 **固定 mask** 下是一个逐元素线性映射 `out_i = c_i · x_i``c_i ∈ {0, 1/(1p)}`
其梯度就是 `dx_i = c_i · d_i`。finite-diff grad-check 之所以成立,关键是**前向缓存的 mask 在 ± 扰动两次
forward 里保持不变**——本设计天然满足mask 只由 `(seed, i)` 决定,与 `x` 的值无关,扰动 `x` 不改 mask。
grad-check 直接对 `ops::dropout` 节点跑:同一个 `seed` 调两次 forward 得到同一 mask函数处处可微。
### 与既有特性的组合
- **bf16T12**activation 流是 bf16 时dropout kernel 走 bf16 分支load→fp32 判 mask→store bf16
mask 判定在 fp32和 cast.cu 既有 bf16 elementwise 同风格grad 也在 activation dtype接回 bf16 链)。
- **重计算T13**见上「counter-based + 确定性 seed」——重算 mask 与前向逐位相同T13 闸门不破。
- **DDPT8**:每 rank 独立跑自己的 forward/backward各自的 mask 由各 rank 的 `base_seed` 决定。
本任务的 DDP 闸门是「loss 对单卡 / 跨 rank 参数一致」,在 **dropout 关(默认 p=0** 的回归配置下跑,
不引入跨 rank mask 同步需求p>0 时各 rank mask 本就该不同,属正常 DDP 语义)。
- **梯度累积T16/ flashT14**:本分支独立于二者,不依赖其未合并改动。
## 验证方法
全部 `#![cfg(not(no_cuda))]` 门控;本地只 `cargo check`/`fmt`,构建 + 实跑在 dash58× RTX 5090, sm_120
**硬闸门(全绿,诚实正确性,不放宽容差)**
1. **固定 seed grad-check**`autograd.rs::dropout_bwd`):对 `ops::dropout(x, p, seed)` 同一 seed
跑 finite-diffmask 跨 ± 扰动固定)→ `dx` 对中心差分通过(线性 op`cfg_linear` 容差)。
2. **train/eval + 期望保持**`dropout.rs`
- eval 恒等:`dropout` 关时 `out == x` **逐位**
- 期望保持:大张量、训练态、对多组随机 mask 取均值,`E[out] ≈ x`inverted scaling 正确),给数值;
- 实际 keep 比例 ≈ `1p`(验证 RNG 分布)。
3. **p=0 逐位一致**`dropout.rs`):同 init 两个模型,一个不设 dropout、一个 `dropout=0`
同 batch forward+backward → **logits/loss/每参数 grad 逐位相同**`|Δ| == 0`)。
4. **p>0 小训练收敛**`dropout.rs`,或 dash5 短跑):小模型开 `p=0.1` 训若干步,**loss 下降、无 NaN**。
5. **全回归套绿**autograd grad-checks、structural、batched==looped、bf16、recompute逐位一致
overfit 27/27、AdamWGPU bit-exact + host vs torch、DDPloss-match + 跨 rank
**xserv 闭环**(导出 md5 vs registry、token-identical导出/推理 dropout **关**,导出模型不受影响)。
dash5 capture 每个闸门的 pass + 关键数字max rel-err、期望 vs input、p=0 的 `|Δ|`、训练 loss 轨迹)。