# PyTorch 随机种子（Seed）全面解释


## 随机种子（Seed）全面解释

在深度学习和计算机科学中，**随机种子**是一个用于初始化伪随机数生成器（PRNG）的数值。通过设置相同的种子，可以确保程序在每次运行时生成相同的随机数序列，从而实现**实验的可重复性**。在 PyTorch 等框架中，随机种子控制着模型参数初始化、数据打乱、数据增强、Dropout 等几乎所有涉及随机性的操作。

---

## 1. 为什么需要随机种子？

- **可重复性**：科学研究要求实验能够被复现。设置固定种子后，其他人（或未来的你）运行同一份代码应得到完全相同的结果。
- **调试**：当模型出现异常时，固定随机性有助于定位问题，排除随机波动干扰。
- **公平比较**：在对比不同算法或超参数时，需要控制随机因素，确保差异来自方法本身而非随机噪声。

---

## 2. PyTorch 中的随机种子设置

PyTorch 使用自己的 PRNG，同时也可能依赖其他库（如 NumPy、Python `random`）。要全面固定随机性，需要同时设置多个种子：

```python
import torch
import numpy as np
import random

def set_seed(seed):
    torch.manual_seed(seed)                 # CPU 随机种子
    torch.cuda.manual_seed(seed)            # 当前 GPU 随机种子
    torch.cuda.manual_seed_all(seed)         # 所有 GPU 随机种子（如果有多卡）
    np.random.seed(seed)                     # NumPy 随机种子
    random.seed(seed)                         # Python 内置随机模块
    # 可选：设置 cuDNN 为确定性算法（可能降低性能）
    torch.backends.cudnn.deterministic = True
    torch.backends.cudnn.benchmark = False
```

**注意事项**：
- `torch.manual_seed` 同时设置 CPU 和当前 GPU 的种子（若 CUDA 可用）。
- `torch.cuda.manual_seed_all` 在多 GPU 环境下为所有 GPU 设置相同种子，确保每张卡的随机性一致。
- `torch.backends.cudnn.deterministic = True` 强制 cuDNN 使用确定性算法，但可能使某些操作变慢。
- `torch.backends.cudnn.benchmark = False` 禁用 cuDNN 的自动优化选择，避免因算法选择引入的不确定性。

---

## 3. DataLoader 中的随机种子与 worker 进程

### 3.1 问题背景
当使用多进程数据加载（`num_workers > 0`）时，每个 worker 是一个独立的子进程，它们会复制主进程的 PRNG 状态。如果所有 worker 使用相同的初始种子，它们将产生完全相同的数据顺序，导致训练时每个 batch 内的样本高度相似（例如所有 worker 同时打乱，但每个 worker 内的打乱结果相同），这会破坏数据随机性并可能降低模型性能。

### 3.2 PyTorch 的自动种子管理
PyTorch 的 `DataLoader` 在创建 worker 进程时，会自动为每个 worker 分配**不同的初始种子**，具体规则如下：
- 每个 worker 的种子基于**主进程的当前种子**和**worker 的 ID** 通过一个固定公式生成。
- 因此，即使不设置 `worker_init_fn`，每个 worker 内部的 PyTorch 随机操作（如数据集的 `__getitem__` 中如果使用了 `torch.rand`）也会有独立的行为。

然而，这个自动机制**只保证 PyTorch 自己的 PRNG 在 worker 之间不同**，**不会自动设置 NumPy 或 Python `random` 的种子**。如果数据预处理中使用了这些库，就需要在 `worker_init_fn` 中手动设置。

### 3.3 使用 `worker_init_fn` 确保全面随机性
```python
def worker_init_fn(worker_id):
    # 获取主进程为每个 worker 分配的种子（PyTorch 内部机制）
    seed = torch.initial_seed() % 2**32
    np.random.seed(seed)
    random.seed(seed)

dataloader = DataLoader(dataset, batch_size=32, num_workers=4,
                        worker_init_fn=worker_init_fn)
```

- `torch.initial_seed()` 返回当前 PyTorch PRNG 的种子值，该值在 worker 中已被 PyTorch 自动设为与 worker_id 相关的唯一值。
- 我们将此值模 2^32 后传递给 NumPy 和 Python `random`，确保它们的随机序列也与 worker_id 唯一关联，且与 PyTorch 的随机性协调。

**重要**：`worker_init_fn` 仅在 worker 进程启动时调用一次（即使 `persistent_workers=True`，也只调用一次），因此 worker 内部的随机状态在整个生命周期中保持一致。

---

## 4. 多 GPU 和分布式训练

在分布式训练中，每个进程通常负责一部分数据，且每个进程有自己的 PRNG。为了确保全局可重复性，需要：
- 在所有进程中设置相同的初始种子。
- 同时确保每个进程（或每个 worker）的随机性彼此独立，避免数据重叠或同步问题。

通常做法是：在初始化每个进程时调用 `set_seed(global_seed + rank)`，其中 `rank` 是进程的全局编号。这样既保持了全局固定，又保证了进程间的独立性。

```python
import torch.distributed as dist

def setup(rank, world_size, seed):
    torch.manual_seed(seed + rank)
    # 同样设置其他库
```

对于 DataLoader 的 worker，PyTorch 的自动种子机制已经考虑了 worker 的局部 rank，因此无需额外处理。

---

## 5. 常见陷阱与最佳实践

### 5.1 随机种子不是“全局开关”
设置种子仅影响初始化后的随机操作。如果代码中某些部分在设置种子之前已经使用了随机数，则无法保证可重复性。因此，**应在程序的最开始调用设置函数**。

### 5.2 cuDNN 的不确定性
cuDNN 的一些算法（如卷积）默认是非确定性的，即使设置了相同的种子，多次运行结果也可能有微小差异。通过设置：
```python
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
```
可以强制 cuDNN 使用确定性算法，但会牺牲一定性能。如果允许轻微浮动，可以不设置这两个选项。

### 5.3 哈希和集合顺序
Python 中的字典、集合等容器的迭代顺序在 Python 3.6+ 中虽已稳定，但某些操作（如 `set` 的遍历）仍可能因随机哈希而不同。可以通过设置环境变量 `PYTHONHASHSEED` 来固定哈希种子：
```python
import os
os.environ['PYTHONHASHSEED'] = str(seed)
```
这应在导入任何模块之前设置。

### 5.4 多线程环境
如果使用了多线程（例如在数据预处理中），每个线程的随机性也需要独立控制。但通常 `DataLoader` 的 worker 已经是多进程，线程安全问题较少。

### 5.5 可重复性的局限性
完全的可重复性很难保证，尤其是在以下情况下：
- 使用了非确定性 GPU 操作（如某些原子操作）。
- 硬件或驱动版本不同。
- 浮点运算顺序变化（例如并行归约）。

因此，通常追求的是**算法层面的可重复性**，而非逐比特一致。

---

## 6. 总结

| 组件 | 种子设置方式 | 注意事项 |
|------|--------------|----------|
| PyTorch (CPU/GPU) | `torch.manual_seed()`, `torch.cuda.manual_seed_all()` | 覆盖所有 PyTorch 操作 |
| NumPy | `np.random.seed()` | 在 worker 中需单独设置 |
| Python random | `random.seed()` | 同上 |
| DataLoader worker | 自动设置 PyTorch 种子；需在 `worker_init_fn` 中设置 NumPy 和 random | 确保不同 worker 独立随机 |
| cuDNN | `torch.backends.cudnn.deterministic = True` | 保证卷积等操作的确定性 |
| Python 哈希 | `PYTHONHASHSEED` 环境变量 | 避免容器顺序随机性 |

**最终建议**：编写一个统一的 `set_seed` 函数，在程序入口处调用，并在 `worker_init_fn` 中为每个 worker 同步设置其他库的种子，这样可以最大程度保证实验的可重复性。


在 `worker_init_fn` 中，我们通常这样写：

```python
seed = torch.initial_seed() % 2**32
np.random.seed(seed)
random.seed(seed)
```

---
---
---
---


## 为什么要对 `torch.initial_seed()` 取模 $2^{32}$（即 4294967296）呢？主要原因是为了**确保种子值落在目标随机数生成器所期望或兼容的范围内**，从而避免潜在的跨平台或版本差异，保证可重复性。


## 1. 不同库对种子值的处理方式

- **PyTorch**：`torch.initial_seed()` 返回的是一个 Python `int`，其值可以很大（例如 64 位甚至更大）。PyTorch 内部使用 64 位或更高精度的种子来初始化其 PRNG（基于 Philox 算法等），因此大整数没有问题。
- **NumPy**：`np.random.seed()` 接受的参数最终会传递给 C 语言的 `mt19937` 实现，该实现期望一个 **32 位无符号整数**。如果传入的整数大于 $2^{32}-1$，NumPy 会将其**截断为低 32 位**（相当于隐式取模 $2^{32}$）。
- **Python 内置 `random`**：`random.seed()` 接受任意大小的整数，但内部会通过一个哈希函数将其转换为一个用于初始化 Mersenne Twister 的 32 位种子。虽然它允许大整数，但哈希结果仍然是一个 32 位的值；并且不同 Python 版本或平台对哈希的处理可能略有差异，直接传入大整数可能引入不确定性。

因此，**直接传递 `torch.initial_seed()` 的原始值（可能很大）给 NumPy 或 Python `random` 是可以工作的**，因为它们最终都会将其映射到 32 位空间。但这种隐式映射（截断或哈希）的具体行为依赖于实现，可能在不同版本或平台上产生细微差异，从而破坏实验的可重复性。

---

## 2. 显式取模 $2^{32}$ 的好处

- **明确意图**：显式取模清楚地表明我们希望将种子限制在 32 位范围内，避免依赖隐式截断。
- **跨平台一致性**：无论在哪个系统或库版本下，取模运算的结果都是确定的，确保了随机数序列的一致性。
- **兼容性**：虽然当前版本可能处理大整数没问题，但未来库的更新可能改变隐式处理方式。显式取模可以防止这种潜在的不兼容。
- **理论合理性**：$2^{32}$ 是 32 位无符号整数的模数，取模后得到的值均匀分布在 $[0, 2^{32}-1]$ 区间，这对 PRNG 的初始化来说已经足够（32 位种子空间对于大部分非加密应用完全够用，且能保证不同 worker 之间的随机性独立）。

---

## 3. 为什么是 $2^{32}$ 而不是其他数？

- 历史上，Mersenne Twister（NumPy 和 Python `random` 使用的算法）的初始化通常需要一个 32 位种子。
- 许多随机数生成器的种子接口都设计为接受 32 位整数。
- 模 $2^{32}$ 操作简单且保留了原始种子的低 32 位信息，不会引入额外的偏差。

---

## 4. 如果不取模会怎样？

假设 `torch.initial_seed()` 返回 `12345678901234567890`（远大于 $2^{32}$）：
- 在 NumPy 中，`np.random.seed(12345678901234567890)` 实际上会隐式转换为 `12345678901234567890 & 0xFFFFFFFF`，结果等于 `12345678901234567890 % 2**32`。所以最终效果与显式取模一致，但依赖隐式截断。
- 在 Python `random` 中，`random.seed(12345678901234567890)` 会将大整数通过哈希函数转换为状态。虽然它也会得到一个确定的结果，但哈希函数的具体实现可能在不同 Python 微版本中变化，而取模则不受影响。

因此，显式取模是**更安全、更可移植**的做法。

---

## 5. 总结

- **取模 $2^{32}$ 是为了将 PyTorch 的种子值（可能很大）规范化为一个 32 位范围内的整数**，符合 NumPy 和 Python `random` 内部 PRNG 的期望输入范围。
- 它消除了依赖隐式截断或哈希的不确定性，确保在多 worker 环境下每个 worker 的随机数生成器具有可预测且独立的种子，从而保证实验的完全可重复性。
- 这是一种广泛采用的编程惯例，体现了对跨平台兼容性和代码清晰度的追求。
