8 votes

Fuite de mémoire lors de l'exécution d'un script python depuis le C++

L'exemple minimal suivant d'appel d'une fonction Python à partir de C++ présente une fuite de mémoire sur mon système :

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();
}

compilé avec

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

et l'exécuter avec valgrind

valgrind --leak-check=yes ./main

produit le résumé suivant

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

J'utilise Linux Mint 18.2 Sonya , g++ 5.4.0 , Python 3.5.2 y TensorFlow 1.4.1 .

Suppression import tensorflow fait disparaître la fuite. S'agit-il d'un bug dans TensorFlow ou ai-je fait quelque chose de mal ? (Je m'attends à ce que la dernière hypothèse soit la bonne).


De plus, lorsque je crée une couche Keras en Python

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

et exécuter l'appel à Python à partir de C++ de manière répétée

//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();
}

la consommation de mémoire de l'application augmente continuellement à l'infini pendant l'exécution.

Je suppose donc qu'il y a quelque chose de fondamentalement erroné dans la façon dont j'appelle la fonction python à partir de C++, mais qu'est-ce que c'est ?

6voto

ead Points 1051

Dans votre question, il y a deux types différents de "fuites de mémoire".

Valgrind vous parle du premier type de fuites de mémoire. Cependant, il est assez habituel pour les modules python de "fuir" de la mémoire - il s'agit principalement de certains globaux qui sont alloués/initialisés lorsque le module est chargé. Et comme le module n'est chargé qu'une seule fois dans Python, ce n'est pas un gros problème.

Un exemple bien connu est la fonction PyArray_API : Il doit être initialisé via _import_array n'est alors jamais supprimée et reste en mémoire jusqu'à ce que l'interpréteur python soit arrêté.

Il s'agit donc d'une "fuite de mémoire" par conception. Vous pouvez discuter de la qualité de la conception, mais en fin de compte, vous ne pouvez rien y faire.

Je n'ai pas une connaissance suffisante du module tensorflow pour identifier les endroits où de telles fuites de mémoire se produisent, mais je suis presque sûr qu'il n'y a pas lieu de s'inquiéter.


La seconde "fuite de mémoire" est plus subtile.

Vous pouvez obtenir une piste, lorsque vous comparez les résultats de valgrind pour 10^4 y 10^5 itérations de la boucle - il n'y aura pratiquement aucune différence ! Il y a cependant une différence dans la consommation maximale de mémoire.

Contrairement au C++, Python dispose d'un collecteur d'ordures (garbage collector) - vous ne pouvez donc pas savoir exactement quand un objet est détruit. Python utilise le comptage de références, de sorte que lorsque le nombre de références atteint 0, l'objet est détruit. Cependant, lorsqu'il y a un cycle de références (par exemple, l'objet A contient une référence à l'objet B et l'objet B contient une référence à l'objet B ), ce n'est pas si simple : le ramasse-miettes doit parcourir tous les objets pour trouver les cycles qui ne sont plus utilisés.

On pourrait penser que keras.layers.Input possède un tel cycle quelque part (et c'est vrai), mais ce n'est pas la raison de cette "fuite de mémoire", qui peut être observée également pour Python pur.

Nous utilisons objgraph -pour inspecter les références, exécutons le python script suivant :

#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') 

et l'exécuter :

>>> python pure.py 1000

Nous pouvons constater ce qui suit : à la fin, il y a exactement 1000 Tersors, cela signifie qu'aucun de nos objets créés n'a été éliminé !

Si nous jetons un coup d'œil à la chaîne qui maintient en vie un objet tenseur (créé avec objgraph.show_chain ), nous voyons donc :

enter image description here

qu'il existe un objet tensorflow-Graph où tous les tenseurs sont enregistrés et qu'ils y restent jusqu'à ce que l'objet tensorflow-Graph soit enregistré. session est fermé.

Jusqu'à présent, c'est la théorie, mais ce n'est pas le cas :

#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()

ni les aquí la solution proposée :

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

a fonctionné pour la version actuelle de tensorflow. Ce qui est probablement un insecte .


En bref : Vous ne faites rien de mal dans votre code c++, il n'y a pas de fuites de mémoire dont vous êtes responsable. En fait, vous verriez exactement la même consommation de mémoire si vous appeliez la fonction foo à partir d'un pur python-script encore et encore.

Tous les tenseurs créés sont enregistrés dans un objet Graph et ne sont pas automatiquement libérés, vous devez les libérer en fermant la session backend - ce qui ne fonctionne cependant pas en raison d'un bug dans la version actuelle de tensorflow 1.4.0.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X