年初那阵子我接手了一摊老代码里面塞了好几个 PyTorch 训练好的模型当时图省事直接拿 LibTorch 在工程里各处硬调torch::jit::load、module.forward。结果就是模型加载散落三个文件、张量生命周期各种裸奔、一换 GPU 设备要翻遍全项目改代码甚至出现过线上偶发崩溃查了一个通宵最后发现是from_blob指向的临时数据被释放。后来我花了两天把整套逻辑收敛成一层独立的 LibTorch 封装把加载、预处理、推理、后处理、资源释放全部锁在类内部再往外只暴露一个干净的 C 接口。这篇文章就把这套封装方案从头到尾拆开讲包括为什么要封装、封装边界怎么划、CMake 怎么配、TorchScript 模型怎么导出、有哪些防不胜防的坑以及我在“单模型单线程”往“多模型多线程”演进过程中积累的优化手段。看完之后你手上那套 PyTorch 模型往 C 服务里搬的思路基本就清晰了。1. 先想清楚为什么非要在 C 里封装一下 LibTorch1.1 LibTorch 在工程里的真实定位LibTorch 是 PyTorch 的 C 发行版包含 ATen、TorchScript 运行时、C 前端 API 等一堆底层库。Python 里import torch能做的事大部分在 C 里也有对应实现加载 TorchScript 模型、执行前向推理、做张量运算、甚至训练的部分能力也都有。但对大多数人来说近距离接触 LibTorch 的场景只有一个公司训练团队用 Python 写模型生产服务是 C 写的需要把模型无缝塞进服务进程里。这里就得先说清楚 LibTorch 解决的问题它让 C 进程直接加载 PyTorch 生态的模型产物不需要转 ONNX、不需要引入 Python 解释器。你用 Python 侧torch.jit.trace或torch.jit.script导出.pt文件C 侧用torch::jit::load读进来后面就是纯 C 推理了。省掉 Python 运行时模型部署体积小一块启动快没有 GIL 锁的干扰同时还能和现有 C 基础设施无缝集成——比如直接接 gRPC 服务、接入已有的图像处理管线、配合消息队列做异步推理。但注意一个容易被忽略的点LibTorch 不是一个“傻瓜式部署框架”它更像一个底层张量库和推理引擎的合集。官方文档给的示例都是几十行的最小可用代码真正放进大型工程里你会立刻感受到它的“裸”。模型加载失败有没有兜底权重和输入特征在 CPU、GPU 之间怎么搬运多个线程同时访问同一个 Module 会不会崩半精度和全精度模型在设备上如何切换这些问题官方不给你答案只有在工程里被封一层才能系统解决。1.2 不封装的痛只有真正改过老代码的人才懂我之前有一段经历特别典型接手一个工业质检项目服务端需要先调用一个缺陷分类模型再决定是否触发一个检测模型两个模型各自有不同的预处理和后处理。最初的实现方式是这样的全局变量里放两个torch::jit::Module每个调用函数的开头都写一遍module.to(device)然后从cv::Mat转 Tensor 的逻辑复制粘贴三份模型路径直接写死在配置文件的不同字段里。结果的问题集中爆发一是模型加载逻辑不可复用。不同模型可能放在 CPU 上跑也可能要上 GPU有的模型输入是 RGB 三通道有的是灰度单通道。这些差异如果放任在业务代码里 if-else模块间耦合会越来越严重。二是资源生命周期不可控。LibTorch 里很多操作返回的是视图或者临时对象尤其是torch::from_blob构造出来的 Tensor 并不拥有底层数据。底层的cv::Mat一旦被释放Tensor 就成了悬垂指针。不封装的话这种 bug 带着极强的随机性release 版本里不定时爆炸调试成本极高。三是设备与环境切换要改多处代码。开发机上没 GPU一切 CPU 运行上线后服务器有 GPU你想切 CUDA。如果模型加载和输入构造散落在各处你会体验到什么叫“改一处崩三处”。四是异常处理基本靠猜。模型文件损坏、版本不匹配、输入维度不符合预期LibTorch 会抛c10::Error不统一捕获的话异常直接穿过业务逻辑把整个服务进程带崩。封装的核心目标就是把这些复杂度全部收拢到一个类或者一套接口里。外部业务方只需要做三件事传模型路径、传输入图像、拿到推理结果。至于模型到底跑在 CPU 还是 GPU、预处理做了什么、底层是不是 LibTorch、未来会不会换成 ONNX Runtime 或者 TensorRT对外部调用方透明。2. 动手封装之前先把接口边界和设计路线定下来2.1 接口抽象宁可多花一小时也别让上层代码碰到 torch封装的第一步不是写代码而是想清楚“哪些东西必须暴露给外部哪些必须藏在内部”。我的经验是对外暴露的接口应该和 torch 类型完全无关。如果你把torch::Tensor直接放进接口签名里那这个“封装”只封装了一半——调用方依然被绑定在 LibTorch 的类型体系上将来换推理引擎等于换接口照样伤筋动骨。所以我通常先定义一套领域层的输入输出抽象比如图像就用cv::Mat这类成熟通用的图像类型模型内部再和torch::Tensor做转换。业务方只认load、infer、release这几个动词不感知 torch 的存在。接口形式我有两种倾向。第一种是纯虚类 工厂函数适合一个进程里可能加载多种推理引擎的场景class IInferenceEngine { public: virtual ~IInferenceEngine() default; virtual bool load(const std::string modelPath) 0; virtual cv::Mat infer(const cv::Mat input) 0; virtual void release() 0; }; class TorchEngine : public IInferenceEngine { public: TorchEngine(); ~TorchEngine() override; bool load(const std::string modelPath) override; cv::Mat infer(const cv::Mat input) override; void release() override; private: struct Impl; std::unique_ptrImpl impl_; };这里用了 Pimpl 手法把torch::jit::Module、torch::Device等所有依赖 torch 的成员变量放到不透明结构体Impl里头文件彻底不暴露 torch 类型。这样做的额外好处是编译时外部代码不需要包含 LibTorch 的大头文件整个项目的编译时间能降一截。第二种是模板化策略适合模型结构固定、但参数配置不同的场景。比如一个超分模型可能支持不同倍率输入尺寸和缩放因子可以由外部传入这时候用函数式接口更灵活。但不管哪种形式原则一致接口是契约内部才是实现。我见过很多团队前期偷懒直接把module暴露出去后期想接 TensorRT 时接口设计返工成本比一开始多几倍。2.2 绕不开的设计决策同步异步、批量推理、设备策略接口形式定了之后还有几个策略性问题必须在动手前拍板否则后续改起来很痛苦。第一个是同步还是异步。我最常用的方案是先做同步接口在内部把耗时逻辑放到线程池执行、通过回调返回结果的异步版本可以在此基础上包装。同步接口实现简单容易验证正确性异步封装本质上是“包一层任务队列”没必要一开始就引入复杂度。第二个是单张还是批量。PyTorch 模型的输入大多带 Batch 维度即使你只推理一张图也经常要构造{1, C, H, W}的四维张量。对外接口传一张cv::Mat是最舒服的内部补全 Batch 维。但如果你的服务有明确的吞吐需求批量推理往往比多次单张推理快得多尤其 GPU 场景下。进阶做法是在接口上同时提供inferOne和inferBatch内部共享同一套预处理管线只是张量拼装方式不同。我前期踩过的坑是只做单张接口后期想批量时发现预处理和参数配置耦合太紧拆都不好拆。第三个是设备策略。这是最容易出错的地方。我在封装里会显式维护一个torch::Device device_成员加载模型时根据torch::cuda::is_available()和外部配置决定跑 CPU 还是 GPU所有预处理后的输入张量都统一to(device_)模型的输出再搬回 CPU 做后处理。这样全链路只有一个设备变量换卡只改一处。另外还有一个容易被忽略但影响体验的细节模型预热。torch::jit::load成功不代表第一次推理就顺畅。GPU 上首次推理要触发 kernel 编译、显存分配耗时会明显偏大服务刚启动时如果有健康检查机制很可能因为首次推理超时被判定不健康。解决办法很简单在load最后用随机数据跑一次空推理。封装里把预热做成可选项默认开启不影响正常接口。3. 手把手落地一个最小可用的 LibTorch 封装实现3.1 工程配置CMake 里找到 LibTorch绕开版本坑LibTorch 本身自带 CMake 配置文件官方推荐的引入方式是find_package(Torch REQUIRED)。我在 Windows 上习惯从官方下载 zip 直接解压在 Linux 上也可以用下面的方式配置。一个最小可用的CMakeLists.txt大概长这样cmake_minimum_required(VERSION 3.18) project(MyInference CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Torch REQUIRED) add_library(torched_engine src/torched_engine.cpp) target_include_directories(torched_engine PRIVATE src include) target_link_libraries(torched_engine ${TORCH_LIBRARIES})有几个细节必须提醒第一TORCH_LIBRARIES在不同平台上展开的内容差异很大。Windows 下可能是一长串/LIBPATH:...和.lib文件列表Linux 下是若干.so别自己手写库名。第二LibTorch 要求 C 标准至少是 14官方更推荐 17。如果你的工程因为历史原因还停留在 C11集成成本会上升很多有些地方可能得绕行。第三ABI 兼容性。PyTorch 从某个版本起默认用了_GLIBCXX_USE_CXX11_ABI1如果你的工程因为接了老库而统一用-D_GLIBCXX_USE_CXX11_ABI0编译链接时就会遇到一堆 undefined reference这是 Linux 上最常见的集成失败原因之一。最稳妥的做法是保持全工程 ABI 一致并且在拿到预编译的 LibTorch 后先用官方 example 跑通一遍再接入自己工程避免一上来就排查混合 ABI 的玄学问题。第四Windows 下的一个大坑是DLL 依赖。LibTorch 运行时要依赖torch_cpu.dll、c10.dll和一堆第三方符号链接库。Debug 和 Release 配置下文件可能不同最后部署时要把一堆 DLL 和模型文件一起带上。我反正吃过一次“本地能跑、服务器缺 DLL”的亏建议打包时直接用依赖扫描工具把 DLL 全部捞出来放进同目录别手动试错。3.2 Python 侧导出 TorchScript 模型这一步最容易埋雷很多人的 C 工程一跑就崩根因其实在 Python 侧导出模型时埋的雷。为此必须单独讲一下导出。最稳妥的导出方式是torch.jit.trace。它打包的是“实际计算路径”不关心控制流和 Python 对象只要你用真实输入跑一遍就行。以下是适用于静态输入模型的导出模板import torch model torch.load(weights.pth, map_locationcpu) model.eval() # 固定 Batch、输入尺寸、通道数、数据类型都要和真实部署一致 example_input torch.rand(1, 3, 224, 224) traced_model torch.jit.trace(model, example_input, strictTrue) traced_model.save(model_traced.pt)导出前必须做三件事model.eval()、权重复制到 CPUmap_locationcpu、确认所有维度确定。为什么放 CPU因为 C 端加载时可能跑在不同 GPU 上从 CPU 状态出发再.to(device)最干净。GPU 状态导出的模型虽然也能加载但设备绑定信息一旦进入图结构后面迁移时会有一堆意想不到的问题。还有几个雷区值得单独说。一是batch 维度。Trace 时如果example_input是[1, 3, 224, 224]导出的模型里 batch 维度基本就固化了。之后你想在 C 里改成[4, 3, 224, 224]可能直接报“维度不匹配”或者输出错误。如果模型对 batch 动态性有要求不得不用torch.jit.script重写模型里的部分逻辑把 Batch 维度写成变量。二是预处理要不要一起 trace。很多模型在 Python 里只接受归一化后的 Tensor归一化逻辑写在 PyTorch 的transforms里。我强烈建议把预处理逻辑一并放进 trace 的nn.Module里比如写一个子模块内部包含ToTensor、Normalize、Resize。这样 C 端接收原始图像数据即可预处理算子会作为图的一部分在 LibTorch 里执行跨语言复刻预处理时不至于“差之毫厘、谬以千里”。三是验证导出的模型。Trace 完成后跑一遍原始模型和 trace 模型的输出比对数值差异。我一般要求 max abs error 在 1e-5 量级。如果差异大优先检查是否忘了eval()、是否混入了在 trace 路径上不稳定的随机算子如 dropout。3.3 C 封装实现加载、推理、资源释放一锅端接下来是核心部分手写一个封装类的实现。这里给出的是我实际用过的一个精简版本去掉了业务耦合但保留了全部关键骨架。// torched_engine.h #pragma once #include memory #include string #include opencv2/core.hpp class TorchEngine { public: TorchEngine(); ~TorchEngine(); bool load(const std::string modelPath, bool useGpuIfAvailable true); cv::Mat infer(const cv::Mat input); private: struct Impl; std::unique_ptrImpl pImpl_; };// torched_engine.cpp #include torched_engine.h #include torch/script.h #include torch/cuda.h #include opencv2/imgproc.hpp struct TorchEngine::Impl { torch::jit::Module module; torch::Device device{torch::kCPU}; bool loaded false; void warmUp() { torch::NoGradGuard no_grad; auto dummy torch::zeros({1, 3, 224, 224}, torch::kFloat32); module.forward({dummy.to(device)}); } }; TorchEngine::TorchEngine() : pImpl_(std::make_uniqueImpl()) {} TorchEngine::~TorchEngine() default; bool TorchEngine::load(const std::string modelPath, bool useGpuIfAvailable) { if (pImpl_-loaded) return true; try { pImpl_-module torch::jit::load(modelPath); if (useGpuIfAvailable torch::cuda::is_available()) { pImpl_-device torch::Device(torch::kCUDA, 0); } pImpl_-module.to(pImpl_-device); pImpl_-module.eval(); pImpl_-warmUp(); pImpl_-loaded true; return true; } catch (const c10::Error e) { // 正式工程里这里应接入统一日志 fprintf(stderr, load model failed: %s\n, e.what()); return false; } } cv::Mat TorchEngine::infer(const cv::Mat input) { if (!pImpl_-loaded) { return cv::Mat(); } torch::NoGradGuard no_grad; // cv::Mat - Tensor 的转换函数接下来单独讲 auto inputTensor cvMatToTensor(input).to(pImpl_-device); auto output pImpl_-module.forward({inputTensor}).toTensor(); output output.cpu(); // 这里按你自己的后处理来以下假设是 224x224 三通道输出 cv::Mat result tensorToCvMat(output); return result; }这段代码基本涵盖了封装的骨架逻辑构造时创建Impl析构由unique_ptr自动释放load里做设备选择、模型迁移、预热infer里构造输入、调用forward、搬回 CPU、转成cv::Mat。注意这里默认inputTensor是四维张量因为预处理函数会补全 Batch 维。有几个细节我特别说明一下。module.forward({inputTensor})返回的是torch::jit::IValue要用.toTensor()取出张量。模型输出如果是 tuple 或者 dict比如同时返回分类和框还需要进一步从IValue里逐个解析。这里不太推荐用module.forward返回裸Tensor就完事应该统一封装成一个InferenceResult结构体把各类输出字段解析好再吐出去避免业务方和IValue打交道。TorchEngine的析构函数为什么要显式定义而不是直接 default因为Impl里有torch::jit::Module它是非标准布局类型如果头文件里unique_ptrImpl没有看到完整类型析构处.cpp也必须完整定义Impl否则会编译报错。显式在.cpp中 default是为了让 unique_ptr 的析构能在能看到Impl完整定义的位置实例化。NoGradGuard是必须加的。虽然推理阶段模型已经是eval模式但 LibTorch 的 autograd 引擎默认仍会记录梯度白白增加内存和计算开销。包一层NoGradGuard等于明确告诉它这条路径不需要反向传播。最后是torch/cuda.h与torch/script.h的包含顺序。这两个头文件在部分版本中会冲突标准做法是先包含 Python 需要的无关头文件再包含 torch 系列。我自己习惯把所有 torch 头写在文件末尾从一个整段里保持稳定顺序不然编译时偶尔报出非常奇怪的重复定义错误。3.4 输入输出转换cv::Mat 与 torch::Tensor 之间的正确姿势这次要重点展开cvMatToTensor。它是最容易被写错、又最容易在“大流量下才暴露”的地方。先给个正确的参考实现torch::Tensor cvMatToTensor(const cv::Mat img) { // 统一通道顺序OpenCV 是 BGRPyTorch 模型一般按 RGB 训练 cv::Mat rgb; cv::cvtColor(img, rgb, cv::COLOR_BGR2RGB); // 转 float 并缩放到 [0, 1] cv::Mat floatImg; rgb.convertTo(floatImg, CV_32FC3, 1.0 / 255.0); // from_blob 不复制数据只是引用 cv::Mat 底层内存 auto tensor torch::from_blob(floatImg.data, {1, floatImg.rows, floatImg.cols, 3}, torch::kFloat32); // HWC - CHW注意必须 contiguous tensor tensor.permute({0, 3, 1, 2}).contiguous(); // 返回一份真正拥有内存的副本 return tensor.clone(); }这里面每一步都有讲究。from_blob是最常用的cv::Mat构造 Tensor 方式但它创建的 Tensor 只是“借用”传入指针指向的内存不持有所有权。如果你直接返回这个 Tensor函数结束时floatImg析构Tensor 内部指针悬空下一次推理数据就是乱的、崩溃、随机数值错乱都可能来。所以最后必须clone()一次把数据完整拷贝到新分配的 Tensor 内存里才谈得上安全。permute之后必须接contiguous()。permute返回的是不连续内存视图虽然逻辑维度变了但底层数据还按 HWC 排着。大部分算子尤其是卷积要求输入是连续内存所以要用contiguous()把内存重新排成 CHW 布局。如果不加这一步模型算出来的结果直接就是错的而且输出数值很稳定地错极难排查。1.0 / 255.0的归一化是相对通用的做法但真实模型训练时可能用的是 ImageNet 的均值方差归一化那就得按训练配置来处理。模型内部如果在 Python 侧已经把预处理封进了 TorchScript这里其实只需要做类型转换和通道转换归一化交给模型图内部完成。这也是我上一节强烈建议“预处理进 graph”的原因——省得两边各归一化一半对不上账。如果是灰度图输入cv::cvtColor的写法要调整。灰度图只有单通道可以先用cv::cvtColor(img, rgb, cv::COLOR_GRAY2RGB)扩展到三通道再走统一逻辑。如果模型本身要求单通道输入就不转 RGBfrom_blob时把通道数改成 1。封装里最好根据模型配置决定分支不能写死。tensorToCvMat的反向转换同样要注意内存连续性。torch::Tensor如果来自module.forward无论理论上是否contiguous先output.cpu()再output output.contiguous()然后用cv::Mat(output.size(2), output.size(3), CV_32FC3, output.data_ptrfloat())构造。之后如果需要展示或保存再转回 8UC3。有一个坑在这里某些算子产出的是半精度 float16 张量data_ptrfloat()会编译报错或取到错误指针保险做法是先.to(torch::kFloat32)再取指针。4. 封装落地后的血泪排查这些坑我替你踩过了4.1 多线程推理为什么“越跑越慢”还偶发崩溃封装层做好了单线程推理没问题可一旦把服务改造成多线程并发推理麻烦立刻出现。LibTorch 的Module实例默认不是线程安全的多个线程同时调用同一个module.forward轻则结果错乱重则直接段错误。这个问题的根源在于模型内部可能共享缓冲、中间状态等可变对象而且线程同时进入 at:: 底层时还可能触发未定义行为。我的应对方案分两级。第一级如果推理量不大最简单的方式是给infer加锁。在封装内部维护一个std::mutex对forward整段加锁。代价是并发能力被锁死但正确性有保证。适合偶尔调用的场景。第二级如果确实要并发就别共享同一个Module而是用module.clone()为每个工作线程创建独立实例。克隆出来的 Module 权重共享但内部状态独立每个线程各持一份互不干扰。注意clone()出来的模型仍然要to(device_)和eval()所以封装里需要提供“按线程取独立推理实例”的方法。经验上线程数不要超过物理核心数太多否则上下文切换的开销会盖过并行收益。多线程下还要管好线程池。PyTorch 默认的 intra-op 线程数可能和你的服务线程池叠加导致几百个线程在机器上狂欢。解决办法是在main或引擎初始化时显式设置at::set_num_threads(4); at::set_num_interop_threads(4);这里的set_num_threads控制单个算子内部并行度set_num_interop_threads控制多算子之间的并行度。一般设置成物理核心数或者按实际压测结果调。这两种设置必须在任何算子执行之前完成且只生效一次封装初始化时设置最合适。4.2from_blob造成的数据悬垂崩溃来得比想象中更晚我在 3.4 里强调过clone()的必要性但很多人问我实测不 clone 也没崩是不是可以不写这类 bug 的可怕之处在于它不会立刻崩。floatImg.data指向的堆内存被释放后当下一次 malloc 还没有重新使用这块地址时Tensor 里的“脏指针”依然能读到看起来正常的数据一旦堆内存被新对象覆盖推理输出就变成随机数值或者在某个算子触发SIGSEGV。我见过最刁钻的场景模型在 Debug 构建下一直正常Release 构建下偶发崩溃。因为 Release 下编译器的内存分配策略和复用时机完全不同悬垂指针暴露得更快。排查这类问题有个好用的工具思路在cvMatToTensor返回后把floatImg重新赋一个全零矩阵再跑推理。如果输出有明显变化说明你的 Tensor 还在引用旧内存那就要检查有没有在clone()之前把数据带出了函数。另外还有一个和from_blob同源的坑如果你把cv::Mat的data指针直接传给from_blob并且在函数外把这个cv::Mat作为“共享数据”被别人并发修改那么 Tensor 读到的数据就会在推理过程中“半路变脸”。最稳的做法始终是把数据 copy 进一个由 Tensor 自己拥有的内存区域彻底切断言柄关系。4.3 设备不一致CPU/GPU 混用引发的经典报错与反常识行为设备不一致的典型报错是terminate called after throwing an instance of c10::Error what(): Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!这种错误通常发生在模型在 GPU 上、而某个输入 Tensor 还在 CPU 上时。修起来也很简单在forward之前统一.to(device_)。我见过反着来的模型在 CPU、输入在 GPU同样报设备不一致所以不要想当然。还有一个更隐蔽的设备问题模型加载时用module.to(device_)一次性搬过去之后就不该再频繁调用module.to()。每调用一次to模型内部所有参数可能有额外的数据移动开销尤其是大模型在 CPU 与 GPU 之间来回切换时代价极大。封装设计上应该保证设备切换只在load阶段做一次推理阶段绝不暴露切换接口。另外如果你的部署机器有 GPU 但显存不足LibTorch 会直接抛出c10::OutOfMemoryError。封装里最好对这种错误单独捕获返回特定错误码让上层逻辑决定是缩批、降精度还是回退 CPU。这比让异常直接冒出去优雅得多。4.4 性能优化三板斧预热、半精度与批量推理性能是部署绕不开的话题。我优化时按收益排序通常做三件事。第一件是预热模型。前面提过首次推理要触发 kernel 编译和显存分配耗时可能是后续推理的好几倍。封装里load完成后用一张随机图跑一遍空推理把该编译的 kernel 全部触发掉。注意预热输入要和真实输入的尺寸、类型完全一致不然白热。维度如果服务里会动态改变可以挑几个常见尺寸全部预热一遍虽然慢但比每次线上首包超时值。第二件是半精度推理。GPU 上torch::kHalf推理通常带来接近两倍的吞吐提升显存占用也减半。做法是加载模型后执行module.to(torch::kHalf);但前提是模型里的算子全部支持 float16。不是所有算子都原生支持保险做法是模型导出前在 Python 侧用torch.autocast验证一遍前向是否正常。CPU 场景下 float16 反而可能更慢真心劝你别在 CPU 上强行半精度。我们曾经在 CPU 上做过测试same model 切成 half 后耗时反而上升 30% 左右因为很多 CPU 内核没有优化 half 计算路径。第三件是批量推理。当请求吞吐是硬指标时把多个请求凑成一个 batch 再送进模型速度提升非常明显因为 GPU 并行计算分摊了 kernel 启动和显存搬移的固定开销。封装里可以在infer之外再提供一个inferBatch接受一组cv::Mat内部预处理后拼接成{N, C, H, W}张量推理完成后把输出拆开返回。批量尺寸不是越大越好显存和延迟都要平衡压测时用 N1/2/4/8 各跑一轮观察 P99 延迟和显存曲线才能找到合适的值。5. 最后分享一点心得封装 LibTorch 这件事表面上是写一个类、包一层接口但实际它是一道“边界控制”的工程题让 C 业务工程和 PyTorch 生态之间保持清晰的距离。我个人的经验是前期多花半小时设计接口、明确设备策略、把预处理和资源生命周期管好后期能省下几个排查疑难 bug 的通宵。如果你现在正准备把 Python 训练好的模型往 C 里搬别急着写第一行推理代码先照上面的思路把模型导出、CMake 工程、封装类这三个骨架立起来再往里面填你自己的业务逻辑。另外一个小建议封装文档里一定要写明 LibTorch 版本、CUDA 版本、C 编译标准这三个关键环境信息不然你同事在 CI 机器上复现时大概率会在 ABI 和 DLL 的问题上卡住个把小时。