Klapring

Hello, PyTorch (1) | 环境配置

安装 Docker

Windows

Windows 系统不能直接安装使用 Docker,需要先安装 WSL2(Windows Subsystem Linux 2)。以下以 x86 架构的 Windows 10 系统为例,进行 WSL2 的安装流程说明。

Linux

Linux 系统可以直接安装使用 docker-ce,由于国内大陆地区网络问题,需要通过国内镜像源安装。下面以清华镜像源与 Ubuntu 系统(其他 Linux 发行版可参考 docker-ce |清华大学开源软件镜像站进行相关配置)为例,进行安装流程说明。

安装 PyTorch

安装完成 Docker 后,就可以开始配置 PyTorch 环境了。创建一个合适的目录用于存放Docker 镜像,以下以 ~/workspace/pinn 作为示例目录进行讲解。

在 Docker 中进行 Python 开发需要一个可以连接到容器内进行开发的集成开发环境(IDE),如 VSCode(Visual Studio Code) 或 Pycharm。如果不想安装集成开发环境,也可使用 Jupyter Lab 在浏览器中进行开发(但建议使用集成开发环境,而非 Jupyter)。

安装 CUDA 与 CuDNN

本节仅 Nvidia 显卡需要进行,默认系统已安装 Nvidia 显卡驱动。在终端中执行

nvidia-smi

可以看到一个报告显卡驱动状态的表格,找到「CUDA Version」,记下后面的版本号。打开Nvidia 的 CUDA 下载网站 CUDA Toolkit Archive | NVIDIA Developer,根据之前记下的版本号,点击进入对应版本的 CUDA 下载页面。下载界面会有 Linux/Windows 的版本选项,尽量选择下载本地(locally)版本。Windows 系统是一个.exe 格式的安装器,下载后执行安装即可;Linux 版本是几行命令,复制到终端中执行即可,注意最新驱动下 Windows 系统无需额外为 WSL 安装 CUDA。

如果 CUDA 为最新版(目前最新版为 12.6),可以直接在 cuDNN Downloads | NVIDIA Developer 下载最新版的 CuDNN,否则需要在 cuDNN Archive | NVIDIA Developer 处下载对应 CUDA 版本的 CuDNN,注意需要注册一个账号才能下载。与 CUDA相同,下载页面也提供了不同系统的不同版本安装下载方式。

对 Windows 系统来说,此时在 WSL 中就已经可以使用 nvidia-smi 命令显示穿透到 WSL中的显卡信息了。

构建镜像

新建一个文件 ~/workspace/pytorch/dockerfile,由于此时不再有 Linux 系统权限限制,Windows系统可以使用

notepad.exe ~/workspace/pytorch/dockerfile

调用 Windows 系统自带记事本进行编辑(如果安装了 VSCode,则 notepad.exe 改为 code);Linux 系统仍可使用 vi 或 vim 进行编辑。

由于不同的电脑配置需要使用不同的镜像,因此以下将分类讲解 Nvidia 显卡、AMD 显卡与纯 CPU 三种不同配置下构建镜像的方式。此处使用前人已构建好的镜像

Nvidia 显卡

Nvidia 显卡可以直接使用 PyTorch 提供的 docker 镜像作为基础进行开发。在dockerfile 文件中输入如下内容:

# ~/workspace/pytorch/dockerfile
FROM cnstark/pytorch:2.3.1-py3.10.15-cuda12.1.0-ubuntu22.04
WORKDIR /workspace
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
RUN rm -r /temp && rm -r /root/.cache/pip

AMD 显卡

AMD 显卡可以使用 ROCm 来进行 PyTorch 开发(需要参考 WSL How to guide - Use ROCm on Radeon GPUs — Use ROCm on Radeon GPUs 进行相关配置)。在 dockerfile 文件中输入如下内容:

# ~/workspace/pytorch/dockerfile
FROM rocm/pytorch:rocm6.1.3_ubuntu22.04_py3.10_pytorch_release-2.1.2
WORKDIR /workspace
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
RUN rm -r /temp && rm -r /root/.cache/pip

纯 CPU

在 dockerfile 文件中输入如下内容:

# ~/workspace/pytorch/dockerfile
FROM cnstark/pytorch:2.3.1-py3.10.15-ubuntu22.04
WORKDIR /workspace
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
RUN rm -r /temp && rm -r /root/.cache/pip

如果使用 Jupyter Lab,则需要在 dockerfile 末尾添加两行:

EXPOSE 8888
ENTRYPOINT ["jupyter","lab","--ip=0.0.0.0","--allow-root","--no-browser"]

接着在同一目录下新建 requirements.txt 文件,这个文件里是其他需要的 Python 包,例如:

# ~/workspace/pytorch/requirements.txt
# Jupyter Lab 配置
# 使用 Pycharm/VSCode/其他 IDE 编辑代码,这部分可以全部注释掉
jupyterlab # Jupyter Lab 本体
jupyterlab-language-pack-zh-CN # Jupyter Lab 中文语言包
jupyterlab-lsp # Jupyter Lab 语言服务器(LSP)支持
jedi-language-server # Jedi 语言服务器
# 其他需要的包
ipykernel # 运行 Jupyter Notebook 的核心包,使用 VSCode 时需要
scipy # 提供一些实用函数
pandas # 数据处理包,可以注释掉
matplotlib # 绘图包,如果需要导出数据用其他软件绘图,可以注释掉

最后再在统一目录下创建 docker-compose.yaml,根据创建 dockerfile 时的不同,分别填入如下内容:

Nvidia 显卡

# ~/workspace/pytorch/docker-compose.yaml
services:
nvidia-pytorch:
build: .
ipc: host
volumes:
- ./data:/workspace
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
tty: true
stdin_open: true

AMD 显卡

# ~/workspace/pytorch/docker-compose.yaml
services:
rocm-pytorch:
build: .
cap_add:
- SYS_PTRACE
security_opt:
- seccomp=unconfined
ipc: host
shm_size: 8G
devices:
- /dev/dxg
volumes:
- ./data:/workspace
- /usr/lib/wsl/lib/libdxcore.so:/usr/lib/libdxcore.so
- /opt/rocm/lib/libhsa-runtime64.so.1:/opt/rocm/lib/libhsa-runtime64.so.1
tty: true
stdin_open: true

需要注意的是以上为 Windows 系统下的 docker-compose.ymal 文件,如果是 Linux,则需删除 volumes 中 workspace 以外的两项,同时修改 devices:

# ~/workspace/pytorch/docker-compose.yaml
services:
rocm-pytorch:
build: .
cap_add:
- SYS_PTRACE
security_opt:
- seccomp=unconfined
ipc: host
shm_size: 8G
devices:
- /dev/dxg
- /dev/kfd
- /dev/dri
volumes:
- ./data:/workspace
- /usr/lib/wsl/lib/libdxcore.so:/usr/lib/libdxcore.so
- /opt/rocm/lib/libhsa-runtime64.so.1:/opt/rocm/lib/libhsa-runtime64.so.1
tty: true
stdin_open: true

纯 CPU

# ~/workspace/pytorch/docker-compose.yaml
services:
pytorch:
build: .
volumes:
- ./data:/workspace
tty: true
stdin_open: true

如果使用 Jupyter Lab,则需要在 volumes 前新增两行:

ports:
- '8888:8888'

最后在 Linux/WSL 终端中执行

docker-compose up -d

即可启动容器,如果使用 Jupyter Lab,此时在浏览器中访问 https://127.0.0.1:8888/即可进入 Jupyter Lab 页面。对 VSCode,需要安装 Docker 扩展,在扩展侧边栏中右键刚刚建立的容器,启动并附加 VSCode 页面;对 Pycharm 或其他 IDE,请参考对应的官方文档。

附录

自建 Docker 镜像

Cloudflare Worker 搭建 Github 与 Docker 加速,来自用GPT融了一个Cloudflare Workers的github下载+ Docke pull加速-开发调优- LINUX DO。建议自行修改部分代码(比如用 LLM 洗一遍),以防有大量相似代码的 Worker 导致被 Cloudflare 认定为滥用 Worker。

// worker.js
'use strict';
const HUB_HOST = 'registry-1.docker.io';
const AUTH_URL = 'https://auth.docker.io';
const WORKERS_URL = 'https://你的域名';
const ASSET_URL = 'https://hunshcn.github.io/gh-proxy/';
const PREFIX = '/';
const Config = { jsdelivr: 0 };
const whiteList = [];
const exp1 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:releases|archive)\/.*$/i;
const exp2 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:blob|raw)\/.*$/i;
const exp3 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/(?:info|git-).*$/i;
const exp4 = /^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+?\/.+$/i;
const exp5 = /^(?:https?:\/\/)?gist\.(?:githubusercontent|github)\.com\/.+?\/.+?\/.+$/i;
const exp6 = /^(?:https?:\/\/)?github\.com\/.+?\/.+?\/tags.*$/i;
/** @type {RequestInit} */
const PREFLIGHT_INIT = {
// @ts-ignore
status: 204,
headers: new Headers({
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET, POST, PUT, PATCH, TRACE, DELETE, HEAD, OPTIONS',
'access-control-max-age': '1728000',
}),
};
/**
* Create a new response.
* @param {any} body
* @param {number} [status=200]
* @param {Object<string, string>} headers
* @returns {Response}
*/
function makeResponse(body, status = 200, headers = {}) {
headers['access-control-allow-origin'] = '*';
return new Response(body, { status, headers });
}
/**
* Create a new URL object.
* @param {string} urlStr
* @returns {URL|null}
*/
function createURL(urlStr) {
try {
return new URL(urlStr);
} catch (err) {
return null;
}
}
addEventListener('fetch', (event) => {
event.respondWith(
handleFetchEvent(event).catch((err) => makeResponse(`cfworker error:\n${err.stack}`, 502))
);
});
/**
* Handle the fetch event.
* @param {FetchEvent} event
* @returns {Promise<Response>}
*/
async function handleFetchEvent(event) {
const req = event.request;
const url = new URL(req.url);
if (url.pathname.startsWith('/token') || url.pathname.startsWith('/v2')) {
return handleDockerProxy(req, url);
}
if (url.pathname.startsWith(PREFIX)) {
return handleGitHubProxy(req, url);
}
return makeResponse('Not Found', 404);
}
/**
* Handle token requests and Docker proxy.
* @param {Request} req
* @param {URL} url
* @returns {Promise<Response>}
*/
async function handleDockerProxy(req, url) {
if (url.pathname === '/token') {
const tokenURL = AUTH_URL + url.pathname + url.search;
const headers = new Headers({
Host: 'auth.docker.io',
'User-Agent': req.headers.get('User-Agent'),
Accept: req.headers.get('Accept'),
'Accept-Language': req.headers.get('Accept-Language'),
'Accept-Encoding': req.headers.get('Accept-Encoding'),
Connection: 'keep-alive',
'Cache-Control': 'max-age=0',
});
return fetch(new Request(tokenURL, req), { headers });
}
url.hostname = HUB_HOST;
const headers = new Headers({
Host: HUB_HOST,
'User-Agent': req.headers.get('User-Agent'),
Accept: req.headers.get('Accept'),
'Accept-Language': req.headers.get('Accept-Language'),
'Accept-Encoding': req.headers.get('Accept-Encoding'),
Connection: 'keep-alive',
'Cache-Control': 'max-age=0',
});
if (req.headers.has('Authorization')) {
headers.set('Authorization', req.headers.get('Authorization'));
}
const response = await fetch(new Request(url, req), { headers });
const responseHeaders = new Headers(response.headers);
const status = response.status;
if (responseHeaders.get('Www-Authenticate')) {
const authHeader = responseHeaders.get('Www-Authenticate');
const re = new RegExp(AUTH_URL, 'g');
responseHeaders.set('Www-Authenticate', authHeader.replace(re, WORKERS_URL));
}
if (responseHeaders.get('Location')) {
return handleHttpRedirect(req, responseHeaders.get('Location'));
}
responseHeaders.set('access-control-expose-headers', '*');
responseHeaders.set('access-control-allow-origin', '*');
responseHeaders.set('Cache-Control', 'max-age=1500');
responseHeaders.delete('Content-Security-Policy');
responseHeaders.delete('Content-Security-Policy-Report-Only');
responseHeaders.delete('Clear-Site-Data');
return new Response(response.body, { status, headers: responseHeaders });
}
/**
* Handle GitHub proxy requests.
* @param {Request} req
* @param {URL} url
* @returns {Promise<Response>}
*/
async function handleGitHubProxy(req, url) {
let path = url.searchParams.get('q');
if (path) {
return Response.redirect('https://' + url.host + PREFIX + path, 301);
}
path = url.href.substr(url.origin.length + PREFIX.length).replace(/^https?:\/+/, 'https://');
if (checkUrl(path)) {
return httpHandler(req, path);
} else if (path.search(exp2) === 0) {
if (Config.jsdelivr) {
const newUrl = path
.replace('/blob/', '@')
.replace(/^(?:https?:\/\/)?github\.com/, 'https://cdn.jsdelivr.net/gh');
return Response.redirect(newUrl, 302);
} else {
path = path.replace('/blob/', '/raw/');
return httpHandler(req, path);
}
} else if (path.search(exp4) === 0) {
const newUrl = path
.replace(/(?<=com\/.+?\/.+?)\/(.+?\/)/, '@$1')
.replace(
/^(?:https?:\/\/)?raw\.(?:githubusercontent|github)\.com/,
'https://cdn.jsdelivr.net/gh'
);
return Response.redirect(newUrl, 302);
} else {
return fetch(ASSET_URL + path);
}
}
/**
* Check if the URL matches GitHub patterns.
* @param {string} url
* @returns {boolean}
*/
function checkUrl(url) {
return [exp1, exp2, exp3, exp4, exp5, exp6].some((exp) => url.search(exp) === 0);
}
/**
* Handle HTTP redirects.
* @param {Request} req
* @param {string} location
* @returns {Promise<Response>}
*/
async function handleHttpRedirect(req, location) {
const url = createURL(location);
if (!url) {
return makeResponse('Invalid URL', 400);
}
return proxyRequest(url, req);
}
/**
* Handle HTTP requests.
* @param {Request} req
* @param {string} pathname
* @returns {Promise<Response>}
*/
async function httpHandler(req, pathname) {
if (req.method === 'OPTIONS' && req.headers.has('access-control-request-headers')) {
return new Response(null, PREFLIGHT_INIT);
}
const headers = new Headers(req.headers);
let flag = !whiteList.length;
for (const i of whiteList) {
if (pathname.includes(i)) {
flag = true;
break;
}
}
if (!flag) {
return new Response('blocked', { status: 403 });
}
if (pathname.search(/^https?:\/\//) !== 0) {
pathname = 'https://' + pathname;
}
const url = createURL(pathname);
return proxyRequest(url, { method: req.method, headers, body: req.body });
}
/**
* Proxy a request.
* @param {URL} url
* @param {RequestInit} reqInit
* @returns {Promise<Response>}
*/
async function proxyRequest(url, reqInit) {
const response = await fetch(url.href, reqInit);
const responseHeaders = new Headers(response.headers);
if (responseHeaders.has('location')) {
const location = responseHeaders.get('location');
if (checkUrl(location)) {
responseHeaders.set('location', PREFIX + location);
} else {
reqInit.redirect = 'follow';
return proxyRequest(createURL(location), reqInit);
}
}
responseHeaders.set('access-control-expose-headers', '*');
responseHeaders.set('access-control-allow-origin', '*');
responseHeaders.delete('content-security-policy');
responseHeaders.delete('content-security-policy-report-only');
responseHeaders.delete('clear-site-data');
return new Response(response.body, {
status: response.status,
headers: responseHeaders,
});
}

参考资料