训练数据加载方面优化总结(
好久没写记录了,也不知道这些花费了我一两天功夫的知识,未来会不会都被ai取代了。
一句话背景:这是个小模型(显存只占 7~9GB/80GB),瓶颈 100% 在数据加载,不是算力。所以全部优化都围绕”怎么把数据更快喂进 GPU”。
第一章 · 认清Pod 资源(区分 8 卡 pod 与单卡 pod)
我们申请到的资源池有不同类型,有的是单卡的,有的是多卡的。卡之间也有差异。我下面列出了需要关注的几种资源
1.1 资源清单对照
| 资源 | 8 卡 pod(实测) | 单卡 pod(实测) | 说明 |
|---|---|---|---|
| GPU | 8× H100 80GB HBM3(SXM) | 1× H100 80GB HBM3 | 显存 HBM ~3.35TB/s;HBM:GPU 计算核心和它自己显存之间每秒能搬多少字节 |
| GPU 互联 | 全互联 NVLink(topo NV18)+ NVSwitch,任意两卡 ≈ 450GB/s 双向 | 无(单卡) | DDP 的 all-reduce 走 NVLink 不走 PCIe → 单机内多卡梯度通信极便宜 |
| NUMA | 2 个:GPU0–3→NUMA0,GPU4–7→NUMA1 | — | 4+4 分卡恰好压在 NUMA 边界,亲和最优 |
| CPU | Intel Xeon 8480+,224 逻辑核 | 224 逻辑核(但和别人共享) | DataLoader worker 在这跑 |
| RAM | 2015 GB(~2TB),buff/cache ~1.3TB | 2015 GB(但和别人共享) | page cache 容得下单任务整个 working set(~600–800GB) |
| 本地盘 | / xfs ~11TB(pod 临时盘,销毁即清空) |
/ overlay ~11TB (但和别人共享) |
唯一本地盘;要本地化需 rsync 过去 |
| /dev/shm | ~2 TB(tmpfs = RAM 装扮的盘) | 2.0 TB (但和别人共享) | 可放常驻 RAM 的特征副本 |
| 数据盘 | /code /data /share_data 全 NFS(datastore.eng-zoom.local,680T,~满) |
同左 | 数据集源头,走网络 |
| 网卡 | 单口 10GbE,无 bond | 单口 10GbE,无 bond (但和别人共享) | 每节点共享,是硬天花板 |
| NFS 挂载 | NFSv3, rsize=1MB, nconnect=4 | NFSv3, rsize=1MB, nconnect=16 | NFSv3=协议版本(无状态、元数据每次都要确认);rsize=1MB=每次读请求的块大小(越大往返越少);nconnect=开几条并行 TCP 喂网卡(越多越易打满 10GbE,但打不破 10GbE 硬上限)。详见 A8 |
1.2 存储速度阶梯(越往下越慢)
1 | flowchart TB |
注意 RAM 内部分三块(cache 不是独立硬件,是 RAM 的一种用途):
1 | flowchart TB |
1.3 NFS 读带宽实测(关键 —— 两类 pod 聚合一致)
| 场景 | 8 卡 pod | 单卡 pod | 说明 |
|---|---|---|---|
| 网卡物理上限 | 10GbE ≈ 1.25 GB/s | 同 | 整机所有进程共享,硬天花板 |
| 冷读·单流(direct I/O 裸读) | ~520 MB/s | ~114 MB/s(受训练争用压低,干净时更高) | 仅决定单线程 preload,不决定训练吞吐 |
| 冷读·并发(多 worker) | ~1.1 GB/s | ~1293 MB/s | 两边都打满 10GbE → 聚合一致 |
| 暖读·page cache 命中 | ~2.8 GB/s | ~1.4 GB/s | 走 RAM,与网卡无关;page cache 整节点共享 |
| 每 GPU 冷启动带宽 | 1.2 GB/s ÷ 8 ≈ ~150 MB/s | 1.2 GB/s ÷ 1 ≈ ~1.2 GB/s | 多卡按 GPU 摊更挤 |
1 | flowchart LR |
两类 pod 聚合带宽一样(都卡在单口 10GbE);多卡没有 8 条网卡,只是把同一条管道按 GPU 摊薄。
三条最重要的结论:
- NFS 带宽是「每节点」属性(网卡+服务器决定,与 GPU 数无关)。多卡 pod 没有 8 条网卡,读盘聚合不会更快,按每 GPU 算还更挤(1/N)。但是单卡pod 需要和别人共同争抢这部分NFS带宽。
- 别用”单流冷读”下结论。训练用的是几十个并行 worker,两边都会并发打满同一条 10GbE。
- GPU 互联很便宜:8 卡全 NVLink 450GB/s,模型小(梯度才几十 MB),实测
barrier_sync ~23ms。所以多卡瓶颈是 barrier 木桶 + 数据/CPU 争抢,不是通信带宽。
第二章 · 代码里真正耗时的环节
把”读一个 batch”拆开看,从磁盘到 GPU 一路上的开销(按是否 preload 分两条路径):
2.1 磁盘 / 网络访问(NFS)
- 冷读(page cache 未命中):
open要 2 趟网络往返(LOOKUP + GETATTR 确认属性)+read传字节。秒级尖刺。 - 暖读(page cache 命中):内容不走网络了,但
open()仍要 1 趟 GETATTR 确认文件是否最新。2.8 万文件 + shuffle 下属性缓存基本都过期,所以这趟 GETATTR 几乎每次都付 —— 这就是nfs_data_io.md强调的”把每个文件的 stat/open/getattr 当昂贵网络往返”。
读图:每一趟网络往返都画成两条线——实线
→是发出去的请求,虚线←是等回来的响应;一来一回 = 一趟往返(一次网络延迟)。编号 ①②③ 数的就是往返次数。
1 | sequenceDiagram |
冷读 = 3 趟往返(秒级尖刺);暖读 = 1 趟往返(只剩 open 的 GETATTR,内容白嫖 RAM)。两种情况之后,下游处理完全相同:
np.load解析 → 整份 astype(f32) 碰所有页 → pickle/IPC → PCIe H2D(见 §2.2)。
对照:开了 --feature_preload 后,文件早在 RAM dict 里,getitem 根本不碰文件系统 —— 零网络往返、零 GETATTR、零缺页:
1 | sequenceDiagram |
三张图并排看:冷读 3 趟、暖读 1 趟、preload 0 趟网络往返 —— preload 用”启动时一次性全读进 RAM”换掉了热路径里每个文件每一步的网络往返。代价是 RAM 占用(§2.5)和单流冷读慢(见上方带宽表 + §A8 的 nconnect);多卡场景见 §2.4 /
nfs_data_io.md。
2.2 内存 / CPU 上的数据处理(这是暖缓存下的真瓶颈)
np.load解析 → 在进程堆里生成 numpy 数组。- **整份
astype(np.float32)**:把**整份**特征转 f32 → **碰到所有页**。这是"只把np.load` 改 mmap 不够”的原因:整份 astype 仍会 fault 全部页,mmap 白改。 - 读放大:不 preload 的懒加载里,w2v 文件 28MB,每个样本读整份只为切 15 帧 → batch128 ≈ 3.5GB/iter 的无效读。mmap 只读 ~15/1525 ≈ **1%**。
- motion
.pkl反序列化:每样本 unpickle,受 GIL 限制。.pkl不能 mmap。 - collate / padding / reshape / emotion 处理:batch128 在主进程拼 batch 的 CPU 开销。
暖缓存稳态下
data_time ~0.16s的真正构成 = getitem 的 CPU + collate(motion unpickle、astype、padding),与数据在本地还是 NFS 无关,local/mmap 都改不动。这是下一步的杠杆。
2.3 DataLoader / worker(num_workers 的知识)
worker 是独立进程、各有自己的 GIL,它能并行两类东西,并和 GPU 计算重叠:
- I/O 延迟重叠:GETATTR/READ 是”等网络响应”的延迟操作,N 个 worker 让 N 个往返并发在飞 → 不 preload(冷/暖读)都吃这个。
- getitem 的 CPU 并行(关键、最易被忽略):
.pklunpickle(受 GIL 限制)、astype、reshape、padding、collate 都是 CPU 活。nw=0时它们全部串行挤在主进程、卡在关键路径上,GPU 干等;nw>0时分到 N 个进程真并行(绕开 GIL)、并 prefetch 下一批 → 与当前 batch 的 GPU 计算重叠。§2.2 说的瓶颈(unpickle/astype/padding ~0.16s)正属此类,所以这恰恰是 worker 该上场的场景 —— 这才是 §3.3 推荐的 NFS+mmap 不 preload 配 48 worker 成立的真正原因(不只是 I/O,更是 CPU/GIL 并行)。
那 preload 这一档 nw=0 会不会反而更快? 曾经这样推理过:preload 把第 2 类重活预先做掉(motion 已 unpickle 成数组 :483→:610、特征已 astype(f32) :463 存进 RAM dict),getitem 只剩”dict 取值 + 切窗口 + collate”这点轻 CPU,于是”省下的 CPU < 搬 batch 过进程边界的 IPC”,nw=0 该赢。⚠️ 但这个先验被实测推翻了(见 §2.6 的 §2bis 表):preload+nw8 的 data=198ms 反而略快于 preload+nw0 的 215ms。** 原因有二:① getitem 就算变轻,残余 CPU 仍非零(切窗口、collate 拼 256 样本、astype/padding/gather),nw=0 时它们全串行卡在关键路径、GPU 干等;② DataLoader 的 prefetch 让下一批的 IPC 在后台和当前 GPU 计算重叠,那个”~278ms IPC”根本不在关键路径上,不会让 nw>0 变慢。即便 preload,仍需一定并行 —— nw>0 ≥ nw0。**
- 一句话判据(修正后):worker 值不值 = “getitem 并行省的 CPU + prefetch 重叠” vs “多出来的 per-worker 固定开销/内存”。实测在本工作负载上,preload 与非 preload 两档 nw>0 都不亏(preload 档 nw8 略优于 nw0,mmap 档更靠多 worker)。别再默认”preload → nw=0”;两档都实测
nw=0 / 4 / 8 / 16取最优。 nw 的唯一硬约束是内存(§3.1bis:大堆 + 高 nw 时 per-worker 固定开销可能顶穿),不是速度。 - PCIe H2D:最后把 batch 拷进显存,所有路径都有。
2.4 多卡(DDP)额外的时间
accelerate launch --multi_gpu 是数据并行,多出来的成本都落在”隐藏区”:
| 单卡 | N 卡 DDP | |
|---|---|---|
| 每步样本 | b | b×N(全局 batch) |
| 梯度 | 直接用 | 每步 all-reduce 跨卡平均 |
| 同步点 | 无 | wait_for_everyone() barrier,等最慢 rank |
| worker | num_workers | num_workers × N(抢 CPU/NFS) |
- 本节点 NVLink 极快,实测
barrier_sync ~23ms(单卡恒 0)→ 通信不是瓶颈。 - 多卡的价值是放大有效 batch / 总吞吐,不是让单个 iter 更快。数据受限场景下多卡 wall/iter 可能和单卡差不多。
- 致命点:多卡 +
--feature_preload时每个 rank 各载全量(代码不按 rank 分片)→ N× RAM。8 卡 × 1.37TB = ~11TB,2TB RAM 必爆。
2.5 三档优化(一图总览)
1 | flowchart LR |
| 模式 | 网络往返 | 内存访问 | IPC | 每步瓶颈 |
|---|---|---|---|---|
| 不 preload·冷 | open+read 2 趟 | 堆(临时) | 有 | 网络(秒级尖刺) |
| 不 preload·暖 | open 1 趟 GETATTR | page cache 读 | 有 | 网络确认延迟 + getitem CPU |
| preload·workers>0 | 无 | RAM dict 切片 | 有(prefetch 重叠,不在关键路径) | 数据体量(§2.6),nw8 略快 |
| preload·workers=0 | 无 | RAM dict 切片 | 无 | 数据体量 + 串行 getitem(略慢于 nw8) |
2.6 batch_index_mode:把”逐样本组装”塌缩成一次 gather(对抗 getitem 的 CPU 开销)
前面 §2.2/§2.3 说清了暖缓存/preload 下的真瓶颈是 getitem 的 CPU:每步要跑 256 次 Python __getitem__(每次十几个小 op + 建对象)+ collate。--batch_index_mode 就是专门冲这块来的第四种模式。
机制
- 建立在 preload 之上(硬依赖
--feature_preload)。init 时把data_dict里各 clip 的变长数组沿时间轴拼成 3 个连续大 tensor:bt_M(motion, fp32)、bt_W(whisper/w2v, fp16)、bt_E(emotion, fp32) +bt_start/bt_len偏移表。 - 每步
sample_batch():按和__getitem__相同的随机规则构造散列下标idx[256, window],然后 每模态一次 gather(M[idx]/W[idx]/E[idx])+ 向量化建 padding_mask/ref/no_prev,直接产出 8 元组 batch。 - 训练循环把
next(data_loader)换成bt_dataset.sample_batch(bs),**train_step不动**。
为什么能对抗 CPU 开销(三点):
- 塌缩 Python 解释器开销:5000+ 个细碎逐样本 op → 几个向量化大 op(构造 idx + 3 次 gather + 向量化 mask)。
- 单进程、不 fork:本身就是
num_workers=0路径 → 无 fork/COW(§3.1bis)、无 per-worker 固定开销。 - 无 IPC:在进程内直接拼 batch → 省掉 §2.3 那条”共享内存 + 管道 + 重建”的 ~278ms。
一图看懂”砍掉了什么、又为什么还有搬运成本”: 把每步组装 batch 的成本拆成两层——层A(Python 组装) 被 bt_mode 塌缩到 ≈0,但 层B(数据体量搬运) 两条路都躲不掉,而它恰恰才是主成本:
1 | flowchart TB |
为什么”之前说搬运不高”是错的:§2 原估把这步当成”90MB 连续内存顺序拷贝”,按 RAM 带宽
100GB/s 算就是 ~1ms。但 bt_mode 实际做的是25GB/s,比 RAM 慢 4 倍)** H2D 进显存。两者叠加,实测就是 ~0.14s 而非 ~1ms。bt_mode 砍的是层A(Python),治不了层B(体量) —— 这就是它只快 ~25–30% 的根因。W[idx]散列 gather ——idx指向 256 个随机 clip × 随机起点,在 ~18GB 的大 tensor 里东一块西一块地取(随机访问、cache 命中差、要碰散落各处的物理页),再把这 35MB 跨 **PCIe(
⚠️ 实测修正(2026-06-24,来自 big_tensor_design.md §2bis)——“0.3s→几ms”是高估:
配置:feature=w2v(9216 维)、whisper fp16、-b128 -n10 -p5(window=15)、单卡、与训练共用 GPU/CPU。四档每步 prof total / data(ms):
| 配置 | prof total | data | 相对 |
|---|---|---|---|
| 基线 NFS + nw48 | 212–296 | 182–254 | — |
| preload + nw0 | 244 | 215 | ~持平 |
| preload + nw8 | 226 | 198 | ~持平 |
| bt_mode(本设计) | ~178 | ~140 | 快 ~25–30% |
结论:bt_mode 确实最快,但只快 ~25–30%,远不是预期的 5×。 因为真瓶颈不是 Python 解释器开销,而是 w2v 9216 维的数据体量 —— 每个 batch 的 whisper 是 128×15×9216 ≈ 35MB(fp16),要从 18GB 的 30%),但躲不开搬这 35MB 的体量成本。这也解释了为什么 §2.3 的 getitem/workers/preload/bt 全卡在 0.14–0.2s 一个量级:搬的字节一样多。bt_W 里散列 gather + H2D。bt_mode 砍掉了 128× Python getitem(0.2→0.14s,
- fp16 是前提:whisper 全程 fp16 已把这块体量砍半;若 fp32,
bt_W翻倍、gather+H2D 会到 ~0.28s → bt_mode 反而更慢。 - 真要大幅提速的唯一杠杆 = 减小 w2v 特征体量(离线把 9216 维投影/聚合到更小维度),但那会改模型输入,属另一个工程。
适用范围: 要求 --feature_preload y + --predumped_motion + rational_obj=disabled,且非 cond_identity/avg_condition/ood_motion_filter/load_gt_images;仅单卡 —— 多卡会自动禁用并回退 DataLoader(自定义采样器绕过了 DistributedSampler,各 rank 不分片会训到相同 batch)。正确性已按”gather 逐位对齐 + 采样分布等价”校验通过(big_tensor_design.md §7)。
一句话定位:bt_mode 用”单进程一次 gather”取代 DataLoader,是 preload 路线在 CPU 侧的进一步榨取——在小/浅特征(Python 占比高)上收益更大;在 w2v 9216 维上被数据体量淹没,只剩 ~25–30%。它对抗的是 CPU,治不了体量。注意它和”preload + nw>0”是同一档的两种实现(§2bis 实测两者都在 0.14–0.2s 一个量级),该实测对比后取优,而不是先验认定谁快。
第三章 · 结论:什么规模用 preload,什么规模该本地化
### 3.1 num_workers↔RAM · COW 机制
🔬 “纸面装得下” ≠ “跑得安全”(4 个原因)
- 1.6T 不是峰值 —— COW 会把它顶上去:48 worker fork 后本该共享,但 Python 的 refcount/GC 一访问就写对象头 → 按页 COW 复制 → 实测 free 掉到 37G、近 OOM(机制见下方图)。
- 就算
nw=0,余量也太薄:1.6T 顶着 2.015T 只剩 ~400G,还要喂 preload 的冗余 page cache、CUDA context/pinned 缓冲、模型+优化器、Python 开销,且这是宿主机共享内存,邻居 pod 也在抢。 - 这 1.6T 是 fp32 的可避免浪费:盘上 fp16 仅 685G,涨一倍只为提前转类型,而模型在 GPU 上本就 fp16→fp32。
- 收益还小:暖缓存下 NFS+mmap 已 ~4.2min/1000it,瓶颈是 getitem 的 CPU(preload 音频特征修不了 motion unpickle)。大 RAM 风险换 ≈0 提速。
num_workers ↔ RAM:worker 是”COW 的乘数”
| 每 worker 的 RAM 成本 | nw 调大的后果 | |
|---|---|---|
| 不 preload | 小(读文件 → 拼小 batch → 送走即丢) | RAM 几乎不涨 → 48 worker 没问题 |
| preload(大堆) | fork 后本应共享,refcount 弄脏 → 按 nw 倍 COW 复制 | RAM ~随 nw 线性涨 → COW 灾难 |
⚠️ 上表是旧的 COW 模型,已被下方”🔬 实测修正”推翻(worker 并不按 nw 倍复制数据缓冲)。保留于此仅为对照。修正后的正确说法:nw 吃的不是”COW 复制的数据”,而是 per-worker 固定开销(预取缓冲 + 页表 + 解释器,GB×N 不是 T×N)。所以 preload 时也不该默认
nw=0—— 速度上 nw>0 通常更优(§2bis / §2.3);只有当”preload 大堆已贴近内存顶 + 高 nw”时,才需为省内存调低 nw。不 preload 没大堆 → nw 随便开。(worker 的”值不值”另见 §2.3。)
COW 逐页机制(图解)
① 一次 fork + 一次”只读访问”发生了什么 —— 对比 mmap:
1 | sequenceDiagram |
② 因果链 + 三条规避路径:
1 | flowchart TD |
关键直觉:fork 给的是”共享只读页”的好意;Python 每次访问对象都去写它的 refcount(GC 还周期性写头标记),把好意一页页拆成私有副本;数据又和小对象交错同页,于是复制量从”对象开销”滚向”整个 1.37T”。worker 越多滚得越狠。 真要并行,别丢给它”一个全是小 Python 对象的大堆”去 fork —— 给它 mmap 的 page cache(内核级真共享,不受 refcount 影响)。
🔬 实测修正:worker 多出的私有内存到底多大(上面①②图的”溅射/滚向整个 1.37T”是高估)
日期:2026-06-24。用
os.fork()+/proc/self/smaps_rollup的Private_Dirty(= 子进程被迫私有复制的页)直接量了一下,结论:fork 出来的 worker 既不复制数据缓冲,对象头 churn 也只有几十 MB —— 根本不是”×N 份 1.37T”。
| 实测场景 | 每 worker 私有多出(Private_Dirty) | 含义 |
|---|---|---|
父进程持 2.15GB fp16 大数组,子进程 sum() 读完全部数据页 |
3 MB(其余 2.11GB 全 Shared) |
读 numpy 数据缓冲不触发复制 → 缓冲区跨 worker 共享 |
父进程建 5万 clip 的 data_dict(每条挂 9216-fp16 数组 + str key + 小字段),子进程随机访问 3 个 epoch |
19 MB | refcount/GC 弄脏的只是对象头那些页,几万对象也就几十 MB |
为什么原来的”溅射”模型不成立:numpy 的数据缓冲是一整块独立分配(单独 malloc/mmap 的大区),而 PyObject 头(ob_refcnt,~50B)在 Python 自己的对象 arena 里——两者不在同一页。所以 Py_INCREF 弄脏的是”对象头页”,不会把旁边的数据缓冲一起复制(①图里”数据和小对象交错同页→溅射”对 numpy 数组基本不发生)。对象头总量 = 对象个数级别 ≈ 几十 MB/worker,不随数据体积走。
那 48-worker 跌到 free 37G 的真凶是什么(既然不是 COW 复制数据):
- 基数 1.6T 本就贴着 2T 顶(只剩 ~400G),任何额外开销都容易顶穿;
- 每 worker 的固定开销(随 nw 线性涨,但量级是 GB×N 不是 T×N):独立 Python 解释器 + import 的 torch/numpy(每个几百 MB)、DataLoader 预取缓冲(
nw × prefetch_factor个 batch,fp32 下 B=16 一个 batch 的 w2v ≈ 737MB)、页表(1.6T 映射若无 THP,每进程 ~3.2GB,×48 ≈ 150G); - 宿主机共享,邻居 pod 也在抢这 2T(见 A6)。
修正后的结论(实践不变、机制变了):
- preload + 多 worker 仍然危险,但根因是**”基数贴顶 + 每 worker 固定开销累加”**,不是”每 worker 把整个堆 COW 复制一份”。
- 因此 fp16 把基数从 1.6T 砍到 0.8T 后(见下方音频特征 fp16 化),离 2T 有 ~1.2T 余量 → 那几十 GB 的 per-worker 固定开销塞得下,w2v2 +
nw=8实际可用、不再爆。这也解释了为什么当年 whisper(preload 基数仅 ~0.22T,2560 维)nw=8一直没事——基数小、离顶远,同样的 per-worker 开销根本不痛。 - 规避手段不变且依然最稳:preload 时
num_workers=0(无 fork、无预取膨胀),或batch_index_mode(3 个连续大张量、单进程、不 fork)。
复现方法(任意机器可跑):os.fork() 后在子进程里读 /proc/self/smaps_rollup 的 Private_Dirty / Shared_Clean;父进程先持有大 numpy 数组或大 dict,子进程分别”读全部数据”与”随机访问条目”,对比两者的 Private_Dirty。
3.2 本地化适用范围(其实很窄 —— mmap 才是真杠杆)
实测三种方式稳态对比(full data, n10p5, b128, 48worker):
| 配置 | 每 1000iter | 相对 |
|---|---|---|
| 本地盘 + mmap | ~3.7 min | 最快基准 |
| NFS + mmap | ~4.2 min(15k 长稳态) | +14% |
| 旧 NFS 无 mmap | ~5.0 min | +35% |
| 全量 preload + 48w | COW 卡死(首批 794s) | ✗ |
核心认知(颠覆了直觉):
- 暖缓存下”数据在本地还是 NFS”几乎无差 —— 都从 page cache(RAM)读。两边 data_time 几乎相同(0.16 vs 0.18s)。本地盘只在 真·冷启动首 epoch / 工作集超 2TB 内存被换出 时才赢。
- 真正的杠杆是 mmap,不是本地盘。
np.load(mmap_mode='r')+ 把整份 astype 推迟到截 15 帧之后 → 每样本只读 ~1%,既避开 preload 的 RAM/COW,又避开懒加载的读放大。从 5.0min → 4.2min 的提升主要是 mmap 的功劳。 - mmap 与 NFS 的关系(修正”mmap 不能用于 NFS”的旧说法):mmap 的收益绑定”数据在 page cache”,不是”在本地盘”。冷 NFS + mmap = 慢(每页一次网络往返);暖 NFS + mmap = 快(缺页命中 RAM)。本地盘的真正价值 = 躲掉冷启动的按页网络读惩罚 + 不怕被换出。
3.3 最终推荐配置
NFS +
--mmap_audio y(不 preload,48 worker) 是当前最优且最省事的方案:稳定、可接受(~4.2min/1000iter,15k iter 零报错/零 NaN),省掉 ~1TB 本地拷贝与维护。本地盘容量够、且要榨那 ~14%,再迁本地;不够时 NFS+mmap 是稳妥退路。
决策速查:
| 场景 | 推荐 |
|---|---|
| 单卡 + 小特征(≤几百 GB) + RAM 够 | preload(nw 实测取优,通常 nw≈8;或 batch_index_mode) |
| 单卡 + 全量 w2v | NFS + mmap(preload 会 COW) |
| 多卡全量 | NFS + mmap,绝不 preload(N× RAM 爆) |
| 想消除冷启动首遍 / 防换出 | 本地盘 + mmap,或 /dev/shm 暂存 |
| 多卡要”只存一份 RAM” | 不 preload 吃整节点共享 page cache(A 方案,零改动) |
1 | flowchart TD |
3.4 下一步杠杆(与存储层无关)
稳态 data_time ~0.16s 的真正构成是 getitem 的 CPU:motion .pkl 的 unpickle(GIL)+ collate。本地/NFS/mmap 都改不动这一块。**真正的下一步 = motion .pkl → .npy/.npz**(去掉每样本 unpickle、可 mmap)。
3.5 起训自检 × 训练需求 → 读取方案对照(怎么选)
因为资源全是 host 级共享、无 cgroup 兜底(§A6/§A9),起训前先扫一眼再选方案。
第 1 步 · 起训自检(每项对应一个决策):
| 自检项 | 命令 | 读数 → 推向 |
|---|---|---|
| CPU 争用 | uptime |
load avg ≪ 224 核 → 可多 worker;逼近/超 224(邻居在抢)→ 单进程路线、少 worker |
| RAM 余量 | free -h |
available ≥ preload 体积 × rank 数 × ~1.3(留余量) → 可 preload;否则只能 mmap(swap=0+overcommit=1,贴顶=直接 OOM 不是变慢,§A9) |
| 网络 | 看 eth0 RX 快照 / nfsiostat |
空闲 → 冷读快、首 epoch 不痛;被邻居占满 → 冷读掉到百 MB 级,倾向 preload(一次性读)/本地化 |
| 本地盘余量 | df -h / |
要本地化时必查:剩余 ≥ 数据集体积(w2v fp16 ~685G / whisper ~380G) + 余量 才能 rsync;不够就别本地化(rsync 中途撑爆盘 → 训练/进程崩)。⚠️ 盘是共享 + 临时(§A6):邻居可能占、pod 重建即清空,所以要”临起临查”,别假设上次的余量还在 |
| GPU 邻居 | nvidia-smi |
确认”单卡 pod 同机是否有人”(§A6) + 自己卡上有无残留进程 |
口诀:RAM 决定能不能 preload;本地盘余量决定能不能本地化;CPU/网络决定 worker 数与冷启动代价。
第 2 步 · 需求 + 资源门槛 → 方案:
| # | 训练需求 | 关键资源门槛(自检) | → 读取方案 | 关键理由 |
|---|---|---|---|---|
| 1 | 单卡 · 小/浅特征(≤几百G) · 多 epoch | available ≥ 特征体积×1.3;CPU 不挤 |
preload + batch_index_mode(单进程) |
Python 占比高 → bt 榨得动(§2.6);RAM 够 |
| 2 | 单卡 · 全量 w2v(f32 1.37T) | available < 1.37T+余量 / 贴顶 |
NFS 或本地 + mmap(48w) | preload 会 COW/OOM(§3.1bis/§A9);mmap 走可回收 cache |
| 3 | 单卡 · 全量 w2v · 想再榨(fp16 基数~0.8T 且离顶远) | available ≫ 0.8T |
preload(fp16)+nw 取优 或 bt | 只快 ~25–30%,体量瓶颈,谨慎(§2.6) |
| 4 | 多卡 · 全量 | 任何(N× 必爆) | NFS + mmap,绝不 preload | N×RAM 爆;bt_mode 多卡自动禁用(§2.6) |
| 5 | 任意 · CPU 被邻居抢(load ≈/> 核数) | load 高 | 压低 num_workers(或 bt 单进程) |
CPU 已被邻居占满,多 worker 抢不到时隙、还徒增 per-worker 内存开销;非”必须归零”,按实测降到不亏为止 |
| 6 | 在意冷启动首遍 / 防换出 | df -h / 剩余 ≥ 数据集体积 + 余量 |
本地盘 + mmap | 躲按页网络读 + 不怕被换出(§3.2);盘不够则退回 NFS+mmap |
第 3 步 · 兼容性矩阵(哪些能叠、哪些互斥):
| preload | mmap | bt_mode | 本地化 | |
|---|---|---|---|---|
| preload | — | ❌ 互斥 | ✅ bt 依赖它 | ✅ 正交 |
| mmap | ❌ 互斥 | — | ❌ 互斥(bt 要 preload) | ✅ 正交 |
| bt_mode | ✅ 需要 preload | ❌ 互斥 | — | ✅ 正交 |
| 本地化 | ✅ | ✅ | ✅ | — |
- preload ↔ mmap = 互斥:同一轴(“getitem 怎么拿特征”)的反向策略——全进 RAM 匿名堆 vs 懒映射只读切片。二选一。
- bt_mode → 依赖 preload:它把 preload 好的数组拼成大 tensor(§2.6),所以能叠 preload、不能叠 mmap。
- 本地化 ⊥ 一切:只改冷读数据源(本地盘 vs NFS),不改 RAM 策略 → 可叠加在方案 1/2/3/6 任意一套上,只用来躲冷启动 + 防换出。
→ 实际可选组合就四套:① mmap(不 preload,当前推荐 §3.3) ② preload + nw 取优(通常 nw>0) ③ preload + bt_mode(单卡单进程榨 CPU) ④ 啥都不开(NFS 懒加载,最慢基准);每套都可选择性叠”本地化”。
附:最新 run
..._n10p5_nfsmmap已跑到 iter 24000(笔记记录到 15000)。data_time 从 7–15k 的 0.16s 轻微上漂到 21–24k 的 0.23–0.30s,需再观察是 NFS 争用抖动还是”工作集超 page cache 渐慢”的真趋势。
附录 · 名词答疑(不影响正文,按需查)
阅读正文第一章硬件指标时常卡住的几个名词,集中解释在这。
A1. 显存 HBM ~3.35TB/s 是什么指标
- HBM = High Bandwidth Memory,H100 那 80GB 显存的物理介质;
3.35 TB/s是它的带宽——GPU 计算核心和它自己显存之间每秒能搬多少字节。 - 区分两个”速度”:带宽=每秒搬多少(吞吐),延迟=搬一次等多久(几百 ns)。这里说的是带宽。
- 它是卡内部 GPU↔自己显存 的速度,不是卡↔卡(那是 NVLink),也不是卡↔CPU内存(那是 PCIe)。
- 意义:GPU 算矩阵乘要反复读写显存,HBM 把带宽堆到 TB/s 才能喂饱算力。对本任务不是瓶颈(小模型,远没吃满)。
A2. 什么情况会触到 HBM 的边界
两个边界分开看:
- 撞带宽(3.35TB/s 喂不过来):当瓶颈是”搬数据”而非”算”。判断看计算访存比——读一个字节能顺带做多少次运算,低=memory-bound。典型:大 batch/长序列的 注意力、LayerNorm、softmax、逐元素激活;推理自回归解码(每个 token 读一遍全部权重);反向传播(反复读写激活/梯度)。
- 不撞带宽(撞算力):**大矩阵乘/卷积(GEMM)**——数据复用多(O(N³) 算 vs O(N²) 读),卡在 Tensor Core,HBM 还有余量。
- 撞容量(80GB 装不下):权重+激活+优化器状态(Adam ×2 动量)+梯度 超 80GB → OOM。大模型靠 ZeRO/offload/梯度检查点/张量并行解决。
- 本任务:容量(占 7~9GB)和带宽(单步才几十 ms)两个边界都离得很远 → HBM 是背景家底,不是优化目标。
A3. “all-reduce 走 NVLink 不走 PCIe → 通信极便宜” = 多卡梯度同步几乎不花时间?
- 是的(仅限单机),实测
barrier_sync ~23ms。 - 背景:DDP 下每张卡各算梯度,每步要把 N 张卡的梯度加起来求平均(=all-reduce),保持模型一致——这就是”梯度通信”。
- 关键看走哪条路:PCIe(通用总线,几十 GB/s,绕 CPU,慢)vs NVLink(GPU 直连,本机 ~450GB/s 双向,快十几倍,不绕 CPU)。
- 便宜的两个原因叠加:① 走 NVLink 不走 PCIe(管道宽);② 模型小、梯度才几十 MB(要传的本就少)。几十 MB ÷ 450GB/s = 微秒级。
- ⚠️ 仅限单机 8 卡。跨机器多节点时梯度走网络(RDMA 网卡),通信可能变成大头,这句就不成立。
A4. NUMA 是什么,”4+4 压在 NUMA 边界,亲和最优”
- NUMA = Non-Uniform Memory Access:这台机器物理上是 2 颗 CPU(2 socket),每颗管一半核+一半内存+一半 GPU,即 NUMA0 / NUMA1。CPU 访问自己这半内存快,访问另一半(跨 socket)慢——访问速度”不统一”。
- 本机布局:
NUMA0 = GPU0–3 + 半数核 + 半数内存,NUMA1 = GPU4–7 + 半数核 + 半数内存。 - 4+4 分卡(任务A
=0,1,2,3、任务B=4,5,6,7)恰好沿 NUMA 边界切:每个任务的 GPU、它的 DataLoader worker(CPU)、读进的数据(内存)全在同一个 NUMA → CPU 取数喂卡都是近距离访问,最快。这叫亲和(affinity)最优。 - 所以 4+4 是”免费午餐”:为分卡(不抢 SM)做的切分,顺带满足了 NUMA 亲和。乱分(如
0,1,4,5横跨两 NUMA)会引入跨 socket 慢访问。
A5. 单卡 pod 的 224 核/内存 和那 1 张卡 有亲和吗
- 几乎没有需要操心的亲和——只有 1 张卡,不存在”选哪个 NUMA 离它近”的分卡决策。
- 物理上 224 核这种规格大概率仍是 2-socket/2-NUMA,”近内存/远内存”差异客观存在;那张卡也只插在某一个 NUMA 上。但实际可忽略,三个原因:
- 没有分卡决策(NUMA 价值在多卡 4+4 对齐,单卡用不上);
- 你是数据受限——跨 socket 那点纳秒级延迟,相比 NFS 往返(几十~几百 ms)、getitem(0.16s) 小到看不见;
- preload 的 1.37TB 数组 / 几百 GB page cache 本就横跨两个 NUMA,内核不会替你对齐,对单卡训练无所谓。
- 只有做到”数据全在 RAM +
num_workers=0+ 纯比内存带宽”且想numactl --cpunodebind绑核时才需在意——对当前 NFS+mmap、data_time 还有 0.16s 的场景属过度优化。
A6. 这些资源(CPU/RAM/盘/shm/NFS/网卡)单卡和多卡是一样的吗?单卡是不是和别人共享?
全是「宿主机(物理节点)级」资源,不是「卡级」。 一个 pod 就是跑在某台物理机上的容器,看到的 CPU/RAM/盘/网卡就是那台宿主机的家底。所以单卡 pod 报出的 224 核 / 2015GB / 11TB / 2TB shm 和 8 卡几乎一样,不是巧合——同机型。
唯一真正按卡分的是 GPU:pod 被授予几张就用几张。所谓”单卡 pod”极可能是一台多 GPU 机切出来、只给 1 张 GPU 的容器,同机其他 pod 在用剩下的 GPU。
这些 host 级资源都不是独享,而是和同机其他租户共享(笔记
memory.max=max= 无 cgroup 上限,能看到也能用满整机,但邻居也能抢):资源 归属 单卡独享? CPU 224 核 / RAM 2015GB / 本地盘 11TB / shm 2TB 宿主机 ❌ 与同机 pod 共享 NFS + 10GbE 网卡 节点 ❌ 每节点共享(硬天花板) GPU 卡级 ✅ 按授予张数分 ⚠️ 两边 NFS
nconnect不同(单卡 16 / 8 卡 4)→ 二者未必同一台物理机,更可能是同机型的两台不同节点;但结论不变:CPU/RAM/盘/网卡都是 host 级、与邻居共享。实践含义:单卡 pod ≠ 清净的专属小机器。起训前
nvidia-smi+uptime看有没有邻居在抢 → 呼应 §8 checklist。
🔬 A6 实测佐证(一台单卡 pod 现场量的,2026-07-01)
在一台单卡 H100 pod上直接量了一遍”共享/独占”和”当下 NFS 速度”,全部对上 A6 的结论,并补两个只有现场才看得到的证据。
① 两个铁证:容器无上限 + 负载来自看不见的邻居
memory.max = max、cpu.max = max 100000(无 cgroup 配额)→ 能用满整机,但邻居也没上限,纯靠抢。uptime报 load average ≈ 121(224 逻辑核),可ps在自己 PID namespace 里只看得到 27 个 root 进程(本训练 1×python + 一堆pt_data_worker,合计才 ~5–6 核)→ 那 ~121 的负载绝大部分来自我根本看不见的邻居进程。这就是”CPU 是 host 级共享”最直观的一幕:承受其争用,却连是谁都看不到。
② 共享 vs 独占(现场读数)
| 资源 | 本 pod 实测 | 与别人共享? |
|---|---|---|
| GPU | 1× H100 80GB(已用 5.9G,util 0%) | ✅ 独占(唯一独占的) |
| CPU | 224 逻辑核 / 2 socket(NUMA0=0-55,112-167,NUMA1=56-111,168-223) |
❌ 共享,且此刻在被抢(load 121) |
| RAM | 2.0 TiB(host 视角 used 310G / cache 656G / avail 1.6T) | ❌ 共享,free 的 used 含邻居 |
| /dev/shm | 2.0 TB tmpfs(用 16G) | ❌ 共享(吃 shm=吃 RAM) |
本地盘 / |
overlay | ❌ 共享 + 重建即清 |
| NFS + 网卡 | eth0 **10GbE(10000Mb/s)**;/code//data//share_data 均 vers=3,rsize=1M,nconnect=16 → datastore.eng-zoom.local |
❌ 每节点共享,硬顶 ~1.25GB/s |
→ 唯一真正独占的就是那 1 张 GPU;nconnect=16 也和 A6”单卡 16”对上。
③ 当下 NFS 读速(/code 上一个 92MB 文件实测)
| 场景 | 实测 | 说明 |
|---|---|---|
冷读·单流(dd iflag=direct,绕 cache 真走网络) |
~0.32 → 0.65 → 0.74 GB/s(逐次升高) | nconnect=16 + 顺序 readahead 把请求铺开 |
| 暖读·单流(命中 page cache) | ~4.0 → 6.2 GB/s | 走 RAM,与网卡无关 |
| 网卡硬上限 | 10GbE ≈ 1.25 GB/s | 整节点共享 |
| 当下 eth0 实际入向 | ≈ 0 MB/s | 关键:此刻几乎没人在读网络 |
关键解读(为何和 §1.3”单卡冷读~114MB/s”不矛盾):§1.3 那个 114MB/s 是**”训练正猛读 NFS、被争用压低”时的数;而现场 eth0≈0(网络空闲)——虽 load 高达 121、同 pod 有 pt_data_worker 在跑,但 GPU util=0、网卡 0,说明它们这会儿在吃暖 page cache / 卡 CPU,没走网络。没人抢网络 → 单流冷读就能冲到 0.7GB/s(逼近 10GbE 一半多)。所以”NFS 速度”没有单一答案,取决于此刻谁在抢那条 10GbE**:网络空闲→单流 0.3–0.7GB/s、并发可顶 ~1.25GB/s;有人猛拉→压到百 MB 级;数据已在暖 cache→4–6GB/s 走 RAM(与网络无关)。
④ 落到实践:这台的瓶颈信号很典型——网络空闲(冷读能到 0.7GB/s)但 CPU 被抢得凶(load 121),正好印证正文”数据受限的真瓶颈在 getitem 的 CPU/争用,不在 NFS 带宽本身”(§2.2 / §3.4)。起训前 uptime+nvidia-smi 看一眼邻居,才知道今天分到的是宽松还是拥挤那种。
A7. “RAM 2015GB,buff/cache ~1.3TB” 是什么意思?2T 里包含了 1.3T cache 吗?
是的,1.3TB cache 包含在那 2TB 之内,不是额外多出来的。 用
free的模型看:1
总物理 RAM (2015GB) = used(进程真正占的) + buff/cache(借去做文件缓存, 此刻~1.3TB) + free(纯空闲)
2015GB = 物理内存总量,固定。buff/cache ~1.3TB = 这 2TB 当中此刻被内核拿去做 page cache(缓存读过的 NFS 文件)的那部分——是个状态切片,不是另一块内存。
关键:page cache 可回收(弹性中间层)。 它算”用掉了”,但进程一要内存内核立刻还回去 → 实际可用 ≈
free+buff/cache,不是只有free。对本任务的意义:这 1.3TB 正是文档说的”暖缓存”——首个 epoch 把 NFS 文件读进来后缓存在此,之后每步从 RAM 命中,不再走网络。这就是 NFS+mmap 稳态能追平本地盘的原因:数据其实都在这 1.3TB page cache 里,与它在 NFS 还是本地盘无关。
A8. NFS 挂载参数:NFSv3 / rsize=1MB / nconnect 是什么
挂载 NFS 时给的配置,决定客户端怎么跟远端存储服务器通信。
- NFSv3 = 协议版本。相对 v4 更老更简单,无状态:服务器不记”谁开了哪个文件”,每个请求自带全部信息。代价是每次访问都要重新确认元数据 → 这就是”暖读也躲不掉 GETATTR 网络往返”的协议根源。v4 的目录委托/复合操作能省元数据开销,但这台挂的是 v3,所以 per-file open/stat/getattr 成本省不掉,只能靠”少碰文件 / preload / mmap 少读”绕开。
- rsize=1MB = 单次 READ 请求一次最多拿多少字节。NFS 读大文件是切成一个个 READ 请求发,块越大 → 同样大小文件需要的往返越少 → 吞吐越高。1MB 已较大(默认常 128/256KB)。例:28MB 的 w2v 文件按 1MB 块 ≈ 28 个 READ。
- nconnect=N = 客户端到该 NFS 服务器开几条并行 TCP。默认只 1 条,单条 TCP 喂不满 10GbE;开 N 条让请求分散 → 更易打满网卡(尤其多 worker 并发读)。所以单卡
nconnect=16比 8 卡nconnect=4更容易、更快逼近网卡上限(单卡并发裸读 ~1293MB/s 就靠它)。 - ⚠️ **nconnect 提高的是”能不能打满网卡”,不是”网卡上限”**。硬天花板仍是单口 10GbE ≈ 1.25GB/s,nconnect 再大也突破不了 → 两边聚合带宽仍一致,只是单卡更快逼近。
A9. preload 往哪放?匿名内存 vs page cache,以及”共享 RAM 不够时”会怎样
承接 §3.1 / §3.1bis 的”preload 会 COW/近 OOM”。这里补底层机制:preload 把数据放进「不可回收的匿名内存」,在共享节点上先挤掉别人的缓存、真顶到墙时直接被 OOM 杀,而不是报”内存不够”。 关键内核参数已在本 pod 实测确认(2026-07-01):
swap=0、vm.overcommit_memory=1、cgroupmemory.max=max且memory.low/min=0。
先分清两种 RAM 占用(回收性天差地别)
| 是什么 | free 里算 |
能被回收吗 | |
|---|---|---|---|
| 匿名内存(anonymous) | np.load/astype 出来的数组、进程堆 |
used | ❌ 只能 swap out;本机 swap=0 → 一旦占上就钉死,不可回收 |
| page cache(文件缓存) | 读过的 NFS/本地文件、mmap 的文件页 | buff/cache | ✅ 内核随时可丢(内容在盘上,重读即可) |
→ preload 灌的是匿名内存(§2.2:astype(f32) 碰所有页把它实体化);mmap 走的是 page cache。这一条决定了下面的全部差异。
共享节点上 preload 撑大匿名堆的三个阶段
1 | flowchart TD |
- 阶段2「别人的缓存被挤掉了吗?」→ 会,且不分你我。 page cache 是整节点共享的弹性层,
memory.low/min=0→ 没有任何租户的 cache 受保护。你的匿名堆一涨,内核按 LRU 回收 page cache,包括邻居正在暖用的文件缓存 → 邻居下次读从”暖读命中 RAM(4–6GB/s)”跌回”冷读走 NFS(百 MB 级 + 每文件 GETATTR 往返)”。你把邻居拖慢了,却收不到任何报错(隐形拖累)。你自己被 preload 那些文件的 cache 也一起丢——但无所谓,数据已在堆里(§3.1「preload 后那些 cache 纯冗余可回收」)。 - 阶段3「RAM 不够时 preload 怎么放?」→ 不会优雅失败,而是被 OOM 杀。 因为
overcommit_memory=1:- 分配阶段永远成功:
malloc/mmap/np.empty无论物理内存够不够都立刻返回指针 → Python **拿不到干净的MemoryError**,照常往下跑。 - 死在”碰页”那一刻:物理页惰性提交,
astype(f32)逐页写才真正吃内存;当 free + 可回收 cache 都归零、又没 swap,内核在某次缺页凑不出页 → 触发 OOM Killer。 - 杀谁不可控:
memory.max=max(无 cgroup 上限)→ OOM 是宿主机级全局,按oom_score(≈总 RSS×调整) 挑最肥的杀。1.6TB 的 preload 进程通常就是靶子(多半杀你自己),但邻居更肥就杀邻居。
- 分配阶段永远成功:
结论(为什么 mmap 是共享节点上的稳妥解)
- preload 大特征 = 往不可回收的匿名堆灌数据 → 先无声吃掉整节点(含邻居)的 page cache,真顶到墙时因 swap=0 + overcommit=1 直接 OOM 杀进程(而非报错软着陆)。
- mmap(§3.2/§3.3)走可回收的 page cache,是内核级真共享,不受 refcount/COW 影响;内存紧张时最多被回收→变慢(回退冷读),不会 OOM。这就是”共享节点上要么 preload+
num_workers=0、要么干脆 NFS+mmap”的底层原因。 - 实测锚点(2026-07-01,本 pod):当前训练 job 真实占用(PSS)≈ 244GB,其中匿名 ≈ 233GB —— 远不是满量 w2v f32 preload 的 1.37T(那是理论峰值)。同机 used 才 311G、cache 668G,离 2T 天花板尚宽松;但正因
swap=0 + overcommit=1 + 无 cgroup 保护,越过 2T 的瞬间不是变慢而是 OOM,所以 §3.1bis”1.6T<2T 仍不推荐”成立。