DOMANDA [Python] Memoria utilizzata cresce fino a bloccare il PC

dgcross

Utente Attivo
1,279
342
CPU
Ryzen 5 5600X
Dissipatore
EK Supremacy EVO (2x Alphacool NexXxoS ST30 360mm)
Scheda Madre
ASRock X370 Gaming K4
HDD
Samsung 980 Pro 512GB, Samsung 870 EVO 2TB, Goodram CX300 480GB, Toshiba P300 3TB
RAM
2x8+2x16 G.Skill TridentZ RGB @3400MHz CL16
GPU
Galax 2080 Ti HOF OC Lab + liquido custom (2x Alphacool NexXxoS ST30 360mm)
Audio
Alientek D8, Fiio Q1 Mark II, Mission LX-1, Sennheiser HD598 SE
Monitor
Samsung U28E590D
PSU
Corsair AX860i
Case
Lian Li O11 Dynamic
Periferiche
Ozone Strike Pro Spectra, Steelseries Rival 500, DualSense
Net
Tiscali
OS
Windows 10 Pro
Ciao a tutti,
ho un problema durante il training di un agente DDQN: da quando ho inserito il prioritized sweeping la memoria utilizzata cresce all'infinito e impalla tutto.
So quindi all'incirca dove avviene questa esplosione, ma leggendo il codice non trovo il punto esatto.

Ho usato il comando sys._debugmallocstats() e ho visto che alloca un sacco di spazio a 8 bit e 96 bit, però più di questo non so.
C'è un modo di capire esattamente che riga genera questa esplosione? Se può essere utile a consigliarmi una soluzione ho installato Python 3.7 su un PC con Windows 7 e uso PyCharm.

Grazie
 

Andretti60

Utente Èlite
6,440
5,091
Sfortunatamente non e' possibile, o almeno, non e' facile. Python gestisce la memoria a modo suo (ossia la libera quando riconosce che non e' piu' usata), quindi trovare "memory leak" non e' facile.
Nel tuo caso e' difficile dire cosa stia succedendo, anche perche' non ci dai il sorgente.
Se chiami librerie esterne e' possibile che il problema sia al loro interno (sono in genere scritte in C)
Ho visto usare con successo la libreria objgraph, ma non la ho mai provata (i miei progetti Python sono sempre stati molto semplici)

Quello che puoi fare e' mettere un sacco di debugmallocstats nel tuo codice, cosi' puoi monitorare lo stato della memoria vedere quando e dove diverge. Inizia con il metterli all'inizio e alla fine di ogni funzione che hai definito, finche' non trovi quella che ha problemi, poi comincia a metterli all'interno delle funzioni stesse, e continua cosi' finche' non trovi la linea (o il gruppo do codice) che genera problemi.

Ma ricorda, Python non libera la memoria subito appena non viene piu' usata, e' il garbage collector che lo fa e non si sa quando "decide" di farlo, per cui devi sempre aspettare un po' per essere certo che la memoria sia effettivamente liberata. In genere questi problemi si risolvono esaminando il codice, guardando dove viene allocata la memoria e vedere se vi siano possibili condizioni per cui la memoria non viene liberata. Io ho lo stesso problema usando C#

Buona fortuna...
 

dgcross

Utente Attivo
1,279
342
CPU
Ryzen 5 5600X
Dissipatore
EK Supremacy EVO (2x Alphacool NexXxoS ST30 360mm)
Scheda Madre
ASRock X370 Gaming K4
HDD
Samsung 980 Pro 512GB, Samsung 870 EVO 2TB, Goodram CX300 480GB, Toshiba P300 3TB
RAM
2x8+2x16 G.Skill TridentZ RGB @3400MHz CL16
GPU
Galax 2080 Ti HOF OC Lab + liquido custom (2x Alphacool NexXxoS ST30 360mm)
Audio
Alientek D8, Fiio Q1 Mark II, Mission LX-1, Sennheiser HD598 SE
Monitor
Samsung U28E590D
PSU
Corsair AX860i
Case
Lian Li O11 Dynamic
Periferiche
Ozone Strike Pro Spectra, Steelseries Rival 500, DualSense
Net
Tiscali
OS
Windows 10 Pro
Grazie per la risposta. Faccio un po' di verifiche e poi carico il pezzo di codice incriminato. Speriamo bene
Post unito automaticamente:

Forse ho risolto, il problema non era nell'agente, ma la mia modifica credo ne abbia accelerato l'insorgenza, per questo che mi era sfuggito.
Nel file da cui lancio il training ho aggiunto un del experiment e ora sembra che la quantità di memoria si stabilizzi.
In poche parole ad ogni ciclo creavo un oggetto experiment e poi al passaggio successivo lo ricreavo assegnandogli lo stesso nome, ma credo che il vecchio oggetto continuasse a persistere anche senza puntatori verso di lui.

Di seguito una versione un po' tagliata del file incriminato altrimenti era troppo lunga (dovrebbero essere coerenti i tagli, anche se non ho provato a lanciarla). La riga che ho aggiunto è verso il fondo.


Python:
# -*- coding: utf-8 -*-

import sys
import numpy as np
import os
import shutil
import datetime
import random as rnd
import yaml
import io


# MIE CLASSI
import lib.utilities as ut
import lib.envs.environmentBookIA as book_env_BookIA
from lib.simulation import Experiment
from lib.agents.AgentDDQN import DoubleDQLearningAgent
import lib.tabella as tab

if "../" not in sys.path:
    sys.path.append("../")


def apprendi(file_config='parametri.yaml'):
    global env
    global agent
    global experiment
    global state_list

    tempo_scaduto = False

    param = ut.carica_parametri(file_config)
    environment_type = param['environment']
    tag = param['tag']
    config_agente = param['parametri_personalizzati']
    num_epoche = param['epoche']
    agente = param['agente']
    t_stop = param['stop_temporale']
    stop_temporale = datetime.datetime(t_stop[0], t_stop[1], t_stop[2], t_stop[3], t_stop[4])
    log_path = 'Output\\' + environment_type + '-' + agente + tag + '\\'
    if not os.path.exists(log_path):
        os.makedirs(log_path)
    with io.open(log_path + 'Parametri-' + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M") + '.yaml', 'w',
                 encoding='utf8') as outfile:
        yaml.dump(param, outfile, default_flow_style=False, allow_unicode=True)
    cartella_ordini = param['training_set_ordini']
    training_set = os.listdir(cartella_ordini)
    cartella_eseguiti = param['training_set_eseguiti']
    tot_round = param['tot_round']  # per epoca
    num_round = int(tot_round / len(training_set))
    ut.timestamp()
    da_stampare = []
    storico_performance = dict()
    file_training_set = open('Output\\' + environment_type + '\\TrainingSet.txt', 'w')
    with open('LOG\\log_agente.txt', 'w') as f:
        f.write('')
    prima_epoca = True
    tabelle_elaborate = []
    ordine_file = []

    for i, file in enumerate(training_set):
        print(file)
        file_training_set.write(file + '\n')
        tab_stati = tab.TabellaStati(cartella_ordini + '\\' + file, cartella_eseguiti, file_config)
        tabelle_elaborate.append(tab_stati)
        ordine_file.append(i)
        
    totale_step = 0
    for epoca in range(0, num_epoche):
        step_epoca = 0
        if tempo_scaduto is True:
            print('APPRENDIMENTO INTERROTTO')
            break
        print('Epoca: #', epoca, '\n')
        rnd.shuffle(training_set)
        rnd.shuffle(ordine_file)
        ut.timestamp()
        for i, file in enumerate(training_set):
            if datetime.datetime.now() > stop_temporale:
                tempo_scaduto = True
                print('TEMPO SCADUTO')
                break
            tab_stati = tabelle_elaborate[ordine_file[i]]
            if environment_type == 'RewardMidAgentSellerFixedStop':
                env = book_env_BookIA.Seller(file_config, tab_stati)
            elif environment_type == 'RewardMidAgentBuyerFixedStop':
                env = book_env_BookIA.Buyer(file_config, tab_stati)

            if prima_epoca and i == 0:
                if config_agente is None:
                    config_agente = 'lib\\agents\\AgentDDQN_param.yaml'
                agent = DoubleDQLearningAgent(config_agente, list(range(env.action_space.n)),
                                              env.state_space_dim, path=log_path)
                state_list = ut.leggi_parametro('stati_da_loggare', config_agente)

                if env.state_space_dim == len(state_list[0]):
                    print("\nCurrent initial target QValues:\n")
                    agent.printerTarget.printValues()

            print('File # ', i, ' ', file)
            experiment = Experiment(env, agent)
            risultati = experiment.run_qlearning(num_round)

            print("Last average return: ", risultati, "   Last agent epsilon:  ", agent.get_epsilon())
            step_epoca += experiment.get_step_num()
            print('Numero step eseguiti: ', experiment.get_step_num(), "\n")
            da_stampare = np.append(da_stampare, risultati)
            storico_performance.setdefault(file, risultati)

        if prima_epoca:
            prima_epoca = False
            ordine_file = list(range(len(tabelle_elaborate)))
        print("Current target QValues:\n")
        if env.state_space_dim == len(state_list[0]):
            agent.printerTarget.printValues()

        print("Numero step eseguiti nell'epoca: ", step_epoca, "\n")
        totale_step += step_epoca

        if agente == 'DoubleDeepQLearn':
            print("Number of samples stored so far: ", agent.get_number_of_saved_samples())
    shutil.copy('LOG\\log_agente.txt', 'LOG\\log_agente-' + agent.nome + '-' +
                datetime.datetime.now().strftime("%Y-%m-%d-%H-%M") + '-funct.txt')
    with open('LOG\\log_agente.txt', 'w') as f:
        f.write('')

    experiment.close()
    del experiment

    agent.backup()

    with open('risultati.txt', 'a') as f:
        f.write('%s\n' % da_stampare)
    with open('sintetici.txt', 'a') as f:
        sintetici = [np.mean(da_stampare), np.std(da_stampare), len(da_stampare)]
        f.write('%s\n' % sintetici)

    print("Numero totale step eseguiti per training: ", totale_step)
    file_training_set.close()
    ut.timestamp()


if __name__ == '__main__':
    if len(sys.argv) == 2:
        apprendi(sys.argv[1])
    elif len(sys.argv) == 1:
        apprendi('parametri.yaml')
        print("Using standard configuration file \"parametri.yaml\" ")
    else:
        print("One parameter only!")
 
Ultima modifica:

Andretti60

Utente Èlite
6,440
5,091
Penso che il tuo problema sia nella dichiarazione delle variabili di tipo “global” in quanto il loro contatore di referenza non viene mai decrementato e quindi la loro allocazione viene liberata solo quando il programma finisce. È pericoloso usarle, vanno create e lasciate poi stare.
 
  • Mi piace
Reazioni: dgcross

dgcross

Utente Attivo
1,279
342
CPU
Ryzen 5 5600X
Dissipatore
EK Supremacy EVO (2x Alphacool NexXxoS ST30 360mm)
Scheda Madre
ASRock X370 Gaming K4
HDD
Samsung 980 Pro 512GB, Samsung 870 EVO 2TB, Goodram CX300 480GB, Toshiba P300 3TB
RAM
2x8+2x16 G.Skill TridentZ RGB @3400MHz CL16
GPU
Galax 2080 Ti HOF OC Lab + liquido custom (2x Alphacool NexXxoS ST30 360mm)
Audio
Alientek D8, Fiio Q1 Mark II, Mission LX-1, Sennheiser HD598 SE
Monitor
Samsung U28E590D
PSU
Corsair AX860i
Case
Lian Li O11 Dynamic
Periferiche
Ozone Strike Pro Spectra, Steelseries Rival 500, DualSense
Net
Tiscali
OS
Windows 10 Pro
Ti ringrazio, avevo inserito la dicitura global su consiglio di PyCharm, perché non mi comparisse il warning.
Eviterò di usarle d'ora in poi :ok:
 

dgcross

Utente Attivo
1,279
342
CPU
Ryzen 5 5600X
Dissipatore
EK Supremacy EVO (2x Alphacool NexXxoS ST30 360mm)
Scheda Madre
ASRock X370 Gaming K4
HDD
Samsung 980 Pro 512GB, Samsung 870 EVO 2TB, Goodram CX300 480GB, Toshiba P300 3TB
RAM
2x8+2x16 G.Skill TridentZ RGB @3400MHz CL16
GPU
Galax 2080 Ti HOF OC Lab + liquido custom (2x Alphacool NexXxoS ST30 360mm)
Audio
Alientek D8, Fiio Q1 Mark II, Mission LX-1, Sennheiser HD598 SE
Monitor
Samsung U28E590D
PSU
Corsair AX860i
Case
Lian Li O11 Dynamic
Periferiche
Ozone Strike Pro Spectra, Steelseries Rival 500, DualSense
Net
Tiscali
OS
Windows 10 Pro
Bene, è saltato fuori che oltre a quella già corretta, vi è una seconda fonte di memory leakage.
Ho isolato 2 funzioni in cui dovrebbe risiedere, ma purtroppo sono complementari e quindi non posso disabilitarne una alla volta per capire dove stia.

Riporto il codice qua sotto
Python:
    def sample_minibatch(self, minibatch_size):
        sampled_indices = []
        for k in range(minibatch_size):
            sampled_indices.append(np.random.randint(self.sampling_boundaries[k], self.sampling_boundaries[k+1]))
        sampled_indices = np.array(sampled_indices)

        minibatch = np.array(self.samples[sampled_indices], dtype=self.samples.dtype)
        self.samples = np.delete(self.samples, sampled_indices)

        return minibatch
    
    def reinserisci_in_memoria(self, ricordo, indice_inf, indice_sup):
        mezzo = int((indice_inf+indice_sup)/2)
        if ricordo[0] <= self.samples['td_error'][indice_inf]:
            self.samples = np.array(np.hstack((self.samples.data[:indice_inf], ricordo, self.samples.data[indice_inf:])), dtype=self.samples.dtype)
            nuovo_inizio = indice_inf
        elif ricordo[0] >= self.samples['td_error'][indice_sup - 1]:
            self.samples = np.array(np.hstack((self.samples.data[:indice_sup], ricordo, self.samples.data[indice_sup:])), dtype=self.samples.dtype)
            nuovo_inizio = indice_sup
        else:
            if self.samples['td_error'][mezzo] <= ricordo[0] <= self.samples['td_error'][mezzo + 1]:
                self.samples = np.array(np.hstack((self.samples.data[:mezzo+1], ricordo, self.samples.data[mezzo + 1:])), dtype=self.samples.dtype)
                nuovo_inizio = mezzo
            elif ricordo[0] < self.samples['td_error'][mezzo]:
                nuovo_inizio = self.reinserisci_in_memoria(ricordo, indice_inf, mezzo)
            elif ricordo[0] > self.samples['td_error'][mezzo + 1]:
                nuovo_inizio = self.reinserisci_in_memoria(ricordo, mezzo + 1, indice_sup)
            else:
                raise Exception('Non ho trovato dove inserire nella memoria')
        return nuovo_inizio
Post unito automaticamente:

dopo numerosi tentativi, credo che il problema potrebbe essere nella funzione hstack dentro a reinserisci_in_memoria

ho provato a modificare leggermente quel punto, vediamo come va

intanto @Andretti60 se ti è balzato agli occhi altro dimmi pure. Alla fine ho iniziato un anno fa come autodidatta con Python e probabilmente di cavolate ne faccio a iosa :D
 
Ultima modifica:

dgcross

Utente Attivo
1,279
342
CPU
Ryzen 5 5600X
Dissipatore
EK Supremacy EVO (2x Alphacool NexXxoS ST30 360mm)
Scheda Madre
ASRock X370 Gaming K4
HDD
Samsung 980 Pro 512GB, Samsung 870 EVO 2TB, Goodram CX300 480GB, Toshiba P300 3TB
RAM
2x8+2x16 G.Skill TridentZ RGB @3400MHz CL16
GPU
Galax 2080 Ti HOF OC Lab + liquido custom (2x Alphacool NexXxoS ST30 360mm)
Audio
Alientek D8, Fiio Q1 Mark II, Mission LX-1, Sennheiser HD598 SE
Monitor
Samsung U28E590D
PSU
Corsair AX860i
Case
Lian Li O11 Dynamic
Periferiche
Ozone Strike Pro Spectra, Steelseries Rival 500, DualSense
Net
Tiscali
OS
Windows 10 Pro
il problema risiedeva nell'uso di self.samples.data.
tolto il .data tutto è andato a posto :ok:

ora devo solo velocizzare il codice che è lentissimo per via delle dimensioni variabili della memoria, ma questa è un'altra storia :patpat:
 

Andretti60

Utente Èlite
6,440
5,091
ora devo solo velocizzare il codice che è lentissimo per via delle dimensioni variabili della memoria, ma questa è un'altra storia :patpat:
Python e' per natura lento, e' un linguaggio interpretato e non ottimizzato. In genere le parti del programma che richiedono ottimizzazione vengono scritte come librarie usando il linguaggio C, richiamate poi dal programma in Python. Per questo motivo Python viene spesso classificato come "glue language".
 

dgcross

Utente Attivo
1,279
342
CPU
Ryzen 5 5600X
Dissipatore
EK Supremacy EVO (2x Alphacool NexXxoS ST30 360mm)
Scheda Madre
ASRock X370 Gaming K4
HDD
Samsung 980 Pro 512GB, Samsung 870 EVO 2TB, Goodram CX300 480GB, Toshiba P300 3TB
RAM
2x8+2x16 G.Skill TridentZ RGB @3400MHz CL16
GPU
Galax 2080 Ti HOF OC Lab + liquido custom (2x Alphacool NexXxoS ST30 360mm)
Audio
Alientek D8, Fiio Q1 Mark II, Mission LX-1, Sennheiser HD598 SE
Monitor
Samsung U28E590D
PSU
Corsair AX860i
Case
Lian Li O11 Dynamic
Periferiche
Ozone Strike Pro Spectra, Steelseries Rival 500, DualSense
Net
Tiscali
OS
Windows 10 Pro
Python e' per natura lento, e' un linguaggio interpretato e non ottimizzato. In genere le parti del programma che richiedono ottimizzazione vengono scritte come librarie usando il linguaggio C, richiamate poi dal programma in Python. Per questo motivo Python viene spesso classificato come "glue language".

Lo so che è lento, però per Machine Learning e Deep Learning è praticamente lo standard.

Comunque a riprova di quel che affermi, passando dalla funzione scritta da me in Python a un comando numpy ho ridotto del 96% il tempo di calcolo di una funzione interna al learning. (la funzione l'avevo scritta perché pareva consumasse meno RAM, non per altro :D)
 
  • Mi piace
Reazioni: Andretti60

Entra

oppure Accedi utilizzando
Discord Ufficiale Entra ora!