Antecedentes
Tengo una red muy pequeña que quiero probar con diferentes semillas aleatorias. La red apenas usa el 1% de la potencia de cálculo de mi GPU, por lo que, en teoría, podría ejecutar 50 procesos a la vez para probar muchas semillas diferentes a la vez.
Problema
Desafortunadamente, ni siquiera puedo importar pytorch en múltiples procesos. Cuando el número de procesos supera los 4 , obtengo un seguimiento con respecto a un archivo de paginación demasiado pequeño.
Código mínimo reproducible§ - dispatcher.py
from subprocess import Popen import sys procs = [] for seed in range(50): procs.append(Popen([sys.executable, "ml_model.py", str(seed)])) for proc in procs: proc.wait()§Aumenté el número de semillas para que las personas con mejores máquinas también puedan reproducir esto.
Código reproducible mínimo - ml_model.py
import torch import time time.sleep(10) Traceback (most recent call last): File "ml_model.py", line 1, in <module> import torch File "C:\Users\user\AppData\Local\Programs\Python\Python38\lib\site-packages\torch\__init__.py", line 117, in <module> import torch File "C:\Users\user\AppData\Local\Programs\Python\Python38\lib\site-packages\torch\__init__.py", line 117, in <module> raise err OSError: [WinError 1455] The paging file is too small for this operation to complete. Error loading "C:\Users\user\AppData\Local\Programs\Python\Python38\lib\site-packages\torch\lib\cudnn_cnn_infer64_8.dll" or one of its dependencies. raise errInvestigación exahustiva
Noté que cada proceso carga muchos dll en la RAM. Y cuando cierro todos los demás programas que usan mucha RAM, puedo obtener hasta 10 procesos en lugar de 4. Por lo tanto, parece una limitación de recursos.
Preguntas
¿Hay una solución?
¿Cuál es la forma recomendada de entrenar muchas redes pequeñas con pytorch en una sola gpu?
¿Debería escribir mi propio Kernel CUDA en su lugar, o usar un marco diferente para lograr esto?
Mi objetivo sería ejecutar alrededor de 50 procesos a la vez (en una máquina de 16 GB de RAM, 8 GB de GPU de RAM)
Bueno, logré resolver esto. abra "Configuración avanzada del sistema". Vaya a la pestaña avanzada y luego haga clic en la configuración relacionada con el rendimiento. Vuelva a hacer clic en la pestaña avanzada --> cambiar --> anular la selección de 'automáticamente......'. para todas las unidades, establezca 'tamaño administrado por el sistema'. Reinicie su PC.
Para mi caso, el sistema ya está configurado para el tamaño administrado por el sistema, pero tengo el mismo error, eso se debe a que paso una variable de gran tamaño a múltiples procesos dentro de una función. Es probable que deba configurar un archivo de paginación muy grande, ya que Windows no puede crearlo sobre la marcha, sino que opte por no hacerlo para reducir la cantidad de procesos, ya que no es una función que siempre se use.
Si está en Windows, puede ser mejor usar 1 (o más) núcleos menos que el número total de núcleos físicos, ya que el módulo de multiprocesamiento en python en Windows tiende a obtener todo lo posible si usa todos y realmente intenta obtener todos los núcleos lógicos .
import multiprocessing multiprocessing.cpu_count() 12 # I actually have 6 pysical cores, if you use this as base it will likely hog system import psutil psutil.cpu_count(logical = False) 6 #actual number of pysical cores psutil.cpu_count(logical = True) 12 #logical cores (eg hyperthreading)Consulte aquí para obtener más detalles: Multiprocesamiento: ¿usar solo los núcleos físicos?
He mirado un poco en esto esta noche. No tengo una solución (edición: tengo una mitigación, vea la edición al final) , pero tengo un poco más de información.
Parece que el problema se debe a que los fatbins de NVidia (.nv_fatb) se cargan en la memoria. Varias DLL, como cusolver64_xx.dll, torcha_cuda_cu.dll y algunas otras, tienen secciones .nv_fatb. Estos contienen toneladas de diferentes variaciones de código CUDA para diferentes GPU, por lo que termina siendo de varios cientos de megabytes a un par de gigabytes.
Cuando Python importa 'antorcha', carga estas DLL y asigna la sección .nv_fatb a la memoria. Por alguna razón, en lugar de ser solo un archivo asignado a la memoria, en realidad está ocupando memoria. La sección está configurada como 'copiar al escribir', por lo que es posible que algo escriba en ella. No sé. Pero de todos modos, si observa Python usando VMMap ( https://docs.microsoft.com/en-us/sysinternals/downloads/vmmap ), puede ver que estas DLL están asignando grandes cantidades de memoria comprometida para esta sección .nv_fatb. La parte frustrante es que no parece estar usando la memoria. Por ejemplo, en este momento mi Python.exe tiene 2,7 GB asignados, pero el conjunto de trabajo es solo de 148 MB.
Cada proceso de Python que carga estas DLL comprometerá varios GB de memoria al cargar estas DLL. Entonces, si 1 proceso de Python está desperdiciando 2 GB de memoria e intenta ejecutar 8 trabajadores, necesita 16 GB de memoria de repuesto solo para cargar las DLL . Realmente no parece que se use esta memoria, solo se confirma.
No sé lo suficiente sobre estos fatbinarios para tratar de solucionarlo, pero al observar esto durante las últimas 2 horas, parece que realmente son el problema. ¿Quizás es un problema de NVidia que estos están comprometiendo memoria?
editar: hice este script de python: https://gist.github.com/cobryan05/7d1fe28dd370e110a372c4d268dcb2e5
Consíguelo e instala su dependencia de pefile ( python -m pip install pefile ).
Ejecútelo en su antorcha y cuda DLL. En el caso de OP, la línea de comando podría verse así:
python fixNvPe.py --input=C:\Users\user\AppData\Local\Programs\Python\Python38\lib\site-packages\torch\lib\*.dll(También desea ejecutar esto donde sea que estén su cusolver64_*.dll y sus amigos. Esto puede estar en su carpeta torch\lib, o puede ser, por ejemplo, C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vXX.X \bin .Si está en Archivos de programa, deberá ejecutar el script con privilegios administrativos)
Lo que hará este script es escanear todas las DLL especificadas por el globo de entrada, y si encuentra una sección .nv_fatb, hará una copia de seguridad de la DLL, deshabilitará ASLR y marcará la sección .nv_fatb como de solo lectura.
ASLR es 'aleatorización del diseño del espacio de direcciones'. Es una característica de seguridad que aleatoriza dónde se carga una DLL en la memoria. Lo deshabilitamos para esta DLL para que todos los procesos de Python carguen la DLL en la misma dirección virtual base. Si todos los procesos de Python que usan la DLL la cargan en la misma dirección base, todos pueden compartir la DLL. De lo contrario, cada proceso necesita su propia copia.
Al marcar la sección como "solo lectura", Windows sabe que el contenido no cambiará en la memoria. Si asigna un archivo a la memoria de lectura/escritura, Windows tiene que asignar suficiente memoria, respaldada por el archivo de paginación, en caso de que realice una modificación. Si la sección es de solo lectura, no es necesario respaldarla en el archivo de paginación. Sabemos que no tiene modificaciones, por lo que siempre se puede encontrar en la DLL.
La teoría detrás de la secuencia de comandos es que al cambiar estos 2 indicadores, se comprometerá menos memoria para .nv_fatb y se compartirá más memoria entre los procesos de Python. En la práctica, funciona. No tan bien como esperaba (todavía compromete mucho más de lo que usa), por lo que mi comprensión puede ser defectuosa, pero disminuye significativamente el compromiso de memoria.
En mis pruebas limitadas, no me encontré con ningún problema, pero no puedo garantizar que no haya rutas de código que intenten escribir en esa sección que marcamos como "solo lectura". Sin embargo, si comienza a tener problemas, simplemente puede restaurar las copias de seguridad.
editar 2022-01-20: según NVIDIA: "Seguimos adelante y marcamos la sección nv_fatb como de solo lectura, este cambio tendrá como objetivo la próxima versión importante de CUDA 11.7. No estamos cambiando el ASLR, ya que se considera una característica de seguridad ."
Esto sin duda debería ayudar. Si no es suficiente sin ASLR, entonces el script aún debería funcionar
Siguiendo la respuesta de @ chris-obryan (comentaría pero no tengo reputación), descubrí que la utilización de la memoria cae bastante bruscamente en algún momento del entrenamiento con su solución aplicada (en órdenes de aproximadamente los 2 GB mencionados por proceso).
Para obtener un poco más de rendimiento, puede valer la pena monitorear la utilización de la memoria y generar una nueva instancia del modelo cuando ocurren estas caídas en la memoria, dejando suficiente espacio (~ 3 o 4 GB para estar seguro) para un poco de sobrecarga.
Estaba viendo ~ 28 GB de RAM utilizados durante la fase de configuración, que se redujo a aproximadamente 14 GB después de iterar durante un tiempo.
(Tenga en cuenta que mi caso de uso es un poco diferente aquí, ya que tengo un cuello de botella debido a las transferencias de host<->dispositivo debido a la optimización con un GA, ya que se necesita una cantidad razonable de procesamiento vinculado a la CPU después de cada generación, por lo que esto podría jugar en También estoy usando concurrent.futures.ProcessPoolExecutor() en lugar de usar subprocesos manualmente)