【Bug已解决】PyTorch‘s dataloader “too many open files“ error when no files should be open 解决方案
【Bug已解决】PyTorchs dataloader too many open files error when no files should be open 解决方案问题描述在使用 PyTorch 的DataLoader进行多进程数据加载时许多开发者会遇到一个令人困惑的错误OSError: [Errno 24] Too many open files或RuntimeError: Too many open files. Communication with the workers is no longer possible.这个错误尤其令人困惑因为错误信息说太多打开的文件但开发者可能并没有在代码中显式打开任何文件——数据可能完全在内存中或者通过数据库连接获取。这种没有打开文件却报太多打开文件的矛盾让很多人束手无策。问题的根源在于操作系统对每个进程可以同时打开的文件描述符File Descriptor数量有上限。PyTorch 的DataLoader在使用多进程num_workers 0时每个 worker 子进程都会继承父进程的文件描述符并且主进程与每个 worker 之间需要建立通信管道pipe这些管道也占用文件描述符。当 worker 数量多、数据集大、或系统默认的文件描述符限制较低时就会触发这个错误。本文将深入分析这个问题的根因并提供从临时修复到永久解决方案的完整指南。错误复现错误场景一多 worker DataLoader 触发错误import torch from torch.utils.data import Dataset, DataLoader import numpy as np class InMemoryDataset(Dataset): 完全在内存中的数据集不打开任何文件 def __init__(self, num_samples10000, dim1000): # 所有数据都在内存中 self.data np.random.randn(num_samples, dim).astype(np.float32) self.labels np.random.randint(0, 10, num_samples) def __len__(self): return len(self.labels) def __getitem__(self, idx): # 没有文件操作 return torch.from_numpy(self.data[idx]), self.labels[idx] # 创建数据集 dataset InMemoryDataset(num_samples10000) # 使用大量 worker dataloader DataLoader( dataset, batch_size32, shuffleTrue, num_workers16, # 大量 worker persistent_workersTrue ) # 训练循环 for epoch in range(5): for batch_x, batch_y in dataloader: pass print(fEpoch {epoch} done)运行结果Epoch 0 done Epoch 1 done OSError: [Errno 24] Too many open files错误场景二文件数据集 多 workerimport torch from torch.utils.data import Dataset, DataLoader from PIL import Image import os class ImageFolderDataset(Dataset): def __init__(self, image_dir): self.image_paths [os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith(.jpg)] def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img Image.open(self.image_paths[idx]) # 打开文件 img img.resize((224, 224)) return torch.from_numpy(np.array(img)), 0 dataset ImageFolderDataset(/path/to/images) dataloader DataLoader(dataset, batch_size64, shuffleTrue, num_workers8) try: for batch_x, batch_y in dataloader: pass except OSError as e: print(fError: {e}) # OSError: [Errno 24] Too many open files: /path/to/images/xxx.jpg错误场景三查看当前文件描述符限制import resource # 查看当前文件描述符限制 soft_limit, hard_limit resource.getrlimit(resource.RLIMIT_NOFILE) print(fSoft limit: {soft_limit}) # macOS 默认 256Linux 默认 1024 print(fHard limit: {hard_limit}) # 通常为 4096 或更高根因分析1. 什么是文件描述符在 Unix/Linux/macOS 系统中一切皆文件。操作系统通过文件描述符File DescriptorFD来管理进程对各种资源的访问。以下资源都会占用文件描述符普通文件.txt,.jpg,.csv等网络套接字Socket管道Pipe标准输入/输出/错误stdin/stdout/stderrGPU 设备文件共享内存进程间通信的队列操作系统对每个进程可以同时打开的文件描述符数量设置了上限称为RLIMIT_NOFILE。2. DataLoader 多进程架构当num_workers 0时PyTorch 的DataLoader使用以下架构主进程 (Main Process) ├── Worker 0 ←→ [管道/共享内存] ←→ 主进程 ├── Worker 1 ←→ [管道/共享内存] ←→ 主进程 ├── Worker 2 ←→ [管道/共享内存] ←→ 主进程 ... └── Worker N ←→ [管道/共享内存] ←→ 主进程每个 worker 与主进程之间需要2 个管道双向通信每个管道占用 2 个文件描述符读端 写端共享内存用于高效传输数据信号量/锁用于同步因此每个 worker 大约占用4-6 个文件描述符。如果有 16 个 worker仅通信就需要约 64-96 个文件描述符。3. 文件描述符的累积文件描述符数量会在以下情况下累积情况一每个 epoch 重新创建 worker如果persistent_workersFalse默认每个 epoch 结束后 worker 被销毁下一个 epoch 重新创建。在销毁和创建之间如果文件描述符没有被正确释放例如由于 Python 的垃圾回收延迟就会累积。情况二子进程继承父进程的 FD当DataLoader创建 worker 子进程时子进程会继承父进程的所有打开的文件描述符。如果父进程已经打开了很多文件如模型文件、日志文件、数据库连接每个 worker 都会复制这些 FD。情况三PIL/Pillow 的图像文件PIL.Image.open()返回一个惰性加载的图像对象文件描述符在图像对象被垃圾回收之前不会关闭。如果在一个 batch 中打开了很多图像但没有及时关闭FD 就会累积。情况四PyTorch 内部资源PyTorch 自身也会占用文件描述符CUDA 上下文每个 GPU 进程约 10-20 个 FD共享缓存约 5-10 个 FD日志和调试信息4. macOS vs Linux 的差异系统默认软限制默认硬限制备注macOS256 或 4864无限macOS 限制较低更容易触发Ubuntu Linux10244096CentOS/RHEL10244096Docker 容器继承宿主机继承宿主机可能被 Docker 限制macOS 的默认限制特别低256这使得在 macOS 上开发时更容易遇到这个问题。5. 文件描述符计算公式粗略估算总 FD 基础 FD num_workers × (通信 FD 继承 FD) 数据 FD例如如果父进程已打开 50 个 FD使用 8 个 worker总 FD ≈ 20 8 × (6 50) 468如果系统限制是 256就会触发错误。解决方案方案一提高系统文件描述符限制最直接临时修改当前终端会话有效# 查看当前限制 ulimit -n # 临时提高到 65536 ulimit -n 65536 # 验证 ulimit -n # 应该输出 65536永久修改macOS# 编辑 ~/.zshrc 或 ~/.bash_profile echo ulimit -n 65536 ~/.zshrc source ~/.zshrc对于 macOS Catalina 及更高版本可能需要额外步骤# 编辑 /etc/sysctl.conf需要 sudo sudo tee -a /etc/sysctl.conf EOF kern.maxfiles65536 kern.maxfilesperproc65536 EOF # 重启系统 sudo reboot永久修改Linux# 编辑 /etc/security/limits.conf sudo tee -a /etc/security/limits.conf EOF * soft nofile 65536 * hard nofile 65536 root soft nofile 65536 root hard nofile 65536 EOF # 重新登录或重启生效在 Python 代码中修改import resource import platform def increase_fd_limit(new_limit65536): 在 Python 代码中提高文件描述符限制 soft, hard resource.getrlimit(resource.RLIMIT_NOFILE) if soft new_limit: try: resource.setrlimit(resource.RLIMIT_NOFILE, (new_limit, hard)) print(fFD limit increased from {soft} to {new_limit}) except ValueError: try: resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard)) print(fFD limit increased from {soft} to {hard} (hard limit)) except ValueError as e: print(fCannot increase FD limit: {e}) print(fCurrent: soft{soft}, hard{hard}) print(Please run: ulimit -n 65536) else: print(fFD limit already sufficient: {soft}) # 在脚本开头调用 increase_fd_limit(65536)方案二减少 worker 数量import os def get_optimal_num_workers(dataset_size, batch_size, max_workersNone): 根据数据集大小和系统资源计算最优 worker 数量 cpu_count os.cpu_count() or 1 if max_workers: cpu_count min(cpu_count, max_workers) num_batches dataset_size // batch_size num_workers min(cpu_count, num_batches, 8) return max(0, num_workers) # 使用 num_workers get_optimal_num_workers(dataset_size10000, batch_size32) dataloader DataLoader(dataset, batch_size32, num_workersnum_workers)方案三使用 persistent_workers 减少 FD 泄漏from torch.utils.data import DataLoader # persistent_workersTrue 避免 epoch 间重复创建/销毁 worker dataloader DataLoader( dataset, batch_size32, shuffleTrue, num_workers4, persistent_workersTrue, # 关键保持 worker 存活 prefetch_factor2 # 预加载 2 个 batch )方案四正确关闭文件和资源from PIL import Image import io from torch.utils.data import Dataset class CorrectImageDataset(Dataset): 正确处理图像文件的 Dataset def __init__(self, image_paths, transformNone): self.image_paths image_paths self.transform transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): # 使用 with 语句确保文件关闭 with open(self.image_paths[idx], rb) as f: img_bytes f.read() # 从字节流加载图像文件已关闭 img Image.open(io.BytesIO(img_bytes)) img img.convert(RGB) if self.transform: img self.transform(img) return img, 0完整修复代码以下是一个完整的、可运行的解决方案包含 FD 监控、自动限制提升和优化的 DataLoader 配置import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader import numpy as np import os import resource import platform import gc import time # # 1. 文件描述符管理器 # class FileDescriptorManager: 文件描述符监控和管理工具。 staticmethod def get_current_limit(): 获取当前 FD 限制 soft, hard resource.getrlimit(resource.RLIMIT_NOFILE) return soft, hard staticmethod def increase_limit(target65536): 提高 FD 限制 soft, hard FileDescriptorManager.get_current_limit() if soft target: return soft try: resource.setrlimit(resource.RLIMIT_NOFILE, (target, max(hard, target))) new_soft, _ FileDescriptorManager.get_current_limit() print(f[FD Manager] Limit increased: {soft} - {new_soft}) return new_soft except ValueError as e: print(f[FD Manager] Cannot set to {target}: {e}) try: resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard)) print(f[FD Manager] Limit set to hard limit: {hard}) return hard except ValueError: print(f[FD Manager] Failed. Current: soft{soft}, hard{hard}) return soft staticmethod def get_open_fd_count(): 获取当前进程打开的 FD 数量 system platform.system() pid os.getpid() try: if system Darwin: # macOS import subprocess result subprocess.run( [lsof, -p, str(pid)], capture_outputTrue, textTrue ) return max(0, len(result.stdout.strip().split(\n)) - 1) elif system Linux: fd_dir f/proc/{pid}/fd if os.path.exists(fd_dir): return len(os.listdir(fd_dir)) except Exception: pass return -1 staticmethod def monitor(): 打印当前 FD 状态 soft, hard FileDescriptorManager.get_current_limit() current FileDescriptorManager.get_open_fd_count() if current 0 and soft 0: print(f[FD Monitor] Open FDs: {current}, fLimit: {soft} (hard: {hard}), fUsage: {current/soft*100:.1f}%) else: print(f[FD Monitor] Limit: {soft} (hard: {hard})) # # 2. 安全的 DataLoader 工厂 # class SafeDataLoaderFactory: 创建不会触发 too many open files 错误的 DataLoader。 staticmethod def create(dataset, batch_size32, shuffleTrue, num_workersNone, **kwargs): 创建优化的 DataLoader。 自动处理 FD 限制和 worker 数量。 FileDescriptorManager.increase_limit(65536) if num_workers is None: cpu_count os.cpu_count() or 1 soft_limit, _ FileDescriptorManager.get_current_limit() max_workers_by_fd max(1, (soft_limit - 100) // 10) num_workers min(cpu_count, max_workers_by_fd, 8) defaults { persistent_workers: num_workers 0, prefetch_factor: 2 if num_workers 0 else None, pin_memory: torch.cuda.is_available(), drop_last: False, timeout: 0, } defaults.update(kwargs) defaults {k: v for k, v in defaults.items() if v is not None} print(f[SafeDataLoader] num_workers{num_workers}, fpersistent_workers{defaults.get(persistent_workers, False)}) return DataLoader( dataset, batch_sizebatch_size, shuffleshuffle, num_workersnum_workers, **defaults ) # # 3. 数据集和模型 # class SyntheticDataset(Dataset): 合成数据集用于测试 def __init__(self, num_samples5000, dim784, num_classes10): self.data np.random.randn(num_samples, dim).astype(np.float32) self.labels np.random.randint(0, num_classes, num_samples) self.dim dim self.num_classes num_classes def __len__(self): return len(self.labels) def __getitem__(self, idx): return torch.from_numpy(self.data[idx]), self.labels[idx] class SimpleClassifier(nn.Module): def __init__(self, input_dim784, num_classes10): super().__init__() self.net nn.Sequential( nn.Linear(input_dim, 256), nn.ReLU(), nn.Dropout(0.2), nn.Linear(256, 128), nn.ReLU(), nn.Linear(128, num_classes) ) def forward(self, x): return self.net(x) # # 4. 训练函数 # def train_with_fd_monitoring(num_epochs5, batch_size64, num_workers4): 带 FD 监控的训练函数 print( * 60) print(Initializing File Descriptor Management) print( * 60) FileDescriptorManager.increase_limit(65536) FileDescriptorManager.monitor() device torch.device(cuda if torch.cuda.is_available() else cpu) print(f\nDevice: {device}) # 创建数据集 dataset SyntheticDataset(num_samples5000) # 使用安全 DataLoader print(\nCreating DataLoader...) dataloader SafeDataLoaderFactory.create( dataset, batch_sizebatch_size, shuffleTrue, num_workersnum_workers ) # 创建模型 model SimpleClassifier().to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) # 训练循环 print(f\nStarting training for {num_epochs} epochs...\n) for epoch in range(num_epochs): model.train() epoch_loss 0.0 num_batches 0 epoch_start time.time() for batch_x, batch_y in dataloader: batch_x batch_x.to(device) batch_y batch_y.to(device) optimizer.zero_grad() output model(batch_x) loss criterion(output, batch_y) loss.backward() optimizer.step() epoch_loss loss.item() num_batches 1 epoch_time time.time() - epoch_start avg_loss epoch_loss / num_batches print(fEpoch [{epoch1}/{num_epochs}] ({epoch_time:.1f}s) fLoss: {avg_loss:.4f}) FileDescriptorManager.monitor() gc.collect() print(\nTraining completed successfully!) del dataloader gc.collect() return model # # 5. FD 泄漏检测器 # class FDLeakDetector: 检测文件描述符泄漏 def __init__(self): self.baseline None def start(self): self.baseline FileDescriptorManager.get_open_fd_count() print(f[Leak Detector] Baseline FDs: {self.baseline}) def check(self, label): current FileDescriptorManager.get_open_fd_count() if self.baseline is not None and current 0: diff current - self.baseline status OK if diff 20 else WARNING: Possible leak! print(f[Leak Detector] {label} FDs: {current} f(delta: {diff:d}) {status}) return current # # 6. 主程序 # def main(): print(PyTorch DataLoader Too Many Open Files - Complete Solution) print( * 60) print(fOS: {platform.system()} {platform.release()}) print(fPython: {platform.python_version()}) print(fPyTorch: {torch.__version__}) print(fCPU cores: {os.cpu_count()}) soft, hard FileDescriptorManager.get_current_limit() print(fFD limit: soft{soft}, hard{hard}) # 演示 FD 泄漏检测 print(\n--- FD Leak Detection Demo ---) detector FDLeakDetector() detector.start() files [] for i in range(50): f open(f/tmp/test_fd_{i}.txt, w) files.append(f) detector.check(After opening 50 files) for f in files: f.close() detector.check(After closing 50 files) # 运行训练 print(\n--- Training Demo ---) model train_with_fd_monitoring( num_epochs3, batch_size64, num_workers4 ) print(\n--- Final Status ---) FileDescriptorManager.monitor() detector.check(After training) print(\nAll done! No too many open files errors.) if __name__ __main__: main()常见陷阱与注意事项1. ulimit 只影响当前 shell 会话在终端中运行ulimit -n 65536只影响当前 shell 及其子进程。如果你在另一个终端或通过 IDE 运行 Python 脚本需要确保那个进程也有足够的 FD 限制。2. macOS 的特殊限制macOS 对 FD 限制有额外的系统级约束kern.maxfiles和kern.maxfilesperproc。即使ulimit -n设置成功如果系统级限制不够仍然会报错。# 查看系统级限制 sysctl kern.maxfiles sysctl kern.maxfilesperproc3. Docker 容器中的 FD 限制Docker 容器可能有自己的 FD 限制。需要在 Dockerfile 或 docker-compose 中设置# docker-compose.yml services: training: build: . ulimits: nofile: soft: 65536 hard: 655364. persistent_workers 的注意事项persistent_workersTrue可以减少 FD 泄漏但需要注意worker 进程在整个训练期间保持存活如果数据集对象有状态如游标需要正确处理内存占用可能增加5. PIL/Pillow 的惰性加载PIL.Image.open()是惰性加载的文件描述符在图像数据被实际读取或图像对象被关闭之前不会释放# 错误FD 泄漏 def __getitem__(self, idx): img Image.open(path) # FD 打开 return transform(img) # FD 可能仍然打开 # 正确确保 FD 关闭 def __getitem__(self, idx): with open(path, rb) as f: img_bytes f.read() img Image.open(io.BytesIO(img_bytes)) # FD 已关闭 return transform(img)6. 数据库连接的 FD 泄漏如果数据集从数据库读取数据确保数据库连接在使用后正确关闭class DatabaseDataset(Dataset): def __getitem__(self, idx): conn create_connection() # 打开连接占用 FD try: data conn.query(idx) return data finally: conn.close() # 确保关闭7. num_workers 不是越多越好过多的 worker 不仅可能导致 FD 问题还会增加进程创建和通信的开销。一般建议num_workers不超过 CPU 核心数通常 4-8 个就足够了。8. Jupyter Notebook 中的特殊问题Jupyter Notebook 的 IPython 内核会保持文件描述符打开。如果频繁创建和销毁 DataLoaderFD 可能会累积。建议在 Notebook 中使用persistent_workersTrue并在不需要 DataLoader 时显式删除。9. NCCL 和分布式训练在分布式训练中NCCL 后端会额外占用文件描述符用于 GPU 间的通信。如果使用多节点训练需要在所有节点上提高 FD 限制。10. 监控 FD 使用在长时间训练中建议定期监控 FD 使用量及时发现泄漏# 在训练循环中定期检查 if batch_idx % 100 0: FileDescriptorManager.monitor()总结PyTorch DataLoader 的 too many open files 错误虽然令人困惑但理解其根因后并不难解决。关键要点理解文件描述符不仅文件占用 FD管道、套接字、共享内存等也占用 FD。DataLoader 的多进程通信需要大量 FD。提高 FD 限制这是最直接的解决方案。通过ulimit -n或在代码中使用resource.setrlimit将限制提高到 65536。使用 persistent_workers避免 epoch 间重复创建 worker减少 FD 泄漏。正确关闭资源确保所有文件、图像、数据库连接在使用后正确关闭。合理设置 num_workers不要盲目使用大量 worker根据 CPU 核心数和数据集大小合理设置。监控 FD 使用在长时间训练中定期检查 FD 数量及时发现和修复泄漏。通过本文提供的完整解决方案和工具类你应该能够彻底解决 DataLoader 的 too many open files 错误并构建稳定的多进程数据加载流程。