# xtrain — Known Issues & Perf Backlog 已知问题(性能 / 正确性 / 建模)与延后项的活文档:记录现象、复现、根因、拟修复、优先级、状态。 发现即记,修复即标 `FIXED`(附 commit)。 --- ## Open _(KI-1 fixed in T10. KI-5 fixed in T11. KI-2 fixed in T12. **KI-3(激活重计算 / gradient checkpointing)FIXED in T13**——per-block checkpoint(no-tape forward + recompute-on-backward),梯度对非重计算版**逐位一致**(fp32/bf16 max rel 0.00e0);dim768 bf16 batch32 峰值显存 31.1→14.6GB(−53%)/ tok/s −20%;**dim1024 batch32 不开 OOM → 开了 16.6GB 装得下**,解锁 v8。见下方 Fixed。)_ --- ## Fixed ### DDP-dropout wiring(launcher 漏接 dropout)— `FIXED` (T21) - **背景(V9-PILOT 暴露)**:T18 dropout 在**单卡** `train.rs` 完整接好(`--dropout` flag → `cfg.dropout`,每步 `model.train()`,eval 用 `model.eval()`),op 级 + 单卡都测过。但 V9-PILOT 全栈端到端跑(DDP 8 卡 + dropout0.1 + flash + GQA + accum + bf16)时发现 **DDP 路径根本没接 dropout**:`train_ddp` bin **无 `--dropout` flag、从不设 `cfg.dropout`**,且 `ddp.rs::train_rank` **从不调 `model.train()`** → 模型停在默认 eval 模式,`ops::dropout` 恒等 → DDP 下 dropout **被静默忽略,无论 config 怎么设**。模型 + autodiff 完全支持 dropout(T18),漏的纯是 **DDP launcher / 训练 loop 的 wiring**。 - **为何 op/单卡测试没抓到**:dropout 的测试只覆盖**单卡训练循环 + op 级 grad-check**,从没在 **DDP 路径下**跑过 dropout。`train_rank` 是独立于单卡 `train()` 的另一条 loop,二者共享 model/autodiff 但**各自布线 train/eval 纪律** —— 单卡那条对了不代表 DDP 那条对。**元教训:op 级 + 单 GPU 单元测试能漏掉 launcher 级 integration gap**;只有把特性放进**真实启动器路径**端到端跑(pilot 做的事)才暴露。修复随手补了 DDP 路径的回归测试堵这个缺口。 - **修复([docs/17-dropout.md](17-dropout.md))**:① `train_ddp.rs` 加 `--dropout
` flag(默认 0 = 关,对齐旧行为)并设 `cfg.dropout`;② `ddp.rs::train_rank` 每步 micro-batch 循环前调 `model.train()`,镜像单卡 loop 的 train/eval 纪律——**关键**:`eval_loss()` 内部 `model.eval()` 翻成 eval 模式且**不还原**,所以每步重新 assert `model.train()`,dropout 才能跨 eval 边界保持活跃。
- **正确性(新增 DDP-dropout 回归测试 `ddp_dropout_is_live_and_p0_bit_identical`,跑真实 `train_rank` 启动器路径)**:① **GATE A**——`p=0` 下 DDP loss 轨迹 + 末态参数对无 dropout 路径**逐位一致**(`ops::dropout(p=0)` 是 clone no-op,回归保护);② **GATE B**——`p=0.2` 的 loss 轨迹对 `p=0` **有可观差异**(>1e-3),证 dropout mask 真在训练 forward 应用(pre-T21 代码停在 eval 模式 → 二者会逐位相同,此 gate 会 FAIL);③ **GATE C**——run 后 `model.is_training()==true`(直接证 `model.train()` 被调用且跨末步 eval 存活);④ p>0 run 无 NaN/Inf。测试故意启用 `eval_every < steps` 让 eval 中途翻 eval 模式,验证每步 `model.train()` 的 restore 纪律。默认 `--dropout 0` 下既有 DDP loss-match + 跨 rank 测试**不变**(回归保护)。
- **commit**:见 T21 提交链(`distributed: --dropout flag + model.train() per step in train_rank` / `test: DDP-dropout regression (live under DDP + p=0 bit-identical)` / 文档更新)。
---
### process-per-GPU(torchrun 式独立 CUDA context)— `CLOSED / 实测负结果` (T17)
- **背景**:KI-5(T11)修掉 per-op `cudaMalloc` 串行后,8 卡 scaling 从 ~1.3× 恢复到 **~5×@8**,但残留 ~5×@8 非完美线性。T11 doc / KI-5「残留」推测下一步是 **process-per-GPU**(每 rank 独立进程 + 独立 CUDA context,torchrun 式)——理由是「N rank 线程共享单 CUDA primary context,kernel-launch/cuBLAS 仍在 context 级串行」。**T17 把这条 torchrun 式链路落地并实测,证伪了该推测。**
- **实现([docs/16-process-per-gpu.md](16-process-per-gpu.md))**:`xtrain-distributed` 加 `proc.rs`——`launch_processes` 每卡 spawn 一个 worker 进程(re-exec current_exe + `XTRAIN_{RANK,WORLD,LOCAL_RANK,NCCL_ID}` env);**launcher 一次性铸 `ncclUniqueId` 后 hex 编码注入子进程 env**(无共享 FS/TCP、无轮询、无竞态——id 在子进程出生前就原子就绪);worker 读 env → bind device(独立 CUDA context)→ `DdpContext::init` + `build_model` + `train_rank` **全部复用 T8 零改动**。新 `train_ddp_mp` bin + `ddp_proc` test;**保留 thread-per-GPU 旧路径**(回归 baseline)。scope=process-per-GPU only(ZeRO-1 用户 drop)。
- **正确性(全绿,无回归)**:proc vs 单卡 loss `5.67e-7`、**proc vs thread-per-GPU `1.5e-7`**(两路数值同量级)、跨 rank `1.19e-7`(<1e-6);全回归套 `--test-threads=1` 全绿 + **xserv 闭环 v3 重导 md5 逐位一致 `b04fc9f9`**(T17 不碰任何数值路径)。
- **实测结果(关键,dash5 8× RTX 5090, dim384 per-rank batch32 seq256, steady-state)**:
| world | thread-per-GPU (`train_ddp`) | speedup | process-per-GPU (`train_ddp_mp`) | speedup |
|---|---|---|---|---|
| 1 | 93257 | 1.00× | 92952 | 1.00× |
| 2 | 149747 | 1.61× | 148809 | 1.60× |
| 4 | 278276 | 2.98× | 273308 | 2.94× |
| 8 | **491360** | **5.27×** | **493128** | **5.31×** |
(world=8 各重复 2 次:thread 493671/493292、proc 491102/494123——**两路差异 <1%,落在噪声内**。)
- **诊断(证伪原推测)**:process-per-GPU world=8 跑时 `nvidia-smi` 抽样 **8 卡全部 95–99% util**(每卡 ~23GB)——GPU **已 compute-bound 喂满、非串行空转**(KI-5「1–2/8 在忙」的串行病 T11 allocator 已治好)。8 卡满载却仍只 5.3× ⇒ 缺的 ~35% 吞吐去向**每步 grad all-reduce + 本机 PCIe-only 拓扑在 8 rank 下的通信开销**(T11 早点明的「~7% all-reduce + PCIe 余量」那一层,8 卡放大),换独立 context 不动这一层。**结论:本尺度(dim384–1024、单机 8× PCIe RTX 5090)残留非线性是通信/拓扑墙,不是 launch 模型**——要再逼近线性须动 all-reduce overlap / NVLink 互联(非本尺度优先)。
- **方法论一致**:T11 实测证伪「分桶 all-reduce」、T17 实测证伪「process-per-GPU 解残留串行」——两次都靠 measure 推翻假设而非硬上(profile/measure-first)。**净价值**:落地 torchrun 式 process-per-GPU 标准链路(项目本职「学训练全栈」)+ 把这个误导性 backlog 项**实测钉死关闭**。**默认训练路径不变**(thread-per-GPU),process-per-GPU 作并列可选路径留档。
- **commit**:见 T17 提交链(`distributed: process-per-GPU launcher + worker` / `distributed: train_ddp_mp bin` / `test: process-per-GPU DDP correctness` / 设计文档 `docs: Phase T17 — process-per-GPU DDP design`)。
---
### KI-3 · 激活重计算(gradient checkpointing)— `FIXED` (T13)
- **触发点(v8 surfaced)**:容量轴放大到 dim1024(核心 ~210M+)测是否 capacity-limited。autograd tape 为反向保存所有中间激活,激活显存随 dim 线性增长——dim768 bf16 batch32 已 31.1GB(T12 甜点区),**dim1024 batch32 再次 OOM**(实测撞 32100/32607MiB → `OutOfMemory`)。
- **设计(per-block gradient checkpointing,opt-in,[docs/12-activation-recompute.md](12-activation-recompute.md))**:新增 `xtrain_autodiff::checkpoint(segment_fn, input, params)` 高阶原语(类比 `torch.utils.checkpoint`)。**前向**:把 input/params detach 成局部 leaf 跑 `segment_fn`,只取输出值,局部 tape 立即 drop → 段内激活释放(不留在外层 tape);checkpoint 节点 parents=[input, ..params]。**反向**:从保存的 input + 未变的 param 值重跑 `segment_fn` 重建局部 tape,用上游 grad seed(`Var::backward_seeded`,新增——段输出非标量)回传,恢复的 input/param 梯度 push 给真 parents,局部 tape drop → 重算激活释放。模型每个 transformer block 前向用它包裹(`--recompute` flag,默认关)。切粒度 = 每 block。
- **正确性(exact,硬闸门全绿)**:重计算数学精确(同 `segment_fn`、同输入、同参数值、确定性 kernel → 重算激活逐位等于原激活)。**on-vs-off 梯度对拍 fp32/bf16 双路逐位一致**:loss rel `0.00e0`、logits max rel `0.00e0`、**每个参数梯度 max rel `0.00e0`**(不是容差内,是逐位)。全套回归开/关重计算全绿:autograd 15、structural 5、batched、bf16、overfit 27/27、AdamW(GPU bit-exact + host 对 torch)、checkpoint roundtrip、**DDP loss 对单卡 5.67e-7 + 跨 rank 0.0**;DDP+recompute 2 卡短训单调降(11.079→6.010)。非重计算路径图不变(默认关)→ T10/T11/T12 数值不回归。
- **显存 + 吞吐(payoff,dash5 1× RTX 5090 32GB, bf16, batch32 seq256, steady-state)**:
| config | per-rank batch | 峰值显存 | tok/s | fits 32GB? |
|---|---|---|---|---|
| dim768 (18L/24h ffn2048, core 127M) off | 32 | 31144 MiB | 39.7K | ✅ |
| **dim768 on** | 32 | **14562 MiB(−53%)** | **31.5K(−20%)** | ✅ |
| **dim1024** (18L/32h ffn2730, core 226M) off | 32 | 32100 → **OOM** | — | ❌ **OOM** |
| **dim1024 on** | 32 | **16596 MiB** | 23.1K | ✅ **解 OOM** |
→ dim768:重计算砍 ~半激活(**31.1→14.6GB,−53%**),代价 tok/s **−20%**(多一次前向,落在预测 20–35%)。**dim1024 batch32:不开 OOM → 开了 16.6GB 稳稳装下**,~23K tok/s 正常收敛 → **dim1024 解锁,v8 可展开**。
- **commit**:T13 提交链(`autodiff: checkpoint primitive (recompute-on-backward)` / `model: per-block activation recompute (--recompute)` / `perf: KI-3 fixed …` 本条带 before/after / 文档 `docs: Phase T13 — activation recompute`)。
---
### KI-2 · bf16 混合精度(fp32 master)— `FIXED` (T12)
- **触发点(v4 surfaced)**:dim768 fp32 在单卡 32GB 里 per-rank batch 32(global 256)OOM,被迫降到 per-rank 16。bf16(激活减半)找回 batch-32 甜点区,并加速已 compute-bound 的 dim768 GEMM;附带:xserv 推理 BF16-only,bf16 训练更贴闭环。
- **设计(标准 AMP,opt-in,[docs/11-bf16-mixed-precision.md](11-bf16-mixed-precision.md))**:**fp32 master weights** + AdamW/clip/DDP all-reduce 全程 fp32;**bf16 compute**=linears(q/k/v/o, gate/up/down, lm_head)走 `cublasGemmEx`(CUDA_R_16BF in/out, CUBLAS_COMPUTE_32F 累加)+ 激活流 bf16(含 attention probs / logits);**fp32 稳定**=RMSNorm/QK-norm、softmax、RoPE、cross-entropy 内部 upcast→fp32→downcast。**无 loss scaling**(bf16 8-bit 指数=fp32 动态范围)。关键钩子=autodiff `cast` 算子:前向把 fp32 master leaf 降到 bf16 喂 matmul,**反向把 bf16 grad 升回 fp32** → fp32 leaf 累加 fp32 grad,优化器一行不改。fp32 路径按 dtype 分派、逐字节不变(hard gate)。
- **正确性(双闸门全绿)**:
- **fp32 不回归**:全套在原紧容差绿——autograd 15、structural 5、GEMM 对 cuBLAS 5、batched==looped、overfit 27/27、AdamW GPU bit-exact + host 对 torch、checkpoint 逐位、DDP loss 对单卡 + 跨 rank、**xserv 闭环**(v3 ckpt 用 T12 代码重导 safetensors 与 registry **md5 逐位一致** `b04fc9f9…`)。
- **bf16 looser-tol**:同 fp32 master 跑 fp32 vs bf16——loss rel **1.2e-4**、logits mean rel **2.0e-3** / p99 **6.8e-3**、grad worst scaled-mean **1.0e-2**,无 NaN,grad 仍 fp32。
- **收敛**:dim768 短训 150 步,bf16-b16 loss 轨迹对住 fp32-b16(step50 4.40 vs 4.40、step149 **3.984 vs 3.988**),单调降、无发散。
- **显存 + 吞吐(payoff,dash5 1× RTX 5090 32GB, dim768/18L/24h×32 ffn2048 seq256, steady-state)**:
| config | per-rank batch | 峰值显存 | tok/s | fits 32GB? |
|---|---|---|---|---|
| fp32 | 16 (v4 fallback) | 27.2 GB | 31.5K | ✅ |
| **bf16** | 16 | **19.3 GB(−29%)** | **35.5K(+13%)** | ✅ |
| fp32 | 32 | — | — | ❌ **OOM** |
| **bf16** | **32(甜点区)** | **31.1 GB** | **40.8K** | ✅ **解 OOM** |
→ **同 batch 16:bf16 显存 27.2→19.3GB(−29%)、tok/s 31.5K→35.5K(+13%)**;**bf16 解 fp32-batch32 OOM**(31.1/32.6GB fit),batch32 达 **40.8K tok/s(+29% vs fp32-b16)**。残留:norm/softmax/CE 的 fp32 upcast 是 transient,但仍占峰值——若 v5 要更大 batch,下一杠杆是 KI-3 激活重计算。
- **commit**:见 T12 提交链(`cuda: bf16 cuBLAS GemmEx` / `autodiff: bf16 mixed-precision path` / `train: --bf16 flag` / `perf: keep bf16 logits` / 本条)。
---
### KI-5 · DDP 弱扩展性 — `FIXED` (T11, device caching/pool allocator)
- **根因(T11 重诊断,all-reduce **不是**瓶颈)**:每个 tape op 输出走 `Tensor::zeros`→`GpuBuffer::alloc`→`cudaMalloc`(同步、进程级串行的 driver 调用)。单进程 thread-per-GPU 下 N rank 每步几百次 alloc 在单 CUDA context 排队串行(`NOCOMM=1` 完全不通信时 fwd+bwd 仍 136→780ms 膨胀 ~6×,`nvidia-smi` 抽样 8 卡只 1–2 张在忙轮流跑);单卡也吃这笔 per-op alloc。
- **原拟修复「分桶 all-reduce」经 T11 实测证伪并 revert**:grad all-reduce 每步只占 ~6–7%,融成一发对 1/2/4/8 卡几乎无差(详见下方历史诊断)。
- **修复**:`xtrain-cuda` 加 **device caching/pool allocator**([docs/10-caching-allocator.md](10-caching-allocator.md))——`GpuBuffer::alloc` 从 per-device、size-classed free-list 取,miss 才 `cudaMalloc`;`Drop` 归还 free-list(不 `cudaFree`)。训练定形状→命中率极高,warm-up 后每步 `cudaMalloc`≈0。线程安全:global registry 按 device id 分桶,每 device 的 free-list 各自 `Mutex`(registry 锁只在 clone 出 `Arc