文章

开发疑难杂症汇总

It's time to funk the code.

开发疑难杂症汇总

尽管在 conda你真的可以去死了 一文里面已经对conda进行了详细的批判,然而,批判归批判,用还是得用,挺无奈的就。

本文还处于持续更新中。

由于是汇总,本文内容较长,请善用目录跳转。

前言

本文是对目前的工作状态(2024-?)所遇到的疑难杂症的大汇总,基本开发环境是在Linux(远程)下的Python开发,主要使用Conda的环境配置,因为涉及深度学习,所以也涉及到CUDA和Torch的配置。

根据遇到的问题,分为以下几类:

  1. 基于Python,纯粹的Python特性问题(内部库),还有一些与其工具Pip、Conda之类的连携问题
  2. 基于Python与深度学习,即Python深度学习常见外部库在配置上容易诱发冲突的问题与特性
  3. 基于CUDA,主要为CUDA自身的特性和一些常见异常状况,包括Linux配置、与gcc的连携关系等
  4. 基于深度学习库PyTorch/Torch的常见异常,包括和CUDA的连携异常等

Python

参数列表的*和/是什么?

参见Python官方文档:4.7.3 特殊参数

一则实战参见鏖战mask2former(一)

Pip、Conda,包是如何管理的?

参见Conda开发两则

我的Conda更新了,但是没更新?

参见Conda你真的可以去死了

Future泛型错误(concurrent.futures)

Future简介

concurrent.futures模块是Python中用于实现异步执行的高级接口。它提供了一种编写多线程和多进程代码的简洁方式,而无需直接处理threadingmultiprocessing模块的复杂细节。

它的核心是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

考虑以下几种解法:

  1. 升级到Python 3.9及以上,并使用泛型语法,这是最佳实践。
  2. 在环境强依赖于Python 3.8的情况下,使用typing提供的泛型定义,如typing.Future[None]
  3. 我个人的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__钩子方法,允许标准库和用户自定义类更方便地成为“泛型类”,然而大量内置类(如Futurelistdict)依旧不是泛型。

由于Future此时未实现__class_getitem__,因此无法使用泛型语法。

在Python 3.9,进一步引入PEP 585 — Type Hinting Generics In Standard Collections。让标准库的容器类和并发原语等原生支持泛型,避免依赖typing.Listtyping.Dict这类“包装类”,typing中的别名(ListDictTuple等)被标记为deprecated,并在Python 3.11中移除;标准库类(如listdicttupleconcurrent.futures.Future)都通过实现__class_getitem__,成为泛型类。

深度学习

LabelMe(安装)

省流:最新的labelme别装有兼容性问题,python==3.8labelme==5.1.1这个组合暂时确定是稳定的。

详细分析见下文:

LabelMe的安装说明(Windows)

Pillow

Pillow是一个Python图像处理库,用于处理图像,例如读取、写入、显示、转换等。

AttributeError: module ‘PIL.Image‘ has no attribute ‘ANTIALIAS‘

在版本Pillow>=10.0.0PIL.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.intnp.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

单独指定一张卡来训练

参见迎击mask2former

找不到对应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环境时比较方便。

迎击mask2former

在多CUDA环境中,通常需要用export方法来指定当前环境下的关键环境变量(如tmux线程)。

给出以下一个案例。

首先,明确gcc编译需求的CUDA版本为11.3。

通过ls /usr/local/,可以看到cudacuda-11.3cuda-11.7三个目录。

通过nvcc --version,可以看到当前CUDA版本为11.7,表明cuda是一个symbolic link,指向cuda-11.7

接下来,需要检查CUDA_HOMEPATHLIBRARY_PATHLD_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默认的进程启动方式是forkfork的工作方式是:它会复制父进程的内存空间。如果主进程在创建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中运行。

本文由作者按照 CC BY 4.0 进行授权