Pytorch: La mémoire CPU fuit progressivement lorsque num_workers > 0 dans le DataLoader

Créé le 29 oct. 2018  ·  79Commentaires  ·  Source: pytorch/pytorch

Note de l'éditeur : il existe une solution de contournement connue plus loin sur ce problème, qui consiste à NE PAS utiliser les listes Python, mais à la place d'utiliser autre chose, par exemple, un tableau numpy ou un tenseur directement.

🐛 Bogue

La mémoire CPU fuira si le DataLoader num_workers > 0 .

Reproduire

Exécutez l'extrait de code suivant :

from torch.utils.data import Dataset, DataLoader
from PIL import Image
from torchvision import transforms
import os

class DataIter(Dataset):
    def __init__(self):
        path = "path/to/data"
        self.data = []

        for cls in os.listdir(path):
            for img in os.listdir(os.path.join(path, cls)):
                self.data.append(os.path.join(path, cls, img))

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        with Image.open(self.data[idx]) as img:
            img = img.convert('RGB')
            return transforms.functional.to_tensor(img)


train_data = DataIter()
train_loader = DataLoader(train_data, batch_size=300,
                          shuffle=True,
                          drop_last=True,
                          pin_memory=False,
                          num_workers=18)

for i, item in enumerate(train_loader):
    if i % 200 == 0:
        print(i)

Comportement attendu

La mémoire du processeur commencera progressivement à augmenter, remplissant éventuellement toute la RAM. Par exemple, le processus commence avec environ 15 Go et remplit l'ensemble des 128 Go disponibles sur le système.
Lorsque le num_workers=0 , l'utilisation de la RAM est constante.

Environnement

PyTorch version: 1.0.0.dev20181028
Is debug build: No
CUDA used to build PyTorch: 9.0.176

OS: Ubuntu 16.04.4 LTS
GCC version: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609
CMake version: version 3.5.1

Python version: 3.5
Is CUDA available: Yes
CUDA runtime version: 9.0.176
GPU models and configuration: 
GPU 0: GeForce GTX 1080 Ti
GPU 1: GeForce GTX 1080 Ti
GPU 2: GeForce GTX 1080 Ti

Nvidia driver version: 390.67
cuDNN version: Probably one of the following:
/usr/lib/x86_64-linux-gnu/libcudnn.so.7.1.4

Versions of relevant libraries:
[pip] Could not collect
[conda] Could not collect

PIL.__version__
'5.3.0'

information additionnelle

Il y a environ 24 millions d'images dans l'ensemble de données et tous les chemins d'accès aux images sont chargés dans une seule liste, comme présenté dans l'extrait de code ci-dessus.

J'ai également essayé plusieurs versions de Pytorch (0.4.0 et 0.4.1) et l'effet est le même.

cc @ezyang @gchanan @zou3519 @SsnL

high priority dataloader memory usage molly-guard multiprocessing triaged

Commentaire le plus utile

Après quelques recherches supplémentaires, j'ai trouvé un scénario exact lorsque la fuite se produit. Considérez l'exemple de code ci-dessous :

from torch.utils.data import Dataset, DataLoader
import numpy as np
import torch


class DataIter(Dataset):
    def __init__(self):
        self.data_np = np.array([x for x in range(24000000)])
        self.data = [x for x in range(24000000)]

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        data = self.data[idx]
        data = np.array([data], dtype=np.int64)
        return torch.tensor(data)


train_data = DataIter()
train_loader = DataLoader(train_data, batch_size=300,
                          shuffle=True,
                          drop_last=True,
                          pin_memory=False,
                          num_workers=18)

for i, item in enumerate(train_loader):
    if i % 1000 == 0:
        print(i)

Si nous utilisons la variable self.data qui est une liste Python standard d'entiers, la fuite de données se produira. Cependant, si la variable self.data_np est utilisée, qui contient les mêmes données mais sous la forme d'un tableau Numpy, la fuite ne se produira pas.
Une autre observation est que la fuite est nettement moins grave si le shuffle=False dans le DataLoader .

Tous les 79 commentaires

Voyez-vous l'utilisation de la mémoire augmenter lors de l'itération, ou avant même de commencer à itérer ?

@SsnL Pendant l'itération uniquement.

Lorsque nous corrigeons #13243, nous devrions vérifier si celui-ci est également corrigé.

J'ai vécu quelque chose de similaire où l'utilisation de la mémoire grimpe continuellement jusqu'à ce qu'un MOO soit déclenché lors de l'utilisation d'un batch_sampler avec num_workers>0 .

Reproduire

import math

from torch.utils.data import DataLoader


class Sampler:
    def __init__(self, n=100000, batch_size=32):
        self.n = n
        self.batch_size = batch_size

    def __len__(self):
        return math.ceil(float(self.n)/self.batch_size)

    def __iter__(self):
        batch = []
        for i in range(self.n):
            batch.append(i)
            if len(batch) == self.batch_size:
                yield batch
                batch = []
        if batch:
            yield batch


N = 100000000
train_data = list(range(N))


def ok():
    train_sampler = Sampler(len(train_data))
    train_loader = DataLoader(train_data,
                              num_workers=0,
                              batch_sampler=train_sampler)

    for i, item in enumerate(train_loader):
        if i % 10000 == 0:
            print(i)


def leaky():
    train_sampler = Sampler(len(train_data))
    train_loader = DataLoader(train_data,
                              num_workers=8,
                              batch_sampler=train_sampler)

    for i, item in enumerate(train_loader):
        if i % 10000 == 0:
            print(i)


print('Starting ok')
ok()
print('ok done, starting leaky()')
leaky()
print('leaky done')

Environnement

$ python3 collect_env.py
Collecting environment information...
PyTorch version: 0.4.0
Is debug build: No
CUDA used to build PyTorch: 9.1.85

OS: Ubuntu 16.04.5 LTS
GCC version: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609
CMake version: version 3.5.1

Python version: 3.5
Is CUDA available: Yes
CUDA runtime version: 9.1.85
GPU models and configuration: GPU 0: GeForce GTX 1050 Ti with Max-Q Design
Nvidia driver version: 390.77
cuDNN version: Probably one of the following:
/usr/lib/x86_64-linux-gnu/libcudnn.so.7.1.2
/usr/lib/x86_64-linux-gnu/libcudnn_static_v7.a

Versions of relevant libraries:
[pip] Could not collect
[conda] Could not collect

@ezyang

Lorsque nous corrigeons #13243, nous devrions vérifier si celui-ci est également corrigé.

Le problème est toujours présent dans 1.0.0.dev20181105 , où le #13243 est corrigé.

Après quelques recherches supplémentaires, j'ai trouvé un scénario exact lorsque la fuite se produit. Considérez l'exemple de code ci-dessous :

from torch.utils.data import Dataset, DataLoader
import numpy as np
import torch


class DataIter(Dataset):
    def __init__(self):
        self.data_np = np.array([x for x in range(24000000)])
        self.data = [x for x in range(24000000)]

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        data = self.data[idx]
        data = np.array([data], dtype=np.int64)
        return torch.tensor(data)


train_data = DataIter()
train_loader = DataLoader(train_data, batch_size=300,
                          shuffle=True,
                          drop_last=True,
                          pin_memory=False,
                          num_workers=18)

for i, item in enumerate(train_loader):
    if i % 1000 == 0:
        print(i)

Si nous utilisons la variable self.data qui est une liste Python standard d'entiers, la fuite de données se produira. Cependant, si la variable self.data_np est utilisée, qui contient les mêmes données mais sous la forme d'un tableau Numpy, la fuite ne se produira pas.
Une autre observation est que la fuite est nettement moins grave si le shuffle=False dans le DataLoader .

Je suis confronté à un problème similaire, mais dans mon cas, cela se produit également avec un tableau numpy. J'utilise Python 3.7 et la version nocturne de PyTorch.

Je ne sais pas comment le multitraitement fonctionne vraiment sous le capot de pytorch, mais nous avons longuement discuté de ce problème de "fuite de mémoire" (qui n'est probablement pas une fuite de mémoire !) sur les forums fast.ai (https://forums. fast.ai/t/runtimeerror-dataloader-worker-is-killed-by-signal/31277/55?u=marcmuc). Conclusions préliminaires qui, espérons-le, ajoutent un aperçu ici (si cela ne s'applique PAS, veuillez commenter !) :

Python Multiprocessing : Il n'y a aucun moyen de stocker des objets python arbitraires (même des listes simples) dans la mémoire partagée de Python sans déclencher un comportement de copie sur écriture en raison de l'ajout de refcounts, chaque fois que quelque chose lit à partir de ces objets. Les refcounts sont ajoutés page mémoire par page mémoire, c'est pourquoi la consommation croît lentement. Les processus (travailleurs) finiront par avoir tout/la plupart de la mémoire copiée petit à petit, c'est pourquoi nous obtenons le problème de débordement de mémoire. La meilleure description de ce comportement est ici (SO).

Solution possible:
Utiliser le multitraitement comme maintenant : pour que le multitraitement python fonctionne sans ces effets de refcount, les objets doivent être rendus "compatibles avec" et enveloppés dans multiprocessing.Array avant que le pool de processus ne soit créé et que les travailleurs ne soient forkés. Cela garantit soi-disant que la mémoire sera réellement partagée et qu'aucune copie sur écriture ne se produit. Cela explique comment le faire pour les tableaux numpy et cela explique à nouveau le raisonnement derrière cela. Ne vous laissez pas confondre par certaines fausses déclarations, même par les auteurs de ces bonnes réponses indiquant que la copie sur écriture rend tout cela inutile, ce qui n'est pas vrai. Un commentaire le signale également :

"Juste pour noter, sur Python fork() signifie en fait copier lors de l'accès (car le simple fait d'accéder à l'objet changera son nombre de références)."

Je ne suis pas familier avec le remplacement de torch.multiprocessing que je comprends que pytorch utilise, mais je suppose qu'il ne sera pas non plus en mesure de supprimer le problème de base de refcount de python.

@mprostock torch.multiprocessing est simplement un multitraitement Python, avec un pickler personnalisé. Le pickler personnalisé, chaque fois qu'il rencontre un torch.tensor , le déplacera automatiquement vers la mémoire partagée, et donc au moins sur les objets torch.tensor , aucune copie sur écriture ne se produit.

Merci pour l'explication! J'ai expérimenté l'exemple de reproduction de @bfreskura et je pense pouvoir maintenant identifier le problème :

L'exemple de reproduction par bfreskura ci-dessus a montré la différence entre une liste python régulière et un tableau numpy. Mais le problème n'est pas (seulement) la liste python elle-même, la même chose se produit dans un tableau numpy de type objet. Les listes Python ne stockent que les références aux objets, les objets sont conservés séparément en mémoire. Chaque objet a un refcount, donc chaque élément de la liste a un refcount.

Les tableaux numpy (de types np standard) sont stockés sous forme de blocs continus en mémoire et ne sont qu'un seul objet avec un seul refcount.

Cela change si vous créez explicitement le tableau numpy de type objet, ce qui le fait commencer à se comporter comme une liste python normale (ne stockant que les références aux objets (chaîne)). Les mêmes "problèmes" de consommation mémoire apparaissent désormais.

Cela expliquerait pourquoi, avec des listes régulières (ou des tableaux numpy de type objet), nous voyons la "fuite de mémoire", qui est en fait le problème de copie sur accès des processus python fourchus en raison de la modification des refcounts, et non d'une fuite de mémoire.

Ainsi, le problème n'a probablement (souvent) rien à voir avec les tenseurs ou les objets torche réels, mais plutôt avec les listes de noms de fichiers et de dicts d'étiquettes, qui sont généralement utilisées dans les chargeurs de données/ensembles de données.

J'ai créé un cahier essentiel , si quelqu'un veut l'essayer rapidement.
Regardez la consommation de mémoire (mémoire rapide et sale du système total, donc influences mineures par d'autres processus, essayé de garder le système propre)

Consommation de mémoire en Go avec un tableau de chaînes de longueur fixe :
image

Consommation de mémoire en Go avec tableau d'objets (seulement changer !)
image

Je suis confronté au même problème. Il remplit ma RAM très rapidement si le num_workers> 0.
Je supprime les variables qui, à mon avis, ne sont plus nécessaires dans mon code, j'appelle également gc.collect() à chaque itération, mais rien n'y fait.
Des solutions de contournement ?

Passer de dict à pandas et de listes à tableaux numpy m'aide

Je suis confronté au même problème. Il remplit ma RAM très rapidement si le num_workers> 0.
Je supprime les variables qui, à mon avis, ne sont plus nécessaires dans mon code, j'appelle également gc.collect() à chaque itération, mais rien n'y fait.
Des solutions de contournement ?

Merci pour la réponse. Je vais essayer ça et j'espère que ça marche.

Puis-je demander la solution à ce problème ? J'ai essayé le code @samgd sur le dernier pytorch construit quotidiennement, et il fuyait toujours.

@Godricly Voir les commentaires de @mprostock et @soumith ci-dessus. Ce n'est pas vraiment une fuite, mais un comportement malheureux d'utilisation de la liste native python. L'utilisation d'un tenseur de torche ou d'un réseau np résoudra ce problème de mémoire.

@mprostock Voulez-vous dire que c'est la copie créée par la copie sur accès qui utilise la mémoire, pas autre chose? Et la copie ne se libère-t-elle pas après utilisation ?

Quelqu'un doit intensifier et écrire une opération d'augmentation appropriée pour les ensembles de données d'image au moins. La raison de toutes ces manigances de multitraitement est que les ensembles de données de vision doivent décoder et recadrer les images sur plusieurs cœurs. S'il y avait une opération qui s'occupait du décodage et des transformations d'image géométriques (redimensionnement, recadrage, cisaillement, affine) et produisait directement des tenseurs par lots, il n'y aurait pas du tout besoin d'utiliser le multitraitement, et d'autres étapes d'augmentation non géométriques (couleurs, blanchiment/normalisation, bruit) pourraient utiliser le parallélisme intra-op pour déchirer l'ensemble du tenseur. Des précautions doivent être prises lors de la conception d'une telle opération pour exposer les paramètres de transformation pour chaque échantillon dans le tenseur à l'extérieur, afin de permettre la transformation parallèle des annotations (boîtes englobantes, masques, points clés, etc.).
Ou mieux encore, faites-en un serveur, afin que plusieurs processus (ainsi que d'autres frameworks DL) puissent également l'utiliser.

@mprostock merci pour la grande explication!

Cependant, aucune solution n'a encore été proposée. Stocker des listes de noms de fichiers dans l'objet Dataset semble juste, alors comment peut-on les utiliser ? Est-ce que quelqu'un l'a compris?

@ 1e100 Je crois que @fmassa travaille sur l'ajout d'opérations d'augmentation d'image natives à torchvision , ce qui devrait résoudre ce problème.

Une mise à jour à ce problème?

L'utilisation de beaucoup de mémoire partagée a résolu le problème pour moi. Voici un hack pour définir la mémoire partagée dans le script au cas où vous exécuteriez le code dans un conteneur Docker et que vous ne parvenez pas à définir la mémoire partagée autrement :

os.system(f"mount -o remount,size={args.shared_memory_size} /dev/shm")

La taille de la mémoire partagée pourrait être par exemple la moitié de la RAM totale, disons `80G' pour une grosse machine.

J'ai trouvé une solution de contournement à l'erreur unable to open shared memory object </torch_22291_1137042840> in read-write mode associée à ce problème en modifiant le nombre de descripteurs de fichiers autorisés, bien que la mémoire _toujours_ rampe jusqu'à un certain point.

Pour vérifier votre nombre autorisé de descripteurs de fichiers, entrez ulimit -a dans bash et il sera sous la balise -n . Pour augmenter cette limite pour votre shell actuel (c'est-à-dire si vous n'avez pas d'autorisations pour le serveur), exécutez ce qui suit :
COUP : ulimit -n NEW_VALUE

Pour le changer pour l'ensemble du système, voir ici .

Donc, si je comprends bien, les processus de travail créent une copie de la longue liste de chemins de fichiers chaque fois qu'ils accèdent à la liste ? Mais alors cette copie temporaire ne sort-elle pas de la portée (et est-elle par conséquent détruite) dès que la fonction __getitem__ pour ce processus revient ? Pourquoi la consommation de RAM augmente-t-elle sans limite ?

Ce serait bien si quelqu'un faisait un petit guide avec quelques meilleures pratiques sur la façon d'éviter ce problème. Avec des valeurs numériques, il est facile de remplacer les listes Python par des tableaux NumPy, mais on ne sait pas exactement comment atténuer le problème avec des chaînes (de taille variable).

Dans mon cas, j'ai une liste d'objets de classe personnalisés qui sont créés/remplis dans le constructeur. Il ne contient essentiellement que des ensembles de chemins de fichiers. Ensuite, à l'intérieur __getitem__ , je charge ces images, fais un prétraitement, convertis en tenseurs de torche, puis cale explicitement del sur les images chargées avant de revenir. Le problème est que l'ajout d'étapes de prétraitement supplémentaires, apparemment inoffensives, introduit ce problème d'utilisation de la mémoire hors limites.

mp.shared_memory Py 3.8 peut fournir une solution de contournement assez agréable pour partager de nombreux objets non tenseurs/nparray, par exemple, une liste partagée : https://docs.python.org/3.8/library/multiprocessing.shared_memory.html #multiprocessing.shared_memory.ShareableList. :)

avis de non-responsabilité : je n'ai pas lu attentivement la documentation.

Pouvons-nous faire quelque chose d'action ici ? Avons-nous suffisamment de transformations d'image prises en charge dans torchvision pour documenter le déplacement de certains cas d'utilisation vers cela ?

Juste pour clarifier le point ici: la mise en œuvre de ce que @ 1e100 a proposé est quelque chose que nous avons dans la feuille de route de torchvision, mais ce n'est pas en haut de notre liste et nécessiterait probablement d'abord un support de tenseur imbriqué.

Cela étant dit, ce ne serait pas une solution générale à ce problème : cela contournerait simplement le besoin de traitement multiple dans le chargement des données en utilisant une approche différente (par exemple, des transformations sur le GPU).

cc @cpuhrsch car j'ai vu quelqu'un mentionner le tenseur imbriqué. (Au fait, @cpuhrsch , pouvez-vous créer une étiquette de module pour le tenseur imbriqué et vous y ajouter sur https://github.com/pytorch/pytorch/issues/24422 ?)

Pourquoi ce bug n'a-t-il pas été résolu en un an ?

@IMLHF Voir la première ligne de cette description du problème ou la discussion ci-dessus. Parce qu'il ne s'agit pas vraiment d'une fuite mais d'une conception malheureuse de python, qui nous échappe. Pytorch et numpy ont tous deux essayé de contourner ce problème en implémentant une sérialisation personnalisée pour les tenseurs et les ndarrays. Pourtant, nous ne pouvons pas vraiment rendre compte de la structure générale des données. Ceci est ouvert car nous mettons en œuvre davantage d'utilitaires pour les utilisateurs afin de contourner ce problème.

L'ajout torch.cuda.empty_cache() à chaque fin d'itération m'aide à résoudre ce problème. L'utilisation de la mémoire fluctue au lieu de s'accumuler après l'ajout de cela.

peut-être devrions-nous ajouter un avertissement.

@VitalyFedyunin avez-vous de la bande passante pour regarder ça ? Au minimum, pouvons-nous déterminer s'il s'agit du même problème que https://github.com/pytorch/pytorch/issues/17499 ?

Je pense avoir résolu ce problème en utilisant ndarray au lieu d'utiliser tenseur dans mon projet.

Mon code avant était

def df2var(x):
    return (torch.LongTensor(token2id(x['Query'], max_char = max_length_char)), 
            torch.tensor(coll2id[x['Agg_Coll']], dtype = torch.long))

class Making_Dataset(Dataset):
    def __init__(self, input_dataframe):
        self.dataset = input_dataframe.apply(lambda x : df2var(x), axis = 1)

    def __len__(self):
        return len(self.dataset)

    def __getitem__(self, data_index):
        return self.dataset[data_index]

Et j'ai reformé le code comme

class Making_Dataset(Dataset):
    def __init__(self, input_dataframe):
        self.text = np.array([token2id(q, max_char = max_length_char) for q in input_dataframe.Query])
        self.labels = np.array([coll2id[coll] for coll in input_dataframe.Agg_Coll])

    def __len__(self):
        return len(self.text)

    def __getitem__(self, data_index):
        return self.text[data_index], self.labels[data_index]

Après avoir corrigé le code, le problème d'augmentation de la mémoire à chaque époque avait disparu dans mon projet.
Parce que je ne sais pas exactement ce qui cause ce problème, tout commentaire à ce sujet est le bienvenu !

Je vois un problème similaire avec Torch 1.3.0 avec CUDA 10 sur Ubuntu 18.04. Ce n'était pas un problème sur une machine AWS avec 64 Go de RAM, mais sur une machine locale avec 128 Go de RAM et 128 Go d'échange, je ne peux même pas passer à travers 150 époques - l'utilisation de la mémoire ne cesse de passer de quelques Go (attendu) à 128 + Go.

Mettre à jour

Mon problème était un petit bogue insidieux - lors de l'enregistrement des statistiques d'entraînement, j'enregistrais des informations de gradient en plus des valeurs pures, ce qui est à la fois inutile et ajoute à votre empreinte mémoire à chaque époque.

mp.shared_memory de Py 3.8 peut fournir une solution de contournement assez agréable pour partager de nombreux objets non tenseurs/nparray, par exemple, une liste partagée
https://github.com/pytorch/pytorch/issues/13246#issuecomment -513480017

L'utilisation de beaucoup de mémoire partagée a résolu le problème pour moi
https://github.com/pytorch/pytorch/issues/13246#issuecomment -487042977

Salut, un peu en retard sur ce sujet, mais je suis confronté aux mêmes problèmes mais avec des dictionnaires.
Quelqu'un a-t-il eu du succès avec ces hacks?

C'est toujours un problème valable. Quelqu'un pourrait-il fournir la liste des meilleures pratiques pour utiliser plusieurs travailleurs dans DataLoaders sans provoquer de fuites de mémoire ?

@marrrcin Je pense que le mieux est de considérer les tenseurs comme chers, et vous devez donc être prudent quant à la quantité d'utilisation que vous en faites, surtout s'il y a une chance qu'ils aient des informations de gradient.

Par exemple, il vaut probablement la peine de tout stocker sous forme de listes ou numpy.ndarray s jusqu'à ce que vous ayez besoin d'effectuer des opérations torch

@AudreyBeard merci pour la réponse. Dans mon code Dataset, tout est stocké sous la forme numpy/lists/strings/int et la seule partie où j'utilise des tenseurs est __getitem__ et plus tard dans collate_fn (en appliquant un rembourrage). Dois-je créer des tenseurs avec requires_grad défini sur false ? Une fois que mon code entre dans num_workers> 0, la mémoire commence à fuir.

Désolé pour le mauvais formatage, je suis sur mobile.

@marrrcin En général, je ne convertis mes données d'entrée (l'image ou le signal ou autre) qu'en tensor dans __getitem__ . Mes étiquettes et autres sont généralement renvoyées sous forme de listes. Je ne sais pas quel type de données vous utilisez ou si vous faites un type particulier de rembourrage, mais j'utilise généralement torchvision.transforms dans mon __getitem__ . Pour ce que ça vaut, j'implémente très rarement un collate_fn personnalisé.

Une pensée : j'ai initialement posté ici parce que je vivais ce que je pensais être une fuite de mémoire. Il s'est avéré que je m'accrochais à des données inutiles à chaque époque, et cela avait pour symptômes une fuite alors qu'il s'agissait vraiment d'une gestion variable très subtile de ma part. Il m'a fallu un certain temps pour comprendre exactement ce qui se passait.

@AudreyBeard mon cas n'est pas lié aux images / torchvision, j'ai utilisé un rembourrage pour les jetons extraits de textes de longueur variable, c'est pourquoi j'ai besoin d'utiliser collate_fn .

class PaddingCollateFn:
    def __call__(self, batch):
        sorted_batch = sorted(batch, key=lambda x: x[0].shape[0], reverse=True)
        sequences = [x[0] for x in sorted_batch]
        sequences_padded = torch.nn.utils.rnn.pad_sequence(sequences, batch_first=True)

        attention_masks = [torch.tensor([1 for _ in x[0]]) for x in sorted_batch]

        attention_masks_padded = torch.nn.utils.rnn.pad_sequence(
            attention_masks, batch_first=True
        )
        lengths = torch.tensor([len(x) for x in sequences])
        labels = torch.tensor([x[1] for x in sorted_batch])

        return (sequences_padded, lengths, attention_masks_padded), labels

Dois-je supprimer les tenseurs d'origine après les avoir remplis (par exemple en utilisant del ) ? Je pensais qu'une fois les collate_fn terminés, ils seraient hors de portée et supprimés car il n'y aurait aucune référence à eux.

J'ai rencontré cela dans la version 1.3.1 de Pytorch....Lorsque je forme ImageNet...Quelqu'un a-t-il des idées ?
Dans mon cas, j'utilise num_workers de 24, normalement cela coûte environ 110G de mémoire à l'époque 1, mais quand j'entraîne la deuxième époque, cela coûtera toute ma mémoire, et le système tuera le chargeur de données ..... Je ne le fais vraiment pas ' je ne sais pas pourquoi....

Pour moi, le problème était que je convertissais déjà des tableaux numpy en tenseurs de torche dans le chargeur de données __getitem__

Les tableaux numpy ne doivent être convertis en tenseurs de torche que dans la boucle d'entraînement, juste avant d'être envoyés au modèle. Sinon, les tenseurs feront croître la mémoire partagée hors limites.

Vous pouvez surveiller la mémoire partagée en exécutant la commande watch -n .3 df -h
La mémoire partagée correspond à la ligne /dev/shm
La quantité utilisée ne doit pas augmenter après chaque époque.

j'ai le même problème

ce bogue n'est pas résolu dans pytorch 1.4.0

J'ai aussi le même problème

Moi aussi je rencontre le même problème, même après :
1) supprimer toutes les variables inutiles
2) en utilisant des tableaux numpy au lieu de listes
3) en utilisant gc.collect()

@annukkaa et autres : il ne suffit pas d'utiliser np.array(list_of_paths) car il stocke la liste des chaînes sous forme de nombreux objets. Utilisez np.array(list_of_paths).astype(np.string_) pour convertir le tableau en un tableau d'octets de forme carrée (et assurez-vous de convertir de bytes en str lorsque vous utilisez réellement la chaîne). Cela devrait aider. Réglez également la mémoire partagée sur une valeur élevée, par exemple 100 Go.

Je ne l'ai pas vu mentionné explicitement dans ce fil, alors j'ai pensé partager ma solution.
Dans mon cas, j'avais un objet de classe personnalisé et une liste de chaînes dans mon jeu de données auquel on accédait à chaque itération et qui épuisait rapidement la mémoire de mon processeur.
En enveloppant la classe et la liste à l'aide de l' objet Manager multitraitement qui gère les états partagés, j'ai pu éliminer la fuite de mémoire.

Pour faire le lien avec l'exemple minimal, le code ressemblerait à ceci.

from torch.utils.data import Dataset, DataLoader
import torch
from multiprocessing import Manager


class DataIter(Dataset):
    def __init__(self):
        manager = Manager()
        self.data = manager.list([x for x in range(24000000)])

    def __len__(self):
        return len(self.data)

    def __getitem__(self, idx):
        data = self.data[idx]
        return torch.tensor(data)


train_data = DataIter()
train_loader = DataLoader(train_data, batch_size=300,
                          shuffle=True,
                          drop_last=True,
                          pin_memory=False,
                          num_workers=18)

for i, item in enumerate(train_loader):
    if i % 1000 == 0:
        print(i)

Il y a des frais généraux puisque les objets sont décapés, mais c'est une bonne alternative à l'explosion de la mémoire.

Est-ce que cette chose va être réparée ????

Apparemment le sujet est toujours ouvert.

L'utilisation de ndarrays n'aide pas non plus. Il augmente la RAM du processeur environ 4 fois avec des travailleurs à zéro.
Essayé del mais pas d'amélioration significative.

Salut tout le monde,

Je viens d'essayer une solution pour cela et cela fonctionne comme une beauté absolue.

Pour moi, je stocke les données imagenet sous forme de tableaux numpy stockés localement.

J'ai écrit mon jeu de données personnalisé comme ---

`importer la torche
depuis torch.utils importer des données
importer numpy en tant que np

classe DataSetBuilder(data.Dataset):
"""Ensemble de données TinyImagenet."""

def __init__(self, rootpath, train=True, transform=None):
    """
    Args:
        rootpath: Path to the pytorch file with annotations.
        root_dir (string): Directory with all the images.
        transform (callable, optional): Optional transform to be applied
            on a sample.
    """
    self.path = rootpath
    self.transform = transform
    self.train = train
    # Load input data
    if self.train:
        self.X_ = np.load(self.path +'x_train.npy')
    else:
        self.X_ = np.load(self.path +'x_test.npy')
    # Load target data
    if self.train:
        self.y_ = np.load(self.path +'y_train.npy')
    else:
        self.y_ = np.load(self.path +'y_test.npy')

def __len__(self):
    if self.train:
        dataFile = self.path + 'x_train.npy'
    else:
        dataFile = self.path + 'x_test.npy'

    data = np.load(dataFile)
    return data.shape[0]

def __getitem__(self, idx):
    if torch.is_tensor(idx):
        idx = idx.tolist()
    X = self.X_[idx, :, :, :]
    y = self.y_[idx]
    if self.transform is not None:
        X = self.transform(X)
    return X, torch.from_numpy(y).type(torch.LongTensor)`

Au lieu de charger des données dans le __getitem__, je le charge au moment de la construction de l'objet, ce qui signifie qu'il ne charge pas à chaque fois les mêmes tableaux numpy en mémoire, mais plutôt le charge immédiatement au moment de la création de l'objet.

J'espère que cela t'aides!

Laissez un commentaire si cela fonctionne pour vous... :-)

Salut @varinder-singh,
C'est bien que vous ayez trouvé une solution. Je ne vois pas en quoi cela est différent de l'exemple numpy donné par @bfreskura plus tôt ? Votre __getitem__ tranche également les données d'un tableau numpy.
Peut-être que je lis mal le code, pourriez-vous m'expliquer pourquoi ils affecteraient différemment la consommation de mémoire?

Après avoir rencontré ce problème dans mon projet actuel et avoir lu ce fil, je pense qu'il pourrait être utile d'ajouter mes propres réflexions et de fournir une solution quelque peu polyvalente.

Tout d'abord:

1) Compte tenu de tout ce que je peux observer ici, le diagnostic de @mprostock est correct. Votre travail m'a fait gagner beaucoup de temps à creuser par moi-même.
2) Bien sûr, la réponse de @soumith est également correcte, mais elle n'est pas applicable dans ce cas pour les raisons indiquées par le post ultérieur de @mprostock sur les tableaux d'objets.

Ce n'est pas un problème de pyTorch. C'est un problème Python et, par conséquent, il devrait y être résolu. Mais comme le problème est causé par le comptage des références, qui fait partie intégrante de la gestion de la mémoire de Python, cela pourrait ne pas se produire de si tôt. Certaines des solutions de contournement proposées ci-dessus sont intéressantes, mais pourquoi aller aussi loin ? En supposant que la tâche consiste à accéder conjointement à un certain nombre de séquences de longueur variable comme des noms de fichiers, vous n'avez pas besoin d'inventer quoi que ce soit de nouveau. Utilisez simplement numpy pour emballer les séquences et effectuer une recherche indirecte. Pour comprendre ce que je veux dire, voir le code ci-dessous, qui évite complètement le problème discuté dans ce fil.

@mprostock et @smolendawid Les chaînes ne sont essentiellement que des séquences d'entiers, un type qui peut être facilement géré dans numpy. L'exemple ci-dessous est conçu pour partager n'importe quelle liste de chaînes (par exemple, les noms de fichiers d'images) entre plusieurs chargeurs de données.
@marrrcin Vous avez demandé une meilleure pratique. Ceci est robuste et fonctionne pour n'importe quelle liste de séquences de longueur variable. Dans mon projet actuel, j'utilise une variante légèrement plus sophistiquée pour les données multidimensionnelles où chaque dimension a une longueur variable.
@SsnL Cela résout implicitement le problème dont vous discutez avec @zhiweifang dans /issues/20433 sans utiliser de constructions sophistiquées Python 3.8.

import numpy as np
import torch
from typing import Union

# --- UTILITY FUNCTIONS ---
def string_to_sequence(s: str, dtype=np.int32) -> np.ndarray:
    return np.array([ord(c) for c in s], dtype=dtype)

def sequence_to_string(seq: np.ndarray) -> str:
    return ''.join([chr(c) for c in seq])

def pack_sequences(seqs: Union[np.ndarray, list]) -> (np.ndarray, np.ndarray):
    values = np.concatenate(seqs, axis=0)
    offsets = np.cumsum([len(s) for s in seqs])
    return values, offsets

def unpack_sequence(values: np.ndarray, offsets: np.ndarray, index: int) -> np.ndarray:
    off1 = offsets[index]
    if index > 0:
        off0 = offsets[index - 1]
    elif index == 0:
        off0 = 0
    else:
        raise ValueError(index)
    return values[off0:off1]


# --- OUR DATASET CODE STARTS HERE ---
class MyDataset(torch.utils.data.Dataset):

    def __init__(self):
        strings = [
            'I like', # You can use np.int8 for ASCII strings.
            'chocolate',
            '我喜欢', # If you use anything that is not standard ASCII,
            '巧克力', # need to use np.int16, or even np.int32.
        ]

        # Convert each string to sequence of codepoints (integer),
        # and then pack them into a numpy array.
        seqs = [string_to_sequence(s) for s in strings]
        self.strings_v, self.strings_o = pack_sequences(seqs)

    def __len__(self): return 4

    def __getitem__(self, i):
        # Use indirect lookup to fetch the i-th sequence. This only uses integer numpy
        # array lookups, which avoids that the objects are subsequently replicated by
        # child processes.
        seq = unpack_sequence(self.strings_v, self.strings_o, i)
        string = sequence_to_string(seq)
        # ACTION NEEDED: You probably do not want to return the string itself ;-).
        return string


m = MyDataset()
for i in range(len(m)):
    print(i, '=', m[i])

# Output
# -------
# 0 = I like
# 1 = chocolate
# 2 = 我喜欢
# 3 = 巧克力

J'ai pu résoudre ce problème et j'aime partager mes 2 centimes. J'ai essentiellement suivi les idées soulignées par @harpone et d'autres selon lesquelles les chaînes étaient le problème. J'ai eu 2 arguments problématiques dans ma classe Dataset :

  1. un tableau numpy de chaînes (le casting à l'aide de .astype(str) n'a pas aidé)
  2. un dictionnaire des chaînes aux vecteurs numpy

J'ai dû réparer à la fois 1 et 2 pour arrêter la fuite de mémoire. Pour 1, mes chaînes sont en fait des hachages pour accéder aux vecteurs numpy du dictionnaire, j'ai donc converti toutes les chaînes en entiers puisque j'avais un dictionnaire de taille fixe.

Pour 2, j'ai converti le dictionnaire pour utiliser des clés entières, mais la fuite de mémoire persistait toujours. Ce qui a fonctionné n'était en fait pas du tout de transmettre le dictionnaire à la classe Dataset, mais simplement de renvoyer la clé entière dans __getitem___ et de faire l'indexation/pormotion du dictionnaire vers le tenseur Pytorch/la promotion vers le GPU dans ma boucle de train.

Un moyen de faire en sorte que les processus du chargeur de données se réinitialisent à chaque époque et nettoient toute cette mémoire perdue ?

@Pozimek ils réinitialisent déjà chaque époque.

Alors, quelle est la meilleure pratique MAINTENANT ?

@wangchust : la solution proposée par @bashimao a fonctionné à merveille pour moi, même sur des ensembles de données modérément volumineux (plus de 25 millions de séquences de texte).

@wangchust : la solution proposée par @bashimao a fonctionné à merveille pour moi, même sur des ensembles de données modérément volumineux (plus de 25 millions de séquences de texte).

Moi aussi. La solution de @bashimao fonctionne très bien.

Bonjour à tous, je suis de nouveau ici. Quelqu'un rencontre-t-il "OverflowError : impossible de sérialiser un objet d'octets supérieur à 4 Gio" lorsque les travailleurs du chargeur de données quittent le processus principal ?

Bonjour à tous, je suis de nouveau ici. Quelqu'un rencontre-t-il "OverflowError : impossible de sérialiser un objet d'octets supérieur à 4 Gio" lorsque les travailleurs du chargeur de données quittent le processus principal ?

@wangchust Si vous sérialisez, vous faites probablement quelque chose de mal. Chaque processus désérialisera les 4 ou la taille de votre objet en gigaoctets et reconstruira les objets sérialisés. Par conséquent, vous répliquerez la mémoire et finirez par en manquer s'il existe de nombreux processus parallèles. Tout l'intérêt des mesures proposées par moi-même et par d'autres dans ce fil est d'éviter de reproduire la mémoire. Comme dit dans ma première phrase, je crois que vous faites quelque chose de mal, probablement à un niveau assez fondamental.

Un tableau de chaînes basé sur un tenseur personnalisé semble aider https://gist.github.com/vadimkantorov/86c3a46bf25bed3ad45d043ae86fff57 :

import torch

class TensorBackedImmutableStringArray:
    def __init__(self, strings, encoding = 'utf-8'):
        encoded = [torch.ByteTensor(torch.ByteStorage.from_buffer(s.encode(encoding))) for s in strings]
        self.cumlen = torch.cat((torch.zeros(1, dtype = torch.int64), torch.as_tensor(list(map(len, encoded)), dtype = torch.int64).cumsum(dim = 0)))
        self.data = torch.cat(encoded)
        self.encoding = encoding

    def __getitem__(self, i):
        return bytes(self.data[self.cumlen[i] : self.cumlen[i + 1]]).decode(self.encoding)

    def __len__(self):
        return len(self.cumlen) - 1

    def __list__(self):
        return [self[i] for i in range(len(self))]

Peut-être que quelque chose comme ça vaut même la peine d'être inclus dans le noyau PyTorch

Quelqu'un a-t-il réussi à faire fonctionner les dictionnaires sans fuite ?
J'ai vu ce post ci-dessus , mais j'aimerais avoir accès à un type de table de hachage dans le travailleur au lieu de faire le travail en dehors de celui-ci, comme le suggère ce commentaire.

J'envisage l'un des éléments suivants :

  • dict du gestionnaire de multitraitement
  • mémoire partagée ou mmap
  • table de hachage maison basée sur numpy sans aucun pyobjects.

La mémoire partagée semble être l'option la plus prometteuse et native de Python. Je suis curieux de savoir pourquoi vous utilisez dict? Le modèle commun ici est d'avoir une liste d'éléments (généralement des chaînes) et de les indexer.

Pour moi, c'était à l'origine une liste de dicts (une liste d'exemples de métadonnées, chaque exemple était un dict)

J'ai compris. Généralement, les dicts le rendent encore plus difficile, car le modèle d'accès à la mémoire n'est pas séquentiel. Je pense à ajouter la prise en charge des structures de données Fork-safe (je ne sais pas s'il s'agit d'un référentiel intégré ou séparé).

La mémoire partagée semble être l'option la plus prometteuse et native de Python. Je suis curieux de savoir pourquoi vous utilisez dict? Le modèle commun ici est d'avoir une liste d'éléments (généralement des chaînes) et de les indexer.

@VitalyFedyunin Merci pour le conseil. Je peux d'abord essayer la mémoire partagée.
La raison d'un dict est en ce moment pour la recherche O (1) d'éléments pour une fonction d'échantillonnage aléatoire dans l'étape de génération de données. Plus précisément, "triplet mining" où le dict est indexé sur user_id, et les valeurs sont une liste d'exemples positifs associés à cet utilisateur. Voir ici pour un exemple.

@marrrcin En général, je ne convertis mes données d'entrée (l'image ou le signal ou autre) qu'en tensor dans __getitem__ . Mes étiquettes et autres sont généralement renvoyées sous forme de listes. Je ne sais pas quel type de données vous utilisez ou si vous faites un type particulier de rembourrage, mais j'utilise généralement torchvision.transforms dans mon __getitem__ . Pour ce que ça vaut, j'implémente très rarement un collate_fn personnalisé.

Une pensée : j'ai initialement posté ici parce que je vivais ce que je pensais être une fuite de mémoire. Il s'est avéré que je m'accrochais à des données inutiles à chaque époque, et cela avait pour symptômes une fuite alors qu'il s'agissait vraiment d'une gestion variable très subtile de ma part. Il m'a fallu un certain temps pour comprendre exactement ce qui se passait.

@AudreyBeard merci. cela a été utile et a résolu mon problème.

Ce qui m'intéresse, c'est (1) pourquoi le shuffle a un tel impact sur la consommation de mémoire et (2) pourquoi l'utilisation totale de la mémoire semble être bien supérieure au nombre de processus * taille de l'attribut de données.

Dans l'exemple de @bfreskura , la taille de self.data est de 24e7 entiers, soit environ 1,83 Go. Si nous ramenons cela à 24e5 (afin que le script puisse s'exécuter rapidement jusqu'à la fin), la taille de l'objet de données est d'environ 18,92 Mo.

Dans le cas de la liste Python, si set shuffle=False, je mesure que le processus consomme 298,17 Mo. J'ai ensuite mis shuffle=True, je mesure que le processus consomme 1,44 Go.

Ainsi, plus de 18 processus de travail + 1 processus parent principal, même si toutes les données sont copiées dans chaque processus, cela ne devrait représenter au maximum que 359,48 Mo de RAM supplémentaires. Comment se fait-il que lorsque shuffle=True j'obtienne presque 4 fois ce montant ? J'imagine que cela doit avoir à voir avec l'accès mémoire séquentiel ou aléatoire et les défauts de page qui en résultent, mais je suis curieux de savoir si quelqu'un peut décrire plus précisément ce qui se passe ici.

Pour référence mes modifications (fire CLI + rapport de consommation de mémoire) au script de @bfreskura sont ici :

https://gist.github.com/Erotemic/3f017de31529dc64c1a54948f37da1d5

L'accès aléatoire forcera Python à réécrire les compteurs d'objets dans la mémoire, provoquant une copie sur écriture des trames mémoire. L'accès séquentiel peut potentiellement être optimisé en n'écrivant pas un compteur inchangé (dépend probablement des cycles GC). De plus, il est beaucoup plus sûr (jusqu'à présent) d'estimer par utilisation maximale qui est le nombre de travailleurs * (toutes les tailles d'objets + nombre d'objets * pointeur d'objet python + taille de compteur). Nous travaillons actuellement sur la solution pour empêcher la copie complète de la mémoire, mais cela nécessite une ré-architecture importante et prendra du temps.

@VitalyFedyunin merci pour l'explication, mais j'ai bien peur de ne pas encore tout comprendre :sourire:

J'ai réussi à résoudre le problème ci-dessus en utilisant un tableau numpy au lieu d'une liste, et par exemple un tableau d'octets numpy carré de type np.string_ , mais maintenant je suis confronté à un problème apparemment similaire avec webdataset (https:/ /github.com/tmbdev/webdataset/issues/24#issuecomment-709101119). Apparemment, je ne manque pas de shm, mais comme @tmbdev l' a souligné plus tôt dans le fil du jeu de données Web, le problème pourrait être le _nombre_ de segments de mémoire partagée...

Avez-vous des conseils sur la façon de déboguer ce problème et/ou des hacks temporaires autour de lui ? J'ai essayé ipcs mais cela ne montre rien d'utile pour moi (je pense). lsof /dev/shm affiche des informations sur les objets et les tailles shm, mais je ne suis pas sûr de ce qu'ils signifient...

Pour moi, mesurer proportional set size (pss dans psutil) a aidé à mesurer la taille du problème. J'ai travaillé autour de cela par des classes personnalisées StringArray et DictArray: https://gist.github.com/vadimkantorov/86c3a46bf25bed3ad45d043ae86fff57 qui emballent des chaînes/dicts dans des tenseurs plats (pas de NumPy utilisé)

@wangchust : la solution proposée par @bashimao a fonctionné à merveille pour moi, même sur des ensembles de données modérément volumineux (plus de 25 millions de séquences de texte).

Désolé peut-être qu'il me manque quelque chose sur l'utilisation de github mais je ne vois aucune solution de @bashimao sur ce fil, juste un commentaire. Quelqu'un pourrait-il m'indiquer s'il vous plait ?

Beaucoup plus simple de simplement convertir en np.string_ (notez PAS str etc.). Dis que tu as

strings = ['hello', 'world']

alors fais

strings_byte = np.array(strings).astype(np.string_)

Le résultat sera alors un seul tableau d'octets carrés (notez le dtype):

array([b'hello', b'world'], dtype='|S5')

Vous devez ensuite encoder à nouveau en chaîne lorsque vous choisissez une chaîne à partir de celle-ci, par exemple str(strings_byte[0], encoding='utf-8') .

Notez que cela ne fonctionnerait pas :

strings_byte = np.array(strings).astype(str)

notez le dtype:

array(['hello', 'world'], dtype='<U5')

Ce n'est pas un tableau d'octets carrés , c'est-à-dire pas un seul objet.

Compte tenu de la persistance de ce problème et du nombre de fois où mes collègues ou moi-même avons été confrontés à ce problème, il serait utile d'avoir une recette pour déterminer si c'est la cause ou non. Après avoir lu ce fil assez attentivement, il semble qu'il y ait de bonnes suggestions pour atténuer le problème (https://github.com/pytorch/pytorch/issues/13246#issuecomment-436632186, https://github.com/pytorch/pytorch/issues /13246#issuecomment-612396143), mais aussi certains comportements déroutants (https://github.com/pytorch/pytorch/issues/13246#issuecomment-708067670).

  1. Est-il suffisant d'exécuter le chargeur de données seul dans une boucle while True et d'observer l'utilisation de la mémoire pour exclure ce problème ? Je suppose que si la mémoire augmente au fur et à mesure que la boucle s'exécute, nous pouvons conclure que l'objet de notre ensemble de données a un comportement pathologique qui accumule des objets ou que nous rencontrons ce problème?
  2. Ce que je ne comprends vraiment pas dans ce fil, c'est pourquoi c'est un problème si vous avez des classes d'ensembles de données qui ne contiennent que quelques Mo de données. Si je comprends bien, le problème décrit ici est que tous les objets python auxquels l'ensemble de données accède seront éventuellement copiés dans la mémoire d'un thread de travail. Si j'ai une classe de jeu de données simple qui charge des vidéos à partir d'une liste de chemins stockés sous forme de champ dans la classe de jeu de données, pourquoi cela poserait-il un problème ? Les données partagées sont si petites qu'elles sont négligeables. Pourquoi l'utilisation de shuffle=True dans le chargeur de données entraîne-t-elle une utilisation de la mémoire aussi élevée, comme indiqué dans https://github.com/pytorch/pytorch/issues/13246#issuecomment -708067670 ?

Une solution qui fonctionne pour moi - https://t.me/snakers4/2577

Une solution qui fonctionne pour moi - https://t.me/snakers4/2577

C'est sympa! Je suppose que le seul avantage de ma méthode dans https://gist.github.com/vadimkantorov/86c3a46bf25bed3ad45d043ae86fff57 est que les objets remplis de tenseurs peuvent être partagés entre les travailleurs DDP avec des primitives DDP (c'est-à-dire que nous lisons un objet de jeu de données géant dans un seul thread, puis dispersez l'objet d'ensemble de données rempli de tenseurs vers d'autres rangs DDP). De la même manière, le maître de travail DDP peut rassembler des tableaux de chaînes remplis de tenseurs à partir des rangs DDP.

Une autre occurrence réelle de ce bogue : https://github.com/NVIDIA/NeMo/issues/1467

Cette page vous a été utile?
0 / 5 - 0 notes