minimind/course/notes/15_ddp_sampler_notes.md

179 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 第15节:数据加载器构建(DDP + 断点续训 + batch 化)
> 核心目标:在 DDP 或单卡环境下,构造一个"可 shuffle + 可恢复 + 可 batch 化"的数据加载器
---
## 📚 本节目录
1. [DDP sampler + epoch shuffle 控制](#1-ddp-sampler--epoch-shuffle-控制)
2. [单卡随机索引 fallback](#2-单卡随机索引-fallback)
3. [断点续训 step 计算](#3-断点续训-step-计算)
4. [SkipBatchSampler = 数据源 + batch切分 + resume控制](#4-skipbatchsampler--数据源--batch切分--resume控制)
5. [总体流程图](#5-总体流程图)
6. [一句话终极总结](#6-一句话终极总结)
---
## 1. DDP sampler + epoch shuffle 控制
```python
train_sampler and train_sampler.set_epoch(epoch)
```
**含义**:如果是 DDP(`train_sampler` 不为空),告诉 sampler 当前是第几个 epoch
**作用**:让每个 epoch 的 shuffle 在所有 GPU 上"同步变化"
**原理**:DistributedSampler 在 shuffle 时用随机种子 `seed = base_seed + epoch`
---
## 2. 单卡随机索引 fallback
```python
setup_seed(42 + epoch)
indices = torch.randperm(len(train_ds)).tolist()
```
**含义**:
- 设置随机种子(保证可复现)
- 生成 shuffle 后的 index 序列
**作用**:单卡情况下模拟"shuffle dataset"
---
## 3. 断点续训 step 计算
```python
skip = start_step if (epoch == start_epoch and start_step > 0) else 0
```
**含义**:
- 只有在恢复训练的起始 epoch 才跳过 batch
- 如果是恢复 epoch → skip 前面 step
- 否则 → 不跳
**作用**:实现"从某个 global step 继续训练"
---
## 4. SkipBatchSampler = 数据源 + batch切分 + resume控制
### 构造方式
```python
batch_sampler = SkipBatchSampler(
train_sampler or indices, # 数据源:DDP切分 or 单卡shuffle列表
args.batch_size, # batch大小
skip # 跳过前N个batch(resume用)
)
```
### 三合一机制
| 组件 | 作用 |
|------|------|
| `train_sampler or indices` | 数据从哪来:DDP分布式切分 / 单卡shuffle |
| `batch_size` | 把单个index聚成batch |
| `skip` | 跳过前N个batch(断点续训) |
### 源码解析
**跳过的是 batch,不是单条样本**
```python
class SkipBatchSampler(Sampler):
def __init__(self, sampler, batch_size, skip_batches=0):
self.sampler = sampler # 数据源(DDP切分 or shuffle列表)
self.batch_size = batch_size # 每个batch多少条
self.skip_batches = skip_batches # 跳几个batch
def __iter__(self):
batch = []
skipped = 0
for idx in self.sampler: # 遍历数据源
batch.append(idx)
if len(batch) == self.batch_size: # 凑满一个batch
if skipped < self.skip_batches: # 还在跳过区间
skipped += 1
batch = []
continue
yield batch # 正常输出batch
batch = []
# 处理尾巴(不足batch_size的剩余部分)
if len(batch) > 0 and skipped >= self.skip_batches:
yield batch
```
### 执行流程举例
```
数据: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
batch_size = 3
skip_batches = 2
凑batch过程:
[0], [1], [2] → 凑满 → 第1个batch → 跳过
[3], [4], [5] → 凑满 → 第2个batch → 跳过
[6], [7], [8] → 凑满 → 第3个batch → 输出
[9] → 凑不满 → 但已跳过2个 → 输出
输出: [[6,7,8], [9]]
```
### DataLoader 最终封装
```python
loader = DataLoader(
train_ds,
batch_sampler=batch_sampler, # SkipBatchSampler控制顺序
num_workers=args.num_workers,
pin_memory=True
)
```
**DataLoader 只做**:
| 功能 | 说明 |
|------|------|
| 按 batch_sampler 取 batch | 顺序由 SkipBatchSampler 控制 |
| 多进程加载 | `num_workers` |
| CPU→GPU 加速传输 | `pin_memory` |
**注意**:用了 `batch_sampler` 后,DataLoader 不再负责 shuffle 或 batch_size
---
## 5. 总体流程图
```
DDP?
↓
train_sampler.set_epoch(epoch)
↓
┌────────────────────┐
│ │
│ DDP Single GPU
│ ↓ ↓
│ DistributedSampler randperm(indices)
└──────────┬─────────┘
↓
train_sampler or indices
↓
SkipBatchSampler(batch_size, skip)
↓
batch_sampler
↓
DataLoader(loader)
↓
model
```
---
## 6. 一句话终极总结
**这段代码通过 DDP sampler + 随机索引 + skip 控制,构造了一个支持分布式 shuffle 和断点续训的 batch-level 数据流,最终由 DataLoader 负责并行加载。**