开发疑难杂症汇总
It's time to funk the code.
尽管在 conda你真的可以去死了 一文里面已经对conda进行了详细的批判,然而,批判归批判,用还是得用,挺无奈的就。
本文还处于持续更新中。
由于是汇总,本文内容较长,请善用目录跳转。
前言
本文是对目前的工作状态(2024-?)所遇到的疑难杂症的大汇总,基本开发环境是在Linux(远程)下的Python开发,主要使用Conda的环境配置,因为涉及深度学习,所以也涉及到CUDA和Torch的配置。
根据遇到的问题,分为以下几类:
- 基于Python,纯粹的Python特性问题(内部库),还有一些与其工具Pip、Conda之类的连携问题
- 基于Python与深度学习,即Python深度学习常见外部库在配置上容易诱发冲突的问题与特性
- 基于CUDA,主要为CUDA自身的特性和一些常见异常状况,包括Linux配置、与gcc的连携关系等
- 基于深度学习库PyTorch/Torch的常见异常,包括和CUDA的连携异常等
Python
参数列表的*和/是什么?
一则实战参见鏖战mask2former(一)。
Pip、Conda,包是如何管理的?
我的Conda更新了,但是没更新?
Future泛型错误(concurrent.futures)
Future简介
concurrent.futures
模块是Python中用于实现异步执行的高级接口。它提供了一种编写多线程和多进程代码的简洁方式,而无需直接处理threading
或multiprocessing
模块的复杂细节。
它的核心是Executor
抽象类和两个具体实现:
ThreadPoolExecutor
:使用线程池来异步执行任务。适用于I/O密集型操作(如网络请求、文件读写)。ProcessPoolExecutor
:使用进程池来异步执行任务。适用于CPU密集型操作(如数学计算、图像处理),可以绕过GIL(全局解释器锁)充分利用多核CPU。
该模块的核心概念是Future
对象。当你向Executor
提交一个任务(使用submit
方法)时,它会立即返回一个Future
对象。这个Future
对象是一个占位符,它代表着异步操作的“未来”结果。你可以通过这个对象来查询任务的状态(是否完成)、获取结果(会阻塞直到结果就绪)或取消任务。
在类型系统中,Future的理想用法是能够指定内部结果类型,例如Future[int]
表示返回int
的任务。
由Python 3.8诱发的问题:TypeError: 'type' object is not subscriptable
。
考虑以下几种解法:
- 升级到Python 3.9及以上,并使用泛型语法,这是最佳实践。
- 在环境强依赖于Python 3.8的情况下,使用
typing
提供的泛型定义,如typing.Future[None]
。 - 我个人的HACK实践:在环境强依赖于Python 3.8,且
typing.Future[None]
似乎不太能解决问题的情况下,直接回避Future
泛型,例如把原先的List[Future[None]]
改为了List
,这一做法通过牺牲类型注解的所有精确度实现了代码的兼容性。
以下是本问题详细的分析
在Python 3.6及以前,原生Future不支持泛型语法,例如Future[int]
,这会导致直接的、不可修复的TypeError: 'type' object is not subscriptable
错误。
在Python 3.7,引入PEP 560 – Core support for typing module and generic types,为类定义新增__class_getitem__
钩子方法,允许标准库和用户自定义类更方便地成为“泛型类”,然而大量内置类(如Future
、list
、dict
)依旧不是泛型。
由于Future
此时未实现__class_getitem__
,因此无法使用泛型语法。
在Python 3.9,进一步引入PEP 585 — Type Hinting Generics In Standard Collections。让标准库的容器类和并发原语等原生支持泛型,避免依赖typing.List
、typing.Dict
这类“包装类”,typing
中的别名(List
、Dict
、Tuple
等)被标记为deprecated,并在Python 3.11中移除;标准库类(如list
、dict
、tuple
、concurrent.futures.Future
)都通过实现__class_getitem__
,成为泛型类。
深度学习
LabelMe(安装)
省流:最新的labelme
别装有兼容性问题,python==3.8
和labelme==5.1.1
这个组合暂时确定是稳定的。
详细分析见下文:
Pillow
Pillow
是一个Python图像处理库,用于处理图像,例如读取、写入、显示、转换等。
AttributeError: module ‘PIL.Image‘ has no attribute ‘ANTIALIAS‘
在版本Pillow>=10.0.0
,PIL.Image.ANTIALIAS
被移除,取而代之的是PIL.Image.LANCZOS
。因此要么降级Pillow
,要么使用PIL.Image.LANCZOS
作为替代。
NumPy的分水岭
NumPy
是一个Python库,用于处理多维数组。NumPy
提供了大量的数学函数,用于对数组进行操作。
Numpy 1.24
AttributeError: module 'numpy' has no attribute 'int'
NumPy 1.20版本开始弃用,并在1.24版本中彻底移除了np.int
、np.float
等别名,以与Python内置类型保持一致。因此,要么降级NumPy,要么使用Python内置类型,通常来说,前者发生的概率较大,这是因为很多现在的深度学习开源代码都依赖于旧版的NumPy。
Numpy 2.0
Numpy 2.0是一个重大版本升级,改变了很多底层行为和API,对于那些在使用了旧版代码特性/语法糖来偷懒的代码,极容易出现不兼容的问题。
这种问题是极其显著而深远的,同时,也与诸如Pandas、Matplotlib、Scikit-learn、OpenCV等库的某些特定版本区间形成了强依赖绑定关系,强行适配NumPy 2.0的兼容成本过大(除了函数、函数签名对不上这类问题,依赖于旧版API的库都需要重新编译和开发,包括一系列C API),除非项目还处于起步阶段,否则更推荐不去升级到NumPy 2.0以上。
CUDA
单独指定一张卡来训练
找不到对应CUDA工具
例如,/usr/bin/ld: cannot find -lxxx
,因为找不到工具而无法完成一个带有gcc编译的Python库安装。其中,xxx为工具名,例如/usr/bin/ld: cannot find -lhdf5
表明gcc没有找到libhdf5.so
这个库。
该问题与以下几个关键环境变量有关:
CUDA_HOME
:该环境变量通常指向CUDA的安装目录,例如/usr/local/cuda
,该环境变量在安装CUDA时会被自动添加到环境变量中。其中,/usr/local/cuda
是默认CUDA安装目录的symbolic link,指向实际的CUDA安装目录,例如/usr/local/cuda-11.7
。PATH
:该环境变量通常包含CUDA的bin目录,例如/usr/local/cuda/bin
,该环境变量在安装CUDA时会被自动添加到环境变量中。LIBRARY_PATH
:该环境变量通常包含CUDA的lib目录,例如/usr/local/cuda/lib64
,该环境变量在安装CUDA时会被自动添加到环境变量中。LD_LIBRARY_PATH
:该环境变量通常包含CUDA的lib目录,例如/usr/local/cuda/lib64
,该环境变量在安装CUDA时会被自动添加到环境变量中。与LIBRARY_PATH
不同的是,LIBRARY_PATH
描述了编译时链接库的路径(即gcc编译时使用的库),而LD_LIBRARY_PATH
描述了运行时链接库的路径(即运行时使用的库)。
第一种解决方案参见下文,这种做法在单机只有一个CUDA环境时比较方便。
在多CUDA环境中,通常需要用export
方法来指定当前环境下的关键环境变量(如tmux线程)。
给出以下一个案例。
首先,明确gcc编译需求的CUDA版本为11.3。
通过ls /usr/local/
,可以看到cuda
、cuda-11.3
、cuda-11.7
三个目录。
通过nvcc --version
,可以看到当前CUDA版本为11.7,表明cuda
是一个symbolic link,指向cuda-11.7
。
接下来,需要检查CUDA_HOME
、PATH
、LIBRARY_PATH
、LD_LIBRARY_PATH
四个环境变量。通常来说,这些环境变量或多或少都会出现问题,例如CUDA_HOME
有时会为空,LD_LIBRARY_PATH
仅包括当前anaconda的env而并不包含CUDA的lib目录等。
接下来,逐个检查并设置环境变量。对于为空的环境变量,使用类似export CUDA_HOME=/usr/local/cuda-11.3
的命令进行设置;对于非空的变量,使用类似export PATH=$PATH:/usr/local/cuda-11.3/bin
的命令进行环境的追加。
通常来说,再次进行编译即可通过。
此外,还有一些非常规的特例,也一并记录下来。
tiny-cuda-nn:这是一个轻量级的用于CUDA加速的Python库,已知的在安装过程中会出现的问题为ld: cannot find -lcuda
。该问题被汇报于Issue 183。
事实上,此处丢失的文件libcuda.so
并不在常规的/usr/local/cuda/lib64
目录下,而是在/usr/local/cuda/lib64/stubs
这个路径,因此对应地,需要额外添加export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda/lib64/stubs
,问题得以解决。
Torch / PyTorch
Jupyter里面多线程卡住了?
假设你兴高采烈地调了类似下面的代码:
1
2
3
4
5
6
7
8
9
from torch.utils.data import DataLoader
loader = DataLoader(
dataset,
batch_size=32,
num_workers=4, # 主要是这一行
pin_memory=True,
shuffle=True
)
观察到的现象:
- 程序启动后没有任何输出,或者打印了极少量信息后完全停止
- 控制台光标停止闪烁,程序看似运行中但实际上已停滞
- 使用
nvidia-smi
或类似方案查看发现GPU利用率始终为0%或不太高 - CPU使用率可能很高,但训练进度毫无进展
- 程序不会崩溃,也没有抛出任何错误信息,就是永远卡住,如果使用信号强行终止,大概率会发现程序没有进入第一个batch
最简单粗暴的解决方案:把num_workers
设置为0,这也就意味着DataLoader
不会使用多线程,而是直接在主线程中加载数据。
那么,这是怎么回事呢?
首先,DataLoader
的工作机制:当num_workers>0
时,PyTorch会通过multiprocessing
在后台启动多个worker进程来并行加载数据。这些worker使用队列把数据传回主进程。
在Linux上,Python默认的进程启动方式是fork
。fork
的工作方式是:它会复制父进程的内存空间。如果主进程在创建DataLoader
之前已经创建了任何锁、文件描述符、或初始化了特定的全局状态(如CUDA驱动),这些状态会被原样复制到子进程中。但对于子进程来说,它并不知道这些资源已经被锁定或占用,当它尝试使用这些资源时,就会因为资源被父进程占用而陷入无限等待,最终导致死锁。
PyTorch 的官方推荐写法如下,即强制使用spawn
模式启动子进程。
1
2
3
if __name__ == "__main__":
mp.set_start_method("spawn", force=True)
...
在Jupyter Notebook或IPython环境中,情况更加复杂。这些环境本身就是一个长期运行的父进程,代码不是在一个标准的__main__
模块里运行,而是被包装进交互式解释器的环境。这会导致spawn
模式无法正常工作(子进程要重新导入__main__
,但Jupyter环境下没有标准的__main__
文件)。
此外,Jupyter cell里的函数、类在worker里序列化传递时,依赖pickle。但cell定义的对象没有固定的模块名,pickle/unpickle亦会失败或阻塞。
除了num_workers=0
这一粗暴的解决方案外,利用Jupyter Notebook调用多线程的实践是:把Dataset
、模型等定义写在.py
文件中再导入。
由于Jupyter Notebook本身也只是交互式的Python交互式计算环境,不太适用于多线程等复杂环境,因此,最佳实践是:
把代码放在
.py
文件中,使用python命令来运行,不要在Jupyter Notebook中运行。