背景
尝试在 NixOS 系统中编译 ONNX Runtime 时,笔者发现 NixOS 自带的 ROCm 版本较低,仅为 6.0.2,这导致编译过程中会出现奇奇怪怪的问题。因此,需要将 ROCm 升级至 6.2.2 版本,顺便可以进一步了解 ROCm 的细节。由于 NixOS 是一个独特的 Linux 发行版,采用函数式包定义方式,而 ROCm 本身又相当复杂,在这样一个小众发行版上打包一个小众软件,自然会遇到诸多挑战。本系列将记录升级 ROCm 的全过程,本文记录基础编译环境的调试过程,代码存放在 rocm-6.2.2 中。
NixOS 中 ROCm 的组织结构
NixOS 通过 GitHub 管理所有软件包。在官方仓库中,我们可以找到 ROCm 的函数式定义。 ROCm 的编译顺序如下:
- 定制化的 LLVM
- rocm-core/cmake/thunk/smi/device-libs/runtime/comgr/info 等基础组件
- hip-common/hipcc/hipify 等基础 HIP 工具链
- CLR(Common Language Runtime)运行库
- roc/hip 系列库
- miopen/migraphx/mivisionx 等深度学习库
为了顺利完成 ROCm 的升级,主要参考了以下资源:
- NixOS 的 ROCm 维护团队提供的[tracking] ROCm packages,其中包含了源代码升级脚本。
- Archlinux 的新版 6.2.2 ROCm 库,例如 rocm-llvm中的 PKGBUILD 文件。
升级源代码
笔者首先尝试使用了 ROCm 维护团队提供的源代码升级脚本,的确可以自动更新源代码以及校验值。然而,device-libs/comgr/hipcc 等库已经整合进了 LLVM。因此,需要首先解决 LLVM 的编译问题。
升级 LLVM
旧版本 LLVM 的编译
旧版本的 Nix 定义位于 llvm/default.nix:
{ # stdenv FIXME: Try changing back to this with a new ROCm release https://github.com/NixOS/nixpkgs/issues/271943
gcc12Stdenv
, callPackage
, rocmUpdateScript
, wrapBintoolsWith
, overrideCC
, rocm-device-libs
, rocm-runtime
, rocm-thunk
, clr
}:
let
## Stage 1 ##
# Projects
llvm = callPackage ./stage-1/llvm.nix { inherit rocmUpdateScript; stdenv = gcc12Stdenv; };
clang-unwrapped = callPackage ./stage-1/clang-unwrapped.nix { inherit rocmUpdateScript llvm; stdenv = gcc12Stdenv; };
lld = callPackage ./stage-1/lld.nix { inherit rocmUpdateScript llvm; stdenv = gcc12Stdenv; };
# Runtimes
runtimes = callPackage ./stage-1/runtimes.nix { inherit rocmUpdateScript llvm; stdenv = gcc12Stdenv; };
## Stage 2 ##
# Helpers
bintools-unwrapped = callPackage ./stage-2/bintools-unwrapped.nix { inherit llvm lld; };
bintools = wrapBintoolsWith { bintools = bintools-unwrapped; };
rStdenv = callPackage ./stage-2/rstdenv.nix { inherit llvm clang-unwrapped lld runtimes bintools; stdenv = gcc12Stdenv; };
in rec {
inherit
llvm
clang-unwrapped
lld
bintools;
# Runtimes
libc = callPackage ./stage-2/libc.nix { inherit rocmUpdateScript; stdenv = rStdenv; };
libunwind = callPackage ./stage-2/libunwind.nix { inherit rocmUpdateScript; stdenv = rStdenv; };
libcxxabi = callPackage ./stage-2/libcxxabi.nix { inherit rocmUpdateScript; stdenv = rStdenv; };
libcxx = callPackage ./stage-2/libcxx.nix { inherit rocmUpdateScript; stdenv = rStdenv; };
compiler-rt = callPackage ./stage-2/compiler-rt.nix { inherit rocmUpdateScript llvm; stdenv = rStdenv; };
## Stage 3 ##
# Helpers
clang = callPackage ./stage-3/clang.nix { inherit llvm lld clang-unwrapped bintools libc libunwind libcxxabi libcxx compiler-rt; stdenv = gcc12Stdenv; };
rocmClangStdenv = overrideCC gcc12Stdenv clang;
# Projects
clang-tools-extra = callPackage ./stage-3/clang-tools-extra.nix { inherit rocmUpdateScript llvm clang-unwrapped; stdenv = rocmClangStdenv; };
libclc = callPackage ./stage-3/libclc.nix { inherit rocmUpdateScript llvm clang; stdenv = rocmClangStdenv; };
lldb = callPackage ./stage-3/lldb.nix { inherit rocmUpdateScript clang; stdenv = rocmClangStdenv; };
mlir = callPackage ./stage-3/mlir.nix { inherit rocmUpdateScript clr; stdenv = rocmClangStdenv; };
polly = callPackage ./stage-3/polly.nix { inherit rocmUpdateScript; stdenv = rocmClangStdenv; };
flang = callPackage ./stage-3/flang.nix { inherit rocmUpdateScript clang-unwrapped mlir; stdenv = rocmClangStdenv; };
openmp = callPackage ./stage-3/openmp.nix { inherit rocmUpdateScript llvm clang-unwrapped clang rocm-device-libs rocm-runtime rocm-thunk; stdenv = rocmClangStdenv; };
# Runtimes
pstl = callPackage ./stage-3/pstl.nix { inherit rocmUpdateScript; stdenv = rocmClangStdenv; };
}
这里分成了三个阶段:
- 阶段一编译核心的 LLVM、 CLANG 和 LLD 二进制,分别是 LLVM 项目的后端,C前端和链接器。不过此时只有孤零零的二进制,参考 C-NixOS Wiki,需要 wrapper 设置好环境变量才能正常使用
- 阶段二编译核心的库,比如 libc、libunwind、libcxxabi、libcxx 等
- 阶段三构建类似 NixOS 中 stdenv 的编译环境,用来编译剩下的库
在每个阶段内部,都会调用 base.nix,比如 llvm.nix:
{ stdenv
, callPackage
, rocmUpdateScript
}:
callPackage ../base.nix {
inherit stdenv rocmUpdateScript;
requiredSystemFeatures = [ "big-parallel" ];
isBroken = stdenv.isAarch64; # https://github.com/ROCm/ROCm/issues/1831#issuecomment-1278205344
}
而 base.nix 的内容如下:
{ lib
, stdenv
, gcc12Stdenv
, fetchFromGitHub
, rocmUpdateScript
, pkg-config
, cmake
, ninja
, git
, doxygen
, sphinx
, lit
, libxml2
, libxcrypt
, libedit
, libffi
, mpfr
, zlib
, ncurses
, python3Packages
, buildDocs ? true
, buildMan ? true
, buildTests ? true
, targetName ? "llvm"
, targetDir ? "llvm"
, targetProjects ? [ ]
, targetRuntimes ? [ ]
, llvmTargetsToBuild ? [ "NATIVE" ] # "NATIVE" resolves into x86 or aarch64 depending on stdenv
, extraPatches ? [ ]
, extraNativeBuildInputs ? [ ]
, extraBuildInputs ? [ ]
, extraCMakeFlags ? [ ]
, extraPostPatch ? ""
, checkTargets ? [(
lib.optionalString buildTests (
if targetDir == "runtimes"
then "check-runtimes"
else "check-all"
)
)]
, extraPostInstall ? ""
, hardeningDisable ? [ ]
, requiredSystemFeatures ? [ ]
, extraLicenses ? [ ]
, isBroken ? false
}:
let stdenv' = stdenv; in
let stdenv =
if stdenv'.cc.cc.isGNU or false && lib.versionAtLeast stdenv'.cc.cc.version "13.0"
then gcc12Stdenv
else stdenv';
in
let
llvmNativeTarget =
if stdenv.isx86_64 then "X86"
else if stdenv.isAarch64 then "AArch64"
else throw "Unsupported ROCm LLVM platform";
inferNativeTarget = t: if t == "NATIVE" then llvmNativeTarget else t;
llvmTargetsToBuild' = [ "AMDGPU" ] ++ builtins.map inferNativeTarget llvmTargetsToBuild;
in stdenv.mkDerivation (finalAttrs: {
pname = "rocm-llvm-${targetName}";
version = "6.0.2";
outputs = [
"out"
] ++ lib.optionals buildDocs [
"doc"
] ++ lib.optionals buildMan [
"man"
"info" # Avoid `attribute 'info' missing` when using with wrapCC
];
patches = extraPatches;
src = fetchFromGitHub {
owner = "ROCm";
repo = "llvm-project";
rev = "rocm-${finalAttrs.version}";
hash = "sha256-uGxalrwMNCOSqSFVrYUBi3ijkMEFFTrzFImmvZKQf6I=";
};
nativeBuildInputs = [
pkg-config
cmake
ninja
git
python3Packages.python
] ++ lib.optionals (buildDocs || buildMan) [
doxygen
sphinx
python3Packages.recommonmark
] ++ lib.optionals (buildTests && !finalAttrs.passthru.isLLVM) [
lit
] ++ extraNativeBuildInputs;
buildInputs = [
libxml2
libxcrypt
libedit
libffi
mpfr
] ++ extraBuildInputs;
propagatedBuildInputs = lib.optionals finalAttrs.passthru.isLLVM [
zlib
ncurses
];
sourceRoot = "${finalAttrs.src.name}/${targetDir}";
cmakeFlags = [
"-DLLVM_TARGETS_TO_BUILD=${builtins.concatStringsSep ";" llvmTargetsToBuild'}"
] ++ lib.optionals (finalAttrs.passthru.isLLVM && targetProjects != [ ]) [
"-DLLVM_ENABLE_PROJECTS=${lib.concatStringsSep ";" targetProjects}"
] ++ lib.optionals ((finalAttrs.passthru.isLLVM || targetDir == "runtimes") && targetRuntimes != [ ]) [
"-DLLVM_ENABLE_RUNTIMES=${lib.concatStringsSep ";" targetRuntimes}"
] ++ lib.optionals finalAttrs.passthru.isLLVM [
"-DLLVM_INSTALL_UTILS=ON"
"-DLLVM_INSTALL_GTEST=ON"
] ++ lib.optionals (buildDocs || buildMan) [
"-DLLVM_INCLUDE_DOCS=ON"
"-DLLVM_BUILD_DOCS=ON"
# "-DLLVM_ENABLE_DOXYGEN=ON" Way too slow, only uses one core
"-DLLVM_ENABLE_SPHINX=ON"
"-DSPHINX_OUTPUT_HTML=ON"
"-DSPHINX_OUTPUT_MAN=ON"
"-DSPHINX_WARNINGS_AS_ERRORS=OFF"
] ++ lib.optionals buildTests [
"-DLLVM_INCLUDE_TESTS=ON"
"-DLLVM_BUILD_TESTS=ON"
"-DLLVM_EXTERNAL_LIT=${lit}/bin/.lit-wrapped"
] ++ extraCMakeFlags;
prePatch = ''
cd ../
chmod -R u+w .
'';
postPatch = ''
cd ${targetDir}
'' + lib.optionalString finalAttrs.passthru.isLLVM ''
patchShebangs lib/OffloadArch/make_generated_offload_arch_h.sh
'' + lib.optionalString (buildTests && finalAttrs.passthru.isLLVM) ''
# FileSystem permissions tests fail with various special bits
rm test/tools/llvm-objcopy/ELF/mirror-permissions-unix.test
rm unittests/Support/Path.cpp
substituteInPlace unittests/Support/CMakeLists.txt \
--replace-fail "Path.cpp" ""
'' + extraPostPatch;
doCheck = buildTests;
checkTarget = lib.concatStringsSep " " checkTargets;
postInstall = lib.optionalString buildMan ''
mkdir -p $info
'' + extraPostInstall;
passthru = {
isLLVM = targetDir == "llvm";
isClang = targetDir == "clang" || builtins.elem "clang" targetProjects;
updateScript = rocmUpdateScript {
name = finalAttrs.pname;
owner = finalAttrs.src.owner;
repo = finalAttrs.src.repo;
};
};
inherit hardeningDisable requiredSystemFeatures;
meta = with lib; {
description = "ROCm fork of the LLVM compiler infrastructure";
homepage = "https://github.com/ROCm/llvm-project";
license = with licenses; [ ncsa ] ++ extraLicenses;
maintainers = with maintainers; [ acowley lovesegfault ] ++ teams.rocm.members;
platforms = platforms.linux;
broken = isBroken || versionAtLeast finalAttrs.version "7.0.0";
};
})
这里是最难以理解的地方,因为普通的 Nix 包一般都是一次编译一整个项目,但这里既区分了三个阶段,又复用了 base.nix 。参考 llvm 文档,分阶段编译的目的在于,先使用现成的编译器编译出新的编译器,然后再用新的编译器回头编译自己,这样可以充分利用新编译器的特性,比如更好的优化,来得到性能最优的编译器。不过这里由于是 llvm-project 的 ROCm 分支,所以完全可以用新的 llvm 编译器,没有分阶段编译的必要。
在 base.nix 中,这段代码:
let stdenv' = stdenv; in
let stdenv =
if stdenv'.cc.cc.isGNU or false && lib.versionAtLeast stdenv'.cc.cc.version "13.0"
then gcc12Stdenv
else stdenv';
in
就是用来区分不同环境的。如果输入的环境是 GNU 编译器,意味着在阶段一,那么就统一使用 gcc12Stdenv,否则意味着在阶段二,使用了阶段一产生的 llvm/clang 环境,那么就保持输入的环境。
需要编译的项目就通过targetProjects
和targetRuntimes
来指定,另外通过extraCMakeFlags
等参数配置每个项目特殊的编译选项,这样就可以保证 base.nix 的通用性,修改新项目的参数不会影响已经编译好的项目,避免不必要的重新编译。
阶段一
- 只更新源码,不更改编译环境,编译过程中会提示需要 GCC 13 及以上版本。
- 将gcc12Stdenv改为gcc13Stdenv/gcc14Stdenv,编译过程中出现
gcc: error: unrecognized command-line option ‘-nostdlib++’
错误,具体原因不清,应该是 CMake 中检测到环境支持-nostdlib++
选项会启用,虽然g++
支持但是gcc
不支持。 - 将编译环境更改为
llvmPackages.stdenv
,改用 llvm 来编译这个定制化的 llvm-project,可以避免 GNU 奇奇怪怪的魔法,编译成功。
这样阶段一的 llvm、clang、lld 都可以正常编译。
阶段二
阶段二原本是使用阶段一编译好的 llvm、clang、lld 组建新环境 rStdenv
来编译libc等库,但是会出现找不到头文件的错误。考虑到后面并没有用到 rStdenv
,所以直接使用 llvmPackages.stdenv
来编译 libc,编译成功。这里不太明白为什么要使用阶段一编译好的 llvm、clang、lld,可能原作者有自己的考虑,不过我们这么做也没有遇到问题,使用现成的 llvm 环境编译 llvm-project 从道理上也没有问题。
阶段三
阶段三首先会使用阶段一二编译好的二进制组建新环境 rocmClangStdenv
,这就是 ROCm 定制的 llvm 环境,也是之后都会用到的环境。这个环境的构建没有问题,但是使用这个环境构建剩余项目时,首先出现了环境不对的问题。还记得 base.nix 使用 isGNU 区分现成环境和我们自己编译的环境吗?由于我们现成环境也是使用的 llvm,所以这里的判断失效了,导致后续项目使用的是 gcc12Stdenv
,而不是我们期望的 rocmClangStdenv
。既然如此,我直接去除了这里的判断,直接使用输入的 stdenv
环境,具体使用哪个环境由调用者自己决定。
现在我们成功的进入了 rocmClangStdenv
环境,可以开始编译后续项目了。但是尝试了几个项目,都出现了找不到标准库头文件的错误。这就很奇怪了,毕竟在阶段二中,我们已经编译了标准库,为什么阶段三找不到呢?
于是我回头编译了旧版本的 rocmClangStdenv
进行对比,最后问题在于路径!旧版 Clang 的安装路径中使用了完整的版本号,所以 nix 中使用
clang_version=`${clang-unwrapped}/bin/clang -v 2>&1 | grep "clang version " | grep -E -o "[0-9.-]+"`
获取版本号,继而组装出路径 mkdir -p $out/{bin,include/c++/v1,lib/{cmake,clang/$clang_version/{include,lib}},libexec,share}
而新版 Clang 的安装路径中只使用了主版本号,所以后续指向了错误的路径,导致找不到头文件。知道了问题,改成使用主版本号即可。
clang_version=`${clang-unwrapped}/bin/clang -v 2>&1 | grep "clang version " | grep -E -o "[0-9.-]+" | grep -E -o "^[0-9]+"`
这样就可以找到头文件了,成功地编译了后续项目。
小结
俗话说,万事开头难。笔者在这第一步卡了很长时间,中途还搁置了很长时间。为了搞清楚编译流程,先是详细了解了 llvm-project 的整体架构,然后又在 Archlinux 中尝试编译,又回头补课了 NixOS 中的 C 编译环境。其实主要难点在于 llvm-project 的 nix 打包比较复杂,对 base.nix 进行了多次调用,导致细节都被隐藏起来了。测试编译环境的时候也一直在 GCC 和 LLVM 之间犹豫,一方面旧版本使用的是 GCC ,总觉得问题会更少一点,另一方面阶段一改用 LLVM 就能成功编译了,但是后续编译又有问题。后来才知道,后续编译的问题不在于 LLVM ,而是路径问题。整个过程中,有一个成功的案例作为参考还是很重要的,否则很容易陷入迷茫,定位不到问题。
修复 LLVM
问题
在检查 composable kernel 的时候,发现编译出来的测试二进制运行时,都会出现 segment fault 。经过排查,发现是 cout
会触发错误,于是怀疑是自己编译的 llvm
有问题。用一份基础的 HIP 程序测试:
#include <iostream>
#include <hip/hip_runtime.h>
int main() {
std::cout << "Hello world" << std::endl;
int deviceCount;
hipError_t err = hipGetDeviceCount(&deviceCount);
if (err != hipSuccess) {
std::cerr << "Failed to get device count: " << hipGetErrorString(err) << std::endl;
return -1;
}
std::cout << "Number of devices: " << deviceCount << std::endl;
for (int i = 0; i < deviceCount; i++) {
hipDeviceProp_t props;
err = hipGetDeviceProperties(&props, i);
if (err != hipSuccess) {
std::cerr << "Failed to get device properties: " << hipGetErrorString(err) << std::endl;
continue;
}
std::cout << "Device " << i << ":" << std::endl;
std::cout << " Name: " << props.name << std::endl;
std::cout << " Compute capability: " << props.major << "." << props.minor << std::endl;
std::cout << " Total global memory: " << props.totalGlobalMem / (1024 * 1024) << " MB" << std::endl;
std::cout << " Max threads per block: " << props.maxThreadsPerBlock << std::endl;
std::cout << " Max threads per SM: " << props.maxThreadsPerMultiProcessor << std::endl;
std::cout << " Max grid size: " << props.maxGridSize[0] << " x " << props.maxGridSize[1] << " x " << props.maxGridSize[2] << std::endl;
std::cout << " Max block size: " << props.maxThreadsDim[0] << " x " << props.maxThreadsDim[1] << " x " << props.maxThreadsDim[2] << std::endl;
std::cout << " Clock rate: " << props.clockRate / 1000 << " MHz" << std::endl;
std::cout << " Memory clock rate: " << props.memoryClockRate / 1000 << " MHz" << std::endl;
std::cout << " Memory bus width: " << props.memoryBusWidth << " bits" << std::endl;
std::cout << " Multiprocessor count: " << props.multiProcessorCount << std::endl;
std::cout << std::endl;
}
return 0;
}
使用新版的 hipcc -o main main.cpp
编译并运行,触发了 segment fault 错误。回头使用 rocmPackages_5.clr
版本编译,运行正常:
Hello world
Number of devices: 1
Device 0:
Name: AMD Radeon RX 6750 XT
Compute capability: 10.3
Total global memory: 12272 MB
Max threads per block: 1024
Max threads per SM: 2048
Max grid size: 2147483647 x 65536 x 65536
Max block size: 1024 x 1024 x 1024
Clock rate: 2880 MHz
Memory clock rate: 1124 MHz
Memory bus width: 192 bits
Multiprocessor count: 20
使用 hipcc -v -o main main.cpp
检查编译细节,发现问题出在 libstdc++
库上,自己编译的 clang 环境中,出现了:
-isystem /nix/store/74r6shfn042d8a769h4q1kccqry58hfh-rocm-llvm-libcxx-6.2.2/include -isystem /nix/store/8nvzkaaw17aspj99dvi2pwjs5xan9fl2-rocm-llvm-llvm-6.2.2/include -isystem /nix/store/cir85maag3aj3sddq35y59m0wmcr50wk-zlib-1.3.1-dev/include -isystem /nix/store/kbwfxd5b17iy077yjvqdfrvwn0npp6vw-ncurses-6.4.20221231-dev/include -isystem /nix/store/3nbjn7qsin4grg4gh1g76xj22vs7bdz8-rocm-llvm-lld-6.2.2/include -isystem /nix/store/kzp4jy5akbvgi7phqk1y805arsz932hk-rocm-llvm-libunwind-6.2.2/include -isystem /nix/store/v39933mhlgf9ffc8wcx8820b9hsg6kjr-rocm-llvm-libcxxabi-6.2.2/include -isystem /nix/store/jxsg642p2jw1pixz06hm24x65rz7x001-rocm-llvm-compiler-rt-6.2.2/include -isystem /nix/store/b3yr21h6fx5abvq572lyhm4imnigih85-clr-6.2.2/include -isystem /nix/store/4ag8a0d835g4lpji4wsp9sgckhldhmnv-rocm-llvm-comgr-6.2.2/include -isystem /nix/store/vclmjkdk84rxcwszpxycyc58qphdi0pm-rocm-runtime-6.2.2/include -isystem /nix/store/901c80rlps5q05bnjk1sj4zaz5k736nc-python3-3.12.7/include -isystem /nix/store/qjzf3aynbizybqilam8ygqlih95b8ndv-rocprofiler-register-6.2.2/include -isystem /nix/store/74r6shfn042d8a769h4q1kccqry58hfh-rocm-llvm-libcxx-6.2.2/include -isystem /nix/store/8nvzkaaw17aspj99dvi2pwjs5xan9fl2-rocm-llvm-llvm-6.2.2/include -isystem /nix/store/cir85maag3aj3sddq35y59m0wmcr50wk-zlib-1.3.1-dev/include -isystem /nix/store/kbwfxd5b17iy077yjvqdfrvwn0npp6vw-ncurses-6.4.20221231-dev/include -isystem /nix/store/3nbjn7qsin4grg4gh1g76xj22vs7bdz8-rocm-llvm-lld-6.2.2/include -isystem /nix/store/kzp4jy5akbvgi7phqk1y805arsz932hk-rocm-llvm-libunwind-6.2.2/include -isystem /nix/store/v39933mhlgf9ffc8wcx8820b9hsg6kjr-rocm-llvm-libcxxabi-6.2.2/include -isystem /nix/store/jxsg642p2jw1pixz06hm24x65rz7x001-rocm-llvm-compiler-rt-6.2.2/include
但是 rocmPackages_5.clr
中没有。也就是说,旧版的 ROCm 环境中根本没有使用自己编译的 libc++
等基础库,而是复用了 GCC 的。为了进一步验证猜想,笔者把新版 ROCm 中的 libc++
等库去掉了:
{ stdenv
, wrapCCWith
, llvm
, lld
, clang-unwrapped
, bintools
, libc
, libunwind
, libcxxabi
, libcxx
, compiler-rt
}:
wrapCCWith rec {
# inherit libcxx bintools;
inherit bintools;
# We do this to avoid HIP pathing problems, and mimic a monolithic install
cc = stdenv.mkDerivation (finalAttrs: {
inherit (clang-unwrapped) version;
pname = "rocm-llvm-clang";
dontUnpack = true;
# https://github.com/ROCm/llvm-project/blob/rocm-6.2.2/cmake/Modules/GetClangResourceDir.cmake
# rocm-6.2.2 use CLANG_MAJOR_VERSION
installPhase = ''
runHook preInstall
clang_version=`${clang-unwrapped}/bin/clang -v 2>&1 | grep "clang version " | grep -E -o "[0-9.-]+" | grep -E -o "^[0-9]+"`
mkdir -p $out/{bin,include/c++/v1,lib/{cmake,clang/$clang_version/{include,lib}},libexec,share}
for path in ${llvm} ${clang-unwrapped} ${lld} ${libc} ${libunwind} ${libcxxabi} ${libcxx} ${compiler-rt}; do
cp -as $path/* $out
chmod +w $out/{*,include/c++/v1,lib/{clang/$clang_version/include,cmake}}
rm -f $out/lib/libc++.so
done
ln -s $out/lib/* $out/lib/clang/$clang_version/lib
ln -sf $out/include/* $out/lib/clang/$clang_version/include
runHook postInstall
'';
passthru.isClang = true;
});
extraPackages = [
llvm
lld
# libc
# libunwind
# libcxxabi
compiler-rt
];
nixSupport.cc-cflags = [
"-resource-dir=$out/resource-root"
"-fuse-ld=lld"
"-rtlib=compiler-rt"
"-unwindlib=libunwind"
"-Wno-unused-command-line-argument"
];
extraBuildCommands = ''
clang_version=`${cc}/bin/clang -v 2>&1 | grep "clang version " | grep -E -o "[0-9.-]+" | grep -E -o "^[0-9]+"`
mkdir -p $out/resource-root
ln -s ${cc}/lib/clang/$clang_version/{include,lib} $out/resource-root
cp -as ${llvm}/bin/llc $out/bin
# Not sure why, but hardening seems to make things break
echo "" > $out/nix-support/add-hardening.sh
# GPU compilation uses builtin `lld`
substituteInPlace $out/bin/{clang,clang++} \
--replace-fail "-MM) dontLink=1 ;;" "-MM | --cuda-device-only) dontLink=1 ;;''\n--cuda-host-only | --cuda-compile-host-device) dontLink=0 ;;"
'';
}
然后重新编译基础的 HIP 程序,这次就可以正常运行了。
这使得笔者对于 Nix 中的 ROCm LLVM 环境有了很大的疑惑: 明明编译了 libc++ 并且加入了编译环境,编译通过但是为什么连最基础的 std::cout
都无法使用呢?
Nix 的 LLVM 编译环境
Nix 的标准编译环境是 stdenv
,它提供了基础的 GCC 编译环境,包括 C++ 编译器、链接器、库等。根据 Nix C 的文档 , 这些二进制并不能单独使用,而是通过 wrapper 注入环境变量来使用的。如果想要使用 LLVM 编译环境,根据 Nix LLVM 的文档,可以使用 llvmPackages.stdenv
替换标准编译环境 stdenv
。但是当你在 search.nixos.org 中搜索 llvm stdenv
和 llvm clang
时,会发现存在一系列名字类似的包:
llvmPackages.stdenv
llvmPackages.libcxxStdenv
llvmPackages.libcxxClang
llvmPackages.clangNoLibc
llvmPackages.clangUseLLVM
llvmPackages.clangNoLibcxx
llvmPackages.clangNoCompilerRt
llvmPackages.clangNoCompilerRtWithLibc
llvmPackages.clangWithLibcAndBasicRtAndLibcxx
在 nixpkgs issues 中也有类似的讨论,具体的定义都在 llvm/common/default.nix 中,比如:
# pick clang appropriate for package set we are targeting
clang =
if stdenv.targetPlatform.libc == null then
tools.clangNoLibc
else if stdenv.targetPlatform.useLLVM or false then
tools.clangUseLLVM
else if (pkgs.targetPackages.stdenv or args.stdenv).cc.isGNU then
tools.libstdcxxClang
else
tools.libcxxClang;
libstdcxxClang = wrapCCWith rec {
cc = tools.clang-unwrapped;
# libstdcxx is taken from gcc in an ad-hoc way in cc-wrapper.
libcxx = null;
extraPackages = [ targetLlvmLibraries.compiler-rt ];
extraBuildCommands = mkExtraBuildCommands cc;
};
libcxxClang = wrapCCWith rec {
cc = tools.clang-unwrapped;
libcxx = targetLlvmLibraries.libcxx;
extraPackages = [ targetLlvmLibraries.compiler-rt ];
extraBuildCommands = mkExtraBuildCommands cc;
};
这就涉及到工具链的故事了。stdenv
提供了基于 GNU 的工具链,而 LLVM 项目提供了另外一套工具链,在 Assembling a Complete Toolchain 中有详细介绍,其中对于运行时:
compiler-rt
:编译器运行时,类似于 GNU 的libgcc_s
libunwind
:提供_Unwind_*
函数, GNU 的libgcc_s
也有这个功能libc++abi
:C++ ABI 库libc++
:C++ 标准库,对标 GNU 的libstdc++
理论上这两套工具链都能正常编译,但是有些历史遗留问题导致有些代码只能用 GNU 工具链编译,所以 Nix 提供多种 LLVM 编译环境的配置选项。
修复 libcxx 环境
理清楚 Nix 上的 LLVM 编译环境后,下一步就是解决 libcxx
无法正确编译的问题。由于 Nix 中的编译环境是由环境变量配置的,所以只能阅读 cc-wrapper 的源码 ,发现问题出在 libcxx-cxxflags
和 libcxx-cxxflags
上:
# We have a libc++ directly, we have one via "smuggled" GCC, or we have one
# bundled with the C compiler because it is GCC
+ optionalString (libcxx != null || (useGccForLibs && gccForLibs.langCC or false) || (isGNU && cc.langCC or false)) ''
touch "$out/nix-support/libcxx-cxxflags"
touch "$out/nix-support/libcxx-ldflags"
''
# Adding -isystem flags should be done only for clang; gcc
# already knows how to find its own libstdc++, and adding
# additional -isystem flags will confuse gfortran (see
# https://github.com/NixOS/nixpkgs/pull/209870#issuecomment-1500550903)
+ optionalString (libcxx == null && isClang && (useGccForLibs && gccForLibs.langCC or false)) ''
for dir in ${gccForLibs}/include/c++/*; do
echo "-isystem $dir" >> $out/nix-support/libcxx-cxxflags
done
for dir in ${gccForLibs}/include/c++/*/${targetPlatform.config}; do
echo "-isystem $dir" >> $out/nix-support/libcxx-cxxflags
done
''
+ optionalString (libcxx.isLLVM or false) ''
echo "-isystem ${getDev libcxx}/include/c++/v1" >> $out/nix-support/libcxx-cxxflags
echo "-stdlib=libc++" >> $out/nix-support/libcxx-ldflags
''
这里需要 libcxx.isLLVM
为真,才会把 libcxx
的头文件添加到路径中,同时链接器启用 libc++
。而 ROCm LLVM 中:
passthru = {
isLLVM = targetDir == "llvm";
isClang = targetDir == "clang" || builtins.elem "clang" targetProjects;
updateScript = rocmUpdateScript {
name = finalAttrs.pname;
owner = finalAttrs.src.owner;
repo = finalAttrs.src.repo;
};
};
是通过 targetDir
来确定 isLLVM
。而 libcxx
是在 runtimes
目录下编译的,所以 libcxx.isLLVM
为假,导致 cc-wrapper
无法正确配置 libcxx
。找到问题后,解决方案就很简单了:
passthru = {
isLLVM = targetDir == "llvm" || targetName == "libcxx";
isClang = targetDir == "clang" || builtins.elem "clang" targetProjects;
updateScript = rocmUpdateScript {
name = finalAttrs.pname;
owner = finalAttrs.src.owner;
repo = finalAttrs.src.repo;
};
};
使用修复之后的 ROCm 编译环境后,就可以正常使用 libcxx
作为 C++ 标准库使用了。
重构 ROCm LLVM 编译环境
虽然 libcxx
的问题初步解决了,但是在后续编译过程中还是出现了奇奇怪怪的问题,比如 ld.lld: error: target emulation unknown: -m or at least one .o file required
的源头是 lld
与 ld
不兼容。这让笔者思考, ROCm 的编译环境要采用哪种配置,是否有可能尽量保留 GNU 工具链从而避免兼容性问题?
经过一番调研,发现 ALT Linux 就采用了这种编译环境,比如 llvm-rocm,于是笔者简化了 ROCm 编译环境:
{ stdenv
, llvmPackages
, callPackage
, rocmUpdateScript
, wrapBintoolsWith
, overrideCC
, rocm-device-libs
, rocm-runtime
, rocm-thunk
, clr
}:
let
# Projects
llvm = callPackage ./stage-1/llvm.nix { inherit rocmUpdateScript; stdenv = stdenv; };
clang-unwrapped = callPackage ./stage-1/clang-unwrapped.nix { inherit rocmUpdateScript llvm; stdenv = stdenv; };
lld = callPackage ./stage-1/lld.nix { inherit rocmUpdateScript llvm; stdenv = stdenv; };
# Runtimes
runtimes = callPackage ./stage-1/runtimes.nix { inherit rocmUpdateScript llvm; stdenv = llvmPackages.stdenv; };
# Helpers
bintools-unwrapped = callPackage ./stage-2/bintools-unwrapped.nix { inherit llvm lld; };
bintools = wrapBintoolsWith { bintools = bintools-unwrapped; };
in rec {
inherit
llvm
clang-unwrapped
lld
runtimes
bintools
;
clang = callPackage ./stage-3/clang.nix { inherit llvm lld clang-unwrapped bintools runtimes ; stdenv = stdenv; };
rocmClangStdenv = overrideCC stdenv clang;
# Projects
clang-tools-extra = callPackage ./stage-3/clang-tools-extra.nix { inherit rocmUpdateScript llvm clang-unwrapped; stdenv = rocmClangStdenv; };
libclc = callPackage ./stage-3/libclc.nix { inherit rocmUpdateScript llvm clang; stdenv = rocmClangStdenv; };
lldb = callPackage ./stage-3/lldb.nix { inherit rocmUpdateScript clang; stdenv = rocmClangStdenv; };
mlir = callPackage ./stage-3/mlir.nix { inherit rocmUpdateScript clr; stdenv = rocmClangStdenv; };
polly = callPackage ./stage-3/polly.nix { inherit rocmUpdateScript; stdenv = rocmClangStdenv; };
flang = callPackage ./stage-3/flang.nix { inherit rocmUpdateScript clang-unwrapped mlir; stdenv = rocmClangStdenv; };
openmp = callPackage ./stage-3/openmp.nix { inherit rocmUpdateScript llvm clang-unwrapped clang rocm-device-libs rocm-runtime rocm-thunk; stdenv = rocmClangStdenv; };
# Runtimes
pstl = callPackage ./stage-3/pstl.nix { inherit rocmUpdateScript; stdenv = rocmClangStdenv; };
# amd
device-libs = callPackage ./stage-3/device-libs.nix { inherit rocmUpdateScript; stdenv = rocmClangStdenv; };
comgr = callPackage ./stage-3/comgr.nix { inherit rocmUpdateScript device-libs; stdenv = rocmClangStdenv; };
hipcc = callPackage ./stage-3/hipcc.nix { inherit rocmUpdateScript; stdenv = rocmClangStdenv; };
}
最终抛弃了三阶段编译,尽量使用标准编译环境 stdenv
,只有 runtimes
涉及到 gcc
不兼容的参数才改用 llvmPackages.stdenv
。到此为止,我们终于确定了一个兼容性比较强的编译环境了,可以继续后续其他组件的编译了。