¿Cómo afecta el código de multiprocesamiento la función del mapa?

Oct 27 2020

Estoy escribiendo una utilidad de búsqueda de cuadrícula y estoy tratando de usar multiprocesamiento para acelerar el cálculo. Tengo una función objetivo que interactúa con una clase grande que no puedo encurtir debido a limitaciones de memoria (solo puedo encurtir atributos relevantes de la clase).

import pickle
from multiprocessing import Pool


class TestClass:
    def __init__(self):
        self.param = 10

    def __getstate__(self):
        raise RuntimeError("don't you dare pickle me!")

    def __setstate__(self, state):
        raise RuntimeError("don't you dare pickle me!")

    def loss(self, ext_param):
        return self.param*ext_param


if __name__ == '__main__':
    test_instance = TestClass()

    def objective_function(param):
        return test_instance.loss(param)

    with Pool(4) as p:
        result = p.map(objective_function, range(20))
    print(result)

En el siguiente ejemplo de juguete, esperaba durante el decapado de la función object_function, que test_instance también tendría que ser decapado, arrojando así RuntimeError (debido al lanzamiento de una excepción en __getstate__). Sin embargo, esto no sucede y el código se ejecuta sin problemas.

Entonces, mi pregunta es: ¿qué es exactamente lo que se va a encurtir aquí? Y si test_instance no está encurtido, ¿cómo se reconstruye en procesos individuales?

Respuestas

Wilson.F Oct 27 2020 at 20:04

En windows + python3.8, no pude ejecutar el código original que definía test_instance y object_function como variable local para main, error como se muestra a continuación

    AttributeError: Can't get attribute 'objective_function' on <module '__mp_main' from 'xxx.py'>

Moví la definición de función_objetivo y la inicialización de instancia_prueba al alcance global, funciona bien como mencionaste. Sin embargo, a partir de esto , parece que las variables globales se han inicializado nuevamente para procesos diferentes en lugar de encurtidos / sin encurtir.

Finalmente, cambié su código como se muestra a continuación y desencadenó el error que esperaba.

    test_instance1 = TestClass()
    test_instance2 = TestClass()
    with Pool(4) as p:
        result = p.map(objective_function, [test_instance1, test_instance2])
    print(result)

Entonces, los parámetros en p.map en realidad son encurtidos / sin encurtir.

Raven Oct 27 2020 at 23:22

Bien, con la ayuda de Wilson y un poco más de investigación, me las arreglé para responder mi propia pregunta. Insertaré el código modificado de arriba para ayudar con la explicación:

import pickle
from multiprocessing import Pool, current_process


class TestClass:
    def __init__(self):
        self.param = 0

    def __getstate__(self):
        raise RuntimeError("don't you dare pickle me!")

    def __setstate__(self, state):
        raise RuntimeError("don't you dare pickle me!")

    def loss(self, ext_param):
        self.param += 1
        print(f"{current_process().pid}: {hex(id(self))}:  {self.param}: {ext_param} ")
        return f"{self.param}_{ext_param}"


def objective_function(param):
    return test_instance.loss(param)

if __name__ == '__main__':

    test_instance = TestClass()
    print(hex(id(test_instance)))
    print('objective_function' in globals())  # this returns True on my MacOS+python3.7

    with Pool(2) as p:
        result = p.map(objective_function, range(6))

    print(result)
    print(test_instance.param)

# ---- RUN RESULTS BELOW ----
# 0x7f987b955e48
# True
# 10484: 0x7f987b955e48:  1: 0 
# 10485: 0x7f987b955e48:  1: 1 
# 10484: 0x7f987b955e48:  2: 2 
# 10485: 0x7f987b955e48:  2: 3 
# 10484: 0x7f987b955e48:  3: 4 
# 10485: 0x7f987b955e48:  3: 5 
# ['1_0', '1_1', '2_2', '2_3', '3_4', '3_5']
# 0

Como Wilson ha insinuado correctamente, lo único que se conserva durante p.map son los parámetros en sí y no la función objetivo, sin embargo, esto no se reinicializa sino que se copia, junto con test_instance durante el os.fork()proceso que ocurre en algún lugar de la inicialización del Pool. Puede ver que aunque dentro de cada proceso los test_instance.paramvalores son independientes entre sí, comparten la misma memoria virtual que la instancia original de la clase antes de la bifurcación (se puede ver un ejemplo de diferentes procesos que comparten la misma memoria virtual aquí ).

Según la solución a la pregunta inicial, creo que la única forma de resolver correctamente este problema es distribuir los parámetros necesarios en la memoria compartida o el administrador de memoria.