你走进一家网红甜品店,点了一份招牌千层蛋糕。后厨并没有当场从打鸡蛋开始做——而是从冷库取出一块提前研发好的标准模具,几位甜点师各就各位,一炉同时压出好几份,几分钟后甜品就端到了你面前。这家甜品店背后是一家「中央厨房」:菜谱被反复试验、浓缩成模具,统一冻存,门店随取随用。TensorRT 部署做的事情,和这座中央厨房一模一样。开发者先用显卡「研发」出针对特定模型的高效引擎,把它序列化保存成文件,部署到生产服务器上随时加载,再用多条「传送带」并行出餐,按订单量灵活调节「一炉几份」。本文将带你从零看懂这套 AI 推理服务的完整部署流程。

岗位关联

掌握 TensorRT 部署,等于掌握了让 AI 模型在工业级服务中真正跑起来的「最后一公里」——这是推理平台、部署与性能优化工程师的必备核心技能。

目录
  1. 可视化演示
  2. 引擎构建
  3. 引擎序列化
  4. 推理API
  5. 多流并发
  6. 动态Batch
  7. 部署落地
  8. 生产实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示引擎构建优化流水线、CUDA多流并发执行、动态Batch管理三大部署核心环节。

本节配有交互动效(进入视口后自动加载,约 3KB)。

TensorRT引擎构建流水线 ONNX模型经层融合、精度校准、内核自动调优生成序列化引擎,部署时直接加载跳过构建 FP16/INT8量化减少显存占用和计算量,内核选择器为每层挑选最优CUDA内核 ONNX -> optimize(fuse,quantize,tune) -> .engine 引擎构建是一次性优化过程

引擎构建

中央厨房的甜点研发团队拿到一份菜谱:面粉多少克、奶油怎么打、烘烤多少分钟。他们不会直接用菜谱开店,而是反复试验,把步骤浓缩成一套标准模具——以后只要注入原料,一压就是一个完美的千层胚。TensorRT 的引擎构建就是这套「研发」过程。它由 Builder 负责,把训练好的模型当作菜谱,结合显卡的具体型号,打磨出最高效的执行方案。

Builder 读取模型后,先建立网络定义(INetworkDefinition),这是对计算图的完整描述:每一层是什么算子、张量如何流动。接着设置优化配置,比如批大小范围、精度策略(FP16 或 INT8)、显存上限。最后真正「构建」时,TensorRT 会对计算图做算子融合、内核自动调优,生成一个针对当前 GPU 架构高度定制的引擎对象。这个过程可能耗时数分钟甚至更久,但换来的是推理时的大幅加速。

正如中央厨房的模具要反复打磨才投入使用,构建引擎的几分钟等待,换来的是线上每次推理都更快——这笔投资非常划算。

Tfused=T1+T2−CioT_{\text{fused}} = T_1 + T_2 - C_{\text{io}}
算子融合把相邻两个算子的执行时间合并,节省了中间张量在显存中写回再读出的开销 C_io。
  1. 算子一耗时T_1 表示第一个算子单独执行的耗时,比如一次卷积计算的时间。
    T1T_1
  2. 算子二耗时T_2 表示第二个算子单独执行的时间,比如一次 ReLU 激活。
    T2T_2
  3. 中间读写开销C_io 是两个算子之间传递中间结果的显存读写代价,融合后这段搬运被完全省去。
    CioC_{\text{io}}

引擎序列化

模具研发好了,不能每次开店都重新做一遍,否则成本太高。中央厨房的做法是把模具冻存进冷库,哪家门店需要就运过去。TensorRT 的引擎序列化正是如此:构建好的引擎对象还活在内存里,通过序列化把它变成一段字节流,再写入磁盘成为一个 .engine 文件。部署时直接加载文件,省去重复构建的等待。

序列化后的文件包含什么?网络的优化执行计划、权重数据、算子内核的选择信息,全都打包在里面。部署端拿到这个文件后,用 Runtime 对象反序列化——相当于把冻存的模具解冻——就能还原出可直接推理的引擎。需要特别注意的是,这个文件不是通用的「面团」,它绑定特定的 GPU 架构和 TensorRT/CUDA 版本。换一台不同型号的显卡,就像把 8 寸模具塞进 6 寸烤炉,根本放不进去。

序列化是部署流程的「冻存」环节,让研发成果可以复制到千家门店。不过请记住:模具虽可跨店运输,却不能跨烤炉使用。

tload=SfileBiot_{\text{load}} = \frac{S_{\text{file}}}{B_{\text{io}}}
引擎文件的加载耗时约等于文件大小除以 IO 带宽,文件越大、磁盘越慢,服务启动就越慢,因此引擎文件大小也是部署时要关注的指标。
  1. 文件大小S_file 是 .engine 文件占用的存储字节数,通常与模型参数量和优化策略相关。
    SfileS_{\text{file}}
  2. IO 带宽B_io 是磁盘到内存的读取速度,SSD 明显快于机械硬盘,这也直接影响加载耗时。
    BioB_{\text{io}}
  3. 加载耗时t_load 等于两者相除,是服务冷启动时等待引擎就绪的时间。
    tloadt_{\text{load}}

推理API

甜品店出餐时,甜点师把模具放在工作台上,注入原料、压实、脱模、装盒。TensorRT 推理的「工作台」是 IExecutionContext——每个执行上下文都持有一份独立的中间激活内存,负责一次具体的推理执行。开发者通过 Python 或 C++ 接口调用它:把输入张量绑定到指定位置,执行推理,再取出输出张量。

Python 接口适合快速原型和胶水代码,C++ 接口则更贴近底层控制。两者底层执行的是同一个引擎,计算核心完全一致,真正的性能差距往往出在数据搬运上:输入数据要拷到 GPU 显存,输出结果要拷回内存。Tensor 绑定就是管理这些缓冲区的机制,涉及 host 端页锁定内存与 device 显存的一一对应。做好绑定和内存复用,是降低延迟的关键。

工作台备好了,原料从左侧进来,成品从右侧出去。推理 API 的价值正是把这条「进料—压制—出餐」的通路管理得井井有条。

Te2e=Tpre+Tengine+TpostT_{\text{e2e}} = T_{\text{pre}} + T_{\text{engine}} + T_{\text{post}}
端到端推理时延等于预处理、引擎推理、后处理三段耗时之和。想优化整体延迟,不能只盯引擎,前后处理常常才是隐藏的瓶颈。
  1. 预处理耗时T_pre 包括图像解码、缩放、归一化、转 NC4HW4 布局等,通常在 CPU 或 GPU 上的独立步骤完成。
    TpreT_{\text{pre}}
  2. 引擎推理耗时T_engine 是 IExecutionContext 执行引擎本身的耗时,也就是模具压出甜品的那一下。
    TengineT_{\text{engine}}
  3. 后处理耗时T_post 包括输出张量拷贝回内存、softmax 解码、目标框过滤等收尾工作。
    TpostT_{\text{post}}

多流并发

下午三点是甜品店的高峰期,一个甜点师明显忙不过来。店长安排多位师傅同时开工,每人一个工作台(Context),做好的甜品通过传送带(CUDA 流)源源不断送到前台。CUDA 流是 GPU 上任务排队执行的逻辑通道,不同的流可以并行执行互不依赖的 kernel。多流并发就是让一个引擎同时服务多个推理请求的关键手段。

实现上,每个线程或请求通常绑定自己的 CUDA 流,再创建多个 IExecutionContext 分别执行。要注意的是,流只是「传送带」本身,真正占用 GPU 计算资源的是流上排队的任务。流的数量并非越多越好,超过 GPU 的并行能力后,反而会因频繁切换和同步等待降低效率。合理的并发设计,需要结合 GPU 型号、显存容量和请求到达率综合评估。

传送带铺得再密,后厨的烤箱总量有限。多流并发的艺术,是在资源边界内找到让传送带永不停歇的那个平衡点。

Q=Ns×BtcycleQ = \frac{N_s \times B}{t_{\text{cycle}}}
推理吞吐量约等于流的数量乘以每个流的批大小,再除以一个批次周期耗时。增大流数或批大小都能提升吞吐,但受硬件资源上限约束。
  1. 流数量N_s 是并发执行的 CUDA 流个数,对应同时开工的甜点师人数。
    NsN_s
  2. 批大小B 是每条流上每个批次处理的样本数,相当于位甜点师一次压出的份数。
    BB
  3. 批次周期t_cycle 是从提交一批任务到该批完成的平均耗时,包含计算与同步等待。
    tcyclet_{\text{cycle}}

动态Batch

甜品店经常遇到尴尬:一炉烤 8 个,但闲时只有 2 个订单;一炉烤 2 个,忙时又供不应求。聪明的店长把烤炉设计成「可变容量」——订单多就多放模具,订单少就少放,这就是动态 Batch。TensorRT 的动态形状功能允许输入张量的 batch 维在一个范围内灵活变化,并通过优化 Profile 为 min、opt、max 三个档位分别规划最优内核。

动态 Batch 的价值在于提高 GPU 利用率:服务端可以收集一小段时间内的请求,聚合成一个 batch 再提交推理,摊薄 kernel 启动和调度开销。但 batch 不是越大越好,每增加一个样本,显存占用就增加一份,单请求的排队等待也会变长。显存总量决定了一炉最多能放多少模具,这就是为什么设置 max 档时要给足余量,opt 档则要贴近实际流量。

模具越多,占的烤炉空间越大。动态 Batch 的精髓在于「按需调节」:忙时一炉多出,闲时一炉少烤,始终让烤箱(GPU)保持高效运转。

M=B×m1+CM = B \times m_1 + C
推理时总显存占用随批大小近似线性增长:每样本显存 m_1 乘以批大小 B,再加上引擎权重等固定开销 C。
  1. 批大小B 是实际送入引擎的样本数量,动态 Batch 允许它在 min 与 max 之间浮动。
    BB
  2. 单样本显存m_1 是处理一个样本所需的激活显存,与网络深度和特征图大小相关。
    m1m_1
  3. 固定开销C 是引擎权重、工作缓冲区等与批大小无关的常驻显存。
    CC

部署落地

门店光会做甜品还不够,要正常营业还得有接单柜台、打包台和配送通道。TensorRT 部署落地同样是一套端到端工程:请求进来先预处理(解码、缩放、归一化),再交给引擎推理,最后后处理(分类、检测框过滤)返回给上游服务。任何一个环节掉链子,顾客体验都会打折扣。

性能监测是这节链条上的仪表盘:延迟的 p50/p99、吞吐 QPS、GPU 利用率、显存水位,都要持续采集。服务集成层面,TensorRT 推理模块常以 gRPC 或 HTTP 服务的形式嵌入业务系统,或者由 Triton 这类推理服务平台统一管理。部署时还要考虑 batch 聚合策略:是把请求攒够一批再提交,还是来一个算一个?这直接决定了延迟与吞吐的取舍。

接单、制作、打包、送出,每个环节都顺畅,甜品店才能口碑长红。部署落地的功夫不在某一块炫技,而在整条流水线的默契配合。

QPS=CconcTe2eQPS = \frac{C_{\text{conc}}}{T_{\text{e2e}}}
单机 QPS 约等于并发度除以端到端耗时。并发度越高、单请求越快,系统每秒能处理的请求就越多。
  1. 并发度C_conc 是系统同时处理的请求数,由多流、多进程或负载均衡共同决定。
    CconcC_{\text{conc}}
  2. 端到端时延T_e2e 是单个请求从进入系统到返回结果的完整耗时,来自第三节的三段式公式。
    Te2eT_{\text{e2e}}
  3. 系统吞吐QPS 是每秒查询数,衡量服务整体处理能力的核心指标。
    QPSQPS

生产实践

连锁店要想长期经营,不能靠某一天的超常发挥,而要靠稳定的制度:模具定期保养(引擎生命周期管理)、冷库空间合理分配(显存与资源管理)、设备故障有报修预案(错误处理与容灾)、每家门店执行同一套操作手册(最佳实践)。TensorRT 服务的生产实践正是这些「软实力」的集合。

引擎生命周期管理回答三个问题:什么时候构建、什么时候加载、什么时候释放。通常引擎在服务启动时加载一次、常驻内存,避免频繁构建和销毁的开销。资源管理上,GPU 显存是稀缺资源,要用显存池复用缓冲区,防止碎片化。错误处理包括 GPU 掉卡、显存不足、版本不匹配等异常路径,必须有重试与降级方案。最后,把配置、监控、日志沉淀成可复用的部署模板,让团队里任何人都能一键拉起稳定服务。

模具保养得好,冷库管理有序,师傅们按 SOP 操作,甜品店才能日复一日稳定出餐。生产实践没有炫酷的算法,却是 AI 服务从「能跑」走向「可靠」的必经之路。

A=TupTup+Tdown×100%A = \frac{T_{\text{up}}}{T_{\text{up}} + T_{\text{down}}} \times 100\%
服务可用率等于正常运行时间占总时间的百分比。生产实践做得好坏事,最终都会反映在可用率这个数字上。
  1. 正常运行时间T_up 是服务在统计周期内正常响应请求的累计时长。
    TupT_{\text{up}}
  2. 故障时间T_down 是服务不可用的累计时长,包括崩溃、重启和宕机。
    TdownT_{\text{down}}
  3. 可用率A 是两者之比,三个九、四个九的 SLA 承诺都由它计算得出。
    AA

AI 应用场景

广告推荐实时推理

阶段:推理部署与在线服务

解决的问题:在线广告竞价(RTB)要求请求在几十毫秒内返回推荐结果。传统方案用 PyTorch 直接推理,算子调度开销大、GPU 利用率低,高峰期根本扛不住每秒上万次请求。

具体案例:某广告平台将排序模型构建为 FP16 TensorRT 引擎,序列化后部署在 GPU 推理集群上。服务端用多 CUDA 流并发处理请求,并按流量峰值设置动态 Batch 的 opt 档。上线后 p99 延迟从 45ms 降到 18ms,单卡 QPS 提升约 2.8 倍,GPU 利用率稳定在 70% 以上。

视频内容审核

阶段:多路视频流并行推理

解决的问题:直播平台每天产生海量视频流,审核模型需要对每帧做人脸、违规内容检测。单路逐帧推理延迟高、成本大,需要并发处理多路视频。

具体案例:视频审核服务为每路视频流分配一个 IExecutionContext 和独立 CUDA 流,多路帧数据先做预处理(缩放、归一化、转 NC4HW4),再按动态 Batch 聚合送入引擎。系统同时处理 32 路 1080p 视频流,帧率从 15 FPS 提升到 60 FPS,漏检率下降了一个数量级。

自动驾驶车端部署

阶段:车端多传感器实时推理

解决的问题:自动驾驶需要同时处理相机、激光雷达等多传感器输入,车端 GPU 功耗和显存受限,对延迟和稳定性要求极高。

具体案例:某自动驾驶团队将感知模型构建为 INT8 引擎,使用动态 Batch 匹配不同时刻到达的传感器帧数量(相机 6 路、雷达 1 路),再用三条 CUDA 流分别执行检测、分割和融合任务。引擎加载后常驻显存,配合显存池复用,整机推理延迟稳定在 20ms 以内,满足行车安全要求。

Python 演示

python45 行
import numpy as npimport tensorrt as trt class TrtDeployer:    # TensorRT 引擎加载与推理器: 简化版部署示例    def __init__(self, engine_path, batch_size=1):        self.logger = trt.Logger(trt.Logger.WARNING)        self.runtime = trt.Runtime(self.logger)        with open(engine_path, 'rb') as f:            engine_bytes = f.read()        self.engine = self.runtime.deserialize_cuda_engine(engine_bytes)        self.context = self.engine.create_execution_context()        self.batch_size = batch_size        self.host_buffers = []        self.device_buffers = []        self.allocate_buffers()     def allocate_buffers(self):        # 为每个输入输出张量分配 host 页锁定内存和 device 显存        import pycuda.driver as cuda        import pycuda.autoinit        for i in range(self.engine.num_bindings):            shape = self.engine.get_binding_shape(i)            dtype = trt.nptype(self.engine.get_binding_dtype(i))            size = trt.volume(shape) * self.batch_size            host = cuda.pagelocked_empty(size, dtype=dtype)            device = cuda.mem_alloc(host.nbytes)            self.host_buffers.append(host)            self.device_buffers.append(device)     def infer(self, input_array):        # 执行一次推理: 拷贝输入到显存, 执行引擎, 拷贝输出回内存        import pycuda.driver as cuda        np.copyto(self.host_buffers[0], input_array.ravel())        cuda.memcpy_htod(self.device_buffers[0], self.host_buffers[0])        self.context.execute(batch_size=self.batch_size,                             bindings=[int(buf) for buf in self.device_buffers])        cuda.memcpy_dtoh(self.host_buffers[1], self.device_buffers[1])        return self.host_buffers[1].reshape(input_array.shape[0], -1) if __name__ == '__main__':    deployer = TrtDeployer('model.engine', batch_size=4)    dummy = np.random.randn(4, 3, 224, 224).astype(np.float32)    result = deployer.infer(dummy)    print('推理完成, 输出形状:', result.shape)
目的:演示 TensorRT 部署的核心流程——反序列化加载 .engine 文件、分配输入输出缓冲区、执行推理并取回结果。
  1. self.runtime.deserialize_cuda_engine(engine_bytes)反序列化引擎,相当于从冷库取出并解冻模具。
  2. self.engine.create_execution_context()创建执行上下文,即给甜点师分配一个工作台。
  3. cuda.pagelocked_empty(size, dtype=dtype)分配页锁定内存,让 GPU 与 CPU 的数据搬运走快速通道。
  4. cuda.mem_alloc(host.nbytes)在显存上为张量分配空间,对应冷藏柜里预留的存放格。
  5. cuda.memcpy_htod(...)把输入数据从内存拷到显存,这是顾客订单从前台传到后厨。
  6. self.context.execute(batch_size=..., bindings=...)真正执行引擎推理,甜点师压下模具的一刻。
  7. cuda.memcpy_dtoh(...)把输出结果拷回内存,甜品从后厨端到顾客面前。

常见误区

误区把 .engine 文件像 .pt 权重一样拷到另一台服务器就能直接跑,训练好的模型文件不都这样吗?

正解TensorRT 引擎是针对特定 GPU 架构、CUDA 版本和 TensorRT 版本编译的二进制计划。就像按 8 寸烤炉定制的模具放到 6 寸烤炉里根本放不进去。换机器必须用相同环境重新构建引擎,或用 ONNX 在目标机上重新生成。

为何常见:PyTorch 的权重文件可移植性很强,让初学者默认所有模型相关文件都环境无关,忽略了「编译产物」与「权重参数」的本质区别。

误区把动态 Batch 的 min/opt/max 全设成最大值,反正 batch 大吞吐高,一劳永逸。

正解batch 越大,显存占用线性上升(M = B × m1 + C),请求凑批的等待时间也变长,单请求延迟会明显恶化。opt 档应该贴近真实流量分布,max 档只作为上限兜底,而不是日常运行值。

为何常见:人们容易只盯着吞吐量数字,忽略了它背后显存成本和延迟代价,把「更大」误当成「更好」。

误区把 CUDA 流的数量直接开满,比如 32 条流,吞吐就应该翻 32 倍。

正解流只是任务排队执行的逻辑通道,实际算力由 GPU 的 SM 数量和显存带宽决定。流过多时,kernel 切换和同步开销会让 GPU 疲于奔命,吞吐不升反降。流的数量应接近 GPU 可并行执行的 kernel 数,且要和 context 数量、显存容量配套规划。

为何常见:把「并行」直观等同于「线性加速」,忽略了硬件资源总量和调度开销这两个物理约束。

误区生产环境必须用 C++,TensorRT 的 Python API 只适合做原型验证。

正解Python API 和 C++ API 驱动的是同一份引擎,底层 kernel 执行时间完全一样。差距往往来自 Python 侧的数据搬运和 GIL 竞争。只要把预处理/后处理尽量下沉到 GPU、减少 host-device 往返拷贝,Python 完全能支撑生产级吞吐。

为何常见:听过太多「Python 慢」的笼统结论,把语言层面的调用开销与引擎本身的执行效率混为一谈。

面试题

Q1. TensorRT 中哪一步把优化后的引擎转成字节流以保存到磁盘?

单选题 · easy

  • A.序列化← 正确答案
  • B.编译计算
  • C.算子融合
  • D.动态分配

答案:A。序列化把内存中的引擎对象转化为字节流并写入 .engine 文件,相当于把模具冻存进冷库。编译计算和算子融合都发生在构建阶段,动态分配则是运行期行为。

Q2. 部署时加载 .engine 文件需要的核心对象是?

单选题 · easy

  • A.网络定义器
  • B.运行时对象← 正确答案
  • C.构建配置器
  • D.数据加载器

答案:B。Runtime(运行时对象)负责反序列化引擎文件,还原可推理的引擎,相当于冷库管理员取出模具。网络定义器和构建配置器只在构建期使用,数据加载器与引擎加载无关。

Q3. 关于 IExecutionContext 的说法正确的是?

单选题 · mid

  • A.多个上下文可共享引擎推理← 正确答案
  • B.单上下文同时执行两流
  • C.上下文在构建阶段创建
  • D.每上下文单独加载引擎

答案:A。一个引擎对象可以创建多个 IExecutionContext,每个上下文持有独立的激活内存,可并行执行推理——就像多个甜点师共用一套模具、各占一个工作台。上下文在运行期创建,而非构建期;一个上下文对应一个流更规范。

Q4. 要为引擎启用动态 Batch,需要做哪些工作?

多选题 · mid

  • A.设置优化档案与形状范围← 正确答案
  • B.调用 set_shape 指定范围← 正确答案
  • C.将批维度固定为最大值
  • D.关闭所有算子融合

答案:AB。动态 Batch 需要配置优化 Profile,并用 set_shape 为输入指定 min/opt/max 三个档位的形状范围,引擎才能按需调整批大小。固定为最大值就失去了「动态」的意义;关闭算子融合则降低性能,与优化目标背道而驰。

Q5. 多流并发中,「流」的角色最像甜品店里的什么?

单选题 · easy

  • A.师傅的工作台
  • B.出餐的传送带← 正确答案
  • C.顾客的订单票
  • D.食材的冷藏柜

答案:B。CUDA 流是 GPU 上任务排队执行的逻辑通道,甜品做好后经由传送带送到窗口,传送带就是流。工作台对应 IExecutionContext,订单票是输入数据,冷藏柜是显存。

Q6. 影响 .engine 文件跨环境可移植性的因素有哪些?

多选题 · hard

  • A.GPU 计算架构型号← 正确答案
  • B.TensorRT 库版本号← 正确答案
  • C.训练时数据增强方式
  • D.CUDA 运行时版本← 正确答案

答案:ABD。引擎是针对特定 GPU 架构编译的 kernel 计划,并依赖对应版本的 TensorRT 与 CUDA 运行时。就像模具按特定烤炉设计,换了品牌或炉膛尺寸就放不进去。数据增强只影响训练过程,与编译产物无关。

Q7. 端到端推理耗时 T_e2e 包含哪些部分?

单选题 · mid

  • A.预处理与推理及后处理← 正确答案
  • B.只有引擎内部计算
  • C.仅含模型加载时间
  • D.只算显存数据拷贝

答案:A。端到端时延是预处理、引擎推理、后处理三段之和,如同顾客等甜品的时间包含点单、制作、打包三个环节。只看引擎计算会忽略前后处理这两个常见瓶颈。

Q8. 算子融合优化主要减少了哪种开销?

单选题 · mid

  • A.中间张量的读写开销← 正确答案
  • B.卷积核的权重数量
  • C.模型文件的体积
  • D.CPU 调度线程数量

答案:A。算子融合把相邻算子合并执行,省去中间张量在显存中写回再读出的搬运开销,就像把几道工序合并到同一工作台,免去来回搬运原料。权重数量、文件体积、线程调度都不是融合优化的直接目标。

Q9. 想提升在线推理服务吞吐量,合理手段有哪些?

多选题 · hard

  • A.开多条 CUDA 流并发← 正确答案
  • B.加大动态 Batch 大小← 正确答案
  • C.启用 FP16 半精度← 正确答案
  • D.增加每步日志输出

答案:ABC。多流并发提高并行度,动态 Batch 聚合请求摊薄调度开销,FP16 减半显存和带宽需求,三者都能有效提升吞吐。增加日志只是徒增 IO 开销,与吞吐优化无关。

Q10. 批大小从 1 增到 4,显存占用理想情况是?

单选题 · extreme

  • A.约增至原批次的四倍← 正确答案
  • B.与批次大小无关
  • C.反而低于单批用量
  • D.恒为单样本大小

答案:A。显存占用满足 M = B × m1 + C,样本相关部分占主导,因此从 1 增到 4 时显存接近线性增至四倍,同时还有固定开销 C 存在,所以是「约」四倍而不是精确四倍。选其他选项都忽略了激活显存随 batch 增长的基本规律。