【问题标题】:Memory leak when running python script from C++从 C++ 运行 python 脚本时内存泄漏
【发布时间】:2018-06-21 01:57:15
【问题描述】:

以下从 C++ 调用 python 函数的最小示例在我的系统上存在内存泄漏:

script.py:

import tensorflow
def foo(param):
    return "something"

main.cpp:

#include "python3.5/Python.h"

#include <iostream>
#include <string>

int main()
{
    Py_Initialize();

    PyRun_SimpleString("import sys");
    PyRun_SimpleString("if not hasattr(sys,'argv'): sys.argv = ['']");
    PyRun_SimpleString("sys.path.append('./')");

    PyObject* moduleName = PyUnicode_FromString("script");
    PyObject* pModule = PyImport_Import(moduleName);
    PyObject* fooFunc = PyObject_GetAttrString(pModule, "foo");
    PyObject* param = PyUnicode_FromString("dummy");
    PyObject* args = PyTuple_Pack(1, param);
    PyObject* result = PyObject_CallObject(fooFunc, args);

    Py_CLEAR(result);
    Py_CLEAR(args);
    Py_CLEAR(param);
    Py_CLEAR(fooFunc);
    Py_CLEAR(pModule);
    Py_CLEAR(moduleName);

    Py_Finalize();
}

编译

g++ -std=c++11 main.cpp $(python3-config --cflags) $(python3-config --ldflags) -o main

并使用 valgrind 运行

valgrind --leak-check=yes ./main

生成以下摘要

LEAK SUMMARY:
==24155==    definitely lost: 161,840 bytes in 103 blocks
==24155==    indirectly lost: 33 bytes in 2 blocks
==24155==      possibly lost: 184,791 bytes in 132 blocks
==24155==    still reachable: 14,067,324 bytes in 130,118 blocks
==24155==                       of which reachable via heuristic:
==24155==                         stdstring          : 2,273,096 bytes in 43,865 blocks
==24155==         suppressed: 0 bytes in 0 blocks

我正在使用Linux Mint 18.2 Sonya、g++ 5.4.0、Python 3.5.2 和 TensorFlow 1.4.1。

删除import tensorflow 会使泄漏消失。这是 TensorFlow 中的错误还是我做错了什么? (我希望后者是真的。)


另外,当我在 Python 中创建 Keras 层时

#script.py
from keras.layers import Input
def foo(param):
    a = Input(shape=(32,))
    return "str"

并重复从 C++ 运行对 Python 的调用

//main.cpp

#include "python3.5/Python.h"

#include <iostream>
#include <string>

int main()
{
    Py_Initialize();

    PyRun_SimpleString("import sys");
    PyRun_SimpleString("if not hasattr(sys,'argv'): sys.argv = ['']");
    PyRun_SimpleString("sys.path.append('./')");

    PyObject* moduleName = PyUnicode_FromString("script");
    PyObject* pModule = PyImport_Import(moduleName);

    for (int i = 0; i < 10000000; ++i)
    {
        std::cout << i << std::endl;
        PyObject* fooFunc = PyObject_GetAttrString(pModule, "foo");
        PyObject* param = PyUnicode_FromString("dummy");
        PyObject* args = PyTuple_Pack(1, param);
        PyObject* result = PyObject_CallObject(fooFunc, args);

        Py_CLEAR(result);
        Py_CLEAR(args);
        Py_CLEAR(param);
        Py_CLEAR(fooFunc);
    }

    Py_CLEAR(pModule);
    Py_CLEAR(moduleName);

    Py_Finalize();
}

应用程序的内存消耗在运行期间不断增长。

所以我想我从 C++ 调用 python 函数的方式存在根本性的问题,但它是什么?

【问题讨论】:

  • 您可以使用 --trace-origin=yes 查看泄漏内存的分配位置。 python 模块中通常有全局变量,它们在加载时会被初始化并且永远存在——但这不是一个大问题,因为每个模块只加载一次。
  • 你的第二次内存泄漏是陌生的,也许keras有泄漏?如果未导入 keras 或未创建 Input,您是否看到内存泄漏?
  • @ead 只有在我创建 Input 时才会出现循环中不断增长的 memleak。导入 Keras 只会导致第一个示例的内存泄漏。
  • @ead 内存泄漏跟踪始终显示来源。 --trace-origin 用于未初始化的内存。引用手册“要查看有关程序中未初始化数据来源的信息,请使用 --track-origins=yes 选项。这会使 Memcheck 运行得更慢,但可以更容易追踪未初始化数据的根本原因价值错误。”来自valgrind.org/docs/manual/mc-manual.html

标签: python c++ python-3.x tensorflow memory-leaks


【解决方案1】:

您的问题中有两种不同类型的“内存泄漏”。

Valgrind 正在告诉您第一种类型的内存泄漏。然而,python 模块“泄漏”内存是很常见的——它主要是一些在模块加载时分配/初始化的全局变量。而且因为模块在 Python 中只加载一次,所以问题不大。

一个众所周知的例子是 numpy 的 PyArray_API:它必须通过 _import_array 初始化,然后永远不会被删除并保留在内存中,直到 python 解释器关闭。

所以这是每个设计的“内存泄漏”,你可以争论它是否是一个好的设计,但最终你无能为力。

我对 tensorflow 模块没有足够的了解,无法查明发生此类内存泄漏的位置,但我很确定您不必担心。


第二个“内存泄漏”更加微妙。

当您比较循环的 10^4 和 10^5 迭代的 valgrind 输出时,您可以获得领先优势 - 几乎没有区别!然而,峰值内存消耗存在差异。

与 C++ 不同的是,Python 有一个垃圾收集器——所以你无法知道一个对象究竟是什么时候被销毁的。 CPython 使用引用计数,所以当引用计数为 0 时,对象被销毁。但是,当存在循环引用时(例如,对象A 持有对象B 的引用,而对象B 持有对象B 的引用)就不那么简单了:垃圾收集器需要遍历所有对象都找到这样不再使用的循环。

有人可能会认为,keras.layers.Input 在某处有这样的循环(这是真的),但这不是这种“内存泄漏”的原因,对于纯 python 也可以观察到。

我们使用objgraph-package 来检查引用,让我们运行以下python脚本:

#pure.py
from keras.layers import Input
import gc
import sys
import objgraph


def foo(param):
    a = Input(shape=(1280,))
    return "str"

###  MAIN :

print("Counts at the beginning:")
objgraph.show_most_common_types()
objgraph.show_growth(limit=7) 

for i in range(int(sys.argv[1])):
   foo(" ")

gc.collect()# just to be sure

print("\n\n\n Counts at the end")
objgraph.show_most_common_types()
objgraph.show_growth(limit=7)

import random
objgraph.show_chain(
   objgraph.find_backref_chain(
        random.choice(objgraph.by_type('Tensor')), #take some random tensor
         objgraph.is_proper_module),
    filename='chain.png') 

并运行它:

>>> python pure.py 1000

我们可以看到:最后正好有1000 Tersors,这意味着我们创建的对象都没有被释放!

如果我们看一下使张量对象保持活动状态的链(使用objgraph.show_chain 创建),我们会看到:

有一个 tensorflow-Graph-object,所有张量都在其中注册并留在那里直到 session 关闭。

到目前为止的理论,无论多么接近:

#close session and free resources:
import keras
keras.backend.get_session().close()#free all resources

print("\n\n\n Counts after session.close():")
objgraph.show_most_common_types()

也不是here 提出的解决方案:

with tf.Graph().as_default(), tf.Session() as sess:
   for step in range(int(sys.argv[1])):
     foo(" ")

适用于当前的 tensorflow 版本。这可能是bug。


简而言之:您在 c++ 代码中没有做错任何事情,您无需为内存泄漏负责。事实上,如果您一遍又一遍地从纯 python 脚本调用函数 foo,您会看到完全相同的内存消耗。

所有创建的张量都注册在 Graph-object 中并且不会自动释放,您必须通过关闭后端会话来释放它们 - 但是由于当前 tensorflow-version 1.4.0 中的错误,这不起作用。

【讨论】:

  • 很棒的分析!非常感谢。
  • @TobiasHermann 这有点尴尬,但我的分析非常错误。但现在我希望我做对了......
  • 再次感谢。我刚刚测试了您的方法,但无法用它消除日益严重的泄漏。现在的代码是这样的:main.cpp,script.cpp。你知道出了什么问题吗?
  • @TobiasHermann 我认为在你的脚本中你需要import keras 否则clear_session 会抛出,但是你没有在你的cpp 代码中正确处理它。我会首先检查您的 cpp 程序是否作为纯 python 脚本工作。
  • 哦,对不起。我应该检查一下。好的,现在我有this script.py。简单地使用python3 script.py 运行它仍然会使内存消耗增加。有什么想法吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-12-17
  • 2014-02-09
  • 2019-07-16
  • 1970-01-01
  • 1970-01-01
  • 2023-03-16
相关资源
最近更新 更多