Jachin Shen
Jachin Shen

Categories

Tags

背景

尝试在 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 的编译顺序如下:

  1. 定制化的 LLVM
  2. rocm-core/cmake/thunk/smi/device-libs/runtime/comgr/info 等基础组件
  3. hip-common/hipcc/hipify 等基础 HIP 工具链
  4. CLR(Common Language Runtime)运行库
  5. roc/hip 系列库
  6. miopen/migraphx/mivisionx 等深度学习库

为了顺利完成 ROCm 的升级,主要参考了以下资源:

  1. NixOS 的 ROCm 维护团队提供的[tracking] ROCm packages,其中包含了源代码升级脚本。
  2. 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; };
}

这里分成了三个阶段:

  1. 阶段一编译核心的 LLVM、 CLANG 和 LLD 二进制,分别是 LLVM 项目的后端,C前端和链接器。不过此时只有孤零零的二进制,参考 C-NixOS Wiki,需要 wrapper 设置好环境变量才能正常使用
  2. 阶段二编译核心的库,比如 libc、libunwind、libcxxabi、libcxx 等
  3. 阶段三构建类似 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 环境,那么就保持输入的环境。

需要编译的项目就通过targetProjectstargetRuntimes来指定,另外通过extraCMakeFlags等参数配置每个项目特殊的编译选项,这样就可以保证 base.nix 的通用性,修改新项目的参数不会影响已经编译好的项目,避免不必要的重新编译。

阶段一

  1. 只更新源码,不更改编译环境,编译过程中会提示需要 GCC 13 及以上版本。
  2. 将gcc12Stdenv改为gcc13Stdenv/gcc14Stdenv,编译过程中出现 gcc: error: unrecognized command-line option ‘-nostdlib++’ 错误,具体原因不清,应该是 CMake 中检测到环境支持 -nostdlib++ 选项会启用,虽然 g++ 支持但是 gcc 不支持。
  3. 将编译环境更改为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 stdenvllvm clang 时,会发现存在一系列名字类似的包:

  1. llvmPackages.stdenv
  2. llvmPackages.libcxxStdenv
  3. llvmPackages.libcxxClang
  4. llvmPackages.clangNoLibc
  5. llvmPackages.clangUseLLVM
  6. llvmPackages.clangNoLibcxx
  7. llvmPackages.clangNoCompilerRt
  8. llvmPackages.clangNoCompilerRtWithLibc
  9. 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 中有详细介绍,其中对于运行时:

  1. compiler-rt:编译器运行时,类似于 GNU 的 libgcc_s
  2. libunwind:提供 _Unwind_* 函数, GNU 的 libgcc_s 也有这个功能
  3. libc++abi:C++ ABI 库
  4. libc++:C++ 标准库,对标 GNU 的 libstdc++

理论上这两套工具链都能正常编译,但是有些历史遗留问题导致有些代码只能用 GNU 工具链编译,所以 Nix 提供多种 LLVM 编译环境的配置选项。

修复 libcxx 环境

理清楚 Nix 上的 LLVM 编译环境后,下一步就是解决 libcxx 无法正确编译的问题。由于 Nix 中的编译环境是由环境变量配置的,所以只能阅读 cc-wrapper 的源码 ,发现问题出在 libcxx-cxxflagslibcxx-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 的源头是 lldld 不兼容。这让笔者思考, 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。到此为止,我们终于确定了一个兼容性比较强的编译环境了,可以继续后续其他组件的编译了。