发布于  更新于 

基于 WSL2 和 Docker 的深度学习环境指北

为什么要使用 WSL2 和 Docker 来管理深度学习环境?本教程的配置方法旨在日常使用的 Windows 机器上建立 CUDA 加速的深度学习环境,以便进行快速的调试与开发代码,而无需忍受连接到远程服务器的延迟。许多的深度学习库不能在 Windows 上开箱即用(尽管许多库只需少量的代码修改即可兼容 Windows 和 MSVC),或者在 Windows 上难以复现行为,所以需要使用 WSL2。作为虚拟机,WSL2 支持绝大多数的 Linux 内核的特性,相较于其他的虚拟化平台,WSL2 能优雅地与 Windows 宿主机共享同一张 CUDA 显卡。我不喜欢使用 Conda,第一是因为 Resolving Environment 太慢了,第二是 conda 的环境隔离程度实际上并不能满足深度学习的需求。Conda 不能隔离 CUDA 运行库,和其他 apt 管理的 C 库,而 Docker 可以,DevContainers 已包含了一套易用的将 Docker 容器用于开发的方案。

根本碰都不要碰 Docker Desktop, Bug 太多了。本教程直接在 WSL2 中安装 Docker,不需要 Docker Desktop。

实际上,得益于 WSL2 的设计,本文所述的 WSL2 部分的方法也适用于典型的 Ubuntu 主机。Windows 部分的教程需要 Windows 11 或 Windows 10 22H2 (19045) 以上的版本。推荐安装 Windows Terminal 来让命令行体验更美好。

安装 Windows Terminal 并取代 conhost
安装 Windows Terminal 并取代 conhost

Step 1 - 访问互联网

本教程假定必须使用 HTTP 代理才能访问任何互联网上的网站(实际上相当契合中国大陆的情况了),并且这个代理服务器位于 Windows 宿主机的 7890 端口上。

一个典型的 HTTP 代理软件
一个典型的 HTTP 代理软件

我们首先让 Windows 上的程序能使用这个代理服务器。通常来说只需要打开 System Proxy 选项,这会修改 Windows 的 IE 代理设置,适用于多数的图形化程序。不建议打开 TUN 模式,这会让情况变得复杂棘手,建议把 TUN 代理留给 “访问内部资产” 的需求,如 EasyConnect 和 WireGuard,而不是 “访问互联网” 的需求。这里 有关于 TUN 代理的更多讨论和高级用法。

仅仅这样设置是不够的。许多从 Linux 世界移植的命令行程序从环境变量读取代理设置,而不是从 Windows 的 IE 代理设置读取。可添加 Windows 环境变量 http_proxyhttps_proxy,值均为 http://127.0.0.1:7890.

添加 Windows 环境变量
添加 Windows 环境变量

添加完记得重启打开的 Shell,或者直接重启电脑。

如果以上步骤是正确的,你应该能直接在 PowerShell 中直接 curl google.com
如果以上步骤是正确的,你应该能直接在 PowerShell 中直接 curl google.com

之后还要用到 Windows 的主机名,最好顺手改成好看的名字,不要用默认的乱码。

最好把主机名从默认的乱码改成好打的名字
最好把主机名从默认的乱码改成好打的名字

Step 2 - 安装 Visual Studio 和 CUDA

由于 CUDA 的编译器和调试器都依赖于 MSVC,所以我们直接安装一个完整的 Visual Studio 来减少麻烦。

下载 Visual Studio Community 2022,安装时选择 C++ 工作负载。安装过程略,只需要一直点下一步。

至少选择 C++ 工作负载,别的看喜好选择。
至少选择 C++ 工作负载,别的看喜好选择。

如果想要减少麻烦,就不要修改任何东西的默认安装路径。

如果 C 盘不够大,就换一个足够大的 C 盘。

安装完 Visual Studio 之后,按提示重启电脑。随后下载并安装 CUDA Toolkit。

直接安装你看到的最新版本,无论你想用的深度学习库要求使用哪个版本的 CUDA
直接安装你看到的最新版本,无论你想用的深度学习库要求使用哪个版本的 CUDA

只需要一直点下一步。会覆盖显卡驱动,所以屏幕会闪几下。

正确完成本节后,应该能在 Windows 上运行 nvidia-smi 命令,显示显卡的状态。

能在 Windows 上运行 nvidia-smi
能在 Windows 上运行 nvidia-smi

Step 3 - 安装 WSL2

确保是 Windows 11 或 Windows 10 22H2 (19045) 以上的版本。最新版本的 Windows 应当无需额外设置即可使用 wsl 命令,无论是否安装了 WSL。

可以直接运行 WSL 命令
可以直接运行 WSL 命令

如果找不到 wsl 命令说明需要手动安装 WSL2。参考官方教程:

对于有 wsl 命令的用户,直接运行 wsl --install 即可。安装中会多次请求 UAC 权限,确保有 好的网络连接

按照提示重启电脑
按照提示重启电脑

重启电脑后自动继续安装,或者手动运行 wsl 命令。

设置用户名和密码,按照惯例,输入密码时不会回显
设置用户名和密码,按照惯例,输入密码时不会回显
默认为安装了 Ubuntu 22.04,这是我们需要的
默认为安装了 Ubuntu 22.04,这是我们需要的

Step 4 - 在 WSL 中访问互联网

一种方法是让 WSL 使用 Windows 上的代理服务器联网。由于 Hyper-V 的默认网络设置,WSL2 中 Windows 宿主机的 IP 地址会在每次重启时改变(几乎必然改变)。不过在 WSL2 中能解析 Windows 宿主机的主机名,通过主机名访问 Windows 宿主机,在主机名后加 .local

确保能从 WSL 内 ping 通主机
确保能从 WSL 内 ping 通主机

就可以直接在 .bashrc 中设置代理(通常 WSL 的主机名和 Windows 是相同的),直接运行命令追加 .bashrc 文件:

1
2
echo 'export http_proxy=http://$(hostname).local:7890' >> ~/.bashrc
echo 'export https_proxy=http://$(hostname).local:7890' >> ~/.bashrc

重启 shell 或者直接运行 source ~/.bashrc,应该能直接访问互联网。

确保能直接访问互联网
确保能直接访问互联网

apt 不会使用这个代理。我的经验是使用国内镜像站会比使用代理更快,所以修改 /etc/apt/sources.list 文件,将默认的源替换为国内源。

1
2
3
sudo sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
sudo sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
sudo apt update
注意

为了使用 Docker 的 apt 源,还需要设置 apt 代理, 或者也可以按下文的方式使用 apt 国内镜像:

1
2
3
echo "Acquire::http::Proxy \"http://$(hostname).local:7890\";" | sudo tee /etc/apt/apt.conf.d/99proxy
echo "Acquire::https::Proxy \"http://$(hostname).local:7890\";" | sudo tee -a /etc/apt/apt.conf.d/99proxy
sudo apt update

Step 5 - 在 WSL 中安装 CUDA

无论最后需要用什么版本的 CUDA,都在 WSL 中安装最新版的 CUDA Toolkit。

注意

在官网给出的三种安装方式中, 最推荐使用 deb (network) 的方式安装 CUDA Toolkit, 这样符合一般使用 APT 的操作习惯, 在需要升级 CUDA 版本时最为方便, 其他两种方式都非常容易把 APT 搞爆炸. 目前英伟达的国内 CDN 已经正常使用, 使用官网的下载源会自动重定向到 nvidia.cn 域名, 速度很快.

警告:为什么你的显卡驱动在 apt upgrade 之后就挂了?

因为你使用了 runfile (local) 的方式安装了 nvidia 驱动,这是 完全不推荐 的做法。runfile 不会 向 APT 注册它添加的内核模块和库文件,当 apt 尝试升级内核或其他依赖库时,就不会自动重新编译和安装这些模块和库文件,导致重启后系统无法正常工作。请务必使用 APT 安装 NVIDIA 驱动。至少也要使用 deb (local) 的方式安装 CUDA Toolkit,这样才能确保 APT 能正确管理 NVIDIA 驱动和 CUDA 相关的库文件。当然,最好的方法是使用 deb (network)

apt upgrade 和高版本的 CUDA Toolkit 和 nvidia 驱动没那么多毛病,升级后出现错误多半因为之前安装的驱动的方法不对。

1
2
3
4
wget https://developer.download.nvidia.com/compute/cuda/repos/wsl-ubuntu/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install cuda-toolkit

使用 APT 在线安装 cuda-toolkit 包会自动安装最新版本的 CUDA Toolkit, 并且在未来可以使用 apt upgrade 命令升级到最新版本. 也可以使用

1
sudo apt-get -y install cuda-toolkit-12-9

来安装指定版本的 CUDA Toolkit, 例如 cuda-toolkit-12-9。这样安装不会通过 apt upgrade 自动升级. 在手动升级 CUDA 版本, 或者清除以其他方式安装的 CUDA 版本时, 建议先移除所有 CUDA 相关的包

1
sudo apt-get remove --purge '^cuda.*' 'nvidia-.*' 'libnvidia-.*'
注意

在 WSL 中只需安装 CUDA Toolkit, 不需要安装 NVIDIA 驱动. 在一般 Debian 裸机上还需安装 NVIDIA 驱动.

1
sudo apt-get install -y nvidia-open

安装最新稳定版本的 NVIDIA 驱动. 目前看来高版本的驱动没那么可怕, 建议不要守着 535 版本不放. 毕竟 LLM 横行的时代, 高版本的 CUDA Toolkit 和 PyTorch 都有很多重要的新特性.

另外,如果使用数据中心级的 GPU (A100, H100, …),还要安装

1
sudo apt-get install -y datacenter-gpu-manager nvidia-fabricmanager

否则可能出现 nvidia-smi 正常但 PyTorch 报错 system not yet initialized 的问题。

安装完成后应当能在 WSL 中运行 nvidia-smi 命令,显示显卡的状态。

Step 6 - 在 WSL 中安装 Docker

不要使用 Docker Desktop,它有太多的 Bug。我们直接在 WSL 中安装 Docker。

粘到 WSL 里运行(已加入代理设置):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc --proxy http://$(hostname).local:7890
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
说明

也可以考虑使用国内 Docker APT 镜像源, 例如北外镜像. 仍然需要按照上面五行命令从 Docker 官网获得 GPG 密钥。

1
2
3
4
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.bfsu.edu.cn/docker-ce/linux/debian \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null

随后在 Docker Daemon 的 Systemd 单元文件中配置代理环境变量

1
2
3
4
5
6
7
sudo mkdir -p /etc/systemd/system/docker.service.d
echo '[Service]' | sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf
echo "Environment=\"HTTP_PROXY=http://$(hostname).local:7890\"" | sudo tee -a /etc/systemd/system/docker.service.d/http-proxy.conf
echo "Environment=\"HTTPS_PROXY=http://$(hostname).local:7890\"" | sudo tee -a /etc/systemd/system/docker.service.d/http-proxy.conf
echo "Environment=\"NO_PROXY=localhost,127.0.0.1\"" | sudo tee -a /etc/systemd/system/docker.service.d/http-proxy.conf
sudo systemctl daemon-reload
sudo systemctl restart docker

把当前用户加入 docker 组,以免每次用 docker 都要 sudo

1
sudo usermod -aG docker $USER

别用 Rootless Docker, 纯属自找麻烦。

需要重启 shell。创建 Docker Client 的配置文件并设置代理,这样容器中会自动添加代理的环境变量:

1
2
mkdir -p ~/.docker
echo "{\"proxies\":{\"default\":{\"httpProxy\":\"http://$(hostname):7890\",\"httpsProxy\":\"http://$(hostname):7890\",\"noproxy\":\"localhost,127.0.0.1,::1\"}}}" > ~/.docker/config.json

这里不加 .local, 不正确的 noproxy 可能导致 gradio 无法启动。

正确完成本节后,应当能在 WSL 中运行 docker run hello-world 命令,显示 Docker 正常工作。

配置好 Docker Client 的代理设置后,可以直接在容器中运行 curl 命令,访问互联网。

Step 7 - 安装 NVIDIA Container Toolkit

在 WSL 中可以直接安装 Ubuntu 版本的 NVIDIA Container Toolkit。

1
2
3
4
5
6
7
8
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

下载一个带 CUDA 的 PyTorch 镜像试试:

1
docker pull pytorch/pytorch:2.3.1-cuda12.1-cudnn8-devel

注: 使用 Docker 时,可以使用任意的比系统 CUDA 版本低的 CUDA 镜像。开发时请使用 devel 版本的 PyTorch 镜像,只有 devel 版本才包含编译器,可以编译 C++ 扩展。

PyTorch devel 镜像现在已有 8 个多 G
PyTorch devel 镜像现在已有 8 个多 G

docker pull 并不能断点续传,最好用一个好的代理。

成功安装 NVIDIA Container Toolkit 后,能在容器内运行 nvidia-smi 命令,显示显卡的状态。

1
docker run --rm -it --gpus all pytorch/pytorch:2.3.1-cuda12.1-cudnn8-devel nvidia-smi

Docker 快速入门

一个 Docker 镜像包含了运行程序所需的完整文件系统和环境变量。Docker 容器是一个特殊的进程,使用 Docker 镜像提供的文件系统和环境变量,并且网络栈和主机一般是隔离的(实际上,几乎所有的用户态要素都被隔离了)。相比于虚拟机,Docker 能快速地启动和停止,而且 Docker 镜像的大小通常比虚拟机的小很多。

需要注意的是,Docker 容器被设计成无状态的,容器内的文件系统和环境变量都是临时的,任何修改都会在容器停止后丢失。如果需要向容器内安装新的程序或者修改配置文件,应该重新构建一个新的 Docker 镜像。如果需要保存容器内的数据,应该把数据文件挂载(Mount)到宿主机的文件夹或者 Docker Volume。Docker Commit 命令可以把运行中的容器保存为新的 Docker 镜像,但是不推荐用来制作镜像,只适合用于调试或者紧急保存数据的需求。

可以方便地从 Docker Hub 上下载包含各类 Linux 发行版和软件的镜像,由于 Docker 提供了充分的隔离,几乎所有镜像都能下载后开箱即用,省去了手动安装各类环境的麻烦。如果需要自定义镜像,可以使用 Dockerfile 来描述镜像的构建过程,然后使用 docker build 命令构建镜像。Dockerfile 中的每一条指令都会生成一个新的镜像层,Docker 会尽量复用已有的镜像层,以减少镜像的大小。Dockerfile 通常包括 FROM, RUN, COPY, CMD 等指令。

  • FROM 指定基础镜像
  • RUN 在镜像中运行 Shell 命令,可以用来安装软件
  • COPY 复制文件到镜像中。由于 Docker 的设计,要复制的文件必须在构建上下文中,所以通常需要把文件放在 Dockerfile 同一目录下
  • CMD 指定容器启动时默认运行的命令
  • ENV 设置环境变量

Docker 镜像的构建过程会被缓存,如果 Dockerfile 的某一步发生了变化,Docker 会重新构建这一步之后的所有步骤。下面是一个常见的 Dockerfile 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 有了 Docker,可以使用任意老版本的 PyTorch 和 CUDA,而不会影响其他的项目
FROM pytorch/pytorch:1.7.1-cuda11.0-cudnn8-devel

# Dockerfile 中 APT 包的安装比较复杂,建议复制下面的格式
# rm /etc/apt/sources.list.d/cuda.list 如果镜像中没有 CUDA,则删掉这一行
# 如果不指定 DEBIAN_FRONTEND=noninteractive TZ=Asia/Shanghai 和 -y 参数,构建镜像会因为 apt 等待用户输入而卡死
RUN rm /etc/apt/sources.list.d/cuda.list \
&& sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
&& sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
&& apt-get update \
&& DEBIAN_FRONTEND=noninteractive TZ=Asia/Shanghai apt-get install -y git gdb vim curl wget tmux zip cmake ffmpeg libsm6 libxext6 \
&& rm -rf /var/lib/apt/lists/*

# environment.yml 需要放在 Dockerfile 同一目录下
COPY environment.yml /tmp/environment.yml
RUN conda env create -f /tmp/environment.yml
RUN conda init bash

# 除了 Conda,也可以直接用 pip 安装 Python 包,Docker 已经提供了环境隔离
RUN pip install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com h5py einops tqdm matplotlib tensorboard torch-tb-profiler ninja scipy

可以用 docker build 命令构建 Docker 镜像:

1
docker build -t my-image-name .

-t 参数指定镜像的名字,只能包含小写字母和数字,可以用 / 分隔,. 指定 Dockerfile 所在的目录。构建完成后可以用 docker images 命令查看所有的镜像。

要启动 Docker 镜像,可以使用 docker run 命令:

1
docker run --rm -it --gpus all -v $(pwd):/workspace -p 8080:80 my-image-name bash
  • --rm 容器停止后自动删除,一般不用留着因为数据都清除了
  • -it 交互式启动,可以使用 Shell。如果需要作为后台进程运行,换成 -d 参数
  • --gpus all 允许容器使用所有的 GPU,不加用不了 CUDA
  • -v $(pwd):/workspace 把当前目录挂载到容器的 /workspace 目录,可以在容器内的 /workspace 目录中读写文件,数据会随时同步到宿主机,不会丢失
  • -p 8080:80 把容器的 80 端口映射到宿主机的 8080 端口,可以通过 localhost:8080 访问容器内 80 端口的 Web 服务。如果不需要映射端口,可以不加这个参数。
    • 在 WSL2 上,这个命令只负责从容器映射到 WSL2 的网络栈
    • 但通常 WSL2 上的端口能被自动映射到 Windows 上,可以直接在 Windows 上访问 localhost:8080
  • my-image-name 指定要启动的镜像名称
  • bash 指定容器启动时运行的命令,可不加,默认是 CMD 指定的命令

使用 docker ps 命令可以查看所有正在运行的容器,使用 docker stop 命令可以停止容器(容器会在主进程退出后自动停止)。使用 docker exec 命令可以在运行中的容器中运行命令。使用 docker cp 命令可以从容器中复制文件到宿主机。具体用法略。

Docker Compose 可以用来管理多个容器,也能方便的把容器的启动参数写到文件里。具体用法略。

DevContainer 指北

Visual Studio Code 的 DevContainer 功能可以让你在容器中开发代码,能自动启动容器并使用 VS Code 在容器内进行开发调试。DevContainer 会自动挂载当前目录到容器内的 /workspace 目录,所以容器内的文件会和宿主机同步,不会丢失。VS Code 还能自动配置端口映射和 X11 显示并兼容 WSLg,plt.show() 能在 Windows 上显示图像。

要使用 DevContainer,需要安装 Visual Studio CodeRemote - WSLRemote - Containers 插件。由于我们没有使用 Docker Desktop,所以需要先让 VSCode 连接到 WSL,才能使用 DevContainer 功能。

首次配置大致需要以下几个步骤:

  1. 打开 VS Code,按左下角的 >< 图标,选择 Remote-WSL: New Window
  2. 在 Terminal 窗口中 git clone 你的项目,或者打开一个已有的项目, 项目目录最好放在 WSL 的文件系统中 (如 /home/username/project), 然后 cd 到项目目录, 用 code . 命令打开项目
  3. 添加 DevContainer 配置文件 .devcontainer/devcontainer.json.devcontainer/Dockerfile. Dockerfile 可以参考上一节的例子,DevContainer 配置文件可以参考下面的例子
  4. 按左下角的 >< 图标,选择 Remote-Containers: Reopen in Container,VS Code 会自动构建镜像并启动容器,首次启动可能需要下载镜像和安装软件包,耗时较长。如果构建失败,可以在 VS Code 的 Terminal 窗口中查看构建日志。很多构建失败的原因是网络问题,请确保你已经按照上面的步骤在所有地方都设置好了代理。
  5. 在容器中可以使用 VS Code 的所有功能,包括调试、代码补全、代码格式化等。容器内的文件会和宿主机同步,不会丢失。容器内的代码修改会立即反映到宿主机,不需要手动同步文件。

下面是 .devcontainer/devcontainer.json 模板,需要按需修改挂载数据集的配置(如果需要)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"build": {
"dockerfile": "Dockerfile",
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.autopep8",
"ms-vscode.cmake-tools",
"ms-vscode.cpptools",
"GitHub.copilot",
"ms-vscode.hexeditor",
// Add more extensions here to use in the container
],
},
},
"capAdd": [
"SYS_PTRACE", // Required to use gdb
],
"runArgs": [
// Enable host.docker.internal DNS name
"--add-host=host.docker.internal:host-gateway",
// Enable CUDA support
"--gpus",
"all",
],
"mounts": [
// UNCOMMENT AND TYPE YOUR ABSOLUTE PATH TO THE DATASETS FOLDER
// "type=bind,source=/absolute/path/to/datasets,target=/datasets"
],
"shutdownAction": "none",
"hostRequirements": {
"gpu": true,
},
}