Dynet中CPU内存分配失败排查:循环评估引发内存泄漏
从你给出的代码和操作来看,内存耗尽的核心原因是Theano计算图的持续累积,再加上sentence对象对旧计算节点的引用,导致每次循环生成的张量和计算节点都无法被垃圾回收,具体拆解如下:
1. Theano符号计算图的无限制累积
你的代码每次循环调用__evaluate时,都会在getExpr里创建新的inputTensor实例(比如inputTensor(samples_out)),并基于这些新张量构建新的计算分支(XoutLayer = self.outLayer.expr()+inputTensor(samples_out)等)。
Theano的默认行为是将所有创建的符号变量和计算节点都保留在全局计算图中,不会自动清理。循环K次后,计算图会膨胀K倍——每一次循环的噪声张量、中间表达式都会被永久保留,内存自然会被迅速占满。
2. Sentence对象属性的持久引用
代码中你通过if sentence[i].headfov is None来赋值属性,但如果循环中复用同一个conll_sentence实例,第一次循环后headfov和modfov就会被设置为Theano符号表达式。这些表达式会引用对应的计算节点,即使后续循环不再修改这些属性,旧的计算节点也会因为被sentence对象持有而无法被回收,进一步加剧内存泄漏。
解决方案
针对这两个问题,你可以按照以下步骤修复:
(1)预编译带噪声输入的Theano函数,避免计算图累积
不要每次循环都创建新的inputTensor,而是预先定义噪声的符号变量,将整个评估逻辑编译为一次Theano函数,每次循环仅传入新的噪声数值:
# 在模型初始化阶段定义噪声的符号变量 self.noise_out = theano.tensor.matrix('noise_out') self.noise_foh = theano.tensor.matrix('noise_foh') self.noise_fom = theano.tensor.matrix('noise_fom') self.noise_bias = theano.tensor.vector('noise_bias') # 修改getExpr中的逻辑,使用预定义的符号变量 def getExpr(self, sentence, i, j): # 移除原来的samples_out等生成代码,改用预定义的噪声变量 XoutLayer = self.outLayer.expr() + self.noise_out XhidLayerFOH = self.hidLayerFOH.expr() + self.noise_foh XhidLayerFOM = self.hidLayerFOM.expr() + self.noise_fom XhidBias = self.hidBias.expr() + self.noise_bias if sentence[i].headfov is None: sentence[i].headfov = XhidLayerFOH * concatenate([sentence[i].lstms[0], sentence[i].lstms[1]]) if sentence[j].modfov is None: sentence[j].modfov = XhidLayerFOM * concatenate([sentence[j].lstms[0], sentence[j].lstms[1]]) output = XoutLayer * self.activation(sentence[i].headfov + sentence[j].modfov + XhidBias) return output # 编译__evaluate的逻辑为Theano函数(假设__evaluate返回scores和exprs) evaluate_func = theano.function( inputs=[self.noise_out, self.noise_foh, self.noise_fom, self.noise_bias], outputs=[scores, exprs], # 其他必要参数,比如输入的sentence等,根据实际情况调整 ) # 然后在循环中调用编译好的函数 for k in xrange(K): # 生成噪声数值 samples_out = np.random.normal(0,0.001, (1, self.hidden_units)) samples_FOH = np.random.normal(0,0.001,(self.hidden_units, self.ldims * 2)) samples_FOM = np.random.normal(0,0.001,(self.hidden_units, self.ldims * 2)) samples_Bias = np.random.normal(0,0.001, (self.hidden_units)) # 调用函数,传入噪声 scores, exprs = evaluate_func(samples_out, samples_FOH, samples_FOM, samples_Bias) # do something
(2)每次循环重置sentence对象的属性
如果必须复用同一个sentence实例,在每次循环开始前,重置所有相关属性为None,避免旧的计算节点被持续引用:
for k in xrange(K): # 重置sentence的headfov和modfov for token in conll_sentence: token.headfov = None token.modfov = None # 后续执行评估逻辑 scores, exprs = self.__evaluate(conll_sentence, True) # do something
(3)可选:使用Shared变量动态更新噪声
如果噪声需要频繁更新,也可以将噪声定义为theano.shared变量,每次循环仅更新其数值,而不需要修改计算图:
# 初始化时创建shared变量 self.noise_out = theano.shared(np.zeros((1, self.hidden_units)), name='noise_out') # 其他噪声变量同理 # 循环中更新数值 for k in xrange(K): self.noise_out.set_value(np.random.normal(0,0.001, (1, self.hidden_units))) # 其他噪声变量更新 scores, exprs = self.__evaluate(conll_sentence, True) # do something
这样计算图只会构建一次,所有循环复用同一个图,仅更新变量数值,内存占用会保持稳定。
内容的提问来源于stack exchange,提问作者user3639557

