前言
上一篇中,我們介紹了在 PC 如何使用 C++ 加載我們保存的模型并測試。這一篇,我們介紹在 PC 上交叉編譯 aarch64 平臺的 tensorflow 源碼過程,這個難度比我想象的要難太多了。(耗時10天不止,一把心酸一把淚),首先看一下官方在文檔介紹:
這里,我選擇了 tensorflow 官方測試過支持 gcc 的最后版本 2.12.0。然后介紹下 PC 的配置:
然后看下 python3 和 bazel 的版本:
? python3 --version
Python 3.11.9
? bazel-5.3.0-linux-x86_64 --version
bazel 5.3.0
? aarch64-linux-gcc -v
Using built-in specs.
COLLECT_GCC=/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-gcc
COLLECT_LTO_WRAPPER=/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/../libexec/gcc/aarch64-none-linux-gnu/13.3.1/lto-wrapper
Target: aarch64-none-linux-gnu
Configured with: /data/jenkins/workspace/GNU-toolchain/arm-13/src/gcc/configure --target=aarch64-none-linux-gnu --prefix= --with-sysroot=/aarch64-none-linux-gnu/libc --with-build-sysroot=/data/jenkins/workspace/GNU-toolchain/arm-13/build-aarch64-none-linux-gnu/install//aarch64-none-linux-gnu/libc --with-bugurl=https://bugs.linaro.org/ --enable-gnu-indirect-function --enable-shared --disable-libssp --disable-libmudflap --enable-checking=release --enable-languages=c,c++,fortran --with-gmp=/data/jenkins/workspace/GNU-toolchain/arm-13/build-aarch64-none-linux-gnu/host-tools --with-mpfr=/data/jenkins/workspace/GNU-toolchain/arm-13/build-aarch64-none-linux-gnu/host-tools --with-mpc=/data/jenkins/workspace/GNU-toolchain/arm-13/build-aarch64-none-linux-gnu/host-tools --with-isl=/data/jenkins/workspace/GNU-toolchain/arm-13/build-aarch64-none-linux-gnu/host-tools --enable-fix-cortex-a53-843419 --with-pkgversion='Arm GNU Toolchain 13.3.Rel1 (Build arm-13.24)'
Thread model: posix
Supported LTO compression algorithms: zlib
gcc version 13.3.1 20240614 (Arm GNU Toolchain 13.3.Rel1 (Build arm-13.24))
特別要說明的是,交叉編譯工具聯的版本也要選擇合適的,否則會出現莫名奇妙的問題(現在這個版本都還存在問題:在 tensorflow 的 logging.h 這里,嗚嗚嗚)。
基礎性的介紹完了之后,我們開始正文。
構建交叉編譯腳本
交叉編譯的目的是編譯出對應的庫和引用頭文件,為什么選擇交叉編譯是因為 PC 性能強啊(實測在這臺 PC 上一切順利編譯也要3h+,如果是本地編譯可想而知那要多久啊)。首先要說明一下 tensorflow 使用 Bazel 構建。而如何在 bazel 環境下選擇自己的交叉編譯工具鏈呢?不像 makefile 或者 cmake 直接指定就行了,bazel 有點類似 GN,要配置好多東西,但是個人感覺比 GN 還要復雜,想哭。
剛開始,我走了好多彎路,一通晚上亂搜,最后搜的文章水平參差不齊導致徒勞浪費了不少時間,還對自己的能力產生了懷疑。最后還是根據官方文檔,一點一點查漏補缺搭建好的交叉編譯工具鏈的配置文件。首先,附上官方的指導文檔連接 [Bazel Tutorial: Configure C++ Toolchains](Configuring C++ toolchains - Bazel 5.3.0)。
最后創建的涉及到交叉編譯工具鏈的文檔有兩個:
- toolchain/BUILD
- toolchain/cc_toolchain_config.bzl
這里要簡單介紹下 bazel 構建工具的一些基本概念:
- workspace(工作區):文件系統中包含待構建工程的源碼目錄,每一個工作區目錄都有一個名為 WORKSPACE 的文件,可以是空的,也可以是包含有外部依賴。包含有 WORKSPACE 文件的目錄就被認為是一個工作區的根,如果一個工作區根目錄下有其他目錄也包含有 WORKSPACE 文件,那么這個目錄會被認為是另一個工作區而非當前工作區,特別地,WORKSPACE.bazel 是 WORKSPACE 文件的別名,且優先級要比 WORKSPACE 要高;
- repository(倉庫):存儲代碼的地方叫做倉庫,包含 WORKSPACE 文件的目錄被認為是主倉庫的根,也被稱作@;
- package(包):倉庫中代碼組織的最小單元是 package,包是相關文件的集合和他們之間的依賴關系。包是位于頂層倉庫下的目錄,目錄中要含有 BUILD 或者 BUILD.bazel 文件;
- target(目標):目標是一個容器,包中的元素被稱作目標。大多數的目標都是文件或者規則類型的。**文件類型 **又可以進一步劃分為兩種類型:源文件(開發者寫出來的文件)和生成文件(根據源文件和指定的規則生成的文件)。規則類型的文件 描述的是輸入集合和輸出集合之間的關系,包含了從輸入到輸出這個過程的步驟。此外,這里也有另外的一些類型的目標,比如說 package group(包組),這些很少用到;
- label(標簽):所有的目標都歸屬一個包,目標的名字被稱為它的標簽,每一個標簽唯一對應一個目標,比如
@myrepo//my/app/main:app_binary
這個標簽,前半部分@myrepo
表示倉庫的名字是 myrepo 倉庫,如果本來就是在這個倉庫下,那么可以進一步簡寫為//
,另外標簽的第二部分my/app/main
表示這個包的名字,如果本來就在@myrepo//my/app/main
包下,可以簡寫為:app_binary
,進一步地,如果是文件類型的可以間寫為app_binary
但是規則類型的還是要保留這個符號:
。 - rule(規則):規則描述了輸入和輸出之間的關系,以及構建輸出的步驟;規則有很多種,可以生成復雜的可執行文件、庫、測試程序以及其他支持的輸出(特殊的輸出可以使用擴展性極強的函數: genrule())。每一個規則都有一個字符串類型的名字屬性。srcs 字段是一個 label 的 list,每一項都是這個規則對應輸入的 target 名字。
至此,簡單概括了 bazel 中涉及到的關鍵概念,下面結合配置交叉編譯工具鏈添加的文件,我們可以知道在根倉庫下新建了一個 toolchain 的包。看下這個包的 BUILD 文件:
package(default_visibility = ["http://visibility:public"])
# 定義了一個名字是 cross_gcc_suite 的 target,這個 target 是 C++ 工具鏈的集合
cc_toolchain_suite(
# name 和 toolchains 是必須項
name = "cross_gcc_suite",
# 聲明 aarch64 平臺對應的工具鏈名字是當前 package 下的 aarch64_toolchain 目標
# 根據 --cpu 和 --compiler 選項選擇工具鏈
toolchains = {
"aarch64": ":aarch64_toolchain",
},
)
# 定義一個名字為 empty 的空 target
filegroup(name = "empty")
# 定義了一個名字是 aarch64_toolchain 的 c++ 工具鏈 target
cc_toolchain(
name = "aarch64_toolchain",
toolchain_identifier = "aarch64-toolchain",
# 工具聯的配置項
toolchain_config = ":aarch64_toolchain_config",
all_files = ":empty",
compiler_files = ":empty",
dwp_files = ":empty",
linker_files = ":empty",
objcopy_files = ":empty",
strip_files = ":empty",
supports_param_files = 0,
)
# 在當前 package 下的 cc_toolchain_config.bzl 中,導入 cc_toolchain_config 函數
load(":cc_toolchain_config.bzl", "cc_toolchain_config")
# 通過 cc_toolchain_config 函數,定義一個名為 aarch64_toolchain_config 的 target
cc_toolchain_config(name = "aarch64_toolchain_config")
下面我們看下 toolchain 包下的 cc_toolchain_config.bzl 文件內容:
# toolchain/cc_toolchain_config.bzl:
# 加載一些函數
load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "ACTION_NAMES")
load("@bazel_tools//tools/cpp:cc_toolchain_config_lib.bzl", "feature", "flag_group", "flag_set", "tool_path")
# 定義變量
all_link_actions = [ # NEW
ACTION_NAMES.cpp_link_executable,
ACTION_NAMES.cpp_link_dynamic_library,
ACTION_NAMES.cpp_link_nodeps_dynamic_library,
]
all_compile_actions = [
ACTION_NAMES.assemble,
ACTION_NAMES.c_compile,
ACTION_NAMES.clif_match,
ACTION_NAMES.cpp_compile,
ACTION_NAMES.cpp_header_parsing,
ACTION_NAMES.cpp_module_codegen,
ACTION_NAMES.cpp_module_compile,
ACTION_NAMES.linkstamp_compile,
ACTION_NAMES.lto_backend,
ACTION_NAMES.preprocess_assemble,
]
all_cpp_compile_actions = [
ACTION_NAMES.cpp_compile,
ACTION_NAMES.cpp_header_parsing,
ACTION_NAMES.cpp_module_codegen,
ACTION_NAMES.cpp_module_compile,
]
# 定義函數
def _impl(ctx):
tool_paths = [ # NEW
tool_path(
name = "gcc",
# 如下替換為指定的工具鏈的路徑
path = "/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-gcc",
),
tool_path(
name = "g++",
path = "/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-g++",
),
tool_path(
name = "ld",
path = "/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-ld",
),
tool_path(
name = "ar",
path = "/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-ar",
),
tool_path(
name = "cpp",
path = "/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-cpp",
),
tool_path(
name = "gcov",
path = "/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-gcov",
),
tool_path(
name = "nm",
path = "/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-nm",
),
tool_path(
name = "objdump",
path = "/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-objdump",
),
tool_path(
name = "strip",
path = "/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-strip",
),
]
features= [ # NEW
feature(
name = "default_linker_flags",
enabled = True,
flag_sets = [
flag_set(
actions = all_link_actions,
flag_groups = ([
flag_group(
flags = [
"-static", #建議交叉編譯還是帶上這個參數,要不然無法在 PC 簡單地正常執行 aarch64 elf 格式的工具
"-lstdc++",
],
),
]),
),
],
),
feature(
name = "default_compiler_flags",
enabled = True,
flag_sets = [
flag_set(
actions = all_cpp_compile_actions,
flag_groups = ([
flag_group(
flags = [
"-fpermissive",
],
),
]),
),
],
),
]
return cc_common.create_cc_toolchain_config_info(
ctx = ctx,
features = features,
cxx_builtin_include_directories = [ # NEW
# 替換自己相關的頭文件,這部分可以先空起來,在編譯過程中提示缺少頭文件的時候會告訴我們缺少哪些,到時候再追加也可以
"/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/aarch64-none-linux-gnu/include/c++/13.3.1/bits",
"/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/aarch64-none-linux-gnu/include/c++/13.3.1",
"/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/aarch64-none-linux-gnu/libc/usr/include",
"/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/lib/gcc/aarch64-none-linux-gnu/13.3.1/include",
"/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/lib/gcc/aarch64-none-linux-gnu/13.3.1/include-fixed",
],
toolchain_identifier = "aarch64-linux",
host_system_name = "x86_64",
target_system_name = "aarch64",
target_cpu = "aarch64",
target_libc = "unknown",
compiler = "g++",
abi_version = "unknown",
abi_libc_version = "unknown",
tool_paths = tool_paths, # NEW
)
# 定義了一個新的規則
cc_toolchain_config = rule(
# 規則的實現函數
implementation = _impl,
attrs = {},
provides = [CcToolchainConfigInfo],
)
有了這兩個文件,交叉編譯工具鏈就配置好了。下面為了方便使用,修改 .bazelrc 文件,追加如下兩行,可以看到使用 :
build:elinux_aarch64 --crosstool_top=//toolchain:cross_gcc_suite
build:elinux_aarch64 --host_crosstool_top=@bazel_tools//tools/cpp:toolchain
這里使用的 --crosstool_top 和 --host_crosstool_top 是 Bazel 中用于指定交叉編譯工具和主機編譯工具鏈配置的重要參數,這里要注意必須要使用 --host_crosstool_top 選項指定一個默認的 PC(或者 k8)平臺的工具鏈,要不然會報錯的。
然后和 PC 端那樣,首先是 ./configure
然后就是觸發構建了,具體命令如下:
bazel build --config=elinux_aarch64 --copt="-fPIC" --cxxopt="-fPIC" --verbose_failures //tensorflow:libtensorflow.so //tensorflow:install_headers
在編譯過程中,我使用的工具鏈需要修改如下地方:
diff --git a/tensorflow/tsl/platform/default/logging.h b/tensorflow/tsl/platform/default/logging.h
index 3578bedf0f1..24c74607a96 100644
--- a/tensorflow/tsl/platform/default/logging.h
+++ b/tensorflow/tsl/platform/default/logging.h
@@ -310,7 +310,7 @@ inline uint64 GetReferenceableValue(uint64 t) { return t; }
// it uses the definition for operator< < , with a few special cases below.
template < typename T >
inline void MakeCheckOpValueString(std::ostream* os, const T& v) {
- // (*os) < < v;
+ //(*os) < < v;
}
// Overrides for char types provide readable values for unprintable
這部分后續應該可以不用這么修改(暫時還沒有解決)。
然后最重要的是在后期,靜態連接 libtensorflow_framework.so.2.12.0 的時候會提示錯誤:
這時候需要強制動態鏈接才行,怎么做呢? diff 文件是:
diff --git a/tensorflow/BUILD b/tensorflow/BUILD
index 0d27a8294f5..adcdef8f8a9 100644
--- a/tensorflow/BUILD
+++ b/tensorflow/BUILD
@@ -1100,6 +1100,8 @@ tf_cc_shared_library(
],
"http://conditions:default": [
"-Wl,--version-script,$(location //tensorflow:tf_framework_version_script.lds)",
+ "-Wl,-Bdynamic",
],
}),
linkstatic = 1,
下面構建的時候又出現新的錯誤,動態鏈接交叉編譯出來的工具無法正常生成一些鏈接 libtensorflow 庫的文件,這時候我通過 sudo chrpath -r
進行處理一下,記得要提前將需要的庫放在一個指定的目錄,還是很復雜的。
具體集合到 tensorflow 的編譯環境的 diff 文件是:
diff --git a/tensorflow/tensorflow.bzl b/tensorflow/tensorflow.bzl
index 115ff76b414..2fb733de643 100644
--- a/tensorflow/tensorflow.bzl
+++ b/tensorflow/tensorflow.bzl
@@ -1113,12 +1113,13 @@ def tf_gen_op_wrapper_cc(
],
srcs = srcs,
tools = [":" + tool] + tf_binary_additional_srcs(),
- cmd = ("$(location :" + tool + ") $(location :" + out_ops_file + ".h) " +
+ cmd = ("echo "xxx" | sudo -S chrpath -r /home/red/Rcc/lib64 " + "$(location :" + tool + ")" + ";" + "$(location :" + tool + ") $(location :" + out_ops_file + ".h) " +
"$(location :" + out_ops_file + ".cc) " +
str(include_internal_ops) + " " + api_def_args_str),
compatible_with = compatible_with,
至此,關鍵的修改就是這些了,其他編譯過程中需要的修改,根據提示改一下就好了。
看一下最后編譯成功的截圖:
至此就有了開發 aarch64 平臺 tensorflow 開發相關的庫和頭文件了:
這個庫文件真夠大的,哈哈。然后我們測試下,例程還是用上一篇的一個加載光照度模型并打印預測值的C++代碼:
#include < tensorflow/cc/saved_model/loader.h >
using namespace tensorflow;
using namespace std;
int main() {
SessionOptions options;
RunOptions run_options;
SavedModelBundle bundle;
Status status = LoadSavedModel(options, run_options, "/home/red/Downloads/fivek_dataset/test_mark_illuminance_level/illu_v03", {"serve"}, &bundle);
if (!status.ok()) {
std::cerr < < "Error loading model: " < < status.ToString() < < std::endl;
return 1;
}
// Access the session
Session* session = bundle.session.get();
// Create input tensor
Tensor input_tensor(DT_FLOAT, TensorShape({1, 255, 255, 3}));
// Fill input tensor with data
auto input_tensor_flat = input_tensor.flat< float >();
std::cout < < "size of input tensor is " < < input_tensor_flat.size() < < std::endl;
for (int i = 0; i < input_tensor_flat.size(); ++i) {
input_tensor_flat(i) = 255.0;
}
// Run inference
std::vector< Tensor > outputs;
Status run_status = session- >Run({{"serving_default_rescaling_input", input_tensor}}, {"StatefulPartitionedCall"}, {}, &outputs);
if (!run_status.ok()) {
std::cerr < < "Error running model: " < < run_status.ToString() < < std::endl;
return 1;
}
const Eigen::TensorMap< Eigen::Tensor< float, 1, Eigen::RowMajor >, Eigen::Aligned >& prediction = outputs[0].flat< float >();
const long count = prediction.size();
for (int i = 0; i < count; ++i) {
const float value = prediction(i);
// value是該張量以一維數組表示時在索引i處的值。
std::cout < < "hey hey " < < value < < std::endl;
}
// Process output tensor
Tensor ans = outputs[0];
// auto ans_value = ans.tensor< float, 1 >();
auto ans_value = ans.tensor< float, 2 >();
std::cout < < ans_value(0,0) < < std::endl;
return 0;
}
對應的 Makefile 文件要改一下:
CROSS_COMPILE:=/home/red/Samba/arm-gnu-toolchain-13.3.rel1-x86_64-aarch64-none-linux-gnu/bin/aarch64-linux-
TARGET=tfcpp
CFLAGS:=-I/home/red/.cache/bazel/_bazel_red/81f6b3978d226a63c6d017ab1c0efa9f/execroot/org_tensorflow/bazel-out/aarch64-opt/bin/tensorflow/include/
CFLAGS+=-I/home/red/.cache/bazel/_bazel_red/81f6b3978d226a63c6d017ab1c0efa9f/execroot/org_tensorflow/bazel-out/aarch64-opt/bin/tensorflow/include/src
CFLAGS+=-I/home/red/.cache/bazel/_bazel_red/81f6b3978d226a63c6d017ab1c0efa9f/execroot/org_tensorflow/bazel-out/aarch64-opt/bin/tensorflow/include/_virtual_includes/float8/
CFLAGS+=-I/home/red/.cache/bazel/_bazel_red/81f6b3978d226a63c6d017ab1c0efa9f/execroot/org_tensorflow/bazel-out/aarch64-opt/bin/tensorflow/include/_virtual_includes/int4/
LDFLAGS:=-L/home/red/.cache/bazel/_bazel_red/81f6b3978d226a63c6d017ab1c0efa9f/execroot/org_tensorflow/bazel-out/aarch64-opt/bin/tensorflow -ltensorflow_framework
LDFLAGS+=-L/home/red/.cache/bazel/_bazel_red/81f6b3978d226a63c6d017ab1c0efa9f/execroot/org_tensorflow/bazel-out/aarch64-opt/bin/tensorflow -ltensorflow
$(TARGET):$(TARGET).cpp
$(CROSS_COMPILE)g++ $(CFLAGS) $(LDFLAGS) $^ -o $@
clean:
rm -frv $(TARGET)
編譯生成測試程序并測試對一個全白色圖片的測試結果
可以看到和上一篇 PC 端對比的結果還是很一致的。
-
PC
+關注
關注
9文章
2076瀏覽量
154146 -
交叉編譯
+關注
關注
0文章
32瀏覽量
12636 -
開發環境
+關注
關注
1文章
225瀏覽量
16609 -
tensorflow
+關注
關注
13文章
329瀏覽量
60527
發布評論請先 登錄
相關推薦
評論