01 · 设备空闲,不一定是模型算得慢
一批训练数据到设备前,可能经过存储读取、解压、解析、分词、样本筛选、打包和Tensor整理,最后从主机内存复制到设备。只优化模型内核,无法消除输入迟迟不到造成的等待。
推理也有请求解析、分词和元数据准备,但输入不能在请求到来前凭空生成。训练数据可提前组织;在线服务则要兼顾新请求延迟和其他请求的公平性。
02 · 把输入链路的瓶颈拆开看
| 位置 | 可能的瓶颈 | 可以尝试的优化 |
|---|---|---|
| 存储/网络读取 | 小文件、随机访问、远端抖动 | 分片文件、顺序读取、本地缓存和合适并发。 |
| CPU预处理 | 分词、解析、解压或大量小对象 | 批量处理、提前计算可复用结果、工作进程。 |
| 拼批/整理 | 长度分布、复制碎片、重复转换 | 长度分组、有效token预算、减少不必要复制。 |
| 主机缓冲 | 数据尚未准备好、锁页成本高 | 受控预取、复用合适缓冲和按后端处理锁页。 |
| 设备复制 | 传输慢、提交后立刻阻塞 | 批量搬运、可支持的异步复制与stream依赖。 |
缓存预处理结果时,要把Tokenizer版本、词表、模板、清洗/截断配置和数据版本纳入兼容条件。随机增强或随epoch变化的步骤不应无条件变成固定缓存。
03 · 预取不是少做工作,是早点做
没有预取时,设备算完批次1后,才开始准备批次2。预取则在当前批次计算期间准备下一批,使其在设备需要时已就绪。预取队列存放这些已准备但尚未消费的数据。
队列过浅可能被输入抖动耗尽;过深占用RAM、锁页内存或设备缓冲,却不会让持续慢于消费者的生产者无限提速。队列装满时应限制生产,让内存保持可控,这种机制称为背压。
生产者:读取/预处理 → 放入有容量上限的ready队列
消费者:取出就绪批次 → 复制 → 执行设备计算
队列空:消费者等待输入
队列满:生产者等待容量
# 缓存与排队提高容错余量,不能改变最慢阶段的长期速率。04 · 四批数据如何形成输入流水
自设每批CPU准备3 ms、复制1 ms、设备计算6 ms,四批独立数据。一批接一批完全串行为4×(3+1+6)=40 ms;若三个阶段可独立推进、有足够缓冲且依赖正确,四批于28 ms完成。
准备、复制、计算:每批仍须按依赖推进
一个资源负责一个阶段,批次独立,缓冲充足;计算等自己的复制完成,复制等自己的准备完成。无运行时开销与争用,不代表实测。批次1在0–3 ms准备、3–4 ms复制、4–10 ms计算;同时批次2可在3–6 ms准备。下一批计算仍要等自己的复制完成,并等设备计算资源空闲。
05 · pinned memory与non_blocking分别解决什么
在CUDA常见路径中,pinned memory是锁页主机内存,有利于设备DMA搬运。non_blocking=True请求减少主机等待,但“CPU调用提前返回”与“复制确实和计算同时执行”是两件事。
复制与计算要实际重叠,还需要后端支持、合适的复制执行资源、不同队列中可独立的工作和正确依赖。同一个stream里按顺序复制再计算,不能仅因加了non_blocking就宣布二者重叠。
在热点线程里临时pin_memory再复制,也要付出分配和数据复制成本,可能得不偿失。宜观察整个输入链路,而不是只测已经准备好的锁页数据。
06 · 复制未完成,源缓冲不能被下一批覆盖
| 缓冲状态 | 允许做什么 | 危险操作 |
|---|---|---|
| 空闲 | 写入本批CPU数据 | 无 |
| 复制进行中 | 保留源内容与目标容量,等待复制完成 | 重填主机源、释放仍被使用的内存。 |
| 设备输入已就绪 | 按依赖启动计算 | 在计算结束前覆写设备输入。 |
| 消费完成 | 回收对应缓冲给后续批次 | 错误地只等复制结束就复用设备输入。 |
可以让不同批次使用不同槽位,例如双缓冲,但每个槽位都要知道复制和计算何时完成。GPU到CPU的异步结果复制也要等完成后才能读;接口返回不等于CPU结果已可用。
框架管理的Tensor引用、跨stream依赖与分配器生命周期都要遵守其约定。显式缓冲池还要防止取消请求或异常路径过早释放资源。
依据:PyTorch · pin_memory与non_blocking使用指南 ↗ · PyTorch 2.14 · CUDA stream、等待与内存生命周期 ↗
07 · DataLoader的几个参数要一起看
| 参数/策略 | 作用 | 边界 |
|---|---|---|
| num_workers | 用工作进程并行加载/预处理 | 并非越大越好,CPU/存储/内存可能争用。 |
| prefetch_factor | 多worker时每个worker提前准备的批次数 | 目标预取量随worker数增加,不是设备预取深度。 |
| persistent_workers | 跨数据迭代保留worker | worker内部状态与随机性需要管理。 |
| IterableDataset分片 | 按worker和分布式rank划分输入流 | 不划分可能重复读取/训练相同数据。 |
例如4个worker、prefetch_factor=2意味着目标预取8批。若每批CPU数据约64 MiB,仅这些批次就约512 MiB,尚未计工作进程、正在拼装的批次、锁页暂存与设备副本。真实峰值应实测。
08 · 先实现稳定加载,再做跨队列重叠
from torch.utils.data import DataLoader
def main():
loader = DataLoader(dataset, batch_size=..., num_workers=2,
pin_memory=True, prefetch_factor=2,
persistent_workers=True)
for batch in loader:
device_batch = batch.to("cuda", non_blocking=True)
train_step(device_batch)
if __name__ == "__main__":
main()这个片段展示加载和主机非阻塞复制入口,不保证设备复制与计算重叠。真正的copy stream预取器还要保留源数据、在计算队列等待对应完成事件,并管理Tensor/缓冲释放。不要在每次复制后立刻全设备同步,否则会丢掉可重叠的机会。
下方CPU脚本验证输入流水的时间和每批依赖,并演示缓冲状态检查。它不启动真实DataLoader、DMA或设备stream。
昇腾CANN的aclrtMemcpyAsync文档同样区分任务下发与完成,并规定Host锁页与非锁页内存的不同返回行为。CUDA与ACL、torch_npu接口的细节要按产品和安装版本核对,不能只按函数名有Async就推断。
依据:PyTorch 2.14 · DataLoader与输入数据组织 ↗ · CANN Community 9.1.0 · aclrtMemcpyAsync与Host内存约束 ↗
09 · 先找到最慢一段,再调并发和队列
- 计时next(loader)、CPU整理、复制、设备计算及整步,区分等待与实际工作。
- 观察稳态消费速率、队列深度和偶发长尾;预热时队列已装满可能掩盖生产不足。
- 记录RAM、锁页内存、设备输入缓冲及worker总资源,不只看模型显存。
- 验证样本不丢不重、分词配置一致,分布式分片与epoch随机性正确。
- 每秒有效token及训练质量不变时比较吞吐;推理还测请求解析/分词和首token时间。
自查:预取队列为何不能修复永久过慢的存储?为什么同stream里的non_blocking复制未必与计算重叠?复制完成后为什么还不能立刻覆盖正在计算的设备输入?
流程位置与关联方法
这个方法可能出现在多个位置;计算线包含模型计算与算子实现,执行线包含运行时、内存和通信。
资料与核查范围
原理依据以原始论文和官方文档为主;算例与时间线是教学推导。文档核查日期:2026-10-10。引用 main / latest 文档时,实际接口以安装版本为准。
- PyTorch 2.14 · DataLoader与输入数据组织 ↗
- PyTorch · pin_memory与non_blocking使用指南 ↗
- PyTorch 2.14 · CUDA stream、等待与内存生命周期 ↗
- CANN Community 9.1.0 · aclrtMemcpyAsync与Host内存约束 ↗
代码为接入片段或教学伪代码;本页没有声称完成 NVIDIA / 昇腾硬件实测。