想象你开了一家手工糖果厂,生产线是全封闭的:糖浆从一头倒进去,另一头包装好的糖果滚出来。生意顺时你乐得清闲,可一旦顾客投诉糖果变形,你就傻眼了——看不见生产线内部,只能停线、拆机、逐台检查,一折腾就是大半天。 torch.compile 的开发者遇到过一模一样的困境。它是一条“模型编译生产线”:把 Python 写的模型代码,经过追踪、优化、代码生成,变成高效的 GPU 指令。这条线环节多、链路长,任何一环出岔子,整个编译就可能失败。为了不让开发者当“拆机工人”,PyTorch 2.0 提供了调试模式(Debug Mode)——相当于给生产线装上玻璃观察窗、工位摄像头和过路快照:日志级别决定观察窗的透明度,计算图快照是每个工位的留底照片,Pass 轨迹是监控回放,失败诊断则是顺着照片倒查是哪台机器把糖果压坏了。这篇文章,就带你走进这座“透明工厂”,学会看清 torch.compile 的每一个动作。
掌握调试模式,你就能从“编译玄学”的焦虑中解脱:用日志、快照、轨迹三件套分钟级定位编译失败原因,让推理加速方案在生产环境稳稳落地。
目录
可视化演示
展示TORCH_COMPILE_DEBUG三级日志系统(INFO/DEBUG/TRACE)、计算图四阶段快照演进(原始到融合到优化到生成)及编译Pass执行轨迹与优化效果分析。
本节配有交互动效(进入视口后自动加载,约 3KB)。
'+CAPTIONS[0]+'
调试模式概述:黑盒为何要装玻璃
糖果厂一开始是全封闭的。生意兴隆时没人关心里面长什么样,直到某天一批糖果变形漏馅,投诉电话被打爆。老板想检查,却发现无从下手——每台机器都封在铁壳里,看不到原料怎么走、机器怎么转。这就是“黑盒”的代价:好用的时候不知道好在哪,坏的时候也不知道坏在哪。于是老板决定:把工厂改成透明工厂,让每个环节都暴露在视线之下。
torch.compile 的编译管线同样是一座“黑盒工厂”。你的 Python 模型是原料,最终产物是一个高效的编译后执行体。中间隔着多个阶段:Dynamo 先追踪执行路径,把 Python 代码“读”成一张计算图;接着一系列优化 Pass 对图做加工,比如算子融合、冗余消除;最后后端(如 Inductor)把图翻译成 CUDA/Triton 内核。任何一个阶段出错,编译就会失败。调试模式(Debug Mode)就是 PyTorch 2.0 给这条管线开的“玻璃窗”。
调试模式的核心价值是可观测性(Observability):让你看清编译过程发生了什么、发生在哪一步、为什么会发生。它不等于性能剖析器——你关心的不是“运行快不快”,而是“编译过程是否符合预期”。通过它,你能拿到三样东西:结构化日志(对应观察窗的透明度)、计算图快照(对应工位留底照片)、Pass 轨迹(对应监控回放)。从数学上看,整条编译管线就是一系列函数的复合:一个 Pass 的输出,正是下一个 Pass 的输入。
所以,调试模式不是“出了问题才想起来”的消防工具,而是建厂时就该埋好的观察窗。有了它,糖果厂老板不用再拆机,只需抬头看玻璃、翻照片、放回放,就能准确说出“三号机器把糖浆温度调高了”。
- 初始图 Graph_traceDynamo 把 Python 模型“读”成的第一版计算图,相当于刚进入工厂的原料糖浆。
- Pass 加工机 P_i每个优化 Pass 是一台加工机器,负责一类固定改造,比如合并算子或消除冗余节点。
- 复合拼接Pass 按顺序串成流水线,前一台机器的产物是后一台机器的输入,一步接一步。
- 最终产物流水线终点的图交给后端生成机器码,调试模式让这整段接力全程可见。
日志级别设置:观察窗的透明度
透明工厂也有讲究:玻璃不能永远全透明,否则车间里光线刺眼,工人反而没法干活。老板给每扇观察窗装了可调光玻璃,分四档:全黑(啥也看不见)、模糊轮廓(只看机器有没有在转)、清晰可见(看清每个动作)、显微级(连螺丝纹路都数得清)。平时用模糊档,查案时再调亮。
TORCH_LOGS 就是这扇可调光玻璃的旋钮。torch.compile 把编译过程的日志按严重程度分了级:ERROR 表示编译直接失败,WARNING 表示可疑但不致命,INFO 记录关键里程碑(图捕获完成、Pass 开始),DEBUG 记录每个微操细节。级别越低越安静、信息越精炼;级别越高越嘈杂、信息越全面。你通过设置一个阈值,决定“多大声的动静才值得记录”。
实际操作有两种姿势:环境变量 TORCH_LOGS=INFO 做全局粗调,或者在 Python 里用 torch._logging.set_logs(dynamo=logging.DEBUG, compilation=logging.INFO) 做模块级细调。模块级过滤非常实用:dynamo、compilation、graph、scheduler 等各有独立的日志通道。当你只想确认 Dynamo 有没有正确“读”到 Python 代码时,只把 dynamo 调到 DEBUG,其余通道保持安静,输出立刻清爽。
调光玻璃的好处是你不必一直盯着。平时调到“能干活”的模糊档,出问题时再拉高透明度——日志公式告诉你:阈值 θ 越低,放行的日志越多,观察越细,但排查成本也越高。聪明的老板只在查案时调亮玻璃。
- 日志级别 level_i每条日志自带一个级别编码,数值越小越严重,一般约定 ERROR=0、WARNING=1、INFO=2、DEBUG=3。
- 阈值 θ在 TORCH_LOGS 中设置的观察窗旋钮,定下“多大动静值得记录”的筛选标准。
- 筛选条件只有满足 level_i ≤ θ 的日志才被放行写入输出,像玻璃只透特定颜色的光。
- 信息量总和 V把被放行的日志按各自信息量 s_i 累加,得到总输出量——阈值越低,输出越汹涌。
计算图快照:工位上的留底照片
老板很快发现光有实时玻璃还不够:等糖果出了问题再去看,产品早就越过好几个工位了。于是他规定:每个工位旁边配一台相机,每件产品经过时自动拍一张“留底照”。事后追究时,把照片排成一排,立刻就能看出哪一步开始变形。
计算图快照(Graph Snapshot)就是这个作用。torch.compile 允许你把编译过程中的中间计算图导出或打印出来:图里的“节点”是算子(卷积、ReLU、加和……),“边”是张量在算子之间的流动方向。一张快照就是某一时刻计算图的完整副本——它记录了当时有多少节点、节点如何相连、张量的形状信息,相当于把“产品经过该工位时的状态”定格。
开启方式常见有两类:通过 torch._logging.set_logs(graph=True) 或环境变量 TORCH_LOGS 中的 graph/compilation 开关,让关键节点前后把图 dump 到标准输出或日志文件(文本图、dot 文件等)。拿到快照后,你可以把同一位置的前后两张图摆在一起对比,数节点数变化,判断某个 Pass 到底有没有生效。用公式表示:一次优化引起的图规模变化 Δ,等于节点数差与边数差之和。
照片是最客观的证据。老板把 12 号照片和 13 号照片摆在一起,能精确说出“这中间多出两台机器的活”,而不用跟工程师空对空争论。图快照就是编译过程的“12 号照片”。
- 快照 A 的节点与边优化前快照的节点集合 V₁ 与边集合 E₁,节点是算子,边是张量的流向。
- 快照 B 的节点与边优化后快照的节点集合 V₂ 与边集合 E₂,代表 Pass 加工完的状态。
- 集合大小 |·|对集合数元素个数,也就是“数零件”——数图里有几个算子、几条数据流。
- 化简度 Δ前后规模差越大,说明这轮 Pass 合并消除得越激进,图越苗条。
Pass轨迹追踪:监控回放查瓶颈
照片能告诉你“哪两个工位之间出了问题”,但回答不了“每台机器到底在忙什么、磨蹭了多久”。于是老板又装了摄像头:每台机器前都有记录,几点几分开始工作、干了多久、产出是什么。出了事,放回放,一目了然。
Pass 轨迹追踪就是这个回放系统。当你把 compilation 相关日志调到 INFO/DEBUG,编译器会按执行顺序打印每个优化 Pass 的名字和耗时;在部分版本和配置下,还能生成完整的 trace 文件。于是编译过程不再是“唰地一下结束”,而是一段可以逐帧观看的影片:先做模式匹配、再做算子融合、再做死代码消除……
为什么要看轨迹?两个理由。第一,验证优化是否按预期发生——你注册的自定义 Pass 到底有没有被调用、有没有真正改变图;第二,定位编译瓶颈——如果某个 Pass 耗时异常高,说明优化策略本身值得优化。轨迹可以建模为一个带时间的序列:第 k 个执行的是 Pass p_k,耗时 t_k。把这些二元组按顺序排开,就是一张精确的“编译监控回放表”。
有了回放,老板终于能回答那个经典问题:“生产线的时间都去哪儿了?”答案不是拍脑袋,而是指着屏幕:你看,第三台机器花了十分钟。Pass 轨迹把“编译很慢”这种模糊抱怨,变成精确的、可行动的诊断依据。
- Pass 名称 p_k轨迹序列中第 k 个执行的优化 Pass 的名字,比如“算子融合”或“死代码消除”。
- 耗时 t_k该 Pass 从开始到结束的耗时记录,单位通常是毫秒,对应摄像头上的时间戳。
- Pass 总数 K本次编译实际执行的 Pass 总数,序列越长说明编译流程越复杂。
- 序列集合按时间顺序排列的二元组集合,构成可逐帧回放的“监控录像”。
编译失败诊断:顺着照片找坏机器
最让人头疼的场景来了:糖果变形了。玻璃窗、照片、回放全都有,怎么用?老板的经验是:先从最后一张照片往前翻——最先出现变形的那张照片,就是问题工位所在。
torch.compile 编译失败时,报错看似复杂,但按调试模式的思路,路径很清晰:先看 ERROR 级日志,拿到失败发生的位置与原因;再 dump 最近一张图快照,看是不是图本身不合法——比如算子不受支持、张量形状推断失败、出现动态控制流导致 graph break(图捕获断点);最后结合 Pass 轨迹,确认是哪一个 Pass 的改造引入了问题。常见失败原因包括:Dynamo 无法追踪的 Python 特性、后端子图不支持某算子、shape 特化矛盾,以及缓存校验失败。
定位策略与“二分查找”异曲同工:把编译管线切成两半,先看前半段是否正常,快速排除一半嫌疑;再对剩下的区间重复,直到锁定坏机器。如果一共有 K 个阶段,最坏情况下只需 log₂K 量级的探查次数——这就是快照与轨迹组合的价值:不必逐台拆机,用日志当探针,把嫌疑区间越缩越小。
老板还有个心得:别一上来就怀疑最贵的机器。先看照片、看回放,把“嫌疑区间”缩小到一台机器再动手。编译诊断同理——用日志当探针,用快照当证据,用轨迹当时间线,坏机器无处躲藏。
- 嫌疑数 K所有可能出问题的 Pass 或阶段的数量,相当于工厂里“可疑机器的台数”。
- 对数 log₂每次探查把嫌疑区间对半排除,规模以指数速度缩小。
- 向上取整 ⌈·⌉当 K 不是 2 的幂时,向上取整保证探查次数覆盖最坏情况,不留死角。
- 探查次数 N_probe最坏情况下需要的“探针”次数,对应日志与快照的开启轮数。
调试工作流:一本检修手册
光有工具还不够,还得有流程。糖果厂老板把老师傅的经验写成了一本《检修手册》:复现故障 → 调亮观察窗 → 翻照片找变形点 → 修机器 → 重新试产 → 确认糖果完美,收工。每一步做什么、用什么工具,手册里写得清清楚楚。
torch.compile 的调试工作流几乎一模一样。第一步复现:把模型缩到最小可运行脚本,稳定触发失败——没有稳定复现,一切诊断都是空谈。第二步采集:调高日志级别、dump 计算图快照,把黑盒变成透明盒。第三步定位:结合日志、快照、轨迹锁定具体阶段与 Pass。第四步修复:改模型代码(消除 graph break、换受支持的算子)或改编译配置(调整后端与模式)。第五步回归验证:重新编译并确认输出正确,确保修复没有引入新问题。
整个调试周期可以拆成四段时间:复现、定位、修复、验证。调试模式重点优化的是“定位”这一段——日志、快照和轨迹让定位从“小时级”降到“分钟级”。这也是 PyTorch 官方调试文档反复强调的思路:先看日志,再 dump 图,最后才打开代码编辑器。定位时间越短,整个调试周期越短,迭代就越快。
检修手册最重要的不是每条指令,而是顺序:先看再动,先复现再定位。老板的工厂后来几乎所有故障都能在十分钟内化解,不是因为机器变好了,而是因为观察流程变好了。
- 复现 T_repro构造最小脚本、稳定触发编译失败所花的时间,是一切诊断的前提。
- 定位 T_locate借助日志、快照、轨迹锁定病根所花的时间,这是调试模式的主战场。
- 修复 T_fix修改模型代码或编译配置所花的时间,通常动手改代码本身并不长。
- 验证 T_verify重新编译并确认输出正确所花的时间,防止“修好了旧的、弄坏了新的”。
生产部署保障:玻璃不能挡路
最后,老板面临一个新难题:透明工厂很酷,但玻璃会反光、摄像头要耗电、拍照会拖慢产线。顾客不在乎车间长什么样,他们只在乎糖果能不能按时出货。如果让调试设备常年满负荷运转,产线速度就毁了。
生产环境中的 torch.compile 也一样:调试模式是“开发工具”而非“运行配置”。日志和快照主要在编译期产生,但 DEBUG 级日志会显著拖慢编译进程,在分布式大规模任务下还会刷爆标准输出。所以生产部署的标配是:关闭或降级调试日志(默认 ERROR/WARNING),保留编译缓存与产物校验,必要时通过 trace 文件把诊断“导出到体外”,而不是在线上实时铺开。
如何判断调试开销是否可接受?可以用一个比值 η:开启调试的编译时间相比关闭时的相对涨幅。若 η 只是几个百分点,说明玻璃几乎不挡光;若涨到几十上百倍,说明你开的不是观察窗,而是探照灯。合理的做法是把“透明能力”设计成开关:平时关着,出问题时远程打开、采集完立即关闭。这样才能兼顾可观测性与稳定可靠的部署。
老板最终把工厂设计成“白天的透明车间 + 夜晚的节能模式”:玻璃平时调成模糊档,摄像头待机,只有接到投诉才全员点亮。生产稳定、故障可控——这就是调试模式在生产环境里的终极形态。
- 基线 T_off关闭调试模式时的正常编译耗时,作为生产环境的参照基准。
- 观测值 T_on开启调试后编译的耗时,包含了日志写入、快照导出等额外动作。
- 额外开销分子部分由观测值减去基线得到,衡量可观测性带来的编译期拖慢。
- 开销比 η以百分比衡量“挡光程度”,让生产决策从拍脑袋变成看数字。
AI 应用场景
大模型推理服务上线前的编译诊断
阶段:模型部署与推理优化
解决的问题:上线前使用 torch.compile 加速推理,却频繁遇到编译失败,传统做法是盲目改代码或干脆放弃编译回退 eager,性能大幅缩水。用调试模式可以精准定位:先看 dynamo 日志锁定 graph break 位置,再 dump 计算图快照确认图结构,最后小范围修改代码,让编译一次性通过。
具体案例:某团队部署 Llama-2-7B 推理服务时,torch.compile 抛出 TypeError 导致编译失败。他们用 TORCH_LOGS="dynamo" 追踪,发现是 Python 端的 .item() 调用触发了数据相关的动态分支;将 .item() 改写为张量操作后编译成功,推理加速比从 1.0 提升到 1.8。
AI编译器自定义Pass的回归调试
阶段:编译器开发与测试
解决的问题:开发自定义图优化 Pass 时,无法确认 Pass 是否被执行、是否产生了预期简化。用 Pass 轨迹追踪确认执行顺序,用图快照前后对比验证化简效果,把“改完不知道有没有用”变成“每一步都有据可查”。
具体案例:AI 编译器工程师开发一个算子融合 Pass,跑完编译后图结构毫无变化。通过 compilation 日志的轨迹回放,发现新 Pass 被旧的 Pass 注册顺序挡在了后面,根本没有进入执行序列;调整注册顺序后,节点数从 120 降到 85,融合生效。
Python 演示
import loggingimport torch torch._logging.set_logs(dynamo=logging.INFO, compilation=logging.INFO) class Net(torch.nn.Module): def forward(self, x): x = torch.relu(x) + 1 y = x * 2 return y.sum() def fake_backend(gm, sample_inputs): print("[debug-pass] 收到计算图,节点数:", len(gm.graph.nodes)) for node in gm.graph.nodes: print("[debug-pass] 节点:", node.op, str(node.target)[:40]) return gm def main(): model = Net() compiled = torch.compile(model, backend=fake_backend) x = torch.randn(4, 4) out = compiled(x) print("[main] 输出数值:", out.item()) print("[main] 编译与执行完成") if __name__ == "__main__": main()
torch._logging.set_logs(dynamo=logging.INFO, compilation=logging.INFO)把观察窗调到清晰档,只照亮 dynamo 与 compilation 两个工位,避免日志刷屏。def fake_backend(gm, sample_inputs):自定义后端相当于临时接入产线的检测仪,能拿到编译中的计算图快照。len(gm.graph.nodes)数快照里的节点数量,等于给产品零件点个数。torch.compile(model, backend=fake_backend)启动编译流水线,并把默认后端替换成我们的“检测仪”。compiled(x)首次调用触发编译(观察窗开始录像),随后执行得到输出。
常见误区
误区我把日志级别调高之后,发现每次 forward 都变慢,说明调试日志拖累了线上推理性能,生产环境根本不敢开。
正解调试日志主要作用在编译期和图捕获期。第一次调用后的推理走的是已编译产物,执行路径上几乎没有日志开销。你感觉到的“变慢”,通常是首次编译被 DEBUG 日志拉长,或者日志输出打满了终端缓冲区。生产环境确实建议关日志,但别把“首次编译慢”误判成“每次推理慢”。
为何常见:DEBUG 日志让首次编译耗时暴涨,用户只观察到了“慢”,却分不清慢发生在编译期还是运行期。
误区看到日志里出现 graph break 就以为编译废了,只能无奈关掉 torch.compile,放弃加速。
正解graph break 是 Dynamo 遇到无法追踪的 Python 特性时“断图”、退回 eager 执行该子图的机制,编译会继续跑完,只是断点处失去优化收益。它不是致命错误,而是一个“性能提示”。正确做法是定位断点并改写代码,比如把列表推导换成张量操作,让图更完整。
为何常见:日志里警告用红字或醒目符号标出,初学者容易把“警告”当成“致命错误”。
误区日志越详细越好,直接 TORCH_LOGS=DEBUG 全量打开,结果输出几千行,满屏滚动的英文根本看不懂。
正解日志级别是“筛选器”不是“增强器”。DEBUG 全开会造成信息过载,反而淹没真正有用的 ERROR 线索,还会显著拖慢编译。正确做法像调光玻璃:先 INFO 看全局概览,再对 dynamo 或 compilation 单独开 DEBUG,逐模块缩小观察范围。
为何常见:默认“越多越安全”的心理,加上不清楚日志量随级别爆炸式增长的事实。
误区我用调试模式翻编译日志,想找出模型推理慢的深层原因,结果翻半天也没看到 GPU 利用率。
正解调试模式回答的是“编译过程发生了什么”,属于正确性与过程诊断;而“运行期慢在哪”要用 torch.profiler 等性能剖析工具。两者观测对象不同:一个盯编译期,一个盯运行内核,混用只会互相干扰。
为何常见:都是“排查问题”,名字里都带 debug,用户容易把过程诊断与性能剖析混为一谈。
误区把 dump 出来的 graph 文件存好,以为这就是优化后的部署模型,直接拿去上线。
正解快照只是编译中间产物,用于人工分析图结构与 Pass 效果;它不一定可直接执行,也不包含完整的权重序列化信息。真正部署的是编译缓存里生成的实际执行体(如 Triton 内核),而非文本图。
为何常见:图快照长得像模型文件——有节点、有边、有算子名,容易被误认为最终产物。
面试题
Q1. torch.compile 调试模式最核心的价值是什么?
- A.自动改善模型推理精度
- B.提供编译过程可观测性← 正确答案
- C.直接减少 GPU 显存占用
- D.完全替代训练时调参
答案:B。调试模式的核心是“看得见”编译过程:日志、快照、轨迹三件套都属于可观测性能力。精度提升靠模型优化,显存减少靠算子融合与内存规划,调参更是与编译无关,所以 B 正确。
Q2. 下列哪种方式可以查看 torch.compile 的编译过程日志?
- A.调整网络权重初始化方案
- B.修改损失函数的计算方式
- C.通过设置 TORCH_LOGS 变量← 正确答案
- D.增加分布式训练的显卡数量
答案:C。TORCH_LOGS 是控制编译日志的核心环境变量,像调光玻璃的旋钮。权重初始化、损失函数、显卡数量都改变不了编译过程的可见性,故选 C。
Q3. 在计算图快照中,“边”(Edge)代表什么?
- A.训练时梯度的传播路径
- B.数据增强时图像的裁剪区域
- C.权重更新时的优化器步长
- D.算子之间的张量流动方向← 正确答案
答案:D。计算图中节点是算子,边是张量在算子之间的流动方向,即数据依赖关系。梯度传播发生在训练期、裁剪区域是数据增强细节、优化器步长是训练配置,都与图的静态结构无关,故选 D。
Q4. Pass 轨迹追踪的主要用途是什么?
- A.自动调整模型的学习率
- B.查看各 Pass 执行顺序与耗时← 正确答案
- C.压缩模型权重的存储体积
- D.替换模型的激活函数种类
答案:B。Pass 轨迹按时间顺序记录每个优化 Pass 的名称与耗时,相当于工厂监控回放。学习率、存储压缩、激活函数替换都属于模型层改动,不是轨迹追踪的职责,故选 B。
Q5. 关于 graph break,下列理解正确的是?
- A.编译完全失败需要回退 eager
- B.该子图退回解释执行但编译继续← 正确答案
- C.模型精度肯定会下降一个点
- D.说明 torch.compile 存在严重 bug
答案:B。graph break 是 Dynamo 遇到无法追踪的 Python 特性时,在该处断图并退回 eager 执行该子图,其他部分仍走编译优化。它不是致命错误,也不必然降低精度,故选 B。
Q6. 编译失败时推荐的定位思路是?
- A.逐级开启日志并二分缩小范围← 正确答案
- B.直接重装最新版本的 PyTorch
- C.把模型全部替换为线性网络
- D.同时打开所有 DEBUG 日志刷屏
答案:A。正确策略是“先看 ERROR 再说”,配合快照与轨迹把嫌疑区间对半缩小,像二分查找一样快速锁定病根。重装、换模型、全量 DEBUG 都是盲目的低效操作,故选 A。
Q7. torch._logging.set_logs 通常可以调节哪些模块的日志?
- A.dynamo 的图捕获过程← 正确答案
- B.compilation 的 Pass 执行← 正确答案
- C.数据集的在线增强策略
- D.权重初始化时的随机数
答案:AB。set_logs 面向编译栈内模块,dynamo 管图捕获、compilation 管 Pass 执行,二者都是调试模式的核心观察对象。数据增强和权重初始化发生在训练流程中,与编译日志无关。
Q8. 调试模式提供了哪些可观测性能力?
- A.导出各阶段计算图快照← 正确答案
- B.追踪优化 Pass 执行轨迹← 正确答案
- C.自动修复所有编译错误
- D.按模块设置日志过滤级别← 正确答案
答案:ABD。三件套是快照、轨迹、日志级别过滤,分别对应留底照片、监控回放、调光玻璃。调试模式只负责“看见”与定位,不负责自动改代码,C 夸大了能力,故排除。
Q9. 生产环境中 torch.compile 的调试日志宜如何设置?
- A.保持默认关闭仅出错时开启← 正确答案
- B.常年保持 TRACE 全量输出
- C.必须开启所有 DEBUG 日志
- D.完全禁止日志开关的使用
答案:A。生产环境追求稳定与低开销,应默认关闭调试日志、出问题时按需远程开启并采集完立即关闭。常开 TRACE 或 DEBUG 会拖慢编译并刷爆输出,完全禁止又失去诊断能力,故选 A。
Q10. 关于调试模式的开销,下列说法正确的是?
- A.高日志级别会拖慢编译阶段← 正确答案
- B.调试日志常用于运行时推理
- C.可用相对开销比评估影响程度← 正确答案
- D.生产环境应避免常开 DEBUG 日志← 正确答案
答案:ACD。日志与快照主要发生在编译期,会拉长编译时间,所以 A 正确;可用 η 相对开销比量化影响,所以 C 正确;生产应避免常开 DEBUG,所以 D 正确。B 把日志误用到运行期推理路径,违背了调试模式的定位,故排除。