拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Windows下TensorFlow C++ API编译集成实战:VS2015与CMake全流程

简介这是基于 Visual Studio 2015 64 位环境编译完成的 TensorFlow 1.10.0 CPU 版 C API 库文件资源。作者耗时约一天完成编译与验证可在 C 工程中直接调用 TensorFlow 接口运行示例程序也能加载训练好的模型进行推理实测支持读取 GPU 训练出的模型免去自行配置源码编译的大量步骤。压缩包为 rar 格式共 2388 个文件大小 80.87MB其中 2248 个为 h 头文件覆盖 TensorFlow 核心接口及 Eigen 数值计算模块另有 6 个可执行示例程序、5 个文本说明文件以及依赖库、动态链接库、许可证文件等必要组成目录结构清晰。当前已有 518 人学习下载。对想快速获取 TensorFlow C 调用环境的开发者这份编译成果可直接跳过编译环节用于模型部署与二次开发。1. 为什么要折腾CPU版TF 1.10的C API先说清楚我在干什么TensorFlow 1.10.0CPU版本Visual Studio 2015编译出可供C项目直接调用的库文件。这个组合放在今天看确实有点上古但如果你接手过工业项目或者老代码库就会明白这类需求有多真实。我当时面临的场景是这样的一个老的C图像处理服务需要集成一个用TensorFlow训练好的模型做在线推理项目本身是用VS2015建的不能整体切到新工具链。Python端的TensorFlow跑推理倒是简单但放到生产C服务里要么起子进程调Python脚本效率低、部署麻烦要么直接用TensorFlow的C API把模型嵌入到服务进程里。后者明显是正路但问题在于官方预编译的Windows版TensorFlow只提供Python wheel包C库需要自己从源码编译。如果你只是想做研究或者模型实验用Python足够舒服。但如果你需要在C程序里直接调用.pb模型做推理没有对应的C库你基本寸步难行。官方文档里对Windows下源码编译C API的描述非常简略好多坑得自己踩。这篇博文把我从环境准备到最终拿到.lib和.dll的完整过程写清楚尤其是几个非常关键的版本匹配问题和CMake配置细节希望能让后来人少走弯路。先说结论这个任务的核心难点不在于编译本身而在于版本匹配。TensorFlow 1.10.0对编译工具链、依赖库版本、CMake版本、Python版本都有严格要求任何一个环节对不上都会在编译中途报各种莫名其妙的错误。2. 环境准备版本匹配是第一优先级2.1 工具链版本为什么必须是VS2015TensorFlow 1.10.0是在2018年年中发布的当时官方在Windows平台主要支持MSVC 2015即Visual Studio 2015 Update 3。虽说VS2017理论上也能编译但实际上会遇到不少坑比如C标准库头文件的兼容性问题、MSVC版本检测逻辑等。如果你用的是VS2015务必确保Update 3已经安装因为TensorFlow的构建脚本里会检查_MSC_VER的值只有VS2015 Update 3的19.00.24210以上版本才能通过检查。这里有个容易忽略的点VS2015只是用来编译C代码你还需要安装Python工具链因为TensorFlow的构建系统需要在Python环境下运行。我在编译时用的是Anaconda自带的Python 3.6这个版本也是官方推荐的。2.2 CMake版本与依赖项TensorFlow 1.10.0的Windows源码编译走的是CMake流程现在的新版本已经切换到Bazel了但在1.x时代Windows主要用CMake。版本要求如下依赖项推荐版本备注CMake3.12以上我这里用了3.13SWIG3.0.12生成Python绑定需要Python3.6 (64位)必须64位且是Debug或Release要对应Eigen3.3.4编译时自动拉取无需手动装protobuf3.5.1由构建过程自动编译MSVCVS2015 Update 3编译器版本必须匹配实测下来CMake版本不能太老也不能太新。太老会不认识仓库里CMakeLists.txt的某些写法太新比如3.15在我的环境下遇到过一个关于target_include_directories的旧式用法警告被当成错误处理的情况。保守起见3.12或3.13最稳。2.3 源码准备不要用pip install的临时包很多人在这一步会犯一个错误直接从pip show tensorflow获取源码路径然后试图从site-packages里提取头文件和库。我不建议这么做因为pip安装的wheel里并不包含完整的C API头文件结构和编译所需的所有源文件。正确做法是从GitHub仓库checkout指定taggit clone https://github.com/tensorflow/tensorflow.git cd tensorflow git checkout v1.10.0注意TensorFlow的源码仓库非常大完整clone会花不少时间。建议用--depth 1 --branch v1.10.0来只拉这一个版本能节省大量时间和磁盘空间。注意1.10.0这个版本号对应的是v1.10.0这个tag不要拉master分支也不要拉v1.10.1或v1.10.2接口有差异后再细说。3. CMake配置最容易出错的一步3.1 配置命令的完整写法TensorFlow 1.10.0源码目录下的tensorflow/contrib/cmake是Windows编译的入口。用CMake GUI会比较直观但我个人建议用命令行因为你需要反复调整配置参数命令行方式更容易追溯历史。我使用的配置命令如下mkdir build cd build cmake ..\tensorflow\contrib\cmake ^ -G Visual Studio 15 2017 Win64 ^ -DCMAKE_BUILD_TYPERelease ^ -DCMAKE_CONFIGURATION_TYPESRelease ^ -DPYTHON_EXECUTABLED:\Anaconda3\python.exe ^ -DPYTHON_LIBRARIESD:\Anaconda3\libs\python36.lib ^ -DPYTHON_INCLUDE_DIRSD:\Anaconda3\include ^ -Dtensorflow_BUILD_PYTHON_BINDINGSOFF ^ -Dtensorflow_BUILD_CC_EXAMPLEON ^ -Dtensorflow_BUILD_PACKAGEOFF ^ -Dtensorflow_ENABLE_GRPC_SUPPORTOFF ^ -Dtensorflow_BUILD_SHARED_LIBON ^ -DSWIG_EXECUTABLED:\swigwin-3.0.12\swig.exe这里有几个关键参数需要解释一下-Dtensorflow_BUILD_SHARED_LIBON这个必须设为ON否则不会生成tensorflow.dll和tensorflow.lib这两个我们最终需要的文件。C API的核心库就是通过这个开关控制的。-Dtensorflow_BUILD_PYTHON_BINDINGSOFF如果你不需要编译Python的绑定层这个可以关掉能省下不少编译时间。但如果你的应用场景需要同时保留Python和C的加载能力比如调试对比可以打开代价是编译时间增加至少半小时。-Dtensorflow_ENABLE_GRPC_SUPPORTOFFGRPC是TensorFlow Serving相关的支持库纯做本地推理不需要关掉可以避免很多依赖问题。-DPYTHON_LIBRARIES指向的是python36.lib不是python3.lib也不是Anaconda环境下其他乱七八糟的lib。这个文件在Anaconda安装目录下的libs文件夹中。如果你的Anaconda是D盘就写D盘路径别想当然照抄。3.2 关于VS版本选的坑为什么我用了Visual Studio 15 2017 Win64这里有个比较反直觉的事虽然项目要求VS2015但CMake生成器我选择了Visual Studio 15 2017 Win64。原因很简单TensorFlow 1.10.0的CMake脚本中并没有为VS2015做很好的CMake生成器适配容易出现属性表传递的问题。而用VS2017的CMake生成器生成工程文件再用VS2015打开编译只要平台工具集选对是可以正常编译通过的。具体来说用-G Visual Studio 15 2017 Win64生成.sln。用VS2015打开项目后在项目属性中找到平台工具集改为Visual Studio 2015 (v140)。解决方案中可能有多个项目需要把每个项目的工具集都改成v140。如果你直接用-G Visual Studio 14 2015 Win64CMake会报错或不生成完整的工程文件。这算是1.10.0时代CMake脚本的一个移植性缺陷知道怎么绕过去就行。3.3 关键依赖项的处理逻辑配置过程中CMake会自动下载Eigen等第三方库但有时会因为网络问题卡住。我当时遇到的典型问题是Eigen下载超时CMake报错后直接退出。解决办法是去GitHub上找到对应版本的Eigen源码放到CMake缓存目录下C:\Users\用户名\.cache\eigen3\或者你也可以在CMake配置前手动设置缓存路径。虽然麻烦一点但比反复试下载快很多。SWIG也必须预先装好。它的作用是把C代码包装成Python接口虽然我们关掉了Python绑定但CMake脚本里对这个工具的检查还是绕不过去。安装时注意选择swigwin版本解压后不用编译直接可用。4. 编译阶段等待、错误处理、再等待4.1 编译启动方式与时间预期配置成功之后在build目录下会生成tensorflow.sln。你直接用VS2015打开也行用命令行cmake --build . --config Release也行。个人建议后者因为编译过程很长命令行方式更容易保持会话、记录日志。先说下硬件参考我当时的机器是i7-7700K16GB内存SSD硬盘全量编译耗时大约1小时20分钟。内存如果只有8GB建议增加临时交换空间或者直接把其他大型程序关掉不然中途容易出现资源耗尽导致的编译错误。4.2 第一个高频错误编译中间的fatal error C1060: compiler is out of heap space这个错误在编译某些大型源文件时特别容易出现典型报错是编译器堆空间不足。TensorFlow的某些模板元编程代码非常吃内存MSVC在默认设置下容易挂。解决方式有几个打开项目的属性设置在C/C - 命令行中手动添加/Zm1000参数指定预编译头内存上限。如果还不够把编译从多核并行改成单核编译虽然慢一些但能避免同时编译多个大文件导致的内存峰值叠加。命令行的方式是在cmake --build时加上cmake --build . --config Release -- /m:4/m:4表示最多4个并行任务实测比默认设置更稳。4.3 第二个高频错误找不到python36.lib这个问题多出现在路径配置不精确的时候。PYTHON_LIBRARIES这个变量填错或者用了Python 3.7对应的python37.lib都会导致链接阶段找不到符号。解决方案在CMake配置时打印一下实际生效的值cmake -LA build | grep PYTHON如果PYTHON_LIBRARIES是空或者指向错误路径用绝对路径重新指定。这一步值得花时间确认清楚因为链接阶段报错时你往往已经等了大半个小时的编译时间再回头改配置重来非常浪费时间。4.4 编译成功后的大坑DLL与LIB都在那里编译完成后build目录下会在子目录结构中生成以下关键文件以Release为例build/release/tensorflow.dll build/release/tensorflow.lib build/release/tf_cc.dll build/release/tf_cc.lib build/release/tf_cc_ops.dll build/release/tf_cc_ops.lib如果你还开了CC_EXAMPLE开关通过-Dtensorflow_BUILD_CC_EXAMPLEON还会生成一个示例程序tensorflow.exe可以直接用来验证C API是否正常调用。但需要注意一点1.10.0的C API头文件并不和dll自动打包到一个地方。头文件分布在源码目录的多个文件夹下你自己需要把它们整理到一个include目录里。后面会专门讲头文件复制和项目配置的细节。5. C项目中集成头文件、库文件配置与验证5.1 需要哪些头文件TensorFlow C API的头文件分布比较分散不像有些库把所有头文件集中在include/一个目录下。我在项目中实际用到的头文件路径有三块tensorflow/core/public/session.h核心的Session创建、运行接口这是C API的主入口。tensorflow/cc/...cc目录下的C封装比如tensorflow/cc/ops/const_op.h、tensorflow/cc/ops/standard_ops.h。tensorflow/core/framework/...包含tensor.pb.h、graph.pb.h等Protocol Buffers生成的头文件。把这些目录都加入VS2015项目的附加包含目录里。注意顺序不要手动选择只包含某一个子目录而是要把源码根目录加进去因为头文件之间是相对路径引用比如#include tensorflow/core/public/session.h这种写法需要源码根目录作为include的根。5.2 链接器配置链接器需要链接的核心库文件为tensorflow.lib tf_cc.lib tf_cc_ops.lib可能需要把build/release目录加入附加库目录然后在输入 - 附加依赖项里添加上面的lib文件名。另外如果你编译时关闭了GRPC支持链接时就不用担心grpc相关的依赖问题。如果开启了则还需要额外准备grpc的lib非常麻烦。这就是前面建议关闭GRPC的原因。5.3 运行时依赖dll路径问题运行程序时必须保证tensorflow.dll、tf_cc.dll、tf_cc_ops.dll被复制到exe的同级目录或者添加系统环境变量PATH指向build/release目录。我在这一步踩过一个坑程序编译链接都成功但运行时立刻报0xc000007b错误。这个错误通常是DLL位数不匹配或者DLL加载顺序混乱优先检查所有dll都是64位的吗有没有老版本TensorFlow的dll残留在C:\Windows\System32目录环境变量里优先加载了其他路径下的同名dll吗解决方式打开依赖检查工具比如Dependencies逐条查看加载的dll路径确认每个tensorflow相关dll都从build/release目录加载。我发现自己的PATH环境变量中有一个老版本的tensorflow.dll优先级更高程序一启动就加载错了版本直接崩溃。5.4 第一个能跑的C推理程序集成完成后建议先用最简单的代码验证下整体链路#include tensorflow/core/public/session.h #include tensorflow/cc/ops/standard_ops.h #include tensorflow/core/framework/tensor.h #include iostream using namespace tensorflow; int main() { Session* session; Status status NewSession(SessionOptions(), session); if (!status.ok()) { std::cerr Failed to create session: status.ToString() std::endl; return -1; } // 构造一个简单的常量图 GraphDef graph_def; auto node graph_def.add_node(); node-set_name(const_value); node-set_op(Const); tensorflow::NodeDef attr_node; // 这里仅为示例实际构建图建议用tensorflow::ops命名空间下的封装接口 std::cout TensorFlow C API works! std::endl; session-Close(); delete session; return 0; }如果你的环境配置正确编译链接运行后应该能看到TensorFlow C API works!的输出。虽然这个程序没有真正执行推理但已经能证明头文件、库文件、DLL加载这一整条链路是通的。提示在实际项目中建议使用tensorflow::ops命名空间下的高层API构建计算图可读性和可维护性都比直接操作NodeDef强得多。1.10.0版本中tensorflow/cc/ops/standard_ops.h已经覆盖了绝大多数常用算子。6. 训练好的模型如何加载并推理6.1 Freeze Graph与模型文件格式如果模型是用Python训练的最终要部署到C环境需要先冻结成单个.pb文件。freeze_graph脚本会合并权重和计算图结构去掉训练相关的节点如梯度、优化器状态。python freeze_graph.py \ --input_graphmodel.pbtxt \ --input_checkpointmodel.ckpt \ --output_graphfrozen_model.pb \ --output_node_namesoutput_node_name这里千万注意output_node_names要填实际的输出张量名称。我就因为少填了一个输出节点导致加载后的图少了一部分推理路径运行时的输出完全不对。6.2 加载模型并执行推理核心代码示例如下#include tensorflow/core/public/session.h #include tensorflow/core/framework/tensor.h #include tensorflow/core/framework/graph.pb.h #include fstream #include vector using namespace tensorflow; Status load_frozen_graph(Session* session, const std::string pb_path) { GraphDef graph_def; std::ifstream fs(pb_path, std::ios::binary); if (!fs.good()) { return errors::NotFound(Model file not found: , pb_path); } std::string contents((std::istreambuf_iteratorchar(fs)), std::istreambuf_iteratorchar()); if (!graph_def.ParseFromString(contents)) { return errors::InvalidArgument(Failed to parse .pb file); } return session-Create(graph_def); } int main() { Session* session; NewSession(SessionOptions(), session); // 加载冻结图 Status st load_frozen_graph(session, frozen_model.pb); if (!st.ok()) { std::cerr st.ToString() std::endl; return -1; } // 构造输入Tensor Tensor input_tensor(DT_FLOAT, TensorShape({1, 224, 224, 3})); auto input_map input_tensor.tensorfloat, 4(); // 在这里填入预处理后的图像数据 ... std::vectorstd::pairstring, tensorflow::Tensor inputs { {input_tensor_name, input_tensor} }; std::vectortensorflow::Tensor outputs; st session-Run(inputs, {output_tensor_name}, {}, outputs); if (!st.ok()) { std::cerr Run failed: st.ToString() std::endl; return -1; } auto output_map outputs[0].tensorfloat, 2(); std::cout Inference result: output_map(0, 0) std::endl; session-Close(); delete session; return 0; }这段代码里的input_tensor_name和output_tensor_name就是在导出模型时定义的输入输出节点名。怎么确认这个名字用Python加载模型打印一下节点名就能看到import tensorflow as tf from tensorflow.python.platform import gfile with gfile.FastGFile(frozen_model.pb, rb) as f: graph_def tf.GraphDef() graph_def.ParseFromString(f.read()) # 打印所有节点名找输入和输出 for node in graph_def.node: print(node.name, node.op)把最后几层的节点对应到你想要的输出基本就能确定输出节点名。6.3 内存与线程配置的一些经验SessionOptions里可以配置线程数SessionOptions options; options.config.set_intra_op_parallelism_threads(4); // 单算子内部并行线程 options.config.set_inter_op_parallelism_threads(4); // 多算子之间的并行线程 options.config.mutable_gpu_options()-set_allow_growth(true);在CPU环境下intra_op_parallelism_threads设置过高反而会造成性能下降因为线程切换开销超过了并行计算收益。我的建议是从4个线程开始试用真实数据压测后逐步调参。7. 排错全记录我从CMake到运行的完整排查链路这部分想直接给出一份排查清单是我这次编译过程中实际用到的排查顺序你在遇到问题时可以按顺序检查效率最高。7.1 CMake配置阶段的错误链路我第一次CMake配置时遇到的问题是报错信息非常不直观——提示某个第三方库下载失败。很多人看到这种错误的第一反应是重试但反复重试浪费时间。我的排查链路是这样的查看CMake缓存中第三方库的下载地址cmake -LA build | grep URL手动用浏览器下载该第三方库压缩包放到C:\Users\用户名\.cache\对应目录下重新运行CMake配置这比让CMake反复走下载流程靠谱得多。因为TensorFlow的第三方库服务器有些在国外网络不稳定时下载中断是家常便饭。7.2 编译阶段错误排查优先级编译中报错后不要急着去改代码或配置。先看错误是在哪个文件、哪个阶段抛出的如果是在编译.pb.cc文件时出错多半是Protocol Buffers版本不匹配或者生成的头文件路径不对。如果是在链接阶段报LNK2019基本上是缺少某个lib文件或者lib文件的位数/版本与当前编译器不匹配。如果是LNK2038这表示_ITERATOR_DEBUG_LEVEL不匹配常见于调试库和发布库混用。检查你项目配置是否是全Release或全Debug不能混淆。7.3 运行时DLL加载错误0xc000007b这个错误最让人抓狂因为它不告诉你是哪个DLL出了问题。排查顺序先确认exe本身是64位构建dumpbin /headers your_app.exe可以查看。使用工具检查exe依赖的所有dll看哪些dll是32位、哪些是64位。关掉系统环境变量中其他的tensorflow引用只保留build/release目录。把dll直接放到exe同级目录不依赖PATH环境变量从根本上杜绝路径混乱。我实际遇到的情况就是同名的老版本dll被优先加载了把exe同级的dll更新成编译产物后问题就消失了。8. 一些想再强调的实操心得与建议TensorFlow 1.10.0 VS2015的C API编译整个过程最核心的其实是匹配两个字版本匹配、架构匹配、路径匹配。每一个坑的根源都能归结到这三个匹配中的某一个。关于版本匹配我再多啰嗦几句。有人可能会想我装一个更新的TensorFlow 1.x版本或者直接用TensorFlow 2.x的C API不就不用折腾了吗问题是如果你的项目里模型是旧代码训练的算子实现和序列化协议都是按1.10.0的版本兼容性设计的换个大版本很容易遇到模型加载失败或者算子缺失的问题。再说老项目的VS2015工具链也没法支撑新版本的C标准库要求。很多时候不是不想升级而是成本太高、风险太大。另外一个经验是编译脚本一定要记录成文档或脚本文件不要只依赖记忆。我这次把CMake命令写成了一个.bat文件每次重新配置时直接运行方便排查也方便换机器复现。即使过几个月再要用看脚本就想起来了比自己回忆可靠得多。还有一点建议把生成的dll和lib复制到一个单独的部署目录比如D:\third_party\tf110_x64\而不是放在build目录里用。因为build目录在增量编译时可能会被清理或覆盖部署目录更干净也方便其他项目引用。如果你只是需要在C服务里离线推理、不需要训练那CPU版的TF 1.10完全够用。我在实际项目中就是用它跑一个图像分类模型单张图片推理耗时在毫秒级完全满足业务需求。如果模型特别大、推理性能要求更高可以考虑开启MKL支持在编译时加一个-Dtensorflow_BUILD_MKLON开关能利用Intel CPU的指令集加速。不过要注意开启MKL后生成的dll体积会大不少部署时记得一起拷贝。最后整个过程中最让我印象深刻的教训是编译完成只是一个开始真正考验人的是如何把这套C库干净地集成到自己的工程里。头文件路径、链接库依赖、dll分发每一步都有隐蔽的坑。但只要把版本匹配这件基础工作做扎实了后面的问题基本都能靠排查思路解决。希望这篇记录能帮你少踩几个坑。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门