Python, Intel Python и многоядерные процессоры - PullRequest
5 голосов
/ 23 марта 2020

Недавно я провел пару тестов на сервере Linux с двумя процессорами, каждый из которых имеет 20 физических ядер (полное описание оборудования одного процессора приведено ниже), а также 20 дополнительных логических ядер (таким образом, всего 80 ядер). Причиной этого расследования является то, что я работаю в исследовательской лаборатории, где большинство кодов написано в Python, и я нашел много онлайн-сообщений, связанных с Python вариациями производительности от компьютера к другому.

Вот мои настройки:

  • операционная система: CentOS 7.7.1908
  • сравниваются две python версии: Python 3.6.8 и Python Intel 3.6.9 (обновление 2019 г. 5)
  • Процессор: Intel (R) Xeon (R) CPU E5-2698 v4 @ 2,20 ГГц

Я провел сравнения для различных базовых c функций как numpy, так и scipy, а именно:

  • scipy.sparse.linal.spsolve: решение линейной системы (Ax = b) с A разреженная матрица 68000x68000 и разреженная матрица xa 68000x50,
  • scipy.sparse.linalg.eigsh: решение обобщенных задач на собственные значения с разреженными матрицами 68000x68000,
  • numpy.dot
  • scipy.linalg.cholesky и scipy.linalg.svd

По сути, я решил запустить тестовые сценарии (каждый из которых запускается от 25 до 100 раз с использованием * 1 036 * для получения релевантных результатов) для каждой версии Python с учетом выполнения по умолчанию сценария тестирования и его выполнения с использованием различного числа (x-1) ядер с помощью команды taskset --cpu-list 0-x ....

Вот краткое резюме моих результатов:

  • scipy.sparse.linal.spsolve

computation time with respect to the number of cores

  • scipy.sparse.linalg.eigsh

computation time with respect to the number of cores

  • numpy.dot

computation time with respect to the number of cores

  • scipy.linalg.cholesky

computation time with respect to the number of cores

  • scipy.linalg.svd

computation time with respect to the number of cores

Следует добавить, что черные точки и пунктирные линии указывают время выполнения по умолчанию без использования команды taskset.

Как и ожидалось, Python Intel работает лучше, чем Python 3. Однако меня очень удивляет то, как выполнение кода по умолчанию с Python Intel может быть медленнее, чем его выполнение в ограниченном количестве (3 5) количество ядер (особенно для spsolve и eig sh).

Это нормально (я предполагаю, что существует баланс между временем вычислений и временем обмена данными между ядрами)? И есть ли способ оптимизировать выполнение по умолчанию кода Python на многоядерном процессоре?

Вот спецификации одного из ядер моего сервера:

processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 79
model name      : Intel(R) Xeon(R) CPU E5-2698 v4 @ 2.20GHz
stepping        : 1
microcode       : 0xb00002e
cpu MHz         : 1207.958
cache size      : 51200 KB
physical id     : 0
siblings        : 20
core id         : 0
cpu cores       : 20
apicid          : 0
initial apicid  : 0
fpu             : yes
fpu_exception   : yes
cpuid level     : 20
wp              : yes
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch epb cat_l3 cdp_l3 invpcid_single intel_ppin intel_pt ssbd ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm rdt_a rdseed adx smap xsaveopt cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts spec_ctrl intel_stibp flush_l1d
bogomips        : 4389.92
clflush size    : 64
cache_alignment : 64
address sizes   : 46 bits physical, 48 bits virtual
power management:

Также вот коды, которые я использовал: (1) для spsolve:

#!/usr/bin/env python
import timeit

setup = '''
import numpy
import sys
import timeit
import scipy.io
from scipy.sparse.linalg import spsolve
from scipy import sparse

K=scipy.io.loadmat('K0.mat', struct_as_record=False,squeeze_me=True)
K = K.tocsc()

def problin(K):   
    MS = numpy.zeros([numpy.shape(K)[0],50])
    for i in range(50):
        MS[i,i]=1;
    MS = sparse.csc_matrix(MS)
    #
    x = spsolve(-K,MS)
    return x
'''

code = '''  
x = problin(K) 
'''

count = 5
t = timeit.Timer(code,setup = setup) 
print("spsolve:", t.timeit(count)/count, "sec")

(2) для eigsh:

#!/usr/bin/env python
import timeit

setup = '''
import numpy
import sys
import scipy.io
from scipy.sparse.linalg import eigsh

K=scipy.io.loadmat('K0.mat', struct_as_record=False,squeeze_me=True)
M=scipy.io.loadmat('M0.mat', struct_as_record=False,squeeze_me=True)

K = K['K'].tocsc()
M = M['M'].tocsc()

def vp(K,M): # Function is compiled to machine code when called the first time  
    w, z = eigsh(K,10,M,sigma=1)
    f = numpy.sqrt(w)/2/numpy.pi
    return f
'''

code = '''  
f = vp(K,M)  
'''

count = 5
t = timeit.Timer(code,setup = setup) 
print("eigsh:", t.timeit(count)/count, "sec")

(3) для холецкого и СВД, я нашел сценарии онлайн:

#!/usr/bin/env python                                                           
import timeit

setup = "import numpy;\
        import scipy.linalg as linalg;\
        x = numpy.random.random((1000,1000));\
        z = numpy.dot(x, x.T)"
count = 25

t = timeit.Timer("linalg.cholesky(z, lower=True)", setup=setup)
print("cholesky:", t.timeit(count)/count, "sec")

t = timeit.Timer("linalg.svd(z)", setup=setup)
print("svd:", t.timeit(count)/count, "sec")

(4) dot:

#!/usr/bin/env python
import numpy
from numpy.distutils.system_info import get_info
import sys
import timeit

print("version: %s" % numpy.__version__)
print("maxint:  %i\n" % sys.maxsize)

info = get_info('blas_opt')
print('BLAS info:')

for kk, vv in info.items():
    print(' * ' + kk + ' ' + str(vv))

setup = "import numpy; x = numpy.random.random((2000, 2000))"
count = 100

t = timeit.Timer("numpy.dot(x, x.T)", setup=setup)
print("\ndot: %f sec" % (t.timeit(count) / count))
...