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 完全对应):
- 首次构建:构造网络并构建第一个引擎。此时
BuilderConfig已启用可编辑 timing cache,TensorRT 会在日志中输出 profiling 信息,同时把各算子的 tactic 决策记录进 Timing Cache; - 改写缓存:从 profiling 结果中为第一个 MatMul 挑选一个与当前选中项不同的 tactic,通过缓存编辑 API 写入 Timing Cache;
- 再次构建:复用被修改过的缓存重新构建第二个引擎。由于缓存已命中,TensorRT不再执行 profiling,而是直接采用缓存中记录的 tactics。最终效果是:除第一个 MatMul 外,其余所有层的 tactic 与第一次构建保持一致。
网络的具体构造位于 samples/sampleEditableTimingCache/sampleEditableTimingCache.cpp:输入input(128×128 FP32)与权重weight1经addMatrixMultiply得到matMul1,接addSoftMax,再与weight2相乘得到matMul2并标记为网络输出。三个层分别被命名为matMul1、softmax、matMul2,后续日志解析与缓存改写都依赖这些名字。
关键 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.TimingCacheKey与TimingCacheValue:缓存条目的数据模型
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的十六进制串)。二者的转换规则在头文件中明确给出:
- 将二进制 key 中每个字节转换为两个十六进制 ascii 字符(如
0xab→"ab"); - 依序拼接所有字节的 ascii 字符,得到恰好 32 个字符;
- 在字符串前添加前缀
"0x"。
日志中出现的key即算子的唯一标识,也是 Timing Cache 的查询键;tactic hash(如0x665ded9abbf88)则唯一标识某个 kernel 实现。样例源码中 parseKey() 与 parseTactic() 正是按上述规则把日志中的字符串解析回二进制TimingCacheKey与size_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内部实现了一个简单的状态机,依次经历四个状态:
kEXPECT_KEY:匹配形如Autotuning op matMul1(key: 0x1814...)的行,得到算子名与 key;kEXPECT_TACTIC_HEADER:匹配表头tactic_id, cost(in ms), cost/fastest_cost, ...;kEXPECT_TACTIC:逐行匹配候选 tactic(tactic 哈希 + kernel 名称),直到遇到不匹配的行;kEXPECT_SELECTION:匹配The selected tactic is (tactic hash, cost(in ms)):...,记录最终选中项后回到初始状态。
对应的模式匹配函数定义在namespace patterns中(见 sampleEditableTimingCache.cpp),包括matchOpKey、matchTacticKernel、matchSelection、matchLayerKernel。注释特别说明:由于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() 使用IEngineInspector(engine->createEngineInspector()+getLayerInformation(i, LayerInformationFormat::kONELINE))遍历引擎每一层,提取出"层名 → kernel 名"的映射。这正是kDETAILED的TacticName字段(日志中形如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_common与TRT_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(依赖kDETAILED的TacticName字段):
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 被拆分为TraMaxSubExpSum与DivMul两个内部实现;第二个 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。
- 强制指定 kernel:
update()的语义保证——若写入的 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 中IBuilderConfig、ITimingCache的完整接口注释,以及 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),仅供参考