Por que a velocidade dos processos que não interagem com muita memória depende de quantos estão em execução (e como corrigir)?

1

Esta parece ser uma questão básica, mas não consegui encontrá-la em lugar algum. Eu quero obter mais taxa de transferência em um processo pesado de memória, executando muitos deles em uma máquina de muitos núcleos. Esses processos não se comunicam entre si.

Eu esperaria que o tempo para completar para cada processo fosse aproximadamente independente do número de processos em execução até que o número de processos fosse próximo ao número de núcleos físicos (16 no meu caso ).

Eu observo que o tempo-para-completo gradualmente se curva até que seja cerca de 3 vezes mais lento para cada processo ser executado quando 16 estão sendo executados ao mesmo tempo quando apenas um está em execução.

O que está atrasando? (Mais detalhes do que as duas palavras, "alternância de contexto", por favor). Posso fazer algo sobre isso?

Editar: Michael Homer ressalta que estou interessado em um processo pesado de memória, não em um processo pesado de CPU. Suponho que todos esses processadores compartilhem um barramento de memória e esse poderia ser o gargalo. Idealmente, eu gostaria de algum tipo de arquitetura NUMA para colocar a memória do processo "mais próxima" das CPUs. Isso significa que eu preciso procurar hardware diferente para resolver esse problema?

Veja detalhes:

Eu tenho um script simples chamado quickie2.py que faz algum trabalho aleatório, com uso intensivo da CPU. Eu lanço N deles de uma vez com linhas de comando bash como as seguintes para 14 processos.

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

Aqui estão os tempos até a conclusão de 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: A propósito, fixar processos em núcleos torna o processo pior. Veja a linha comentada na listagem de código abaixo.

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 

Aqui está uma captura de tela do htop, mostrando que há exatamente N (14 aqui) 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, aqui está o programa Python que faz algum trabalho. Apenas importa que mantenha a CPU ocupada.

# 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))
    
por Jim Pivarski 02.02.2017 / 00:28

2 respostas

1

Agora que entendi o que estava errado, sei que era uma limitação de hardware, não uma limitação do UNIX, então esse não é o local apropriado para postar. No entanto, pensei que deveria adicionar um pouco de conclusão.

Meus processos independentes, com memória limitada, estavam de fato funcionando em um problema de largura de banda de memória. Repeti isso em um processador Knights Landing e aprendi a alocar os arrays Numpy em seu MCDRAM local. Usando memória local, não houve contenção no barramento de memória, e o processo continua a escalar bem acima do limite que eu observei no hardware normal.

AquiestáumareceitaparaalocararraysNumpynoMCDRAM,emvezdeRAMnormal.

importctypesimportnumpydefmalloc_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)
    
por 06.02.2017 / 23:20
0

O processo é pesado na memória, não pesado no processador. Tente isso:

#!/usr/bin/env python

import datetime
import hashlib

data = "
~$ python cpuheavy.py 
Elapsed: 0:00:20.461652
" * 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))

Estou obtendo resultados consistentes, com cerca de 20 segundos para concluir, em minha máquina de 2 sockets / 8-cores / 16 threads ao executar até 8 execuções em paralelo. Sobre isso, o desempenho cai à medida que os processos começam a brigar pelos recursos da CPU.

Execução única:

~$ 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

8 em paralelo (= 1 para cada núcleo), ainda na mesma hora:

#!/usr/bin/env python

import datetime
import hashlib

data = "
~$ python cpuheavy.py 
Elapsed: 0:00:20.461652
" * 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))

Com 16 execuções em paralelo (= 1 para cada hyperthread), o tempo aumentou para cerca de 31 segundos, à medida que os processos começaram a lutar pelo tempo da CPU. Ca 50% de aumento no tempo.

Com 32 execuções em paralelo, ele desceu a ladeira, pois os processos precisavam compartilhar os encadeamentos da cpu. O tempo para completar aumentou para mais de 2 minutos para cada processo (aumento de 4 vezes no tempo).

    
por 02.02.2017 / 02:23