在第一篇文章中,我解释了我们如何生成一个可以在 Python 解释器中使用的 torch.Tensor 对象。接下来,我将探讨 PyTorch 的构建系统。PyTorch 代码库包含多种组件:
- 核心 Torch 库:TH, THC, THNN, THCUNN
- 供应商库:CuDNN, NCCL
- Python 扩展库
- 其他第三方库:NumPy, MKL, LAPACK
仅仅执行一次 python setup.py install 是如何完成相应工作,从而让你能够调用 import torch 并在代码中使用 PyTorch 库的呢?
本文的第一部分将从最终用户的角度解释构建过程,说明我们如何利用上述组件构建库。文档的第二部分对 PyTorch 开发人员非常重要,它将记录一些通过仅构建你正在处理的那部分代码来提高迭代速度的方法。
Setuptools 与 PyTorch 的 setup( ) 函数
Python 使用 Setuptools 来构建库。Setuptools 是 Python 核心库中原始 distutils 系统的扩展。Setuptools 的核心组件是 setup.py 文件,它包含了构建项目所需的所有信息。其中最重要的函数是 setup() 函数,它是主要的入口点。让我们看看 PyTorch 中的实现:
setup(name="torch", version=version,
description="Tensors and Dynamic neural networks in Python with strong GPU acceleration",
ext_modules=extensions,
cmdclass={
'build': build,
'build_py': build_py,
'build_ext': build_ext,
'build_deps': build_deps,
'build_module': build_module,
'develop': develop,
'install': install,
'clean': clean,
},
packages=packages,
package_data={'torch': [
'lib/*.so*', 'lib/*.dylib*',
'lib/torch_shm_manager',
'lib/*.h',
'lib/include/TH/*.h', 'lib/include/TH/generic/*.h',
'lib/include/THC/*.h', 'lib/include/THC/generic/*.h']},
install_requires=['pyyaml'],
)
该函数完全由关键字参数组成,这些参数有两个作用:
- 元数据(例如:名称、描述、版本)
- 包的内容
我们重点关注第 2 点。让我们拆解各个组件:
- ext_modules:Python 模块分为“纯”模块(仅包含 Python 代码)和用 Python 实现的底层语言编写的“扩展”模块。在此,我们列出了构建中的扩展模块,包括包含我们 Python Tensor 的主
torch._C库。 - cmdclass:当从命令行使用
setup.py脚本时,用户必须指定一个或多个“命令”,即执行特定操作的代码片段。例如,“install”命令负责构建并安装包。此映射将特定命令路由到setup.py中实现它们的函数。 - packages:项目中的包列表。这些是“纯”包,即它们仅包含 Python 代码。这些在
setup.py的其他地方定义。 - package_data:需要安装到包中的附加文件:在本例中,构建生成的头文件和共享库必须包含在我们的安装中。
- install_requires:为了构建 PyTorch,我们需要 pyyaml。Setuptools 将负责确保 pyyaml 可用,并在必要时下载并安装它。
我们将更详细地讨论这些组件,但现在查看安装的最终产物(即 Setuptools 在构建代码后做了什么)是非常有益的。
site_packages
第三方包默认安装到与你的 Python 二进制文件关联的 lib/<version>/site_packages 目录中。例如,因为我使用的是 Miniconda 环境,我的 Python 二进制文件位于:
(p3) killeent@devgpu047:pytorch (master)$ which python
~/local/miniconda2/envs/p3/bin/python
因此,包被安装到了:
/home/killeent/local/miniconda2/envs/p3/lib/python3.6/site-packages
我安装了 PyTorch,让我们看看 site-packages 中的 torch 文件夹:
(p3) killeent@devgpu047:site-packages$ cd torch
(p3) killeent@devgpu047:torch$ ls
autograd backends _C.cpython-36m-x86_64-linux-gnu.so cuda distributed _dl.cpython-36m-x86_64-linux-gnu.so functional.py __init__.py legacy lib multiprocessing nn optim __pycache__ serialization.py _six.py sparse storage.py _tensor_docs.py tensor.py _tensor_str.py _thnn _torch_docs.py utils _utils.py version.py
注意,我们预期的所有内容都在这里。
- 所有的“纯”包都在这里 [待办:从 setup.py 打印包以进行解释]
- 扩展库在这里——._C* 和 ._dl* 共享库。
- package_data 在这里:lib 的内容与我们在 setup 函数中描述的完全一致。
(p3) killeent@devgpu047:torch$ ls lib/
include libnccl.so.1 libTHC.so.1 libTHCUNN.so.1 libTHNN.so.1 libTH.so.1 THCUNN.h torch_shm_manager libnccl.so libshm.so libTHCS.so.1 libTHD.so.1 libTHPP.so.1 libTHS.so.1 THNN.h
Python 解释器在导入时会查看 site_packages。如果我们在 Python 代码中调用 import torch,它会在这里找到该模块,然后初始化并导入它。你可以在此处阅读有关导入系统的更多信息。
构建各个部分
接下来,我们将自始至终查看构建的各个组成部分。这将说明我们是如何组合引言中提到的所有代码的。
后端 Torch 和供应商库
让我们看一下 PyTorch 的 setup.py 中对 install 命令的重写:
class install(setuptools.command.install.install):
def run(self):
if not self.skip_build:
self.run_command('build_deps')
setuptools.command.install.install.run(self)
我们注意到它做的第一件事是运行一个名为 “build_deps” 的命令——让我们看一下它的 run() 方法:
def run(self):
from tools.nnwrap import generate_wrappers as generate_nn_wrappers
build_all_cmd = ['bash', 'torch/lib/build_all.sh']
if WITH_CUDA:
build_all_cmd += ['--with-cuda']
if WITH_NCCL and not SYSTEM_NCCL:
build_all_cmd += ['--with-nccl']
if WITH_DISTRIBUTED:
build_all_cmd += ['--with-distributed']
if subprocess.call(build_all_cmd) != 0:
sys.exit(1)
generate_nn_wrappers()
这里我们注意到,我们在 torch/lib/ 目录中有一个 shell 脚本 build_all.sh。该脚本可以通过我们是否处于启用了 CUDA、启用了 NCCL 库以及启用了 PyTorch 分布式库的系统上进行配置。
让我们查看 torch/lib:
(p3) killeent@devgpu047:lib (master)$ ls
build_all.sh libshm nccl README.md TH THC THCS THCUNN THD THNN THPP THS
在这里我们可以看到所有后端库的目录。TH、THC、THNN、THCUNN 和 nccl 是与诸如 github.com/torch 中的库保持同步的 git 子树。THS、THCS、THD、THPP 和 libshm 是 PyTorch 特有的库。所有这些库都包含 CMakeLists.txt,表明它们是使用 CMake 构建的。
build_all.sh 本质上是一个脚本,它对所有这些库执行 CMake 配置步骤,然后执行 make install。让我们运行 ./build_all.sh,看看会得到什么结果。
(p3) killeent@devgpu047:lib (master)$ ./build_all.sh --with-cuda --with-nccl --with-distributed
[various CMake output logs]
(p3) killeent@devgpu047:lib (master)$ ls
build build_all.sh include libnccl.so libnccl.so.1 libshm libshm.so libTHC.so.1 libTHCS.so.1 libTHCUNN.so.1 libTHD.so.1 libTHNN.so.1 libTHPP.so.1 libTH.so.1 libTHS.so.1 nccl README.md TH THC THCS THCUNN THCUNN.h THD THNN THNN.h THPP THS tmp_install torch_shm_manager
现在该目录中有一些额外的内容:
- 每个库的共享库文件
THNN和THCUNN的头文件build和tmp_install目录torch_shm_manager可执行文件
让我们进一步探索。在 shell 脚本中,我们创建了 build 目录以及每个待构建库的子目录。
# We create a build directory for the library, which will
# contain the cmake output. $1 is the library to be built
mkdir -p build/$1
cd build/$1
因此,例如 build/TH 包含了 CMake 配置输出,包括用于构建 TH 的 Makefile,以及在该目录中运行 make install 的结果。
我们再看看 tmp_install:
(p3) killeent@devgpu047:lib (master)$ ls tmp_install/
bin include lib share
tmp_install 看起来像一个标准的安装目录,包含二进制文件、头文件和库文件。例如,tmp_install/include/TH 包含所有 TH 头文件,tmp_install/lib/ 包含 libTH.so.1 文件。
那么为什么要有这个目录呢?它用于编译相互依赖的库。例如,THC 库依赖于 TH 库及其头文件。这在构建 shell 脚本中作为 cmake 命令的参数被引用。
# install_dir is tmp_install
cmake ...
-DTH_INCLUDE_PATH="$INSTALL_DIR/include" \
-DTH_LIB_PATH="$INSTALL_DIR/lib" \
事实上,如果我们查看我们构建的 THC 库:
(p3) killeent@devgpu047:lib (master)$ ldd libTHC.so.1
...
libTH.so.1 => /home/killeent/github/pytorch/torch/lib/tmp_install/lib/./libTH.so.1 (0x00007f84478b7000)
build_all.sh 指定包含路径和库路径的方式有点混乱,但这代表了总体思路。最后,在脚本结束时:
# If all the builds succeed we copy the libraries, headers,
# binaries to torch/lib
cp $INSTALL_DIR/lib/* .
cp THNN/generic/THNN.h .
cp THCUNN/generic/THCUNN.h .
cp -r $INSTALL_DIR/include .
cp $INSTALL_DIR/bin/* .
正如我们所见,最后我们将所有内容复制到了顶层 torch/lib 目录——这就解释了我们在上面看到的内容。我们稍后会明白为什么要这样做。
NN 包装器
简要地说,让我们谈谈 build_deps 命令的最后一部分:generate_nn_wrappers()。我们使用 PyTorch 的自定义 cwrap 工具绑定到后端库,这一点我们在之前的文章中提到过。对于绑定 TH 和 THC,我们手动为每个函数编写 YAML 声明。然而,由于 THNN 和 THCUNN 库相对简单,我们自动生成了 cwrap 声明和生成的 C++ 代码。
我们将 THNN.h 和 THCUNN.h 头文件复制到 torch/lib 中的原因是,generate_nn_wrappers() 代码期望这些文件位于此处。generate_nn_wrappers() 做了几件事:
- 解析头文件,生成 cwrap YAML 声明并将它们写入输出的
.cwrap文件。 - 使用适当的插件调用
cwrap处理这些.cwrap文件,以生成相应的源代码。 - 第二次解析头文件以生成
THNN_generic.h——一个库,它接收THPPTensors(PyTorch 的“通用”C++ Tensor 库),并根据 Tensor 的动态类型调用适当的THNN/THCUNN库函数。
如果我们在运行 generate_nn_wrappers() 后查看 torch/csrc/nn,我们可以看到输出:
(p3) killeent@devgpu047:nn (master)$ ls
THCUNN.cpp THCUNN.cwrap THNN.cpp THNN.cwrap THNN_generic.cpp THNN_generic.cwrap THNN_generic.h THNN_generic.inc.h
例如,代码生成了如下的 cwrap:
[[
name: FloatBatchNormalization_updateOutput
return: void
cname: THNN_FloatBatchNormalization_updateOutput
arguments:
- void* state
- THFloatTensor* input
- THFloatTensor* output
- type: THFloatTensor*
name: weight
nullable: True
- type: THFloatTensor*
name: bias
nullable: True
- THFloatTensor* running_mean
- THFloatTensor* running_var
- THFloatTensor* save_mean
- THFloatTensor* save_std
- bool train
- double momentum
- double eps
]]
以及相应的 .cpp:
extern "C" void THNN_FloatBatchNormalization_updateOutput(void*, THFloatTensor*, THFloatTensor*, THFloatTensor*, THFloatTensor*, THFloatTensor*, THFloatTensor*, THFloatTensor*, THFloatTensor*, bool, double, double);
PyObject * FloatBatchNormalization_updateOutput(PyObject *_unused, PyObject *args) {
// argument checking, unpacking
PyThreadState *_save = NULL;
try {
Py_UNBLOCK_THREADS;
THNN_FloatBatchNormalization_updateOutput(arg_state, arg_input, arg_output, arg_weight, arg_bias, arg_running_mean, arg_running_var, arg_save_mean, arg_save_std, arg_train, arg_momentum, arg_eps);
Py_BLOCK_THREADS;
Py_RETURN_NONE;
} catch (...) {
if (_save) {
Py_BLOCK_THREADS;
}
throw;
}
...
}
在 THPP 生成的代码中,函数看起来像这样:
void BatchNormalization_updateOutput(thpp::Tensor* input, thpp::Tensor* output, thpp::Tensor* weight, thpp::Tensor* bias, thpp::Tensor* running_mean, thpp::Tensor* running_var, thpp::Tensor* save_mean, thpp::Tensor* save_std, bool train, double momentum, double eps) {
// Call appropriate THNN function based on tensor type, whether its on CUDA, etc.
}
稍后我们将详细讨论这些源文件是如何使用的。
“构建”纯 Python 模块
现在我们已经构建了后端库(即“依赖项”),我们可以继续构建实际的 PyTorch 代码了。下一个运行的 Setuptools 命令是 build_py,它用于构建库中所有的“纯”python 模块。这些就是传递给 setup.py 的“包”。
这些包是通过 Setuptools 的工具函数 find_packages() 找到的:
packages = find_packages(exclude=('tools.*',))
['torch', 'torch._thnn', 'torch.autograd', 'torch.backends', 'torch.cuda', 'torch.distributed', 'torch.legacy', 'torch.multiprocessing', 'torch.nn', 'torch.optim', 'torch.sparse', 'torch.utils', 'torch.autograd._functions', 'torch.backends.cudnn', 'torch.legacy.nn', 'torch.legacy.optim', 'torch.nn._functions', 'torch.nn.backends', 'torch.nn.modules', 'torch.nn.parallel', 'torch.nn.utils', 'torch.nn._functions.thnn', 'torch.utils.data', 'torch.utils.ffi', 'torch.utils.serialization', 'torch.utils.trainer', 'torch.utils.backcompat', 'torch.utils.trainer.plugins']
正如我们所见,find_package 已经递归遍历了 torch 目录,找出了所有包含 __init__.py 文件的目录路径。
使用 Setuptools 构建时,该工具会在分发根目录(即与 setup.py 文件相同的位置)创建一个 build 目录。由于 PyTorch 由“纯”python 模块和扩展模块组成,我们需要在执行构建时保留有关所用操作系统和 Python 版本的信息。所以如果我们查看我的 build 目录,我们会看到:
(p3) killeent@devgpu047:pytorch (master)$ ls build
lib.linux-x86_64-3.6 temp.linux-x86_64-3.6
这表明我是在 linux-x86-64 上使用 Python 3.6 构建的项目。lib 目录包含库文件,而 temp 目录包含构建期间生成的最终安装不需要的文件。
因为“纯”python 模块只是 Python 代码,不需要“编译”,所以 build_py 过程只是将文件从 find_packages 找到的位置复制到 build/ 中的对应位置。所以我们的构建输出中充斥着如下所示的行:
copying torch/autograd/_functions/blas.py -> build/lib.linux-x86_64-3.6/torch/autograd/_functions
我们也在此前提到,可以将文件和目录传递给主 setup() 函数的 package_data 关键字参数,Setuptools 将处理将这些文件复制到安装位置的工作。在 build_py 期间,这些文件会被复制到 build/ 目录,所以我们也看到了如下行:
copying torch/lib/libTH.so.1 -> build/lib.linux-x86_64-3.6/torch/lib
...
copying torch/lib/include/THC/generic/THCTensor.h -> build/lib.linux-x86_64-3.6/torch/lib/include/THC/generic
构建扩展模块
最后,我们需要构建扩展模块,即使用 CPython 后端以 C++ 编写的 PyTorch 模块。这也构成了 setup.py 中大部分的代码逻辑。我们重写的 build_ext 命令在实际构建扩展本身之前有一些特殊的逻辑:
from tools.cwrap import cwrap
from tools.cwrap.plugins.THPPlugin import THPPlugin
from tools.cwrap.plugins.ArgcountSortPlugin import ArgcountSortPlugin
from tools.cwrap.plugins.AutoGPU import AutoGPU
from tools.cwrap.plugins.BoolOption import BoolOption
from tools.cwrap.plugins.KwargsPlugin import KwargsPlugin
from tools.cwrap.plugins.NullableArguments import NullableArguments
from tools.cwrap.plugins.CuDNNPlugin import CuDNNPlugin
from tools.cwrap.plugins.WrapDim import WrapDim
from tools.cwrap.plugins.AssertNDim import AssertNDim
from tools.cwrap.plugins.Broadcast import Broadcast
from tools.cwrap.plugins.ProcessorSpecificPlugin import ProcessorSpecificPlugin
thp_plugin = THPPlugin()
cwrap('torch/csrc/generic/TensorMethods.cwrap', plugins=[
ProcessorSpecificPlugin(), BoolOption(), thp_plugin,
AutoGPU(condition='IS_CUDA'), ArgcountSortPlugin(), KwargsPlugin(),
AssertNDim(), WrapDim(), Broadcast()
])
cwrap('torch/csrc/cudnn/cuDNN.cwrap', plugins=[
CuDNNPlugin(), NullableArguments()
])
回想一下上面,我记录了我们自动生成了用于调用 THNN 等库的 C++ 代码。这就是我们绑定 TH、THC 和 CuDNN 的地方。我们获取 TensorMethods.cwrap 中的 YAML 声明,并使用它们生成输出 C++ 源文件,这些文件包含了在 PyTorch 的 C++ 生态系统中工作的实现。例如,一个简单的声明(如 zero_):
[[
name: zero_
cname: zero
return: self
arguments:
- THTensor* self
]]
生成了如下代码:
PyObject * THPTensor_(zero_)(PyObject *self, PyObject *args, PyObject *kwargs) {
...
THTensor_(zero)(LIBRARY_STATE arg_self);
...
}
在上一篇文章中,我们记录了这些函数是如何与特定 Tensor 类型关联的,所以我在这里不再赘述。对于构建过程,只需知道这些 C++ 文件是在扩展构建之前生成的即可,因为这些源文件在扩展编译期间会用到。
指定扩展
与纯模块不同,仅仅列出模块或包并期望 Setuptools 自动找到正确的文件是不够的;你必须指定扩展名、源文件以及任何编译/链接需求(包含目录、要链接的库等)。
setup.py 的主体(撰写本文时约为 200 行代码)都用于指定如何构建这些扩展。在这里,我们在 build_all.sh 中做出的某些选择开始变得有意义了。例如,我们看到构建脚本指定了一个 tmp_install 目录,我们在其中安装了后端库。在我们的 setup.py 代码中,我们在向要包含的头文件目录列表添加内容时引用了该目录:
# tmp_install_path is torch/lib/tmp_install
include_dirs += [
cwd,
os.path.join(cwd, "torch", "csrc"),
tmp_install_path + "/include",
tmp_install_path + "/include/TH",
tmp_install_path + "/include/THPP",
tmp_install_path + "/include/THNN",
同样,我们在 build_all.sh 脚本末尾将共享对象库复制到了 torch/csrc。在识别我们要链接的库时,我们在 setup.py 代码中直接引用了这些位置:
# lib_path is torch/lib
TH_LIB = os.path.join(lib_path, 'libTH.so.1')
THS_LIB = os.path.join(lib_path, 'libTHS.so.1')
THC_LIB = os.path.join(lib_path, 'libTHC.so.1')
THCS_LIB = os.path.join(lib_path, 'libTHCS.so.1')
THNN_LIB = os.path.join(lib_path, 'libTHNN.so.1')
# ...
让我们考虑一下如何构建主 torch._C 扩展模块:
C = Extension("torch._C",
libraries=main_libraries,
sources=main_sources,
language='c++',
extra_compile_args=main_compile_args + extra_compile_args,
include_dirs=include_dirs,
library_dirs=library_dirs,
extra_link_args=extra_link_args + main_link_args + [make_relative_rpath('lib')],
)
- 主库是我们链接的所有库。这包括诸如
shm(PyTorch 的共享内存管理库)以及系统库(如cudart和cudnn)。注意,TH库没有列在这里。 - 主要源文件是构成 PyTorch C++ 后端的 C++ 文件。
- 编译参数是配置编译的各种标志。例如,我们可能希望在调试模式下编译时添加调试标志。
- 包含目录是所有包含头文件的目录的路径。这也是
build_all.sh脚本很重要的另一个例子——例如,我们在torch/lib/tmp_install/include/TH中查找TH头文件——这就是我们在 CMake 配置中指定的安装位置。 - 库目录是链接时搜索共享库的目录。例如,我们包含了
torch/lib(我们在build_all.sh结尾将.so文件复制到的位置),但也包含了 CUDA 和 CuDNN 目录的路径。 - 链接参数在将目标文件链接在一起以创建扩展时使用。在 PyTorch 中,这包括一些更普通的选项,比如决定静态链接
libstdc++。然而,有一个关键组件:这就是我们链接后端 TH 库的地方。注意我们有这样的行:
# The explicit paths to .so files we described above
main_link_args = [TH_LIB, THS_LIB, THPP_LIB, THNN_LIB]
你可能会想,为什么要这样做,而不是将这些库添加到我们传递给 libraries 关键字参数的列表中?毕竟,那是一个要链接的库列表。问题在于 Lua Torch 安装通常会设置 LD_LIBRARY_PATH 变量,因此我们可能会错误地链接到为 Lua Torch 构建的 TH 库,而不是我们在本地构建的库。这会造成问题,因为代码可能已过期,而且 Lua Torch 的 TH 还有各种与 PyTorch 不兼容的配置选项。
因此,我们手动将我们生成的共享库的路径直接指定给链接器。
还有其他为 PyTorch 提供支持的扩展,它们的构建方式类似。Setuptools 库会调用 C++ 编译器和链接器来构建所有这些扩展。如果构建成功,我们也就成功地构建了 PyTorch 库,可以继续进行安装了。
安装
构建完成后,安装非常简单。我们只需将所有内容从 build/lib.linux-x86_64-3.6 目录复制到相应的安装目录。回想一下上面提到的,这个目录是与我们的 Python 二进制文件关联的 site_packages 目录。结果,我们看到了如下行:
running install_lib
creating /home/killeent/local/miniconda2/envs/p3/lib/python3.6/site-packages/torch
copying build/lib.linux-x86_64-3.6/torch/_C.cpython-36m-x86_64-linux-gnu.so -> /home/killeent/local/miniconda2/envs/p3/lib/python3.6/site-packages/torch
copying build/lib.linux-x86_64-3.6/torch/_dl.cpython-36m-x86_64-linux-gnu.so -> /home/killeent/local/miniconda2/envs/p3/lib/python3.6/site-packages/torch
creating /home/killeent/local/miniconda2/envs/p3/lib/python3.6/site-packages/torch/_thnn
copying build/lib.linux-x86_64-3.6/torch/_thnn/_THNN.cpython-36m-x86_64-linux-gnu.so -> /home/killeent/local/miniconda2/envs/p3/lib/python3.6/site-packages/torch/_thnn
copying build/lib.linux-x86_64-3.6/torch/_thnn/_THCUNN.cpython-36m-x86_64-linux-gnu.so -> /home/killeent/local/miniconda2/envs/p3/lib/python3.6/site-packages/torch/_thnn
最后,让我们启动 Python 解释器。当 Python 解释器执行导入语句时,它会沿着搜索路径搜索 Python 代码和扩展模块。在构建解释器时,搜索路径的默认值会被配置到 Python 二进制文件中。
# note we are now in my home directory
(p3) killeent@devgpu047:~$ python
Python 3.6.1 |Continuum Analytics, Inc.| (default, Mar 22 2017, 19:54:23)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> sys.path
['', '/home/killeent/local/miniconda2/envs/p3/lib/python36.zip', '/home/killeent/local/miniconda2/envs/p3/lib/python3.6', '/home/killeent/local/miniconda2/envs/p3/lib/python3.6/lib-dynload', '/home/killeent/.local/lib/python3.6/site-packages', '/home/killeent/local/miniconda2/envs/p3/lib/python3.6/site-packages', '/home/killeent/github/pytorch', '/home/killeent/local/miniconda2/envs/p3/lib/python3.6/site-packages/setuptools-27.2.0-py3.6.egg']
正如我们所见,我们将 PyTorch 安装复制到的 site-packages 目录是搜索路径的一部分。现在让我们加载 torch 模块并查看其位置:
>>> import torch
>>> import inspect
>>> inspect.getfile(torch)
'/home/killeent/local/miniconda2/envs/p3/lib/python3.6/site-packages/torch/__init__.py'
正如我们所见,我们已经按预期从 site_packages 加载了模块——我们的构建和安装成功了!
注意: Python 会将空字符串预置到 sys.path 中以表示当前工作目录——使其成为我们搜索模块的第一个位置。因此,如果我们从 pytorch 目录运行 Python,我们将意外地加载本地版本的 PyTorch,而不是我们安装的版本。这是需要注意的一点。
附录 – 开发人员效率、第三方库、我未涵盖的内容
整个 PyTorch 的安装循环可能非常耗时。在我的开发服务器上,从源码安装大约需要 5 分钟。通常,在开发 PyTorch 时,我们只想处理整个项目的一个子集,并仅重新构建该子集以测试更改。幸运的是,我们的构建系统支持这一点。
Setuptools 开发模式
支持此功能的主要工具是 Setuptools 的 develop 命令。文档指出:
该命令允许你部署项目的源码以供在一个或多个“暂存区”中使用,以便进行导入。这种部署方式使得对项目源码的更改在暂存区中立即可用,而无需在每次更改后运行构建或安装步骤。
但它是如何工作的呢?假设我们在 PyTorch 目录中运行 python setup.py build develop。build 命令会被运行,构建我们的依赖项(TH, THPP 等)和扩展库。然而,如果我们查看 site-packages:
(p3) killeent@devgpu047:site-packages$ ls -la torch*
-rw-r--r--. 1 killeent users 31 Jun 27 08:02 torch.egg-link
查看 torch.egg-link 文件的内容,它仅仅引用了 PyTorch 目录:
(p3) killeent@devgpu047:site-packages$ cat torch.egg-link
/home/killeent/github/pytorch
如果我们导航回 PyTorch 目录,我们会看到有一个新目录 torch.egg-info:
(p3) killeent@devgpu047:pytorch (master)$ ls -la torch.egg-info/
total 28
drwxr-xr-x. 2 killeent users 4096 Jun 27 08:09 .
drwxr-xr-x. 10 killeent users 4096 Jun 27 08:01 ..
-rw-r--r--. 1 killeent users 1 Jun 27 08:01 dependency_links.txt
-rw-r--r--. 1 killeent users 255 Jun 27 08:01 PKG-INFO
-rw-r--r--. 1 killeent users 7 Jun 27 08:01 requires.txt
-rw-r--r--. 1 killeent users 16080 Jun 27 08:01 SOURCES.txt
-rw-r--r--. 1 killeent users 12 Jun 27 08:01 top_level.txt
此文件包含有关 PyTorch 项目的元数据。例如,requirements.txt 列出了设置 PyTorch 的所有依赖项。
(p3) killeent@devgpu047:pytorch (master)$ cat torch.egg-info/requires.txt
pyyaml
无需深入细节,develop 允许我们基本将 PyTorch 仓库本身视为位于 site-packages 中,因此我们可以导入该模块,它就能直接工作。
(p3) killeent@devgpu047:~$ python
Python 3.6.1 |Continuum Analytics, Inc.| (default, Mar 22 2017, 19:54:23)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> torch.__file__
'/home/killeent/github/pytorch/torch/__init__.py'
因此,具有以下后果:
- 如果我们更改 Python 源文件,这些更改会被自动获取,我们无需运行任何命令即可让 Python 解释器看到此更改。
- 如果我们更改了某个扩展库中的 C++ 源文件,我们可以重新运行
develop命令,它将重新构建该扩展。
因此,我们可以无缝地开发 PyTorch 代码库,并以简单的方式测试我们的更改。
处理依赖库
如果我们正在处理依赖项(如 TH, THPP 等),我们可以通过直接运行 build_deps 命令来更快地重新构建我们的更改。这将自动调用 build_all.sh 来重新构建我们的库,并适当地复制生成的库。如果我们使用 Setuptools 的 develop 模式,我们将使用在 PyTorch 目录中构建的本地扩展库。由于我们在编译扩展库时指定了共享库的路径,更改将被获取。
# we are using the local extension
(p3) killeent@devgpu047:~$ python
Python 3.6.1 |Continuum Analytics, Inc.| (default, Mar 22 2017, 19:54:23)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch
>>> torch._C.__file__
'/home/killeent/github/pytorch/torch/_C.cpython-36m-x86_64-linux-gnu.so'
# it references the local shared object library we just re-built
(p3) killeent@devgpu047:~$ ldd /home/killeent/github/pytorch/torch/_C.cpython-36m-x86_64-linux-gnu.so
# ...
libTH.so.1 => /home/killeent/github/pytorch/torch/lib/libTH.so.1 (0x00007f543d0e2000)
# ...
因此,我们可以在这里测试任何更改,而无需进行完全重建。
第三方库
PyTorch 依赖于一些第三方库。使用这些库的常用机制是通过 Anaconda 安装它们,然后进行链接。例如,我们可以通过以下方式将 mkl 库与 PyTorch 一起使用:
# installed to miniconda2/envs/p3/lib/libmkl_intel_lp64.so
conda install mkl
只要我们在 $CMAKE_PREFIX_PATH 上有这个 lib 目录的路径,它在编译时就能成功找到该库。
# in the site-packages dir
(p3) killeent@devgpu047:torch$ ldd _C.cpython-36m-x86_64-linux-gnu.so
# ...
libmkl_intel_lp64.so => /home/killeent/local/miniconda2/envs/p3/lib/libmkl_intel_lp64.so (0x00007f3450bba000)
# ...
未涵盖,但同样相关
ccache如何用于加快构建速度。- PyTorch 的顶层
__init__.py文件如何处理初始模块导入以及汇总各种模块和扩展库。 - CMake 构建系统,后端库如何使用 CMake 进行配置和构建。