TensorRT 可编辑 Timing Cache 实战:通过修改时序缓存实现确定性引擎构建(sampleEditableTimingCache 深度解析)
2026/9/15 15:51:51 网站建设 项目流程

TensorRT 可编辑 Timing Cache 实战:通过修改时序缓存实现确定性引擎构建(sampleEditableTimingCache 深度解析)

【免费下载链接】TensorRTNVIDIA® TensorRT™ is an SDK for high-performance deep learning inference on NVIDIA GPUs. This repository contains the open source components of TensorRT.项目地址: https://gitcode.com/GitHub_Trending/tens/TensorRT

本指南以 NVIDIA TensorRT 开源仓库中的sampleEditableTimingCache样例(samples/sampleEditableTimingCache/README.md)为核心,系统讲解如何使用可编辑时序缓存(editable timing cache)为指定算子强制指定期望的 kernel 策略(tactic)。读完本文,你将掌握 Timing Cache 编辑 API(ITimingCache::query/update)、kEDITABLE_TIMING_CACHE构建标志的用法,以及如何通过解析 profiling 日志来实现"不重新耗时 profiling、直接复用并改写缓存"的确定性引擎构建流程。

背景:Tactics、Timing Cache 与可编辑能力的由来

在 TensorRT 中,同一个网络层往往存在多种底层实现,这些候选实现被称为tactics(策略)。构建引擎时,TensorRT 会逐一 profile 所有可用 tactics,选出耗时最短的一个写入Timing Cache(时序缓存),并在最终引擎中使用该 tactic。

默认情况下,用户只能"接受"TensorRT 自动选择的结果。但在某些场景下,期望使用的 tactic 并非实测最快的那个(例如出于数值精度、特定硬件行为或业务合规的考虑),此时就需要人为干预:把最佳 tactic 替换为另一个候选 tactic。这个需求可以通过编辑时序缓存来满足——这正是本样例演示的核心能力:借助Timing Cache 编辑 API(ITimingCache::query()/ITimingCache::update()profiling 日志,为指定算子写入(或覆盖)期望的 tactic 哈希,从而让下一次构建直接采用用户指定的实现。

从源码结构看,该能力由BuilderFlag::kEDITABLE_TIMING_CACHE构建标志驱动(见 include/NvInfer.h),在可编辑模式下:

  • 构建日志会输出所有层的 profiling 结果(每个算子可用的 tactics 及最终选中的 tactic);
  • 每一层拥有独立的 tactic 记录,修改某一层不会影响其他层。

样例总体设计:MatMul → Softmax → MatMul

本样例构造了一个仅有 3 个节点的极简网络:MatMul -> Softmax -> MatMul。关键在于:两个 MatMul 除了名字不同,其余属性完全一致。这样一个"对称"的网络让演示效果非常直观——两个算子共享同一组候选 tactics,因此我们可以对第一个 MatMul 换用不同 tactic,并清楚地观察到第二个 MatMul 不受影响。

整个样例的运行流程分为三步(与 samples/sampleEditableTimingCache/README.md 的 Description 完全对应):

  1. 首次构建:构造网络并构建第一个引擎。此时BuilderConfig已启用可编辑 timing cache,TensorRT 会在日志中输出 profiling 信息,同时把各算子的 tactic 决策记录进 Timing Cache;
  2. 改写缓存:从 profiling 结果中为第一个 MatMul 挑选一个与当前选中项不同的 tactic,通过缓存编辑 API 写入 Timing Cache;
  3. 再次构建:复用被修改过的缓存重新构建第二个引擎。由于缓存已命中,TensorRT不再执行 profiling,而是直接采用缓存中记录的 tactics。最终效果是:除第一个 MatMul 外,其余所有层的 tactic 与第一次构建保持一致。

网络的具体构造位于 samples/sampleEditableTimingCache/sampleEditableTimingCache.cpp:输入input(128×128 FP32)与权重weight1addMatrixMultiply得到matMul1,接addSoftMax,再与weight2相乘得到matMul2并标记为网络输出。三个层分别被命名为matMul1softmaxmatMul2,后续日志解析与缓存改写都依赖这些名字。

关键 API 与配置项解析

1.kEDITABLE_TIMING_CACHE构建标志

config->setFlag(BuilderFlag::kEDITABLE_TIMING_CACHE);

该标志(枚举值 26,见 include/NvInfer.h)的作用是启用可编辑时序缓存。在可编辑模式下,构建日志会包含所有层的 profiling 结果,且每层拥有独立的 tactic 记录。

配套使用的还有ProfilingVerbosity::kDETAILED(见 include/NvInfer.h),它要求 TensorRT 在引擎中保存每层实际使用的 tactic 名称(kernel name),这是后续通过IEngineInspector验证"新引擎确实采用了新 tactic"的前提:

config->setProfilingVerbosity(ProfilingVerbosity::kDETAILED);

2. Timing Cache 的创建与绑定

std::unique_ptr<ITimingCache> timingCache{config->createTimingCache(nullptr, 0)}; config->setTimingCache(*timingCache, true);
  • IBuilderConfig::createTimingCache(blob, size)(见 include/NvInfer.h)可从序列化数据创建缓存实例,传入nullptr, 0表示创建空缓存。创建的缓存不隶属于某个具体IBuilderConfig,可被多个 builder 共享;
  • setTimingCache(cache, ignoreMismatch)(见 include/NvInfer.h)将缓存绑定到当前配置,ignoreMismatch=true表示容忍缓存校验头不匹配(例如跨设备复用缓存时);
  • 缓存的生命周期必须长于所有使用它的 builder

3.TimingCacheKeyTimingCacheValue:缓存条目的数据模型

struct TimingCacheKey { uint8_t data[16]; }; struct TimingCacheValue { uint64_t tacticHash; // 选中的 tactic 哈希 float timingMSec; // 该 tactic 的耗时(毫秒),负数与 NaN 为非法值 static constexpr uint64_t kINVALID_TACTIC_HASH = UINT64_MAX; };

定义见 include/NvInfer.h。其中TimingCacheKey有两种表示形式:二进制(16 字节uint8_t数组)与字符串(日志中打印的形如0x1814870c44ff0f8574df6e3dda04cbd7的十六进制串)。二者的转换规则在头文件中明确给出:

  1. 将二进制 key 中每个字节转换为两个十六进制 ascii 字符(如0xab"ab");
  2. 依序拼接所有字节的 ascii 字符,得到恰好 32 个字符;
  3. 在字符串前添加前缀"0x"

日志中出现的key即算子的唯一标识,也是 Timing Cache 的查询键;tactic hash(如0x665ded9abbf88)则唯一标识某个 kernel 实现。样例源码中 parseKey() 与 parseTactic() 正是按上述规则把日志中的字符串解析回二进制TimingCacheKeysize_t形式的 tactic 哈希。

4.ITimingCache编辑 API

ITimingCache(见 include/NvInfer.h)提供以下核心方法:

方法作用
query(key)查询指定 key 的缓存值;key 不存在时返回非法值
update(key, value)更新指定 key 的缓存值;key 不存在时返回false
queryKeys(buffer, capacity)查询缓存条目数量,并把 keys 写入缓冲区
serialize()将当前缓存序列化为IHostMemory,便于持久化复用
combine(inputCache, ignoreMismatch)合并另一个缓存中的条目,冲突条目会被跳过;输入缓存必须由完全相同版本的 TensorRT 构建生成,否则合并失败
reset()清空缓存

其中update()的语义值得特别注意(见 include/NvInfer.h):

  • key 不存在 → 返回false
  • key 存在且新 tactic 耗时为NaN→ 删除该缓存条目并返回true
  • key 存在、耗时非 NaN 且新值合法 → 覆盖原值并返回true
  • 若该层实际上无法使用新写入的 tactic,下一次构建引擎时会直接报错——这是把"不可用 tactic"写入缓存的安全兜底机制。

在样例中,改写缓存的核心函数为 setTactic():它把解析出的TimingCacheKey与 tactic 哈希组装成TimingCacheValue(其中timingMSec被置为1.0F),随后调用cache->update(key, value)完成覆盖。这里的时间值本身并不关键——缓存中记录的时间只在缓存 miss、需要比较候选 tactic 时才会参与排序,而样例的目标是"强制指定",因此直接写入一个合法值即可。

源码级流程剖析:从日志解析到缓存改写

第一步:开启详细日志并用ProfilingLogger收集信息

由于 profiling 信息通过日志输出,样例首先把日志级别设为kVERBOSE(见 sampleEditableTimingCache.cpp),并定义了一个ProfilingLogger类(见 sampleEditableTimingCache.cpp)作为ILogger装饰器:它把每条日志转发给底层 logger,同时从中提取 profiling 信息。

由于 profiling 信息分散在多行日志中,ProfilingLogger内部实现了一个简单的状态机,依次经历四个状态:

  1. kEXPECT_KEY:匹配形如Autotuning op matMul1(key: 0x1814...)的行,得到算子名与 key;
  2. kEXPECT_TACTIC_HEADER:匹配表头tactic_id, cost(in ms), cost/fastest_cost, ...
  3. kEXPECT_TACTIC:逐行匹配候选 tactic(tactic 哈希 + kernel 名称),直到遇到不匹配的行;
  4. kEXPECT_SELECTION:匹配The selected tactic is (tactic hash, cost(in ms)):...,记录最终选中项后回到初始状态。

对应的模式匹配函数定义在namespace patterns中(见 sampleEditableTimingCache.cpp),包括matchOpKeymatchTacticKernelmatchSelectionmatchLayerKernel。注释特别说明:由于std::regex在部分平台上的行为不可靠,这里全部使用基础字符串接口(strstr/sscanf)完成匹配,这是值得借鉴的工程细节。

收集到的信息被组织为ProfilingRecord(算子名 + key + 可用 tactics 列表 + 选中项)与ProfilingTable(算子名到记录的映射),供后续查找与改写使用。

第二步:构建第一个引擎并抽取层信息

config->setProfilingVerbosity(ProfilingVerbosity::kDETAILED); config->setFlag(BuilderFlag::kEDITABLE_TIMING_CACHE); std::unique_ptr<ITimingCache> timingCache{config->createTimingCache(nullptr, 0)}; config->setTimingCache(*timingCache, true); std::unique_ptr<IHostMemory> plan{builder->buildSerializedNetwork(*network, *config)};

构建完成后,样例通过 extractLayerKernels() 使用IEngineInspectorengine->createEngineInspector()+getLayerInformation(i, LayerInformationFormat::kONELINE))遍历引擎每一层,提取出"层名 → kernel 名"的映射。这正是kDETAILEDTacticName字段(日志中形如TacticName: sm80_xmma_gemm_...)发挥作用的地方。

随后 findLayer() 通过"层名以算子名开头"这一前缀规则,定位出由第一个 MatMul 派生出的层(本样例中为matMul1_myl0_0)。

第三步:选择不同 tactic 并改写缓存

ProfilingRecord const& opRecord = table.at(opName); std::optional<Tactic> newTactic = findDifferentTactic(opRecord); setTactic(timingCache.get(), opRecord.key, newTactic->hash);

findDifferentTactic() 在候选 tactics 列表中查找第一个哈希不等于当前选中项的 tactic。本例中,matMul1的候选集包含0x665ded9abbf88(tilesize 32×32×64)、0x393e4ef8ad243(tilesize 64×32×64)等,因此会选中与默认不同的0x393e4ef8ad243对应的 kernelsm80_xmma_gemm_f32f32_tf32f32_f32_nn_n_tilesize64x32x64_stage4_warpsize2x1x2_tensor16x8x8

第四步:复用缓存重建引擎并验证

std::unique_ptr<IHostMemory> newPlan{builder->buildSerializedNetwork(*network, *config)};

第二次构建使用同一个config和同一个timingCache。因为缓存已包含全部算子的 tactic 记录,TensorRT 命中缓存、跳过 profiling,直接按缓存内容确定各层 tactic。最后,样例再次用IEngineInspector抽取新引擎的层信息,并通过断言newMatMulLayer->kernel == newTactic->kernel(见 sampleEditableTimingCache.cpp)验证第一个 MatMul 确实切换到了新 kernel。若验证失败,样例将输出错误并报告失败。

构建与运行

编译

该样例随 TensorRT OSS 一起编译。构建规则见 samples/sampleEditableTimingCache/CMakeLists.txt:add_executable生成名为sample_editable_timing_cache的可执行文件,链接trt_samples_commonTRT_SAMPLES::tensorrt。按仓库 samples/README.md 中列出的方式配置 CMake 并编译后,可执行文件将出现在输出目录中。编译完成后:

./sample_editable_timing_cache

预期输出

样例会输出大量日志,主要包含两类信息(README 中的原始示例):

1. profiling 日志——报告被 profiling 的算子、key、候选 tactics 及最终选中项:

Autotuning op matMul1(key: 0x1814870c44ff0f8574df6e3dda04cbd7): Sorted table of all evaluated tactics: tactic_id, cost(in ms), cost/fastest_cost, prediction_correlation, kernel_name, tactic_hash, tunable_parameter 3, 0.0112640, 1.00000, 0.50673, sm80_xmma_gemm_f32f32_tf32f32_f32_nn_n_tilesize32x32x64_stage3_warpsize2x1x2_tensor16x8x8, 0x665ded9abbf88, 5, 0.0118784, 1.05455, 0.51157, sm80_xmma_gemm_f32f32_tf32f32_f32_nn_n_tilesize64x32x64_stage4_warpsize2x1x2_tensor16x8x8, 0x393e4ef8ad243, 6, 0.0123904, 1.10000, 0.50600, sm80_xmma_gemm_f32f32_tf32f32_f32_nn_n_tilesize64x32x64_stage5_warpsize2x2x1_tensor16x8x8, 0x2ad3a182fb05c, ... The selected tactic is (tactic hash, cost(in ms)):0x665ded9abbf88, 0.011264 Writing the best tactic (0x665ded9abbf88) to cache

每条候选记录依次给出tactic_id、耗时(ms)、相对最快耗时的比值、预测相关性、kernel 名称与 tactic 哈希。cost/fastest_cost列可直观看出各候选与最快实现的性能差距。

2. 引擎层信息日志——报告引擎中各层实际使用的 kernel(依赖kDETAILEDTacticName字段):

Name: matMul1_myl0_0, LayerType: gemm, Inputs: [ { Name: input, Dimensions: [128,128], Format/Datatype: Float }, { Name: weight1, Dimensions: [128,128], Format/Datatype: Float }, { Name: __mye34matMul1_alpha, Dimensions: [1], Format/Datatype: Float }, { Name: __mye35matMul1_beta, Dimensions: [1], Format/Datatype: Float }], Outputs: [ { Name: __myln_k_arg__bb1_4, Dimensions: [128,128], Format/Datatype: Float }], TacticName: sm80_xmma_gemm_f32f32_tf32f32_f32_nn_n_tilesize32x32x64_stage3_warpsize2x1x2_tensor16x8x8, StreamId: 0, Metadata:

结果验证:两次引擎的层对比

由于上述日志可读性一般,样例在结尾打印了精简汇总版本。第一次引擎与第二次引擎的对比如下:

Layers of the first engine: #0: matMul1_myl0_0 =uses=> sm80_xmma_gemm_f32f32_tf32f32_f32_nn_n_tilesize32x32x64_stage3_warpsize2x1x2_tensor16x8x8 #1: __myl_TraMaxSubExpSum_myl0_1 =uses=> __myl_TraMaxSubExpSum_0xcbcb71f14cb4526fd18f61134658c571 #2: __myl_DivMul_myl0_2 =uses=> __myl_DivMul_0x80125aec9f1e9979e47ef2b407811651 #3: matMul2_myl0_3 =uses=> sm80_xmma_gemm_f32f32_tf32f32_f32_nn_n_tilesize32x32x64_stage3_warpsize2x1x2_tensor16x8x8

可见第一次构建中,matMul1_myl0_0使用了默认最快的 tilesize 32×32×64 kernel;softmax 被拆分为TraMaxSubExpSumDivMul两个内部实现;第二个 MatMul 与第一个使用相同 kernel。

改写缓存后,第二次引擎的层信息变为:

Layers of the second engine: #0: matMul1_myl0_0 =uses=> sm80_xmma_gemm_f32f32_tf32f32_f32_nn_n_tilesize64x32x64_stage4_warpsize2x1x2_tensor16x8x8 #1: __myl_TraMaxSubExpSum_myl0_1 =uses=> __myl_TraMaxSubExpSum_0xcbcb71f14cb4526fd18f61134658c571 #2: __myl_DivMul_myl0_2 =uses=> __myl_DivMul_0x80125aec9f1e9979e47ef2b407811651 #3: matMul2_myl0_3 =uses=> sm80_xmma_gemm_f32f32_tf32f32_f32_nn_n_tilesize32x32x64_stage3_warpsize2x1x2_tensor16x8x8

对比结论一目了然:只有#0(第一个 MatMul)切换到了 tilesize 64×32×64 的新 kernel,其余所有层(包括第二个 MatMul)的 tactic 保持不变——这正是"通过编辑缓存实现确定性构建"的预期效果。

若样例运行成功,日志末尾将出现:

&&&& PASSED TensorRT.sample_editable_timing_cache [TensorRT v100800] [b18] # sample_editable_timing_cache

PASSED行中的版本号会随当前 TensorRT 版本变化。)

设计要点与工程细节

1. 为什么强类型网络?

样例在创建网络时使用了kSTRONGLY_TYPED标志(见 sampleEditableTimingCache.cpp),即 include/NvInfer.h 对应的强类型 API(README 的 Changelog 也注明 2025 年 10 月已迁移到 strongly typed APIs)。强类型网络下层的输入输出格式在构建前即被确定,这保证了两次构建之间网络结构完全等价,也使得"除目标层外其余层 tactic 不变"的对比结论干净可信。

2. 时间值只是占位符

setTactic()value.timingMSec = 1.0F并非实际测量值。由于缓存命中时 TensorRT 直接信任缓存中的 tactic 决策、不再比较耗时,写入什么时间值都不影响最终选择;写入一个合法(非负、非 NaN)的浮点数即可通过update()的合法性校验。

3. 错误处理与断言

样例定义了FAIL_IF_NOT宏(见 sampleEditableTimingCache.cpp),对 builder/runtime 创建失败、缓存设置失败、未找到目标层、找不到其他候选 tactic、新引擎未采用新 kernel 等所有关键步骤逐一校验,任何一个环节异常都会以sample::gLogger.reportFail报告失败,配合 samples/common/logger.h 统一日志体系,运行结果非常明确。

适用场景与注意事项

  • 确定性构建:当你不希望 TensorRT 因环境波动而改变 tactic 选择时,可先构建一次生成缓存,再在后续构建中复用该缓存,获得层与 kernel 完全一致的引擎;本样例则更进一步,演示了如何在复用前定向改写某一层的 tactic。
  • 强制指定 kernelupdate()的语义保证——若写入的 tactic 对该层不可用,下一次构建会直接报错,因此改写动作本身是安全的,错误会在构建阶段暴露。
  • 缓存跨版本/跨设备限制combine()要求输入缓存由完全相同版本的 TensorRT 生成,否则合并被跳过并返回false;合并不同设备属性生成的缓存可能引发功能或性能问题(见 include/NvInfer.h),跨设备合并需显式设置ignoreMismatch=true
  • 弱类型网络的变异性:头文件明确警告(见 include/NvInfer.h),若使用弱类型(非强类型)网络,即使复用同一缓存多次构建,生成的引擎在 tactics 与格式上仍可能不同——这正是本样例采用强类型网络的原因之一。
  • 缓存未命中时的行为BuilderFlag::kERROR_ON_TIMING_CACHE_MISS(见 include/NvInfer.h)可让 TensorRT 在某个被计时的 tactic 不在缓存中时报错,该标志仅在IBuilderConfig关联了ITimingCache时生效,可用于严格校验缓存完整性。
  • README 的 Known issues 一节目前为空:本文未发现与该样例相关的已登记问题;具体行为仍以当前仓库中的源码与头文件注释为准。本样例日志中的 kernel 名称(如sm80_xmma_gemm_...)带有架构前缀,说明 tactic 集合与具体 GPU 架构(如 sm80/sm86/sm90)相关,换卡构建时缓存可能 miss 并触发重新 profiling。

总结

sampleEditableTimingCache用不到 200 行核心逻辑,完整演示了 TensorRT "profiling → 编辑缓存 → 确定性重建" 的闭环:通过kEDITABLE_TIMING_CACHE让构建日志携带 profiling 信息,借助ProfilingLogger状态机从日志中提取算子 key 与候选 tactics,再通过ITimingCache::update()将期望的 tactic 写入缓存,最后复用缓存构建出仅目标层发生变化的引擎,并用IEngineInspector完成结果验证。这套方法为需要精细化控制 kernel 选择的用户(如追求特定数值行为或复现特定性能表现)提供了标准化的官方路径。

如需进一步探索相关主题,可以继续阅读仓库中的 samples/README.md(样例总览)与 include/NvInfer.h 中IBuilderConfigITimingCache的完整接口注释,以及 samples/sampleEngines(通用引擎构建样例)、samples/sampleDynamicReshape(动态形状构建)等同属于"构建流程控制"主题的样例。

【免费下载链接】TensorRTNVIDIA® TensorRT™ is an SDK for high-performance deep learning inference on NVIDIA GPUs. This repository contains the open source components of TensorRT.项目地址: https://gitcode.com/GitHub_Trending/tens/TensorRT

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询