¿Por qué la velocidad de los processs que no interactúan y que requieren mucha memory depende de cuántos se estén ejecutando (y cómo corregirlos)?

Parece una pregunta básica, pero no he podido encontrarla en ningún lado. Quiero get más performance en un process de memory pesada ejecutando muchos de ellos en una máquina de muchos núcleos. Estos processs no se comunican entre sí.

Esperaría que el time de finalización para cada process sea aproximadamente independiente de la cantidad de processs que se ejecutan hasta que el número de processs sea similar al número de núcleos físicos (16 en mi caso).

Observé que el time de finalización se curva gradualmente hasta que es aproximadamente 3 veces más lento para que se ejecute cada process cuando 16 se están ejecutando al mismo time que cuando solo se está ejecutando uno.

¿Qué los está frenando? (Más detalles que las dos palabras, "cambio de context", por favor.) ¿Puedo hacer algo al respecto?

Editar: Michael Homer señala que estoy interesado en un process de memory pesada, no uno pesado de CPU. Supongo que todas esas CPU comparten un bus de memory y ese podría ser el cuello de botella. Idealmente, me gustaría algún tipo de architecture NUMA para poner la memory de process "más cerca" de las CPU. ¿Eso significa que necesito search hardware diferente para resolver este problema?

Aquí hay detalles:

Tengo un script simple llamado quickie2.py que hace un trabajo aleatorio, intensivo de CPU. Lanzo N de ellos a la vez con líneas de command bash como las siguientes para 14 processs.

 for x in 1 2 3 4 5 6 7 8 9 10 11 12 13 14; do (python quickie2.py &); done 

Aquí están los times para completar para cada N:

 N_proc Time to completion (sec) 1 7.29 2 7.28 7.30 3 7.27 7.28 7.38 4 7.01 7.19 7.34 7.43 5 8.41 8.94 9.51 10.27 11.73 6 7.49 7.79 7.97 10.01 10.58 10.85 7 7.71 8.72 10.22 10.43 10.81 10.81 11.42 8 10.1 10.16 10.27 10.29 10.48 10.60 10.66 10.73 9 9.94 11.20 11.27 11.35 11.61 12.43 12.46 12.99 13.53 10 9.26 12.54 12.66 12.84 12.95 13.03 13.06 13.52 13.93 13.95 11 12.46 12.48 12.65 12.74 13.69 13.92 14.14 14.39 14.40 14.69 17.13 12 13.48 13.49 13.51 13.58 13.65 13.67 14.72 14.87 14.89 14.94 15.01 15.06 13 15.47 15.51 16.72 16.79 16.79 16.91 17.00 17.45 17.75 17.78 17.86 18.14 18.48 14 15.14 15.22 16.47 16.53 16.84 17.78 18.07 19.00 19.12 19.32 19.63 19.71 19.80 19.94 15 18.05 18.18 18.58 18.69 19.84 20.70 21.82 21.93 22.13 22.44 22.63 22.81 22.92 23.23 23.23 16 20.96 21.00 21.10 21.21 22.68 22.70 22.76 22.82 24.65 24.66 25.32 25.59 26.16 26.22 26.31 26.38 

Editar: Incidentalmente, fijar processs a núcleos empeora la caída. Vea la línea comentada en la list de códigos a continuación.

 N_proc Time to completion (sec) with CPU-pinning 1 6.95 2 10.11 10.18 4 19.11 19.11 19.12 19.12 8 20.09 20.12 20.36 20.46 23.86 23.88 23.98 24.16 16 20.24 22.10 22.22 22.24 26.54 26.61 26.64 26.73 26.75 26.78 26.78 26.79 29.41 29.73 29.78 29.90 

Aquí hay una captura de pantalla de htop, que muestra que realmente hay exactamente N (14 aquí) núcleos ocupados:

  1 [|||||||||||||||98.0%] 5 [|| 5.8%] 9 [||||||||||||||100.0%] 13 [ 0.0%] 2 [||||||||||||||100.0%] 6 [||||||||||||||100.0%] 10 [||||||||||||||100.0%] 14 [||||||||||||||100.0%] 3 [||||||||||||||100.0%] 7 [||||||||||||||100.0%] 11 [||||||||||||||100.0%] 15 [||||||||||||||100.0%] 4 [||||||||||||||100.0%] 8 [||||||||||||||100.0%] 12 [||||||||||||||100.0%] 16 [||||||||||||||100.0%] Mem[|||||||||||||||||||||||||||||||||||||3952/64420MB] Tasks: 96, 7 thr; 15 running Swp[ 0/16383MB] Load average: 5.34 3.66 2.29 Uptime: 76 days, 06:59:39 

Para completar, aquí está el progtwig Python que hace algo de trabajo. Solo importa que mantenga ocupada la CPU.

 # Code of quickie2.py (for completeness). import numpy import time # import psutil # psutil.Process().cpu_affinity([int(sys.argv[1])]) arena = numpy.empty(240*1024**2, dtype=numpy.uint8) startTime = time.time() # just do some work that takes a lot of CPU for i in range(100): one = arena[:80*1024**2].view(numpy.float64) two = arena[80*1024**2:160*1024**2].view(numpy.float64) three = arena[160*1024**2:].view(numpy.float64) three = one + two print(" {:.2f} ".format(time.time() - startTime)) 

Ahora que entiendo lo que estaba mal, sé que era una limitación de hardware, no una limitación de UNIX, por lo que este no es el lugar adecuado para publicar. Sin embargo, pensé que debería agregar algo de cierre.

Mis processs independientes de memory limitada se encontraron con un problema de ancho de banda de memory. Lo repetí en un procesador Knights Landing y aprendí cómo asignar matrices Numpy en su MCDRAM local. Al usar la memory local, no hubo contención en el bus de memory, y el process continúa escalando muy por encima del límite que observé en el hardware normal.

tasa de eventos vs cantidad de procesos independientes

Aquí hay una receta para asignar matrices Numpy en MCDRAM, en lugar de RAM normal.

 import ctypes import numpy def malloc_mcdram(size): libnuma = ctypes.cdll.LoadLibrary("libnuma.so") assert libnuma.numa_available() == 0 # NUMA not available is -1 libnuma.numa_alloc_onnode.restype = ctypes.POINTER(ctypes.c_uint8) return libnuma.numa_alloc_onnode(ctypes.c_size_t(size), ctypes.c_int(1)) def custom_allocator_array(allocator, size, dtype): ptr = allocator(size) ptr.__array_interface__ = {"version": 3, "typestr": numpy.ctypeslib._dtype(type(ptr.contents)).str, "data": (ctypes.addressof(ptr.contents), False), "shape": (size,)} return numpy.array(ptr, copy=False).view(dtype) myarray = custom_allocator_array(malloc_mcdram, sizeInBytes, numpy.float64) 

El process es una memory pesada, no muy pesada. Pruebe esto en su lugar:

 #!/usr/bin/env python import datetime import hashlib data = "\0" * 64 ts_start = datetime.datetime.now() for i in range(10000000): data = hashlib.sha512(data).digest() ts_end = datetime.datetime.now() print("Elapsed: %s" % (ts_end - ts_start)) 

Obtengo resultados consistentes, ca 20 s para completar, en mi máquina 2-sockets / 8-cores / 16-threads cuando corro hasta 8 carreras en paralelo. Por encima de eso, el performance disminuye a medida que los processs comienzan a luchar por los resources de la CPU.

Single run:

 ~$ python cpuheavy.py Elapsed: 0:00:20.461652 

8 en paralelo (= 1 para cada núcleo), aún al mismo time:

 ~$ for i in $(seq 8); do python cpuheavy.py & done Elapsed: 0:00:18.979012 Elapsed: 0:00:19.092770 Elapsed: 0:00:19.873763 Elapsed: 0:00:20.139105 Elapsed: 0:00:20.147066 Elapsed: 0:00:20.181319 Elapsed: 0:00:21.328754 Elapsed: 0:00:21.495310 

Con 16 carreras en paralelo (= 1 para cada hyperthread), el time aumentó a ca 31 s, ya que los processs comenzaron a luchar por el time de la CPU. Ca 50% de aumento en el time.

Con 32 carreras en paralelo, bajó la colina ya que los processs tenían que compartir los hilos de la CPU. El time para completar aumentó a más de 2 minutos para cada process (4 veces más time).