加载器一直交出同一个 Err
主线位置:训练主线的入口,forward 之前:取下一个 batch 这一步出错了。加载器的整体对照见 Dataset 与 DataLoader。
版本:PyTorch 2.15.0(CPU)、burn 0.22.1(Flex 设备)
flowchart TB n["next"] --> g["get_many 4..8"] g -->|"样本 5 失败"| e["返回 Err"] e --> s["current_index 不变"] s -->|"日志后 continue"| n g -->|成功| a["前进 交出 batch"]
实验代码:PyTorch 侧 loader_err.py,burn 侧 crate pitb-exec-data(burn =0.22.1,default-features = false,feature std、flex、autodiff、optim、dataset,release)。数据集 10 个样本,批大小 4,第 5 个样本读不出来。平台 M2 Max、macOS 27、rustc 1.99.0。
PyTorch 的写法
数据集里偶尔有一个坏文件,训练不想因此中断,常见的写法是把 next 包在 try 里,记一条日志,接着取下一批:
it = iter(DataLoader(Flaky(n), batch_size=4, num_workers=workers))
steps, errors, firsts = 0, 0, []
while True:
try:
ids, x = next(it)
except StopIteration:
break
except OSError as e:
errors += 1
print(f"skipping bad batch: {str(e).splitlines()[0]}")
continue
steps += 1
firsts.append(ids[0].item())
10 个样本、单进程,以及 40 个样本、0 个和 2 个 worker:
step on batch [0, 1, 2, 3]
skipping bad batch: cannot read sample 5
step on batch [8, 9]
10 samples, 0 workers: loop ended, 2 steps, 1 errors, first ids [0, 8]
skipping bad batch: cannot read sample 5
40 samples, 0 workers: loop ended, 9 steps, 1 errors, first ids [0, 8, 12, 16, 20, 24, 28, 32, 36]
skipping bad batch: Caught OSError in DataLoader worker process 1.
40 samples, 2 workers: loop ended, 9 steps, 1 errors, first ids [0, 8, 12, 16, 20, 24, 28, 32, 36]
DataLoader 的迭代器在出错之后照样往下走:坏样本所在的那一批([4, 5, 6, 7])丢掉,其余的批一个不少,循环正常结束。
搬到 burn 的现象
burn 的迭代器不抛异常,交出 Result<Batch, DatasetError>。同样的意图写成 match 加 continue。这个循环停不下来,所以实验里加了 take(cap) 兜底,同时数一下 Dataset::get 被调了多少次,最后看迭代器自己报的进度:
let (l, calls) = flaky_loader(0);
let t = Instant::now();
let mut it = l.iter();
let (mut ok, mut err) = (0usize, 0usize);
for b in it.by_ref().take(cap) {
match b {
Ok(b) => {
ok += 1;
println!("step on batch {:?}", b.ids);
}
Err(e) => {
err += 1;
if err <= 3 {
println!("skipping bad batch: {e}");
}
continue;
}
}
}
let p = it.progress();
cap 取 1000 和 100000:
step on batch [0, 1, 2, 3]
skipping bad batch: cannot read sample 5
skipping bad batch: cannot read sample 5
skipping bad batch: cannot read sample 5
take(1000): 1 Ok, 999 Err, get() called 2002 times, 1.283ms; progress 4/10
step on batch [0, 1, 2, 3]
skipping bad batch: cannot read sample 5
skipping bad batch: cannot read sample 5
skipping bad batch: cannot read sample 5
take(100000): 1 Ok, 99999 Err, get() called 200002 times, 12.056ms; progress 4/10
- 第一批之后,每一次
next都是同一个Err(cannot read sample 5),[8, 9]永远到不了。take给多少就交出多少个Err。 - 每次重试调两次
get:样本 4 成功,样本 5 失败。第一批 4 次,加上cap - 1次重试,每次 2 次,正好2002和200002。 - 进度停在
4/10,不再变化。 - 10 万次只用了 12 毫秒。每次都打一行日志的话,一秒钟就是几百万行。Dataset 与 DataLoader 那一页的实验里,没有
take的版本两分钟左右写出了 5 GB 以上的输出。
原因
单线程加载器的迭代器是 BatchDataloaderIterator::next:
fn next(&mut self) -> Option<Self::Item> {
while self.current_index < self.len {
let chunk_size = self
.strategy
.batch_size()
.unwrap_or(1)
.min(self.len - self.current_index);
let indexes = (self.current_index..self.current_index + chunk_size).collect();
let items = match self.dataset.get_many(indexes) {
Ok(items) => items,
Err(err) => return Some(Err(err)),
};
self.current_index += chunk_size;
// ...
get_many 出错时直接返回,current_index += chunk_size 那一行没有执行。下一次 next 从同一个下标开始,再取同一段 4..8。Dataset::get_many 的默认实现按顺序调 get,遇到第一个错误用 ? 返回,所以每次重试是样本 4、样本 5 两次调用。这个迭代器没有「已经出过错」的状态,也不跳过出错的那一段。
多线程加载器的 worker 在 Some(Err(dataset_err)) 分支里把错误发给主线程,然后 break。不会死循环,但这个 worker 那一段剩下的批不会再来。40 个样本、10 批、2 个 worker,每个 worker 分到连续的 5 批(bin mtlost,取前 100 个结果):
Err(dataloader worker 0 failed: cannot read sample 5)
2 workers, 40 samples (10 batches), item 5 fails: 6 Ok batches, first ids [0, 20, 24, 28, 32, 36]
PyTorch 同样的设置交出 9 批,burn 交出 6 批:worker 0 在第二批出错后退出,它后面的 [8..12)、[12..16)、[16..20) 三个好的批也没了,迭代器照常结束,只有一个 Err 提示。
origin/main(2026-10-28)的 BatchDataloaderIterator 加了一个 exhausted 标志,迭代器交出 None 之后再调 next 直接返回 None;出错的那条路径没有改,仍然在推进下标之前返回,也不设这个标志,所以上面的死循环在 main 上不变。多线程 worker 出错后 break 的行为在 main 上也没有变。这是上游的问题。
改法
遇到 Err 就结束这个 epoch。 用 ? 把错误交给调用方,或者 break:
let r = (|| {
for b in l.iter() {
let b = b?;
steps += 1;
println!("step on batch {:?}", b.ids);
}
Ok::<(), DatasetError>(())
})();
step on batch [0, 1, 2, 3]
loop ended after 1 steps, get() called 6 times: Err(DatasetError(Custom { kind: Other, error: "cannot read sample 5" }))
要 PyTorch 那种「跳过坏数据、继续训练」,在数据集这一层处理。 加载器看到的 get 不再失败:把样本类型换成 Option<Sample>,坏样本变成 None,Batcher 把 None 去掉:
struct SkipBad<D>(D);
impl<D: Dataset<Sample>> Dataset<Option<Sample>> for SkipBad<D> {
fn get(&self, index: usize) -> Result<Option<Sample>, DatasetError> {
match self.0.get(index) {
Ok(s) => Ok(Some(s)),
Err(e) => {
eprintln!("skipping sample {index}: {e}");
Ok(None)
}
}
}
fn len(&self) -> usize {
self.0.len()
}
}
impl Batcher<Option<Sample>, Batch> for MlpBatcher {
fn batch(&self, items: Vec<Option<Sample>>, device: &Device) -> Batch {
Batcher::<Sample, Batch>::batch(self, items.into_iter().flatten().collect(), device)
}
}
0 个和 2 个 worker,各跑两个 epoch(每个 epoch 用 take(10) 兜底):
skipping sample 5: cannot read sample 5
0 workers, epoch 0: [[0, 1, 2, 3], [4, 6, 7], [8, 9]]
skipping sample 5: cannot read sample 5
0 workers, epoch 1: [[0, 1, 2, 3], [4, 6, 7], [8, 9]]
get() called 20 times
skipping sample 5: cannot read sample 5
2 workers, epoch 0: [[8, 9], [0, 1, 2, 3], [4, 6, 7]]
skipping sample 5: cannot read sample 5
2 workers, epoch 1: [[8, 9], [0, 1, 2, 3], [4, 6, 7]]
get() called 20 times
每个样本每个 epoch 只取一次,两种加载器都正常结束。和 PyTorch 的差别是:PyTorch 丢掉整批 [4, 5, 6, 7],这里只丢样本 5,那一批变成 3 个。批大小会变,Batcher 要能处理;一批全是坏样本时,flatten 之后交给它的是空的 Vec。多 worker 时批的先后不固定,原因见 Dataset 与 DataLoader 的「多 worker」一节。
怎么检查
- 搜训练循环里
Err(分支:分支里只有日志和continue的,就是这个坑。 - 给 epoch 设一个上限:每个 epoch 的批数不会超过
ceil(num_items() / batch_size),超过就是在重复交出同一个结果。 - 每个 epoch 结束时数一下拿到的样本数,和
num_items()对一下:单线程加载器卡住时它停在出错之前,多 worker 时会少掉出错 worker 的后半段。 - 测试里放一个故意失败的样本,用
take(n)跑一遍,确认循环在有限步内结束。
小结
- PyTorch 的
DataLoader出错后照样前进,「接住异常、记日志、continue」只丢掉坏的那一批。 - burn 0.22.1 的单线程加载器出错时不推进下标,之后每次
next都是同一个Err:10 万次 12 毫秒,循环永远不结束;origin/main 上没有改。 - 多 worker 加载器不会死循环,但出错的 worker 直接退出,它那一段后面的好批也一起丢了(40 个样本里 9 批对 6 批)。
- 改法:遇到
Err用?或break结束;要跳过坏样本,在Dataset里把它换成None,在Batcher里去掉。