1. 为什么要把 TensorFlow 编译成静态库把 TensorFlow 编译成静态库这件事听起来像是那种只有框架开发者才会碰的底层操作但实际工作中遇到的需求远比想象中多。我在几个边缘计算和嵌入式推理项目里都踩过这条路径最典型的场景是目标设备没有完整的包管理环境动态库依赖链一塌糊涂ldd一查几十个.so找不到部署时只能把整个运行时环境打包进去镜像体积直接爆炸。这时候静态库的价值就出来了——把 TensorFlow 的核心算子、运行时、依赖全部塞进一个.a文件最终链接出一个几乎零外部依赖的可执行文件扔到目标机器上就能跑。TensorFlow 官方从 2.x 开始主推libtensorflow_cc.so这种动态库形态预编译包也只提供动态版本。想要静态库基本只有两条路一是用 Bazel 从源码构建时指定--configmonolithic加静态链接参数二是自己写 CMake 工程去链接 Bazel 产出的中间静态库。前者是官方支持的正统路线后者是社区里被逼出来的野路子。我两种都试过下面会把完整流程、参数含义、踩坑记录全部摊开讲。这篇文章适合三类人看第一类是做嵌入式 AI 部署的工程师需要在资源受限设备上跑推理第二类是想深度定制 TensorFlow 算子、裁剪功能模块的开发者第三类是对大型 C 项目构建体系感兴趣、想拿 TensorFlow 练手的学习者。不管你是哪一类只要跟着走一遍应该能少走不少弯路。需要提前说明的是TensorFlow 的源码构建对机器配置要求不低。我实测下来完整编译一次含所有依赖在 16 核 32G 内存的机器上大约需要 40 到 90 分钟磁盘占用峰值能到 30G 以上。如果你用的是笔记本或者云主机建议先确认资源再动手否则中途 OOM 或者磁盘写满会非常难受。2. 构建环境准备与依赖梳理2.1 硬件与系统层面的硬性要求TensorFlow 源码构建对内存的消耗主要来自链接阶段尤其是静态库合并的时候ld或者lld会把大量.o文件拼在一起内存占用能飙到 10G 以上。我遇到过在 8G 内存机器上编译链接阶段直接被系统 kill 的情况日志里只留下一句Killed排查半天才发现是 OOM。所以我的建议是物理内存至少 16G推荐 32G如果实在只有 8G那就得把并行度降下来用--jobs2甚至--jobs1代价是编译时间成倍增加。磁盘方面Bazel 的缓存目录默认在~/.cache/bazel源码目录下的bazel-out、bazel-bin这些软链接指向实际输出。整个构建过程产生的中间文件加起来我见过最大的一次是 28G。所以预留 40G 以上空闲磁盘比较稳妥而且最好是 SSD机械硬盘在大量小文件读写时会拖慢整体速度。操作系统我用的是 Ubuntu 20.04 和 22.04 两个版本都比较顺利。CentOS 7 也能跑但 glibc 版本较老某些依赖需要手动升级新手不建议。macOS 理论上可以但静态库在 macOS 上的链接行为和 Linux 差异较大本文以 Linux 为主。2.2 工具链版本匹配的坑TensorFlow 每个版本对 Bazel、GCC、Python 的版本都有明确要求这个在官方configure.py运行时会自动检测并报错。我整理了一个常见版本的对应关系供参考TensorFlow 版本Bazel 版本GCC 版本Python 版本2.135.3.09.33.8-3.112.125.3.09.33.8-3.112.115.3.09.33.7-3.102.105.1.19.33.7-3.102.95.0.09.33.7-3.10注意Bazel 版本不匹配是最常见的报错来源。比如用 Bazel 6.x 去编译 TF 2.12会在加载 WORKSPACE 阶段就失败报ERROR: Skipping //tensorflow/tools/pip_package:build_pip_package: no such package。这时候别急着改源码先检查 Bazel 版本。安装 Bazel 我推荐用官方提供的bazelisk它能根据项目里的.bazelversion文件自动下载对应版本省去手动切换的麻烦。安装方式很简单# 下载 bazelisk wget https://github.com/bazelbuild/bazelisk/releases/download/v1.18.0/bazelisk-linux-amd64 chmod x bazelisk-linux-amd64 sudo mv bazelisk-linux-amd64 /usr/local/bin/bazel这样bazel命令就指向了 bazelisk进入 TensorFlow 源码目录后它会自动读取.bazelversion并拉取正确的 Bazel 版本。Python 环境我强烈建议用conda或者venv隔离因为构建过程中需要安装numpy、wheel、keras等一堆包污染系统 Python 会很麻烦。我一般这样建环境conda create -n tfbuild python3.10 conda activate tfbuild pip install numpy wheel packaging requests opt_einsum pip install keras_preprocessing --no-deps2.3 源码获取与分支选择源码从官方仓库克隆但要注意分支选择。如果你要的是稳定版就 checkout 对应的 release 分支比如r2.13如果要最新特性就用master但 master 分支的构建失败率明显更高不建议新手碰。git clone --depth 1 --branch r2.13 https://github.com/tensorflow/tensorflow.git cd tensorflow--depth 1只拉取最近一次提交能省不少时间和空间。如果你后续需要切换分支或者查看历史可以去掉这个参数但克隆时间会从几分钟变成十几分钟。3. 配置阶段的关键参数解析3.1 configure.py 交互式配置逐项说明TensorFlow 源码目录下有个configure.py运行后会进入交互式问答。很多人第一次跑的时候一路回车默认结果编译出来的东西不是自己想要的。我把每个问题的含义和推荐选择列出来./configure第一个问题通常是Please specify the location of python直接回车用当前环境的 Python 即可。接着会问是否支持 CUDA如果你不需要 GPU 推理一定要选 N否则会引入大量 CUDA 依赖静态库体积翻倍不说链接时还需要libcudart_static.a等一堆东西非常麻烦。然后会问Do you wish to build TensorFlow with ROCm support?同样选 N。再往后是Do you wish to configure the build with optimization flags?这个选 Y会加上-marchnative等优化对推理性能有提升。关键的一项来了Please specify optimization flags to use during compilation when bazel option --configopt is specified默认是-marchnative -Wno-sign-compare保持默认即可。但如果你编译的机器和目标部署机器 CPU 架构不同必须把-marchnative去掉否则生成的二进制在目标机器上会报Illegal instruction。我在这上面栽过跟头编译机是支持 AVX-512 的服务器部署到只支持 AVX2 的工控机直接崩。最后会问Would you like to interactively configure ./WORKSPACE for Android builds?选 N。3.2 静态库构建的 Bazel 参数拆解配置完成后真正的构建命令才是核心。官方文档里给的命令是bazel build --configopt //tensorflow:libtensorflow_cc.so这是动态库。要静态库需要改成bazel build --configopt --configmonolithic //tensorflow:libtensorflow_cc.so等等这里有个容易混淆的点--configmonolithic并不是直接产出.a文件它的作用是让 TensorFlow 把所有依赖都编进同一个目标里减少动态库数量。真正要静态库得构建//tensorflow:libtensorflow_cc.a这个目标吗实际上 TensorFlow 的 BUILD 文件里并没有直接定义.a的构建目标社区常用的做法是构建//tensorflow:libtensorflow_cc.so时加上--configmonolithic然后从bazel-bin/tensorflow/目录下找到生成的.a中间文件或者自己写一个cc_library去聚合。更靠谱的方式是使用--configmonolithic配合--dynamic_modeoffbazel build --configopt --configmonolithic --dynamic_modeoff \ //tensorflow:libtensorflow_cc.so--dynamic_modeoff会强制所有依赖以静态方式链接最终产出的.so其实是一个伪动态库——它本身是动态库格式但内部不依赖其他 TensorFlow 动态库。如果你要的是纯.a可以在构建完成后从bazel-bin/tensorflow/下找到libtensorflow_cc.so同目录的_objs文件夹里面有一堆.o和.a用ar命令合并ar rcs libtensorflow_cc_static.a bazel-bin/tensorflow/_objs/**/*.o但这种方式容易漏文件更推荐的做法是写一个自定义的cc_library规则把 TensorFlow 的依赖全部聚合进来。这个后面在实操章节会详细讲。3.3 裁剪不需要的模块以减小体积完整编译出来的 TensorFlow 静态库体积能到 1G 以上链接进最终可执行文件后二进制大小轻松超过 500M。对于嵌入式设备来说这个体积往往不可接受。所以裁剪是必须的。裁剪的主要手段是在configure.py阶段关闭不需要的功能以及修改.bazelrc添加编译宏。比如关闭 XLA--definewith_xla_supportfalse关闭 GPU--confignoaws --confignogcp --confignohdfs关闭 Java 支持--defineno_java_supporttrue关闭 TensorBoard 相关--defineno_tensorboardtrue这些参数可以写在.bazelrc文件里避免每次命令行输入。我在项目里常用的.bazelrc追加内容如下build --configopt build --configmonolithic build --dynamic_modeoff build --definewith_xla_supportfalse build --defineno_java_supporttrue build --defineno_tensorboardtrue build --defineno_aws_supporttrue build --defineno_gcp_supporttrue build --defineno_hdfs_supporttrue这样每次bazel build都会自动带上这些配置。实测下来裁剪后的静态库体积能从 1.2G 降到 400M 左右链接出的可执行文件大约 150M对于大多数边缘设备已经可以接受了。4. 完整编译流程与实操记录4.1 从零开始的完整命令序列假设你已经装好了 Bazel、Python 环境并且克隆了源码。下面是我在实际项目中使用的完整命令序列可以直接抄# 1. 进入源码目录 cd tensorflow # 2. 运行配置按前面说的选项选择 ./configure # 3. 追加 .bazelrc 配置 cat .bazelrc EOF build --configopt build --configmonolithic build --dynamic_modeoff build --definewith_xla_supportfalse build --defineno_java_supporttrue build --defineno_tensorboardtrue build --defineno_aws_supporttrue build --defineno_gcp_supporttrue build --defineno_hdfs_supporttrue EOF # 4. 开始构建jobs 根据 CPU 核数调整 bazel build --jobs16 //tensorflow:libtensorflow_cc.so # 5. 构建完成后查看产物 ls -lh bazel-bin/tensorflow/libtensorflow_cc.so*构建过程中Bazel 会先下载一堆外部依赖包括 Eigen、protobuf、absl、llvm 等这一步耗时取决于网络。如果网络不稳定可以配置代理或者使用国内镜像源但注意不要使用任何违规的网络工具这里指的是合法的包管理镜像。4.2 生成真正的静态库文件前面说了libtensorflow_cc.so即使加了--dynamic_modeoff产出的还是.so格式。要得到.a有两种方法。方法一从 Bazel 输出目录提取。构建完成后执行find bazel-bin/tensorflow -name *.a | head -20你会看到很多.a文件比如libtensorflow_framework.a、libtensorflow_cc.a等。其中libtensorflow_cc.a就是核心静态库但它可能不包含所有算子实现需要配合libtensorflow_framework.a一起使用。方法二自己写一个聚合规则。在 TensorFlow 源码目录下新建一个BUILD文件内容如下cc_library( name tensorflow_static, deps [ //tensorflow:libtensorflow_cc, //tensorflow:libtensorflow_framework, ], linkstatic 1, )然后构建这个目标bazel build --configmonolithic --dynamic_modeoff //:tensorflow_static这样 Bazel 会生成一个libtensorflow_static.a里面聚合了所有依赖。但实测这个方法有时候会因为符号冲突失败需要加--allow_undefined_symbols之类的参数比较折腾。我一般用方法一简单直接。4.3 链接测试写一个最小推理程序编译出静态库后必须验证它能正常工作。我写了一个最小的 C 程序加载一个简单的模型并执行推理#include tensorflow/core/public/session.h #include tensorflow/core/platform/env.h #include iostream int main() { tensorflow::Session* session; tensorflow::Status status tensorflow::NewSession( tensorflow::SessionOptions(), session); if (!status.ok()) { std::cerr Session creation failed: status.ToString() std::endl; return 1; } std::cout TensorFlow static lib works! std::endl; session-Close(); delete session; return 0; }编译命令g -stdc17 test_tf.cpp -o test_tf \ -I bazel-bin/tensorflow/include \ -I bazel-bin/tensorflow/include/external \ -L bazel-bin/tensorflow \ -ltensorflow_cc -ltensorflow_framework \ -lpthread -ldl -lz -lrt如果链接时报undefined reference大概率是缺少某个系统库根据报错信息补上即可。常见的还有-lcurl、-lssl、-lcrypto取决于你编译时开启了哪些功能。运行./test_tf如果输出TensorFlow static lib works!说明静态库可用。接着可以加载一个真实的.pb模型文件测试推理流程。5. 常见问题与排查技巧实录5.1 编译阶段典型报错速查报错信息原因解决方法no such package local_config_cuda//cuda配置时选了 CUDA 但环境没装重新运行./configureCUDA 选 NKilled无其他信息内存不足被 OOM killer 终止降低--jobs数量或增加内存undefined reference to dlopen缺少-ldl链接选项编译命令加-ldlerror: numeric_limits is not a member of stdGCC 版本过低升级 GCC 到 9.3Bazel version mismatchBazel 版本不对用 bazelisk 自动匹配No space left on device磁盘满清理~/.cache/bazel预留 40Gfatal error: Eigen/Core: No such file外部依赖未下载完整删除bazel-*软链接重新构建5.2 链接阶段的符号冲突处理静态库链接最头疼的问题是符号重复定义。比如你的项目里同时用了 protobuf 和 TensorFlow而 TensorFlow 静态库里已经包含了一份 protobuf 实现链接时就会报multiple definition of google::protobuf::...。解决办法有两个一是用-Wl,--allow-multiple-definition让链接器忽略重复符号但这是治标不治本可能导致运行时行为异常二是用objcopy把静态库里的 protobuf 符号剥离出来或者干脆在你的项目里不链接 protobuf直接用 TensorFlow 自带的。我一般推荐第二种具体操作是# 查看静态库里包含哪些目标文件 ar t libtensorflow_cc.a | grep protobuf # 如果确认不需要独立 protobuf链接时不要加 -lprotobuf还有一种情况是 C 标准库版本不一致导致的符号冲突比如 TensorFlow 用_GLIBCXX_USE_CXX11_ABI1编译你的项目用 0链接时会报一堆std::__cxx11::basic_string相关的错误。解决办法是统一 ABI 设置在编译 TensorFlow 时加--cxxopt-D_GLIBCXX_USE_CXX11_ABI1你的项目也加同样的宏。5.3 运行时崩溃的排查思路静态库编译成功、链接成功不代表运行没问题。我遇到过几种运行时崩溃第一种是Illegal instruction前面提过是-marchnative导致的。解决办法是编译时去掉这个选项或者用-marchx86-64-v2这种更通用的指令集。第二种是Segmentation fault发生在调用NewSession时。排查后发现是静态库初始化顺序问题TensorFlow 的某些全局对象在main之前没有正确构造。解决办法是在main函数开头手动调用tensorflow::port::InitMain或者确保链接顺序正确。第三种是模型加载失败报Not found: Op type not registered XXX。这是因为裁剪时把某些算子编译掉了。解决办法是重新编译不要关闭with_xla_support或者手动把需要的算子加回BUILD文件。实操心得每次修改编译配置后先编译一个最小测试程序验证不要直接上完整项目。这样出问题时排查范围小定位快。6. 静态库体积优化与部署建议6.1 用 strip 和 objcopy 精简二进制编译出的静态库和最终可执行文件里包含大量调试符号用strip可以去掉strip --strip-debug libtensorflow_cc.a strip --strip-unneeded test_tf实测下来strip后静态库体积能减少 30% 到 50%。如果还想更激进可以用objcopy --gc-sections在链接时回收未使用的段但需要编译时加-ffunction-sections -fdata-sections。6.2 按需裁剪算子减少体积TensorFlow 的算子注册机制允许你只保留需要的算子。在tensorflow/core/kernels/BUILD文件里可以找到各个算子的cc_library定义把不需要的从依赖里去掉。但这个方法风险较高容易漏掉间接依赖导致运行时找不到算子。我一般只在极端资源受限的场景下才这么做而且会配合完整的回归测试。更稳妥的裁剪方式是在configure.py阶段关闭整个模块比如不用 TensorBoard、不用 Java、不用云存储支持这些对推理核心功能没有影响但能省下不少体积。6.3 部署时的注意事项静态链接的可执行文件虽然依赖少但也不是完全绿色。它仍然依赖系统的 glibc、libstdc、libm 等基础库。如果目标设备的 glibc 版本比编译机低运行时会报GLIBC_2.XX not found。解决办法是在低版本系统上编译或者用 musl libc 静态链接但 TensorFlow 对 musl 支持不好不推荐。另外静态链接的程序启动时会加载整个二进制到内存如果二进制有 150M启动内存占用就会增加 150M。对于内存紧张的设备这一点要提前评估。我在实际项目中的做法是编译机和目标设备用同一版本的操作系统镜像确保 glibc 一致如果做不到就在 Docker 容器里编译容器基础镜像选和目标设备一致的版本。这样能最大程度避免运行时环境问题。最后分享一个小技巧编译完成后用nm -C libtensorflow_cc.a | grep T | wc -l统计一下导出的函数数量和官方动态库对比如果差异过大说明裁剪过度或者链接不完整需要检查配置。这个命令我每次编译完都会跑一遍算是最后一道保险。
阅读完成 · 觉得有帮助?