
如果MindSpore训练刚开始日志里突然冒出一行英文提示Constant value tensor are detected in tuple or list, which might cause recompiling我的建议是你别慌但也别完全无视。我第一次遇到这个报警是在一个图像分类模型上。训练没有中断Loss还在正常下降但仔细看每个step的耗时比预期慢了差不多三分之一——那会儿我还不知道这就是编译器在后台反复重画计算图造成的隐性开销。这个提示几乎是所有从PyTorch迁移到MindSpore的玩家都会碰到的老熟人因为它和静态图编译机制深度绑定。今天就把这个报警从原理到排查再到修复掰开揉碎讲清楚。文章里所有修复方案我都实际跑过结论可以直接拿来用。1. 报警现场还原它出现在哪一步又意味着什么1.1 一段能稳定触发报警的Demo代码先给一段可以稳定复现报警的代码。你不需要理解它的业务逻辑只需要看它犯的“结构性错误”。import mindspore as ms from mindspore import nn, ops, Tensor import numpy as np class DemoNet(nn.Cell): def __init__(self): super(DemoNet, self).__init__() self.fc nn.Dense(16, 4) # 注意这是Python的list里面装的是直接创建的常量Tensor self.gamma_list [ Tensor(np.ones(4, np.float32)), Tensor(np.full(4, 0.5, np.float32)) ] def construct(self, x): out self.fc(x) results [] for gamma in self.gamma_list: results.append(out * gamma) return ops.stack(results) if __name__ __main__: ms.set_context(modems.GRAPH_MODE) net DemoNet() input_data Tensor(np.random.randn(8, 16).astype(np.float32)) print(net(input_data))在MindSpore 2.x、Graph模式下跑这段代码日志里大概率会出现类似这样的一整段警告[WARNING] CORE(..., pid, tid): Constant value tensor are detected in tuple or list, which might cause recompiling. You could set const_arg to False by mental.mutable ...它提示的关键词就是Constant value tensor、tuple or list、recompiling。这段代码里有两个错误模式self.gamma_list是一个Python list里面装着用Tensor()直接创建出来的常量张量construct里又在动态构造results这个list并往里append中间结果。这两件事单独出现都问题不大但叠加在一起就精准踩中了图编译器对“常量容器”的识别逻辑。1.2 Warning还是Error严重程度其实有分级这个提示在绝大多数场景下是一条Warning不会中断训练。但这不代表可以放置不管它的严重程度是分级的场景表现实际影响只在首次构图时出现一条之后不再出现Warning一次训练正常低基本无害每次训练迭代都出现每个step都慢一截日志刷屏中典型性能陷阱list里装的常量值在训练中频繁变化反复触发重编译CPU占用飙升高有可能让训练整体崩溃叠加在ms.jit、动态shape、分布式并行等场景重编译变成递归行为甚至出现Recursive compilation之类的错误高直接阻断训练我在实际项目中见过最离谱的情况一个显存占用很小的模型因为这个问题单step耗时从80ms上涨到接近200ms。查了半天没有内存泄漏最后定位到就是recompiling在作怪。所以我的态度很明确只要看到这条Warning就当Bug去修不要心存侥幸。2. 为什么list里的常量Tensor会触发recompiling图编译机制拆解2.1 静态图编译的三个阶段MindSpore的Graph模式静态图模式不是Python边解释边执行而是先把Python代码“翻译”成一张计算图再让后端执行。这个过程大体可以分成三步Parse扫描construct函数以及它调用的子函数把Python语法转成MindSpore的中间表示Optimize对中间表示做算子融合、常量折叠、内存规划等优化Codegen/Execute生成可执行图并运行。在第二步里有一个非常关键的优化动作叫常量折叠。如果一个值在编译期就能确定下来比如一个整型数字、一个字符串、一个直接拿np.array创建的Tensor编译器会把它直接“烙”进图结构里不再当成运行时输入。这是合理的、也是静态图性能好的原因之一——省掉了很多运行期的动态判断。但问题来了如果某个常量被烙进图里之后下一次运行时它的值变了那编译器手里那张旧图就失效了。它必须从头再解析一遍、再优化一遍、再生成一遍。这个“后补”的过程就是报错里的recompiling。2.2 list和tuple在编译器眼中是“结构节点”你可能觉得list就是Python的list里面存几个Tensor能有多大事在Python世界确实没什么大事。但在图编译器里list和tuple不是简单的数据容器它们会变成图中的结构节点。随便搜一下IR文件就能看到MakeTuple、ListAppend这样的节点——每一个都代表“程序里出现了一个元组/列表结构”。当一个list里的元素是常量Tensor编译器为了支持“这个list在运行期可以继续使用”最省事的做法就是把这些常量的值直接内联到结构节点里。比如[Tensor(1.0), Tensor(2.0)]会被编译成“包含两个值节点的tuple结构”如果Tensor(1.0)是一个Constant value tensor它的具体数值就成了这个结构的一部分。一旦你下一次迭代时list里的Tensor数值变了、引用变了、或者list本身多了一个元素少了一个元素这个结构就不再匹配旧图编译器只能选择重新编译。这就解释了为什么报警原文说的是“detected in tuple or list”——它检测到的不是普通计算Tensor而是嵌在容器结构里的常量Tensor。2.3 缓存键失效重编译的直接导火索再往深一层讲MindSpore的图编译器为了方便做编译缓存会给每个方法/模块生成一个缓存键。我个人的理解是这个键可以粗略看作“源码位置 当前参与构图的对象特征”的组合。list里的常量Tensor因为参与了结构生成它的值会被纳入缓存键的计算范围。一旦常量Tensor的值变成另一副面孔缓存键就对不上了。编译器查缓存查不到只能老老实实重新编译一遍。如果你每个step都在变那就每个step都重编一次。提示如果你观察到的报警频率是“每个step都报”基本可以认定这个list里的Tensor值在每次运行都不一样——比如你在__init__里写死了一个Tensor但construct里又去改它或者这个list本身就是动态算出来的。这种情况单靠“改list为tuple”往往解决不了必须让编译器从源头上不再把它当常量。2.4 从PyTorch迁过来的用户为什么最容易中招我从PyTorch转向MindSpore的前几周几乎天天踩这种坑。原因很简单PyTorch的Dynamic Graph是“边跑边建图”list就是普通list你随便append什么它都ok完全没有“编译期/运行期”的界限。可静态图不同它需要提前知道图结构。习惯PyTorch写法的人很自然就会在forward里这么写lst [] for i in range(n): lst.append(some_tensor) return torch.stack(lst)这段代码搬到MindSpore的construct里就成了最标准的报警模板。所以如果你也刚从PyTorch过来看到这条Warning不要太紧张——它不是你写错了只是你的编程习惯还没适配静态图模式。3. 从报警到定位一次完整的排查链路3.1 先分清报警频率别一上来就改代码看到Warning后第一件事不是改代码而是观察它出现的规律。只出现一次说明只是首次构图时检测到某个常量为图结构的一部分后续没再变影响很小每个step稳定出现说明list内容在迭代中被动态改变这是重编译的主犯与数据加载过程同步出现很可能是Dataset里返回了Tensor被塞进了construct的list结构这个要注意后面会讲。判断方法就是开一个训练脚本盯着日志跑10个step数一数Warning出现的次数。次数不上涨可以晚点处理次数跟着step走立刻进入定位流程。3.2 开启save_graphs让IR文件说话排查这类问题最硬核的工具是保存IR图。在训练脚本入口加一行ms.set_context(save_graphsTrue)或者旧版本用环境变量export MS_DEV_SAVE_GRAPHS1跑几个step之后当前工作目录下会生成类似rank_0/的目录里面是各种.ir文件和.dot文件。.ir是文本格式直接打开就能看图结构。接着重点搜索这几个节点名MakeTuple代表把多个值打包成tupleListAppend代表对list执行appendTensorConstant、ScalarConstant、Constant代表常量节点。排查逻辑是找到与报警行号对应的子图看MakeTuple或ListAppend的输入里有没有挂着TensorConstant节点。有就是它们触发了报警。不同版本的IR文件命名有差异常见的有xx_validate.ir、xx_after_optimize.ir等我一般直接看以validate或数字编号开头的.ir文件信息更靠近原始源码结构。3.3 用二分法把嫌疑代码缩到一行如果IR文件太大、看起来费劲更笨但更有效的方法是二分注释法。回到能稳定复现报警的最小脚本把construct里不影响整体结构的业务逻辑逐块注释掉只保留list相关操作。比如先把results.append(...)删掉、把for gamma in self.gamma_list改成只取第一个元素……每改一次跑一次脚本看Warning是否还出现。我一般会把网络结构调整为一个“哑网络”比如一个nn.Dense加一个list操作最小化到10行以内。只要还能复现范围就锁定了。之后再按原结构逐步加回来定位真正的触发点。3.4 快速判断list里的Tensor到底是不是“常量”这是个经验活儿。以下特征只要中一个基本可以判定它是常量Tensor会被编译器拿去折叠特征说明用Tensor(np.array(...))直接创建没有经过任何计算图抵消构造来自超参数、配置文件训练过程中一般不更新内部元素数量固定与输入shape无关纯Python对象与Parameter无关没有参与优化器更新在__init__里创建后未被修改值保持初始状态反过来如果某个Tensor来自Parameter、或者经过了算子输出比如self.fc(x)、x 1它就是一个计算节点不会轻易被当常量折叠。排查的目的就是把那些“看着像数据、实际被当成常量”的对象揪出来。4. 从应急到根治四类修复写法4.1 方案A用 ops.mutable() 把容器标记为可变这是MindSpore官方给出的应对方法也是我认为最省事的应急方案。ops.mutable()可以给list、tuple、Tensor打上“可变”标签告诉编译器这个容器里的值运行期可能会变不要把它固化成图结构。拿前面DemoNet为例改成这样from mindspore.ops import mutable class DemoNet(nn.Cell): def __init__(self): super(DemoNet, self).__init__() self.fc nn.Dense(16, 4) self.gamma_list [ Tensor(np.ones(4, np.float32)), Tensor(np.full(4, 0.5, np.float32)) ] def construct(self, x): out self.fc(x) results mutable([]) # 标记为可变list for gamma in mutable(self.gamma_list): # 标记整个list为可变 results.append(out * gamma) return ops.stack(results)我实测下来mutable list里的append操作在图模式下是可以跑的报警也会消失。但有两个注意点ops.mutable()标记的是对象本身不是里面的每个元素。如果list里某个元素本身就是常量Tensor你可以单独把那个Tensor也mutable掉mutable机制会引入额外的运行时判断性能上有一定代价。所以它是“应急”方案不是“最优”方案。如果版本较低比如1.x可能没有ops.mutable这个API当时用的是set_const_arg之类的接口思路完全一样就是让编译器别把对象当常量建议优先升级到2.x再试。4.2 方案B把参与训练的常量升级为Parameter这一条放在第一位容易被人忽略很多“常量Tensor”其实是模型的训练参数只是碰巧写完就没动过。比如常用来存EMA影子参数、手动更新的BatchNorm参数、或者某些中间统计量用Python list装Tensor结果被编译器当成常量。正确的做法是让这些东西变成一个真正的Parameterfrom mindspore import Parameter, ParameterTuple class DemoNet(nn.Cell): def __init__(self): super(DemoNet, self).__init__() self.fc nn.Dense(16, 4) self.gamma_params ParameterTuple([ Parameter(Tensor(np.ones(4, np.float32)), namegamma1), Parameter(Tensor(np.full(4, 0.5, np.float32)), namegamma2) ]) def construct(self, x): out self.fc(x) results [] for gamma in self.gamma_params: results.append(out * gamma) return ops.stack(results)ParameterTuple是MindSpore官方提供的容器专门用来存放多个Parameter。它天然不会被当成常量折叠而且里面的Parameter还能正常参与梯度更新语义上更准确。唯一的门槛是如果你这些Tensor真的只是常量、不参与训练把它们设成Parameter反而有点“滥用训练设施”的味道。所以这个方案更适合“本意是参数但被误写成常量”的场景。4.3 方案C用 ops.stack / ops.concat 替代动态list这个方案最稳定也是我说“根治问题”时最推荐的方式。不要真的去绕开list而是干脆不构造动态list。很多人的真实目的是“把几个张量拼起来再统一处理”而不是“拥有一个Python list”。这种情况直接用ops.stack就够了class DemoNet(nn.Cell): def __init__(self): super(DemoNet, self).__init__() self.fc1 nn.Dense(16, 8) self.fc2 nn.Dense(16, 8) def construct(self, x): out1 self.fc1(x) out2 self.fc2(x) return ops.stack((out1, out2))注意这里传给ops.stack的是一个tuple不是list。tuple本身是固定的、不可变的编译器对tuple的识别比list要轻松很多。这也是一个小技巧只要数据个数固定就用tuple别用list。如果数据个数不固定比如某个维度是动态的那就用ops.concat或者先补成固定shape再stack。核心原则是把“容器结构”的负担转嫁给算子让算子去处理张量拼接而不是让Python容器进来掺和。4.4 方案D把容器和分支控制移出construct第三种情况是list里装的确实是常量而且跑训练时根本不会变那它就不该出现在编译期“视线”里。写死在__init__里construct里只用固定下标访问class DemoNet(nn.Cell): def __init__(self): super(DemoNet, self).__init__() self.fc nn.Dense(16, 4) # 常量列表放这里 self.fixed_gamma (Tensor(np.ones(4, np.float32)), Tensor(np.full(4, 0.5, np.float32))) def construct(self, x): out self.fc(x) # 用固定的下标访问编译器能正确折叠 return out * self.fixed_gamma[0]但要提醒一点如果下标是动态的比如self.fixed_gamma[idx]中的idx是一个Tensor变量那这种写法依然是雷区——动态索引list是Python层面的操作静态图不支持直接把这个操作烙进图里。动态取数请优先用Tensor API比如ops.gather、ops.select、ops.stack之后统一处理。同样如果list里面装的是Cell层对象优先用nn.CellList而不是Python list。CellList的身份是“模块容器”图编译器对它有一套专门的处理逻辑比裸list安全得多。4.5 方案对比一览方案适用场景改动量注意点A: ops.mutable()临时绕过list内容运行期会变小有性能损耗建议后续优化B: Parameter / ParameterTuple常量实际是带梯度参数中语义更准确但别滥用C: ops.stack / ops.concat固定数量的张量聚合小尽量传tuple不要传listD: 移出construct / CellList常量列表与模型结构无关中动态下标还是会炸用Tensor API5. 踩过几次坑之后我总结的避坑习惯5.1 写construct时的三条容器纪律第一次遇到这个Warning花了我一个晚上才定位到问题。但吃一堑长一智之后我给自己立了三条纪律基本从源头上杜绝了这个报警。第一construct里不临时append list来收集Tensor。如果数量固定直接定义多个变量名如果数量不固定优先交给Tensor算子去构造实在不行用ops.mutable包裹。第二常量数据要么放__init__里要么用tuple不要用list。学习Rate、固定权重、超参表这几个是最常规的雷区。第三涉及动态分支或动态索引绝不直接操作Python容器。从控制流到数据访问全部切到Tensor API的语义上。这三条不只是为了消掉一个Warning更是为了让静态图的编译缓存能最大化起作用把图结构的稳定性当成性能优化的一部分。5.2 自检清单每次写完训练代码先过一遍写代码的时候人容易上头等写完再回头检查成本就高了。我这里有一个非常简单的自查清单几条而已但命中率很高搜索代码里所有的[Tensor(、List(...)、list(...)搜索construct方法里所有的.append(搜索for ... in循环内部是否在使用list容器搜索if条件里是否有Tensor参与判断搜索range()的长度是否来自Tensor属性。如果这五项全是“否”那这个报警基本和你无缘。任何一项命中动手改之前先想清楚这个容器到底该不该留在计算图里。5.3 用训练速度回归验证重编译是否根除修复完代码之后我建议不要只看日志里有没有Warning就万事大吉。更有效的方法是做一个训练速度回归在修复前后各跑相同数量的step分别记录平均单step耗时。我自己的实测经验是修复前如果每个step都触发recompiling修复后单step耗时能下降20%到50%。如果改动完速度完全没变化说明报警分支可能没有被真正清除或者刚才报警的重编译路径与主训练路径没有重合。还有一个辅助验证手段监听日志中“Warning”出现的次数。如果修复后连续跑几万个step都一次不出现这个Bug才算真正闭环。别小看这个过程——有一次我正在修另一个问题结果发现某个Warning已经连续几千行在刷屏训练却一直都是“正常”的那种无声的磨损最可怕。回头看我那次分类模型训练排查这个报警的收获不只是把单step时间拉回了正常值还顺藤摸瓜发现了一个藏在权重初始化阶段的多余list拷贝——那个地方虽然没报错但白占了每次构图的内存分配。所以遇到这类编译期提示别总觉得是框架在“小题大做”它往往是在帮你指出那些平时看不见的高成本代码路径。如果你也在跑MindSpore训练时看到这行英文照着上面的链路过一遍基本都能落地解决。