对应版本: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
-
NVMe 版的主要价值是扩训练可行域,不是大幅提速。 大白话:NVMe 是 GPU HBM 和 CPU RAM 之外的第三层 activation 存储。它比 CPU RAM 慢,所以不会天然让 27B 这种已经能用 GPU+CPU 跑起来的设置快很多; 它真正有意义的场景是更大模型、更大 batch、更紧 CPU 内存预算,或者 alpha/8bit 方案为了省内存不得不大量重算时,NVMe 可以把一部分冷 activation 放到 SSD, 让训练仍然可行。
-
当前 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.12880vs0.12863,step768 为0.12625vs0.12626。这说明第四版不是靠 NVMe 提供显著速度提升,而是验证 “第三层存储可以被 MIP 纳入统一调度,并且在当前设置下不明显拖慢训练”。
对每个 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 boundarys到e是否组成一个 backward recompute block。d2h_{i,w}, h2d_{i,w}:boundaryi的 activation 在第w个计算窗口里占用多少 PCIe fabric 通信量;CPU offload 是 GPU<->CPU,NVMe GDS offload 是 GPU<->NVMe。nvme_write_{i,w}, nvme_read_{i,w}:boundaryi的 activation 在第w个计算窗口里占用多少 NVMe 设备读写量。exposed_*_w >= 0:如果某个窗口里的通信塞不进可 overlap 的计算窗口,暴露出来的通信量。
大白话:对每一层之间的中间结果,求解器要决定“留显存、放内存、写固态、还是到时候重算”。
- 唯一放置约束
checkpoint_i = gpu_i + cpu_i + nvme_i
大白话:一个 checkpoint 只能放在一个地方,不能又在 GPU 又在 CPU 又在 NVMe。
- 路径约束
edge_{s,e} 组成从 0 到 N 的一条合法路径
每个被选中的中间 checkpoint 有一条入边和一条出边
大白话:checkpoint 不能散着选,必须能把模型从第 0 层串到最后一层。
- GPU 显存硬约束
sum_i gpu_i * activation_bytes <= gpu_activation_budget_bytes
本次 NVMe 版本仍使用 peak <= 70GB。
- CPU activation 硬约束
sum_i cpu_i * activation_bytes <= cpu_activation_budget_bytes
当前默认 v2 设定 cpu_activation_budget = 14GB;旧 v1 为了强行压 CPU activation
空间曾使用 8GB,因此会产生更多 NVMe checkpoint。
- NVMe activation 硬约束
sum_i nvme_i * activation_bytes <= nvme_activation_budget_bytes
本次设定 nvme_activation_budget = 32GB。
- 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 版默认路径。
- 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 任意选”的问题。
.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| 指标 | 结果 |
|---|---|
| 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 读回”。
| 资源 | 当前机器/路径 |
|---|---|
| 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
为准。
| 项 | 当前设置 |
|---|---|
| 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 只是“公路”,不代表每辆车都能跑一样快。GPU 和 CPU 内存通信时,另一端是 DRAM;GPU 和 NVMe 通信时,另一端是 SSD。DRAM 本身带宽很高,通常瓶颈主要在 GPU 到主机侧的 PCIe/NUMA 路径;NVMe 除了走 PCIe,还会被 SSD 控制器、闪存介质、队列深度、文件系统、块大小、 对齐方式和 GDS/kernel 配置继续限制。
更具体地说:
- CPU RAM 不是慢盘。Pinned CPU memory 到 GPU 的 H2D/D2H 通常由 GPU copy engine
通过 PCIe DMA 完成,主机 DRAM 带宽远高于单块 SSD,所以我们这里按
12GB/s建模是保守的 PCIe/NUMA 有效带宽。 - NVMe 虽然也插在 PCIe 上,但一块 SSD 常见是 PCIe x4 设备,不是 GPU 那种 x16 端点;即使 PCIe fabric 有空,SSD 自己的连续读写、随机 IO、写放大和控制器调度 也会限制吞吐。
- 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 路径,但性能要以实测为准。 - 本机 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。
这里有三个不同口径,不能混在一起:
| 口径 | 当前值 | 含义 |
|---|---|---|
| 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。
| 版本 | 核心差异 |
|---|---|
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。
- MIP 已经显式包含 NVMe 容量约束、NVMe read/write bandwidth 约束,以及 NVMe activation 对 PCIe 资源池的占用。
- Runtime 已经能用 cuFile 执行 NVMe activation spill:GPU tensor -> NVMe raw file,反向时 NVMe raw file -> GPU tensor。
- YAML 里
memory.lazy_nvme_mode: "gds"是默认主线;"cpu_staging"只作为显式 fallback。 - 如果 schedule 里有
offload_nvme但 YAML 没有设置memory.lazy_nvme_offload_dir,训练会直接报错,避免显存约束失真。 - 本机 GPU3 上的短探针结果:
gdsio -x 064MiB write 约3.115GiB/s,read 约2.638GiB/s;Python cuFile 1MiB CUDA tensor roundtrip 测试通过。 - 环境探针也要诚实记录:
gdscheck -p显示 GPU 支持 GDS,但NVMe: Unsupported且properties.use_compat_mode: true。因此 runtime 用的是 cuFile/GDS API direct tensor 路径,但底层是否完全绕开兼容模式,要以后结合 nvidia-fs/系统配置继续确认。 - GDS runtime 需要处理 CUDA stream 生命周期:GDS read 前必须等
compute_stream完成,读回的 CUDA tensor 还要record_stream(ctx.compute_stream)。否则 cuFile 文件本身可能是好的,但读回 buffer 会被上一段 backward 的 CUDA 内存复用踩踏,表现为 activation 写前 finite、读后出现1e+24级别坏值,最终梯度 NaN。 lazy_nvme_prefetch_distance默认改为0。prefetch2 的 ctx future 生命周期 bug 修复后短跑可用,但 GPU3 full probe 在 step6 又出现grad_norm=nan,因此当前默认回到保守同步 GDS read 路径;prefetch2 只保留为后续 profiling/修复路径。- 启动脚本默认使用带
RUN_STAMP的独立 NVMe offload 目录,并在退出时清理本次 run 的整个 offload 子目录,避免实验越跑越堆。启动前还会清理 24 小时以上、且没有活跃 PID 文件的旧 run 目录;预清理会显式排除本次 run 目录,防止刚创建、还没写入 activation 文件的活跃目录被误删。清理动作由scripts/cleanup_lazy_nvme_offload.py完成,只允许删除nvme_offload根目录的直接子目录,不扫描全盘、不跟随 symlink。 - 这个版本适合研究“CPU 内存也紧张时如何把冷 activation 放到 SSD”;对当前 27B 单卡主实验来说,是否更快要看实测,不能只看 MIP objective 下结论。
早期 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 完全一致。
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 |
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 阻塞”的方向。
这个 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 读盘等待。
原 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。
在相同 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。
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-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 结论写死。
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 的修正是:
- 锁住 hybrid8bit 已经实测更快的
selected_edges,也就是 backward recompute block path。 - 锁住 hybrid8bit 的 GPU checkpoint 集合,避免为了 NVMe 分层把 GPU/CPU placement 换慢。
- 只让 PySCIPOpt 在原 CPU checkpoint 集合中选择
19个继续放 CPU、3个 spill 到 NVMe。 - 当时尝试打开
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”。然后求解器继续负责第三层存储分配。
.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 结果 |
|---|---|
| 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 的问题,是当前最合理的安全版本。
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。
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 真放不下时才出场。
.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 结果 |
|---|---|
| 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 通信风险压低。
尝试过把 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 路径。
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 |
- 继续让默认 prefetch0 v5 CPU15/NVMe1 full run 跑到 epoch end,观察最终 test eval loss / EM 是否继续和 8bit_optim 对齐。
- 用更大模型或更紧 CPU activation budget 做下一组实验。当前 27B 只用了
1个 NVMe checkpoint,主要证明稳定和无明显吞吐损失;NVMe 的真正价值要在 alpha/8bit_optim 放不下、或必须大量重算的设置里体现。 - prefetch2 暂不作为默认;如果要重新打开,必须先修复 step6
grad_norm=nan并用至少 12-step smoke 覆盖。 - 用 profiler 记录真实 NVMe read/write 时间,继续校准当前
2.83GB/s read / 3.35GB/s write的短探针带宽。 - 如果要做严格 GDS direct 性能,需要让系统侧把
nvidia-fs/NVMe GDS 支持配置到gdscheck不再显示 NVMe unsupported。