Skip to content

Latest commit

 

History

History
823 lines (657 loc) · 40.3 KB

2026-07-14-lazy-train-nvme-version.zh.md

File metadata and controls

823 lines (657 loc) · 40.3 KB

LAZY-Train 第四版本:NVMe 第三层存储建模与求解结果

对应版本:nvme。当前默认入口已经切到 nvme-v5 / CPU15 / NVMe1 / refhybrid-path / GDS prefetch0;旧的 CPU14 / NVMe3 refhybrid、CPU14 v2 和 CPU8 / NVMe11 schedule 保留为历史对照。

核心入口:

  • 配置:examples/sft/configs/qwen3_6_27b_metamathqa_b72_lazy_peak70_nvme.yaml
  • 调度:schedules/qwen3_6_27b_metamathqa_b72_lazy_peak70_nvme_cpu15_refhybrid_pcie_nvme_pool.json
  • 运行脚本:scripts/run_qwen3_6_27b_metamathqa_b72_lazy_peak70_nvme_tmux.sh
  • 四版本脚本索引:scripts/README.md

0. 当前最重要的两个结论

  1. NVMe 版的主要价值是扩训练可行域,不是大幅提速。 大白话:NVMe 是 GPU HBM 和 CPU RAM 之外的第三层 activation 存储。它比 CPU RAM 慢,所以不会天然让 27B 这种已经能用 GPU+CPU 跑起来的设置快很多; 它真正有意义的场景是更大模型、更大 batch、更紧 CPU 内存预算,或者 alpha/8bit 方案为了省内存不得不大量重算时,NVMe 可以把一部分冷 activation 放到 SSD, 让训练仍然可行。

  2. 当前 27B / batch72 / peak70 实验说明 NVMe/v5 稳定,且比 8bit_optim 有小幅吞吐优势,但优势不大。 截至 step 971/3841,NVMe/v5 没有 skipped optimizer step 或 nonfinite grad;step2-971 平均 52.819s、 225.65 TFLOPS、1396.0 tokens/s。同区间 8bit_optim 是 53.333s、 223.66 TFLOPS、1383.7 tokens/s,所以 NVMe/v5 约 +0.89% TFLOPS。 eval loss 也基本对齐:step384 为 0.12880 vs 0.12863,step768 为 0.12625 vs 0.12626。这说明第四版不是靠 NVMe 提供显著速度提升,而是验证 “第三层存储可以被 MIP 纳入统一调度,并且在当前设置下不明显拖慢训练”。

1. 模型最开头先讲清楚:变量、约束、目标函数

决策变量

对每个 layer boundary i,MIP 会决定:

  • checkpoint_i in {0,1}:这个 boundary 要不要保存成 checkpoint。
  • gpu_i in {0,1}:如果保存,是否放在 GPU HBM。
  • cpu_i in {0,1}:如果保存,是否 offload 到 CPU RAM。
  • nvme_i in {0,1}:如果保存,是否 spill 到 NVMe SSD。
  • edge_{s,e} in {0,1}:从 checkpoint boundary s 到 e 是否组成一个 backward recompute block。
  • d2h_{i,w}, h2d_{i,w}:boundary i 的 activation 在第 w 个计算窗口里占用多少 PCIe fabric 通信量;CPU offload 是 GPU<->CPU,NVMe GDS offload 是 GPU<->NVMe。
  • nvme_write_{i,w}, nvme_read_{i,w}:boundary i 的 activation 在第 w 个计算窗口里占用多少 NVMe 设备读写量。
  • exposed_*_w >= 0:如果某个窗口里的通信塞不进可 overlap 的计算窗口,暴露出来的通信量。

大白话:对每一层之间的中间结果,求解器要决定“留显存、放内存、写固态、还是到时候重算”。

约束

  1. 唯一放置约束
checkpoint_i = gpu_i + cpu_i + nvme_i

大白话:一个 checkpoint 只能放在一个地方,不能又在 GPU 又在 CPU 又在 NVMe。

  1. 路径约束
edge_{s,e} 组成从 0 到 N 的一条合法路径
每个被选中的中间 checkpoint 有一条入边和一条出边

大白话:checkpoint 不能散着选,必须能把模型从第 0 层串到最后一层。

  1. GPU 显存硬约束
sum_i gpu_i * activation_bytes <= gpu_activation_budget_bytes

本次 NVMe 版本仍使用 peak <= 70GB。

  1. CPU activation 硬约束
sum_i cpu_i * activation_bytes <= cpu_activation_budget_bytes

当前默认 v2 设定 cpu_activation_budget = 14GB;旧 v1 为了强行压 CPU activation 空间曾使用 8GB,因此会产生更多 NVMe checkpoint。

  1. NVMe activation 硬约束
sum_i nvme_i * activation_bytes <= nvme_activation_budget_bytes

本次设定 nvme_activation_budget = 32GB。

  1. PCIe 资源池约束
sum_i d2h_{i,w} + mandatory_grad_d2h_w <= pcie_d2h_capacity_w + exposed_d2h_w
sum_i h2d_{i,w} + mandatory_param_h2d_w <= pcie_h2d_capacity_w + exposed_h2d_w

这里 sum_i 同时包含 CPU offload activation 和 NVMe offload activation。

大白话:NVMe activation 也不是凭空到固态上的。当前主线是 lazy_nvme_mode=gds,也就是 cuFile/GPUDirect Storage API 直接在 GPU HBM 和 NVMe 文件之间搬数据;它仍然走 PCIe fabric,所以必须和参数 H2D、梯度 D2H 共享同一个 PCIe 资源池。cpu_staging 只保留为显式 fallback,不是 NVMe 版默认路径。

  1. NVMe 读写资源池约束
sum_i nvme_write_{i,w} <= nvme_write_capacity_w + exposed_nvme_write_w
sum_i nvme_read_{i,w}  <= nvme_read_capacity_w  + exposed_nvme_read_w

大白话:固态盘自己也有读写带宽上限。即使 PCIe 有空,NVMe 读写太慢也会暴露成等待时间。

目标函数

minimize
  recompute_cost
+ exposed_pcie_d2h_time
+ exposed_pcie_h2d_time
+ exposed_nvme_write_time
+ exposed_nvme_read_time
+ hidden_nvme_tiebreak
+ tiny_checkpoint_penalty

大白话:目标不是“尽量多 offload”,而是“GPU 尽量不等”。能藏在 forward/backward 计算窗口里的通信不算坏事;藏不住、让 GPU 等的通信才进入惩罚。NVMe 比 CPU 慢,所以只有当 GPU/CPU 都不适合放时,才会把更冷的 activation 放到 NVMe。

hidden_nvme_tiebreak 是一个很小的二级目标:当两个解在重算成本、暴露通信、 checkpoint 数量上打平时,优先选更少 NVMe checkpoint 的解。它不是主惩罚项, 不会因为“通信有代价”就把 offload 禁掉;它只解决“所有通信都能藏住时,SCIP 对 CPU 和 NVMe 任意选”的问题。

2. 本次 Qwen3.6-27B / batch 72 求解设置(默认 v2)

.venv/bin/python3 scripts/solve_lazy_train_schedule.py \
  --model /data1/dataarc/models/Qwen3.6-27B \
  --output schedules/qwen3_6_27b_metamathqa_b72_lazy_peak70_nvme_cpu14_pcie_nvme_pool.json \
  --batch-size 72 \
  --max-seq-len 1024 \
  --dtype-bytes 2 \
  --peak-cap-gb 70 \
  --baseline-peak-gb 60.4 \
  --baseline-checkpoint-interval 4 \
  --max-segment-layers 4 \
  --cpu-activation-budget-gb 14 \
  --enable-nvme-offload \
  --nvme-activation-budget-gb 32 \
  --nvme-transfer-mode gds \
  --layer-recompute-ms 20 \
  --enable-overlap-aware-offload \
  --enable-overlap-aware-nvme \
  --activation-comm-capacity-fraction 1.0 \
  --activation-comm-window-ms-scale 5.0 \
  --nvme-comm-capacity-fraction 1.0 \
  --nvme-comm-window-ms-scale 5.0 \
  --activation-h2d-bandwidth-gbps 12 \
  --activation-d2h-bandwidth-gbps 12 \
  --nvme-read-bandwidth-gbps 2.83 \
  --nvme-write-bandwidth-gbps 3.35 \
  --include-param-grad-pcie \
  --optimizer-state-mode hybrid_8bit_adam \
  --optimizer-max-8bit-fraction 0.02 \
  --optimizer-alpha-step-sec 119.18 \
  --optimizer-fast-grad-clip-saving-sec 18 \
  --optimizer-full-8bit-extra-step-sec 123 \
  --hidden-nvme-offload-tiebreak-ms 0.001 \
  --checkpoint-penalty 1e-5 \
  --time-limit-sec 60 \
  --trust-remote-code

3. 求解结果

指标 结果
Model /data1/dataarc/models/Qwen3.6-27B
Batch / Seq len 72 / 1024
Modeled GPU peak cap 70.00GB
GPU activation budget 21.553GB
CPU activation budget 14GB
NVMe activation budget 32GB
NVMe transfer mode gds
NVMe PCIe path gpu_nvme_direct_dma
NVMe read/write bandwidth used by solver 2.83GB/s read / 3.35GB/s write
Num layers 64
Num checkpoints 52
GPU checkpoints 30
CPU offload checkpoints 19
NVMe offload checkpoints 3
Num recompute blocks 51
Objective 182.00352
Exposed PCIe activation comm 0.0ms
Exposed NVMe comm 0.0ms
Mandatory param H2D window total 39.376GB
Mandatory grad D2H window total 39.376GB
Optimizer mode hybrid_8bit_adam
Optimizer 8-bit fraction 0.02
Hidden NVMe tie-breaker 0.001ms * 3 checkpoints = 0.003ms

大白话:在这个建模假设下,NVMe 本身不是比 CPU RAM 更快的层,而是为了在 CPU activation 空间被限制时仍然能维持 peak70 的 checkpoint 结构。v1 把 CPU activation budget 压到 8GB,求解器被迫放了 11 个 NVMe checkpoint;v2 把 CPU activation budget 放宽到 14GB,仍低于 hybrid8bit 的 CPU RSS 轮廓,同时把 NVMe checkpoint 降到 3 个。实测收益预期来自“保留 CPU 内存节省,但减少慢 NVMe 读回”。

3.1 当前资源与放置摘要

物理资源

资源 当前机器/路径
GPU3 显存 81.6GB 总量;full run initial eval 中约 26.5GB 已用
GPU3 train smoke peak 69.54GB
CPU 内存 2.0TiB 总量;查询时 used 165GiB,available 1.7TiB
/data2 NVMe 7.0T 总量;查询时可用约 707G
/data1 NVMe 7.0T 总量;查询时可用约 1.3T

这里的“当前”是 lazytrain_qwen36_b72_lazy_peak70_nvme_gpu3_gds_full_20260714_163407 启动后、initial eval 阶段的观测值。完整训练 step 的显存峰值以 smoke 里的 69.54GB 为准。

MIP 建模预算

项 当前设置
GPU peak cap 70GB
GPU activation budget 21.553GB
CPU activation budget 14GB
NVMe activation budget 32GB
单个 activation checkpoint 约 0.703GiB
NVMe mode gds
NVMe path gpu_nvme_direct_dma

通信速度

通信 建模/实测速度
GPU -> CPU D2H 12GB/s
CPU -> GPU H2D 12GB/s
GPU -> NVMe write 3.35GB/s,来自 GPU3 GDS 短测约 3.115GiB/s
NVMe -> GPU read 2.83GB/s,来自 GPU3 GDS 短测约 2.638GiB/s

换成每个 activation checkpoint 的传输时间:

方向 时间
GPU -> CPU 62.9ms
CPU -> GPU 62.9ms
GPU -> NVMe 225.4ms
NVMe -> GPU 266.8ms

为什么同样走 PCIe,CPU-GPU 比 NVMe-GPU 快

大白话:PCIe 只是“公路”,不代表每辆车都能跑一样快。GPU 和 CPU 内存通信时,另一端是 DRAM;GPU 和 NVMe 通信时,另一端是 SSD。DRAM 本身带宽很高,通常瓶颈主要在 GPU 到主机侧的 PCIe/NUMA 路径;NVMe 除了走 PCIe,还会被 SSD 控制器、闪存介质、队列深度、文件系统、块大小、 对齐方式和 GDS/kernel 配置继续限制。

更具体地说:

  1. CPU RAM 不是慢盘。Pinned CPU memory 到 GPU 的 H2D/D2H 通常由 GPU copy engine 通过 PCIe DMA 完成,主机 DRAM 带宽远高于单块 SSD,所以我们这里按 12GB/s 建模是保守的 PCIe/NUMA 有效带宽。
  2. NVMe 虽然也插在 PCIe 上,但一块 SSD 常见是 PCIe x4 设备,不是 GPU 那种 x16 端点;即使 PCIe fabric 有空,SSD 自己的连续读写、随机 IO、写放大和控制器调度 也会限制吞吐。
  3. GPUDirect Storage 的“direct”不是 GPU 和 SSD 之间有一根神奇直连线。它仍然要经过 PCIe root complex/switch,并且依赖 nvidia-fs、IOMMU/ACS、NVMe 驱动等系统配置。 当前机器上 gdscheck -p 仍显示 NVMe: Unsupported 和 properties.use_compat_mode: true,所以我们采用 cuFile/GDS API direct tensor 路径,但性能要以实测为准。
  4. 本机 GPU3 短测里,GPU-NVMe write/read 只有约 3.115/2.638GiB/s,明显低于 CPU-GPU 的 12GB/s 建模带宽。因此 NVMe 在当前版本里是“容量兜底层”,不是 “更快的 offload 层”。

建模含义:NVMe activation 既要占用 PCIe fabric,又要占用 NVMe 自己的读写资源池。 所以 MIP 会优先把热 checkpoint 留 GPU,把中等热 checkpoint 放 CPU,只有更冷、 且读写能藏进计算窗口的 checkpoint 才放 NVMe。

PCIe pool 的实际上限

这里有三个不同口径,不能混在一起:

口径 当前值 含义
GPU3 硬件 PCIe 链路 PCIe Gen5 x16 纸面约 63GB/s 单方向,约 126GB/s 双向 aggregate
MIP 里的 CPU-GPU PCIe pool 12GB/s H2D + 12GB/s D2H 当前用来建模 activation、参数 H2D、梯度 D2H 的端到端有效带宽
MIP 里的 GPU-NVMe 端点带宽 3.35GB/s write,2.83GB/s read 当前 GPU3 GDS 短测校准后的 NVMe 读写瓶颈

大白话:机器的“PCIe 公路”理论上很宽,GPU3 当前链路是 Gen5 x16,单方向纸面 上限大约 63GB/s。但求解器真正能拿来隐藏通信的,不应该直接填纸面值,而应该填 训练路径里可持续的端到端带宽。所以 CPU-GPU offload 当前按 12GB/s 每方向建模; NVMe offload 虽然也走 PCIe fabric,但还要过 SSD 端点,因此有效上限被 NVMe 读写带宽压到 2.83-3.35GB/s。

在当前 schedule 里,activation_comm_capacity_fraction=1.0、 activation_comm_window_ms_scale=5.0,换算后所有窗口加起来的 CPU-GPU PCIe 容量约 55.43GiB 每方向;参数 H2D 和梯度 D2H 的 mandatory 通信各约 39.38GiB。因此 activation offload 不是占一个无限池子,而是在剩余窗口容量里 和参数/梯度搬运一起排队。

注意:这里的 PCIe pool 是当前 GPU3 单卡训练 rank 的 pool,不是整台 8 卡服务器 共享的总 pool。63GB/s 是 GPU3 这条 PCIe Gen5 x16 链路的单方向纸面上限; 55.43GiB 是这个单卡 schedule 在所有 overlap 窗口里累计出来的每方向容量。 如果以后做 8 卡 FSDP/ZeRO,需要把通信建模扩展成多级资源池:每张 GPU 自己的 PCIe link pool、同一 CPU socket/root complex 下的共享 host pool、NVMe 设备池, 以及 GPU-GPU 的 NVLink/NVSwitch pool。不能简单把单卡 pool 乘以 8 当成一个 全局可用池子。

放置结果

GPU 保留 checkpoint boundaries:

[0, 18, 19, 20, 23, 25, 26, 30, 31, 32, 34, 35, 37, 40, 41, 44, 45, 48, 49, 50, 51, 52, 56, 57, 58, 59, 60, 62, 63, 64]

CPU 内存 offload:

[1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 21, 24, 27, 28, 33, 36]

NVMe offload:

[3, 4, 16]

大白话:热的、马上要用的 checkpoint 留 GPU;中等热的放 CPU;更冷的 3 个 checkpoint 放 NVMe。NVMe 比 CPU 慢很多,所以它不是靠“盘比内存快”赢,而是在 GPU/CPU activation 空间都受限时,把冷 activation 放到第三层存储,同时尽量把读写藏进计算窗口里。当前 MIP 结果里 exposed PCIe/NVMe comm 都是 0.0ms。

4. 和前三个版本的关系

版本 核心差异
baseline 固定 checkpoint interval,不做 PySCIPOpt activation 调度
alpha 在 GPU/CPU/recompute 之间调度 activation,约束 peak <= 70GB
8bit_optim 继承 alpha schedule,优化 CPU optimizer/grad clip 路径
nvme 继承 8bit_optim 训练设置,把 activation 放置扩展到 GPU/CPU/NVMe/recompute

所以 nvme 是第四版,不是替代 8bit_optim;它是在第三版基础上把第三层存储纳入 MIP。

5. 当前实现边界

  1. MIP 已经显式包含 NVMe 容量约束、NVMe read/write bandwidth 约束,以及 NVMe activation 对 PCIe 资源池的占用。
  2. Runtime 已经能用 cuFile 执行 NVMe activation spill:GPU tensor -> NVMe raw file,反向时 NVMe raw file -> GPU tensor。
  3. YAML 里 memory.lazy_nvme_mode: "gds" 是默认主线;"cpu_staging" 只作为显式 fallback。
  4. 如果 schedule 里有 offload_nvme 但 YAML 没有设置 memory.lazy_nvme_offload_dir,训练会直接报错,避免显存约束失真。
  5. 本机 GPU3 上的短探针结果:gdsio -x 0 64MiB write 约 3.115GiB/s,read 约 2.638GiB/s;Python cuFile 1MiB CUDA tensor roundtrip 测试通过。
  6. 环境探针也要诚实记录:gdscheck -p 显示 GPU 支持 GDS,但 NVMe: Unsupported 且 properties.use_compat_mode: true。因此 runtime 用的是 cuFile/GDS API direct tensor 路径,但底层是否完全绕开兼容模式,要以后结合 nvidia-fs/系统配置继续确认。
  7. GDS runtime 需要处理 CUDA stream 生命周期:GDS read 前必须等 compute_stream 完成,读回的 CUDA tensor 还要 record_stream(ctx.compute_stream)。否则 cuFile 文件本身可能是好的,但读回 buffer 会被上一段 backward 的 CUDA 内存复用踩踏,表现为 activation 写前 finite、读后出现 1e+24 级别坏值,最终梯度 NaN。
  8. lazy_nvme_prefetch_distance 默认改为 0。prefetch2 的 ctx future 生命周期 bug 修复后短跑可用,但 GPU3 full probe 在 step6 又出现 grad_norm=nan,因此当前默认回到保守同步 GDS read 路径;prefetch2 只保留为后续 profiling/修复路径。
  9. 启动脚本默认使用带 RUN_STAMP 的独立 NVMe offload 目录,并在退出时清理本次 run 的整个 offload 子目录,避免实验越跑越堆。启动前还会清理 24 小时以上、且没有活跃 PID 文件的旧 run 目录;预清理会显式排除本次 run 目录,防止刚创建、还没写入 activation 文件的活跃目录被误删。清理动作由 scripts/cleanup_lazy_nvme_offload.py 完成,只允许删除 nvme_offload 根目录的直接子目录,不扫描全盘、不跟随 symlink。
  10. 这个版本适合研究“CPU 内存也紧张时如何把冷 activation 放到 SSD”;对当前 27B 单卡主实验来说,是否更快要看实测,不能只看 MIP objective 下结论。

5.1 2026-07-14 GDS 修复与 smoke 结果

修复的问题

早期 GDS smoke 的 loss 是 finite,但 grad_norm 是 NaN/Inf。诊断方式是在 GDS 写前和读后记录每个 NVMe checkpoint 的 finite / absmax / sample_mean_abs:

  • 写前所有 NVMe boundary 都 finite,absmax 大约在 82-452。
  • 读后多个 boundary 出现巨大坏值,例如 1e+24 量级,后续梯度 NaN。

根因不是 optimizer,也不是 CPUAdam;而是 GDS read 的 CUDA 目标 buffer 与训练 compute_stream 的内存生命周期没有对齐。修复后:

  • infinity/nvme_gds.py:GDS helper 内部设置 CUDA device,并串行化 cuFile buffer register/read/write/deregister,避免 Python worker thread 的 CUDA context/cuFile 并发问题。
  • infinity/model/cpu_master.py:GDS materialize 在 read_tensor_gds 前执行 ctx.compute_stream.synchronize(),读回后对 CUDA tensor 执行 record_stream(ctx.compute_stream);同时 materialize 后清掉 ctx.activation_nvme_prefetch_futures[boundary],避免后续 block/step 复用旧 prefetch future。
  • smoke config 不再导出 final checkpoint,避免每次 1-step/5-step 诊断额外保存 27B 模型。

大白话:之前像是“盘里写坏了”,但更准确地说,是“读盘读到了一个 GPU 内存块,而这个内存块还可能被上一段 CUDA 计算/缓存分配器继续碰”。等 compute stream 完成并记录后续使用后,读回 activation 和写前 stats 完全一致。

Correctness smoke

20260714_gds_streamsync_debug 的 1-step debug smoke:

指标 结果
GDS write/read stats 11 个 NVMe boundary 写前/读后 finite=True,absmax/sample_mean_abs 一致
grad_norm 217.9454,finite
optimizer_step executed
GPU peak 69.54GB
CPU RSS 343.46GB

5-step 性能 smoke

20260714_gds_streamsync_5step,batch=72,seq=1024,GPU3:

Step Latency Tokens/s TFLOPS FWD BWD Grad clip Optim step GPU peak CPU RSS
1 89.07s 827.8 133.8 9.71s 29.12s 6.70s 39.64s 69.54GB 343.45GB
2 54.27s 1358.5 219.6 7.21s 28.54s 6.70s 8.05s 69.54GB 343.51GB
3 54.24s 1359.3 219.7 7.25s 28.48s 6.71s 8.04s 69.54GB 343.52GB
4 54.54s 1351.8 218.5 7.22s 28.48s 6.95s 8.07s 69.54GB 343.51GB
5 53.82s 1369.8 221.4 7.21s 28.47s 6.23s 8.10s 69.54GB 343.53GB

后 3 步平均:

版本 Step latency Tokens/s TFLOPS GPU peak CPU RSS
hybrid8bit full run last100 55.14s 1340.3 216.7 68.84GB 354.8-360.2GB
nvme GDS stream-sync smoke last3 54.20s 1360.3 219.9 69.54GB 343.5GB

解释:NVMe 版 backward 比 hybrid8bit 慢,因为多了 NVMe activation 读回;但 CPU RSS 下降后,CPU optimizer step 从 hybrid8bit 稳定段约 11.43s 降到约 8.07s,zero_grad 也更低,所以净效果略快。这个收益不大,但符合“减少 CPU 内存压力,减少 CPU optimizer 阻塞”的方向。

Legacy CPU8/NVMe11 full run 中间结果

这个 full run 是旧 CPU8 / NVMe11 schedule,用来做历史对照;当前默认 NVMe 入口已经切到 CPU14 / NVMe3 v2。

  • W&B run: https://wandb.ai/wuxiaojun/LazyTrain/runs/3sedt8i5
  • Log: logs/qwen3_6_27b_metamathqa_lazy_peak70_nvme_b72_gpu3_20260714_gds_streamsync_full.log
  • Metrics: metrics/qwen3_6_27b_metamathqa_lazy_peak70_nvme_b72_gpu3_20260714_gds_streamsync_full.jsonl

截至 2026-07-14 21:58,full run 已完成 initial eval 和前 110 个训练 step:

指标 结果
Initial eval step 0
Initial eval samples 11850
Initial eval loss / ppl 1.8878 / 6.6049
Initial eval time 1255.76s
Current train step 110 / 3841
Non-finite grad steps 0
Skipped optimizer steps 0
GPU peak 69.54GB
CPU RSS max 343.70GB

跳过冷启动 step 1 后,关键窗口平均值:

口径 Step latency Tokens/s TFLOPS FWD BWD Grad clip Optim step Zero grad
nvme-v1 full run step 2-64 54.28s 1358.6 219.60 7.23s 28.76s 6.31s 8.10s 2.72s
nvme-v1 full run last30 56.30s 1311.4 211.97 7.23s 29.23s 6.50s 8.28s 3.90s
hybrid8bit full run last100 55.14s 1340.3 216.66 7.04s 25.72s 6.30s 11.43s 3.55s

对比 hybrid8bit last100:

  • 早期 step 2-64 latency 改善约 1.57%,tokens/s 和 TFLOPS 提升约 1.34%。
  • 最近 last30 已经慢于 hybrid8bit last100,这说明 v1 的 11 个 NVMe checkpoint 对 backward 的拖累不能忽略,不能只拿早期窗口下结论。
  • CPU RSS 降低约 16.55GB。

这里的结构仍然符合预期:NVMe 版 backward 更慢,但 optimizer step 更快。v2 的目标就是 保留 CPU RSS/optimizer 优势,同时把 NVMe checkpoint 从 11 个降到 3 个,减少 backward 读盘等待。

5.2 2026-07-14 NVMe-v2 / CPU14 更新

为什么要从 CPU8/NVMe11 改到 CPU14/NVMe3

原 CPU8/NVMe11 版本已经能稳定训练,但 metrics 显示主要瓶颈在 backward:

口径 Step latency Tokens/s TFLOPS FWD BWD Optim step CPU RSS
nvme-v1 step 2-64 54.28s 1358.6 219.60 7.23s 28.76s 8.10s 343.63GB
hybrid8bit all train 54.38s 1356.6 219.29 7.08s 25.92s 10.09s 354.60GB
hybrid8bit last30 55.41s 1330.7 215.10 7.08s 25.91s 11.44s 354.72GB

大白话:v1 比 hybrid8bit 省 CPU 内存,optimizer step 也更短;但它把 11 个 checkpoint 放到 NVMe,backward 要多等 NVMe 读回,所以收益很窄。v2 的想法是保留 CPU 内存优势, 但把 CPU activation budget 从 8GB 放宽到 14GB,让慢 NVMe checkpoint 数从 11 降到 3。

CPU budget sweep

在相同 peak70、相同 batch=72、相同 PCIe/NVMe 带宽下,CPU activation budget sweep:

Schedule CPU activation budget GPU ckpt CPU ckpt NVMe ckpt Recompute Objective
CPU8 legacy 8GB 30 11 11 13 182.00052
CPU10 10GB 30 14 8 13 182.00052
CPU12 12GB 30 17 5 13 182.00052
CPU14 v2 14GB 30 19 3 13 182.00352
CPU15 15GB 30 21 1 13 182.00052

为什么 v2 选 CPU14 而不是 CPU15:CPU15 的 NVMe 更少,但 CPU RSS 会进一步靠近 hybrid8bit;CPU14 已经把 NVMe 降到 3 个,同时 smoke 里 CPU RSS 约 351.40GB, 仍比 hybrid8bit 稳定段 354.7GB+ 低。它是当前“少 NVMe”和“仍省 CPU 内存”的折中点。

182.00352 比 182.00052 多出来的 0.003ms 不是性能退化,而是 0.001ms * 3 的隐藏 NVMe 二级 tie-breaker。主目标仍然是重算成本和暴露通信,二级目标只在主目标 打平时偏向少用 NVMe。

NVMe-v2 smoke

20260714_nvme_v2_cpu14_smoke_gpu2,batch=72,seq=1024,GPU2,1 step:

指标 结果
Schedule qwen3_6_27b_metamathqa_b72_lazy_peak70_nvme_cpu14_pcie_nvme_pool.json
Placement 30 GPU / 19 CPU / 3 NVMe / 13 recompute
Loss / grad_norm 1.7794 / 217.9471,均 finite
skipped optimizer step 0
GPU peak / CPU RSS 68.84GB / 351.40GB
FWD / BWD 12.81s / 27.78s
Optimizer step 45.90s,冷启动且与 GPU3 full run 的 CPUAdam 并发,不能作为吞吐结论

这个 smoke 只证明 v2 路径能跑通、梯度 finite、runner override 确实加载了 CPU14 schedule。 正式吞吐需要在没有另一个 CPUAdam 长跑竞争 CPU/内存带宽时启动 isolated full run。

NVMe-v2 isolated full run 早期结果

已停止旧 nvme-v1 CPU8/NVMe11 tmux 后,在 GPU2 启动 isolated v2 full run:

  • tmux: lazytrain_qwen36_b72_lazy_peak70_nvme_v2_cpu14_gpu2_full
  • W&B run: https://wandb.ai/wuxiaojun/LazyTrain/runs/z73uudvk
  • Log: logs/qwen3_6_27b_metamathqa_lazy_peak70_nvme_v2_cpu14_b72_gpu2_full.log
  • Metrics: metrics/qwen3_6_27b_metamathqa_lazy_peak70_nvme_v2_cpu14_b72_gpu2_full.jsonl

启动口径:

项 值
GPU 2
Batch / seq len 72 / 1024
Schedule qwen3_6_27b_metamathqa_b72_lazy_peak70_nvme_cpu14_pcie_nvme_pool.json
Placement 30 GPU / 19 CPU / 3 NVMe / 13 recompute
W&B project LazyTrain
Initial eval 11850 test samples before training
Periodic eval 10000 test samples every 384 steps

Initial eval 和 step 2-10 早期稳定窗口:

指标 结果
Initial eval loss / ppl 1.8878 / 6.6049
Initial eval time 1254.13s
Current train step 10 / 3841
Non-finite grad steps 0
Skipped optimizer steps 0
GPU peak / CPU RSS avg 68.84GB / 351.59GB
Step 2-10 latency 53.276s
Step 2-10 tokens/s 1383.9
Step 2-10 TFLOPS 223.70
Step 2-10 FWD / BWD 7.14s / 26.68s
Step 2-10 optimizer step 8.18s

和对照版本的同口径早期/全局窗口对比:

版本/窗口 Step latency Tokens/s TFLOPS BWD Optim step CPU RSS
nvme-v2 step 2-10 53.28s 1383.9 223.70 26.68s 8.18s 351.59GB
hybrid8bit all train 54.38s 1356.6 219.29 25.92s 10.09s 354.60GB
hybrid8bit last100 55.36s 1331.8 215.28 25.91s 11.43s 354.71GB
hybrid8bit step 2-64 52.93s 1393.1 225.19 25.95s 8.37s 354.49GB
nvme-v1 step 2-64 54.28s 1358.6 219.60 28.76s 8.10s 343.63GB

大白话:v2 已经解决了 v1 backward 太慢的问题,BWD 从 v1 的 28.76s 降到 26.68s,接近 hybrid8bit 的 25.9s;同时 optimizer step 仍保持 8.18s 左右,比 hybrid8bit 后期 11.43s 更短。早期窗口已经优于 hybrid8bit 全局均值和 last100,但还没有跑到足够长的窗口,所以暂时不能把 full-run 结论写死。

5.3 2026-07-14 NVMe-v3 / refhybrid-path 更新

为什么要继续从 v2 改到 v3

v2 的早期窗口已经比 hybrid8bit 的全局和后期窗口快,但同口径早期窗口仍略慢:

版本/窗口 Step latency Tokens/s TFLOPS BWD
hybrid8bit step 2-64 52.93s 1393.1 225.19 25.95s
nvme-v2 step 2-26 53.28s 1383.8 223.69 26.68s

根因不是 optimizer,v2 的 optimizer step 反而更快;主要差在 backward。v2 的 MIP 在主目标上和 hybrid8bit 等价,但它换了具体的 recompute block 和 GPU checkpoint placement。大白话:数学目标觉得“差不多”,真实机器上却把 backward 路径换慢了一点。

v3 的修正是:

  1. 锁住 hybrid8bit 已经实测更快的 selected_edges,也就是 backward recompute block path。
  2. 锁住 hybrid8bit 的 GPU checkpoint 集合,避免为了 NVMe 分层把 GPU/CPU placement 换慢。
  3. 只让 PySCIPOpt 在原 CPU checkpoint 集合中选择 19 个继续放 CPU、3 个 spill 到 NVMe。
  4. 当时尝试打开 lazy_nvme_prefetch_distance: 2。ctx prefetch future 生命周期修复后, GPU3 one-step smoke 恢复 finite gradient;但后续更长 probe 证明 prefetch2 仍不够稳, 默认入口已经回退为 NVME_PREFETCH_DISTANCE=0。

这不是手写 schedule。它仍然是 PySCIPOpt 解,只是增加了两个从真实 profiling 得到的硬约束: “不要改已经证明快的 recompute/GPU path”。然后求解器继续负责第三层存储分配。

v3 求解命令

.venv/bin/python3 scripts/solve_lazy_train_schedule.py \
  --model /data1/dataarc/models/Qwen3.6-27B \
  --output schedules/qwen3_6_27b_metamathqa_b72_lazy_peak70_nvme_cpu14_refhybrid_pcie_nvme_pool.json \
  --batch-size 72 \
  --max-seq-len 1024 \
  --dtype-bytes 2 \
  --peak-cap-gb 70 \
  --baseline-peak-gb 60.4 \
  --baseline-checkpoint-interval 4 \
  --max-segment-layers 4 \
  --cpu-activation-budget-gb 14 \
  --enable-nvme-offload \
  --nvme-activation-budget-gb 32 \
  --nvme-transfer-mode gds \
  --layer-recompute-ms 20 \
  --enable-overlap-aware-offload \
  --enable-overlap-aware-nvme \
  --activation-comm-capacity-fraction 1.0 \
  --activation-comm-window-ms-scale 5.0 \
  --nvme-comm-capacity-fraction 1.0 \
  --nvme-comm-window-ms-scale 5.0 \
  --activation-h2d-bandwidth-gbps 12 \
  --activation-d2h-bandwidth-gbps 12 \
  --nvme-read-bandwidth-gbps 2.83 \
  --nvme-write-bandwidth-gbps 3.35 \
  --include-param-grad-pcie \
  --optimizer-state-mode hybrid_8bit_adam \
  --optimizer-max-8bit-fraction 0.02 \
  --optimizer-alpha-step-sec 119.18 \
  --optimizer-fast-grad-clip-saving-sec 18 \
  --optimizer-full-8bit-extra-step-sec 123 \
  --hidden-nvme-offload-tiebreak-ms 0.001 \
  --reference-schedule schedules/qwen3_6_27b_metamathqa_b72_lazy_peak70_hybrid8bit_pcie_pool.json \
  --lock-reference-edges \
  --lock-reference-gpu-checkpoints \
  --checkpoint-penalty 1e-5 \
  --time-limit-sec 60 \
  --trust-remote-code

v3 求解结果

指标 v3 结果
Schedule qwen3_6_27b_metamathqa_b72_lazy_peak70_nvme_cpu14_refhybrid_pcie_nvme_pool.json
Placement 30 GPU / 19 CPU / 3 NVMe / 13 recompute
NVMe boundaries [1, 3, 31]
Checkpoints / selected_edges 和 hybrid8bit 完全一致
GPU checkpoint set 和 hybrid8bit 完全一致
Recompute boundaries [6, 17, 18, 19, 25, 37, 45, 46, 47, 53, 54, 55, 61]
Objective 182.00352
Modeled peak 70.00GB
Exposed activation PCIe comm 0.0ms
Exposed NVMe comm 0.0ms
Runtime prefetch 历史 v3 probe 使用 lazy_nvme_prefetch_distance: 2;当前默认已回退到 0

理论预期:v3 的 backward 应该更接近 hybrid8bit,因为 block path 和 GPU checkpoint 完全相同;同时 CPU RSS 应该接近 v2,因为仍然只有 19 个 CPU activation checkpoint。 它仍有 3 个 NVMe read 的同步等待,所以 full-run 速度需要实测;但它至少避免了 v2 换慢 backward path 的问题,是当前最合理的安全版本。

v3 smoke 结论

GPU3 上做了三次 one-step smoke:

口径 结果
v3 + prefetch2(修复前) loss finite,但 grad_norm=nan,optimizer step 被跳过
v3 + prefetch0 loss finite,grad finite,optimizer step 正常执行
v3 + prefetch0 GPU peak / CPU RSS 68.84GB / 351.40GB
v3 + prefetch0 FWD / BWD 11.89s / 27.32s,冷启动且和 GPU2 v2 full run 并发,不作为正式吞吐
v3 + prefetch2(ctx future cleanup 后) loss finite,grad_norm=6151907840.0,optimizer step 正常执行
v3 + prefetch2 GPU peak / CPU RSS 68.84GB / 351.46GB
v3 + prefetch2 FWD / BWD 12.11s / 27.21s,one-step cold smoke,不作为正式吞吐

当时的结论是可以重新尝试 prefetch2。更新后的结论更保守:schedule 本身没坏, 但“提前读盘把通信藏进 backward compute window”的 runtime 语义仍有风险; 默认入口使用 prefetch0,prefetch2 留作后续修复和 profiling。

6. v4:CPU15 / NVMe1 refhybrid 默认方案

为什么从 CPU14/NVMe3 改成 CPU15/NVMe1

CPU14/refhybrid 的求解器设置把 CPU activation budget 压到 14GB。每个 Qwen3.6-27B、batch=72、seq=1024 的 activation checkpoint 大约是 0.703GiB,所以 14GB 只能放下 19 个 CPU checkpoint;hybrid8bit 原本有 22 个 CPU checkpoint,于是求解器被迫把 3 个 checkpoint 放到 NVMe。实测说明这太激进:GPU2 full run 到 step 39 时已经出现 5 次 nonfinite/skipped optimizer step,non-skip 窗口也只有约 54.1s / 220 TFLOPS, 低于 hybrid8bit 的 52.93s / 225.19 TFLOPS。

v4 把 CPU activation budget 提到 15GB。这不是放弃 NVMe,而是让 NVMe 回到“第三层兜底存储”的角色:CPU 可以放 21 个 checkpoint,只剩 1 个 checkpoint 需要 NVMe。大白话:别为了证明有 NVMe 就强行用 3 次盘;盘应该在 CPU 真放不下时才出场。

v4 求解命令

.venv/bin/python3 scripts/solve_lazy_train_schedule.py \
  --model /data1/dataarc/models/Qwen3.6-27B \
  --output schedules/qwen3_6_27b_metamathqa_b72_lazy_peak70_nvme_cpu15_refhybrid_pcie_nvme_pool.json \
  --batch-size 72 \
  --max-seq-len 1024 \
  --dtype-bytes 2 \
  --peak-cap-gb 70 \
  --baseline-peak-gb 60.4 \
  --baseline-checkpoint-interval 4 \
  --max-segment-layers 4 \
  --cpu-activation-budget-gb 15 \
  --enable-nvme-offload \
  --nvme-activation-budget-gb 32 \
  --nvme-transfer-mode gds \
  --layer-recompute-ms 20 \
  --enable-overlap-aware-offload \
  --enable-overlap-aware-nvme \
  --activation-comm-capacity-fraction 1.0 \
  --activation-comm-window-ms-scale 5.0 \
  --nvme-comm-capacity-fraction 1.0 \
  --nvme-comm-window-ms-scale 5.0 \
  --activation-h2d-bandwidth-gbps 12 \
  --activation-d2h-bandwidth-gbps 12 \
  --nvme-read-bandwidth-gbps 2.83 \
  --nvme-write-bandwidth-gbps 3.35 \
  --include-param-grad-pcie \
  --optimizer-state-mode hybrid_8bit_adam \
  --optimizer-max-8bit-fraction 0.02 \
  --optimizer-alpha-step-sec 119.18 \
  --optimizer-fast-grad-clip-saving-sec 18 \
  --optimizer-full-8bit-extra-step-sec 123 \
  --hidden-nvme-offload-tiebreak-ms 0.001 \
  --reference-schedule schedules/qwen3_6_27b_metamathqa_b72_lazy_peak70_hybrid8bit_pcie_pool.json \
  --lock-reference-edges \
  --lock-reference-gpu-checkpoints \
  --checkpoint-penalty 1e-5 \
  --time-limit-sec 60 \
  --trust-remote-code

v4 求解结果与 smoke 结果

指标 v4 结果
GPU checkpoint 30
CPU offload checkpoint 21
NVMe offload checkpoint 1,boundary 21
Recompute boundary 13
Modeled peak cap 70GB
Exposed activation / NVMe comm 0.0ms / 0.0ms
Runtime prefetch v4/v5 probe 曾测试 lazy_nvme_prefetch_distance: 2;当前默认为 0

GPU2 上的 3-step smoke:

口径 结果
step 1 cold step,89.94s,optimizer 初始化占 39.73s
step 2 52.94s,225.13 TFLOPS,1392.7 tokens/s
step 3 52.85s,225.49 TFLOPS,1395.0 tokens/s
step 2-3 average 52.90s,225.31 TFLOPS,1393.9 tokens/s
skipped optimizer step 0
peak GPU / CPU RSS 68.84GB / 353.39GB

结论:v4 短跑已经和 hybrid8bit 早期窗口对齐,并略高于 hybrid8bit 的 225.19 TFLOPS。它牺牲了约 +2GB CPU RSS,相比 CPU14/NVMe3 少用两次 NVMe,从而把 NVMe 通信风险压低。

staging buffer 实验结论

尝试过把 GDS 写入的 tensor.clone() 改成可复用 GPU staging buffer,但默认不启用。 原因是 staging pool 会长期持有 3 * 720MB 左右的 GPU buffer,使 peak 从 68.84GB 抬到约 70.95GB,越过 peak70 口径;短跑还出现了 nonfinite gradient。代码里保留了 memory.lazy_nvme_gds_staging: true 的实验开关和单测, 但默认 NVMe/v5 继续使用稳定的 clone 路径。

7. v5:GDS prefetch0 默认 runtime

v5 不改变 PySCIPOpt 的调度解,仍然是 30 GPU / 21 CPU / 1 NVMe, NVMe boundary 仍为 21。它改的是 runtime 默认:主线使用 lazy_nvme_prefetch_distance: 0,即在 materialize 处同步读取 NVMe activation。

大白话:prefetch2 想把 NVMe read 藏进 backward compute window,但更长 probe 在 GPU3 的 step6 出现 grad_norm=nan,说明这条 overlap 路径还不够稳。 prefetch0 少一点 overlap,但 12-step smoke 和截至 step971 的 full run 都稳定, 并且同口径吞吐已经略高于 hybrid8bit。

代码与测试:

  • infinity/nvme_gds.py:read_tensor_gds() 保留返回前的 torch.cuda.synchronize(device),确保 GDS DMA 写入完成。
  • infinity/model/cpu_master.py:保留 prefetch2 代码和单测,但默认配置走 prefetch0 同步读回;prefetched CUDA tensor materialize 时仍保留 record_stream(ctx.compute_stream)。
  • 单测覆盖: tests/test_nvme_gds.py::test_gds_read_synchronizes_before_return 和 tests/test_lazy_checkpoint_runtime.py::test_lazy_nvme_gds_materialize_does_not_global_sync_prefetched_cuda_tensor。

关键 probe:

项 值
failed no-sync probe 76ve96pu,step2 出现 grad_norm=nan,已停止
prefetch2 5-step smoke 52.956s / 225.06 TFLOPS,step2-5,无 skip
prefetch2 GPU3 full probe step6 出现 grad_norm=nan,1 次 skipped optimizer step,已停止
prefetch0 12-step smoke metrics metrics/qwen3_6_27b_metamathqa_lazy_peak70_nvme_v5_cpu15_prefetch0_b72_gpu3_20260715_12step.jsonl
prefetch0 step2-12 52.736s / 225.99 TFLOPS / 1398.1 tokens/s,无 skip
prefetch0 step5-12 52.574s / 226.68 TFLOPS / 1402.4 tokens/s,无 skip
prefetch0 full run W&B https://wandb.ai/wuxiaojun/LazyTrain/runs/d11e3p1f
prefetch0 full run metrics metrics/qwen3_6_27b_metamathqa_lazy_peak70_nvme_v5_cpu15_refhybrid_gds_prefetch0_b72_gpu3_20260715_nvme_v5_prefetch0_full_gpu3.jsonl
prefetch0 full step2-64 52.790s / 225.77 TFLOPS / 1396.7 tokens/s,无 skip/nonfinite
prefetch0 full step2-100 52.761s / 225.89 TFLOPS / 1397.4 tokens/s,无 skip/nonfinite
prefetch0 full step2-108 52.766s / 225.87 TFLOPS / 1397.3 tokens/s,无 skip/nonfinite
prefetch0 full step2-971 52.819s / 225.65 TFLOPS / 1396.0 tokens/s,无 skip/nonfinite
prefetch0 full last100, step872-971 53.386s / 223.24 TFLOPS / 1381.1 tokens/s,无 skip/nonfinite
hybrid8bit reference step2-64 52.932s / 225.19 TFLOPS / 1393.1 tokens/s
hybrid8bit reference step2-100 52.941s / 225.16 TFLOPS / 1392.9 tokens/s
hybrid8bit reference step2-108 52.954s / 225.11 TFLOPS / 1392.6 tokens/s
hybrid8bit reference step2-971 53.333s / 223.66 TFLOPS / 1383.7 tokens/s
hybrid8bit reference step872-971 54.015s / 220.69 TFLOPS / 1365.3 tokens/s
hybrid8bit final last100 55.145s / 216.66 TFLOPS / 1340.3 tokens/s
prefetch0 vs hybrid8bit full step2-971 TFLOPS +0.89%;same-step last100 TFLOPS +1.16%;vs hybrid8bit final last100 TFLOPS +3.04%
NVMe eval loss step0 1.88781;step384 0.12880;step768 0.12625
hybrid8bit eval loss step0 1.88781;step384 0.12863;step768 0.12626
Peak GPU / CPU RSS 68.84GB / 354.57GB
NVMe cleanup smoke 结束后 nvme_offload 目录回到 4K;full run 活跃时只保留本 step 生命周期内的临时文件,最近一次检查为 8K

8. 下一步

  1. 继续让默认 prefetch0 v5 CPU15/NVMe1 full run 跑到 epoch end,观察最终 test eval loss / EM 是否继续和 8bit_optim 对齐。
  2. 用更大模型或更紧 CPU activation budget 做下一组实验。当前 27B 只用了 1 个 NVMe checkpoint,主要证明稳定和无明显吞吐损失;NVMe 的真正价值要在 alpha/8bit_optim 放不下、或必须大量重算的设置里体现。
  3. prefetch2 暂不作为默认;如果要重新打开,必须先修复 step6 grad_norm=nan 并用至少 12-step smoke 覆盖。
  4. 用 profiler 记录真实 NVMe read/write 时间,继续校准当前 2.83GB/s read / 3.35GB/s write 的短探针带宽。
  5. 如果要做严格 GDS direct 性能,需要让系统侧把 nvidia-fs/NVMe GDS 支持配置到 gdscheck 不再显示 NVMe unsupported。