Почему этот код нельзя распараллелить с Numba? - PullRequest
0 голосов
/ 09 апреля 2020

Я работаю над симуляцией физики. Я на самом деле пытаюсь реализовать кластерный алгоритм Вольфа для модели Изинга. Это касается главным образом манипулирования массивами NumPy на стороне программирования. Я хочу составить сюжет с указанием c тепла, энергии, намагниченности с температурой. Для разных температур моделирование полностью независимое. Итак, я попытался распараллелить операцию с помощью функции numba prange. Но это не позволяет. Кажется, причина в другой функции, которую я вызываю из моей основной функции.

Мой код здесь:

# -*- coding: utf-8 -*-
"""
Created on Mon Mar 30 18:39:45 2020

@author: ENDEAVOUR
"""

#%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from numba import jit,prange
import timeit
#----------------------------------------------------------------------
##  semi-block codes################
#----------------------------------------------------------------------

start = timeit.default_timer()
error_model=True

@jit(error_model="numpy",nopython=True)
def initialstate(N):   
    ''' generates a random spin configuration for initial condition'''
    state =2*np.random.randint(0,2, size=(N,N))-1
    return state

@jit(error_model="numpy",nopython=True)
def calcEnergy(config):
    '''Energy of a given configuration'''
    N=len(config)
    energy = 0
    for i in range(len(config)):
        for j in range(len(config)):
               S = config[i,j]
               nb = config[(i+1)%N, j] + config[i,(j+1)%N]  + config[(i-1)%N, j] + config[i,(j-1)%N] 
               energy += -nb*S - H*S
    return energy/(4.)

@jit(error_model="numpy",nopython=True)
def calcMag(config):
    '''Magnetization of a given configuration'''
    mag = np.sum(config) + H
    return mag


def wolff(config,p):##wollff cluster implementation 

   ## the followng is the function for a sngle run of cluster making and flipping.


   #print(config)
   '''Monte Carlo move using Wolff-Cluster Sampling'''

   que = []  ### "que" ; stores the list of coordinates of the neighbours aded to the cluster
   (x0,y0) = np.random.randint(len(config)),np.random.randint(len(config)) ## a random spin is chosen at first
   que.append((x0,y0)) ## then added to the "que"


   while (len(que) > 0):## as mentioned in the documents I havesnt u , code must run untill there is nobody left to be added

       #print('Length of que ',len(que))

       q = que[np.random.randint(len(que))] ## a random element from que is chosen

       neighbours = [((q[0]+1)%N,q[1]), ((q[0]-1)%N,q[1]), (q[0],(q[1]+1)%N), (q[0],(q[1]-1)%N) ] ## neighbours to the selected spin are considered
       for c in neighbours:

           if all([config[q]==config[c]  , c not in que,np.random.rand() < p]):
## process of adding spins to the que based on wolff's criteria if they have same spin
              que.append(c)


       config[q] *= -1 ## the spin'q' that was selected from the que is flipped so to avoid being selected in future
       que.remove(q)  ## the spin'q' is removed from the 'que'

   return config



@jit(error_model="numpy",parallel=True)
def run_simulation(N,eqSteps,mcSteps,T,nt):

    E,M,C,X = np.empty(nt), np.empty(nt), np.empty(nt), np.empty(nt)

    n1, n2  = 1.0/(mcSteps*N*N), 1.0/(mcSteps*mcSteps*N*N) 


    for tt in prange(nt):

        E1 = M1 = E2 = M2 = 0
        config = initialstate(N)
        iT=1.0/T[tt]; iT2=iT*iT;
        p = 1 - np.exp(-2*J*iT)

        for i in range(eqSteps):         # equilibrate
            config=wolff(config,p)                # Monte Carlo moves



        for i in range(mcSteps):
            config=wolff(config,p)            
            Ene = calcEnergy(config)     # calculate the energy
            Mag = abs(calcMag(config))       # calculate the magnetisation



            E1 = E1 + Ene
            M1 = M1 + Mag
            M2 = M2 + Mag*Mag 
            E2 = E2 + Ene*Ene


        E[tt] = n1*E1
        M[tt] = n1*M1
        C[tt] = (n1*E2 - n2*E1*E1)*iT2
        X[tt] = (n1*M2 - n2*M1*M1)*iT

        print ("Temp:",T[tt],":", E[tt], M[tt],C[tt],X[tt])


    return E,M,C,X

#def control():
####################################
N  = 64       #N X N spin field
J  = 1
H  = 0
nt = 50
eqSteps = 150
mcSteps = 20

#############################################No change rquired here
T  = np.linspace(1.33, 4.8, nt) 
E,M,C,X=run_simulation(N,eqSteps,mcSteps,T,nt)



f = plt.figure(figsize=(25, 20)); # plot the calculated values    
plt.title("External Field :%5.2f"%(H))
sp =  f.add_subplot(3, 2, 1 );
plt.scatter(T, E, s=50, marker='o', color='Red')
plt.xlabel("Temperature (T)", fontsize=20);
plt.ylabel("Energy ", fontsize=20);         plt.axis('tight');

sp =  f.add_subplot(3, 2, 2 );
plt.scatter(T, M, s=50, marker='o', color='Blue')
plt.xlabel("Temperature (T)", fontsize=20); 
plt.ylabel("Magnetization ", fontsize=20);   plt.axis('tight');

sp =  f.add_subplot(3, 2, 3 );
plt.scatter(T, C, s=50, marker='o', color='Red')
plt.xlabel("Temperature (T)", fontsize=20);  
plt.ylabel("Specific Heat ", fontsize=20);   plt.axis('tight');   

sp =  f.add_subplot(3, 2, 4 );
plt.scatter(T, X, s=50, marker='o', color='RoyalBlue')
plt.xlabel("Temperature (T)", fontsize=20); 
plt.ylabel("Susceptibility", fontsize=20);   plt.axis('tight');

sp = f.add_subplot(3 ,2 ,5);
plt.scatter(E, M,s=50, marker='+', color='Red') 
plt.xlabel("Energy ", fontsize=20);         plt.axis('tight');
plt.ylabel("Magnetization ", fontsize=20);   plt.axis('tight');

plt.show()
stop = timeit.default_timer()

print('Time taken for this simulation:: ', stop - start)



Отображаемое сообщение об ошибке:

E:/Project/wolff_cluster.2.py:78: NumbaWarning: 
Compilation is falling back to object mode WITH looplifting enabled because Function "run_simulation" failed type inference due to: Untyped global name 'wolff': cannot determine Numba type of <class 'function'>

File "wolff_cluster.2.py", line 94:
def run_simulation(N,eqSteps,mcSteps,T,nt):
    <source elided>
        for i in range(eqSteps):         # equilibrate
            config=wolff(config,p)                # Monte Carlo moves
            ^

  @jit(error_model="numpy",parallel=True,nopython=True)
E:/Project/wolff_cluster.2.py:78: NumbaWarning: 
Compilation is falling back to object mode WITHOUT looplifting enabled because Function "run_simulation" failed type inference due to: cannot determine Numba type of <class 'numba.dispatcher.LiftedLoop'>

File "wolff_cluster.2.py", line 86:
def run_simulation(N,eqSteps,mcSteps,T,nt):
    <source elided>

    for tt in prange(nt):
    ^

  @jit(error_model="numpy",parallel=True,nopython=True)
C:\Users\Sandipan\Anaconda3\lib\site-packages\numba\compiler.py:725: NumbaWarning: Function "run_simulation" was compiled in object mode without forceobj=True, but has lifted loops.

File "wolff_cluster.2.py", line 79:
@jit(error_model="numpy",parallel=True,nopython=True)
def run_simulation(N,eqSteps,mcSteps,T,nt):
^

  self.func_ir.loc))
C:\Users\Sandipan\Anaconda3\lib\site-packages\numba\compiler.py:734: NumbaDeprecationWarning: 
Fall-back from the nopython compilation path to the object mode compilation path has been detected, this is deprecated behaviour.

For more information visit http://numba.pydata.org/numba-doc/latest/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit

File "wolff_cluster.2.py", line 79:
@jit(error_model="numpy",parallel=True,nopython=True)
def run_simulation(N,eqSteps,mcSteps,T,nt):
^

  warnings.warn(errors.NumbaDeprecationWarning(msg, self.func_ir.loc))
E:/Project/wolff_cluster.2.py:78: NumbaWarning: 
Compilation is falling back to object mode WITHOUT looplifting enabled because Function "run_simulation" failed type inference due to: Untyped global name 'wolff': cannot determine Numba type of <class 'function'>

File "wolff_cluster.2.py", line 94:
def run_simulation(N,eqSteps,mcSteps,T,nt):
    <source elided>
        for i in range(eqSteps):         # equilibrate
            config=wolff(config,p)                # Monte Carlo moves
            ^

  @jit(error_model="numpy",parallel=True,nopython=True)
C:\Users\Sandipan\Anaconda3\lib\site-packages\numba\compiler.py:725: NumbaWarning: Function "run_simulation" was compiled in object mode without forceobj=True.

File "wolff_cluster.2.py", line 86:
def run_simulation(N,eqSteps,mcSteps,T,nt):
    <source elided>

    for tt in prange(nt):
    ^

  self.func_ir.loc))
C:\Users\Sandipan\Anaconda3\lib\site-packages\numba\compiler.py:734: NumbaDeprecationWarning: 
Fall-back from the nopython compilation path to the object mode compilation path has been detected, this is deprecated behaviour.

For more information visit http://numba.pydata.org/numba-doc/latest/reference/deprecation.html#deprecation-of-object-mode-fall-back-behaviour-when-using-jit

File "wolff_cluster.2.py", line 86:
def run_simulation(N,eqSteps,mcSteps,T,nt):
    <source elided>

    for tt in prange(nt):
    ^

  warnings.warn(errors.NumbaDeprecationWarning(msg, self.func_ir.loc))
E:/Project/wolff_cluster.2.py:78: NumbaWarning: 
Compilation is falling back to object mode WITHOUT looplifting enabled because Function "run_simulation" failed type inference due to: Untyped global name 'wolff': cannot determine Numba type of <class 'function'>

File "wolff_cluster.2.py", line 94:
def run_simulation(N,eqSteps,mcSteps,T,nt):
    <source elided>
        for i in range(eqSteps):         # equilibrate
            config=wolff(config,p)                # Monte Carlo moves
            ^

  @jit(error_model="numpy",parallel=True,nopython=True)
Temp: 1.33 : -0.9890869140625 0.9942626953125 0.041807132522607905 0.017099021969053343

После показа вышеуказанная ошибка работает как обычно, но занимает 3 раза по сравнению с чистой версией python. Я пытался без вумбы для Вольфа, но безуспешно. Действительно важно распараллелить для l oop в run_simulation, поскольку это самая трудоемкая часть кода.

Я думаю, что проблема с функцией Вольфа. Numba не может ничего сделать, чтобы оптимизировать это. Но я не использовал ни одной функции, не поддерживаемой Numba. (простой список и только numpy). Он помечает условие if - ключевое слово «all». Я пробовал с 'и' внутри if, но тогда программа сталкивается с бесконечным l oop. Он добавляет один и тот же элемент несколько раз в очередь (вероятно, пропускает не в состоянии). Я знаю, что это технически невозможно. (Оба 'и' & 'all' должны работать). Я делаю что-то не так. Но я понятия не имею, что это такое.

Не могли бы вы помочь мне с оптимизацией этого кода. Работает нормально без нумбы. Но при N> 100 он будет слишком медленным для любой полезной работы.

1 Ответ

1 голос
/ 09 апреля 2020

Сначала получите работающий код

Распараллеливание обычно является последним, что нужно учитывать. В лучшем случае вы можете получить немного меньше, чем количество доступных ядер (обычно меньше, чем на порядок). Существует не так много функций, которые обычно недоступны, обычно лучше, чтобы это было как можно более простым (и выполняет эту работу здесь)

При использовании Numba в большинстве случаев рекомендуется избавиться от любых глобальных переменных, которые может привести к сложным отладочным ошибкам или очень длительной перекомпиляции кода. На самом деле реальных глобальных переменных нет, вместо этого они жестко запрограммированы в скомпилированной функции.

Пример

import numba as nb
@nb.njit()
def why_globals_have_to_be_avoided(A):
    print(A+B)

A=0
B=5
why_globals_have_to_be_avoided(A)
#5

A=0
B=8
why_globals_have_to_be_avoided(A)
#5

#If this function is buried in your code it will be extremely hard to debug
#If you turn caching on, it may even get harder to find an error.

Этот пример довольно интересен, потому что входные данные довольно крошечные, но скорость выполнения сильно зависит от температуры T, которую каждый отдельный расчет зависит от. Работа просто расплющена (0,48) и передана ряду потоков. Если все, кроме одного потока, заканчиваются намного раньше, вы не увидите никакого ускорения. Поскольку случайная перестановка входов в этом примере довольно дешевая, перетасовка входов является простым решением этой проблемы.

Пример

@nb.njit()
def why_globals_have_to_be_avoided(A):
    print(A+B)

A=0
B=5
why_globals_have_to_be_avoided(A)
#5

A=0
B=8
why_globals_have_to_be_avoided(A)
#5

#If this function is burried in your code it will be extremely hard to debug
#If you turn caching on, it may even get harder to find an error.

Рабочий пример

import numpy as np
import numba as nb
#----------------------------------------------------------------------
##  semi-block codes################
#----------------------------------------------------------------------

@nb.njit(error_model="numpy")
def initialstate(N):   
    ''' generates a random spin configuration for initial condition'''
    state =2*np.random.randint(0,2, size=(N,N))-1
    return state

@nb.njit(error_model="numpy")
def calcEnergy(config,H):
    '''Energy of a given configuration'''
    N=len(config)
    energy = 0
    for i in range(len(config)):
        for j in range(len(config)):
            S = config[i,j]
            nb = config[(i+1)%N, j] + config[i,(j+1)%N]  + config[(i-1)%N, j] + config[i,(j-1)%N] 
            energy += -nb*S - H*S
    return energy/(4.)

@nb.njit(error_model="numpy")
def calcMag(config,H):
    '''Magnetization of a given configuration'''
    mag = np.sum(config) + H
    return mag

@nb.njit(error_model="numpy")
def wolff(config,p,N):##wollff cluster implementation 
   ## the followng is the function for a sngle run of cluster making and flipping.
    que = []  ### "que" ; stores the list of coordinates of the neighbours aded to the cluster
    x0,y0 = np.random.randint(len(config)),np.random.randint(len(config)) ## a random spin is chosen at first
    que.append((x0,y0)) ## then added to the "que"


    while (len(que) > 0):## as mentioned in the documents I havesnt u , code must run untill there is nobody left to be added
        q = que[np.random.randint(len(que))] ## a random element from que is chosen

        neighbours = [((q[0]+1)%N,q[1]), ((q[0]-1)%N,q[1]), (q[0],(q[1]+1)%N), (q[0],(q[1]-1)%N) ] ## neighbours to the selected spin are considered
        for c in neighbours:
            if config[q]==config[c]  and c not in que and np.random.rand() < p:## process of adding spins to the que based on wolff's criteria if they have same spin
                que.append(c)


        config[q] *= -1 ## the spin'q' that was selected from the que is flipped so to avoid being selected in future
        que.remove(q)  ## the spin'q' is removed from the 'que'

    return config


@nb.njit(error_model="numpy",parallel=True)
def run_simulation(N,eqSteps,mcSteps,T,J,H):
    nt=T.shape[0]

    E,M,C,X = np.empty(nt), np.empty(nt), np.empty(nt), np.empty(nt)

    n1, n2  = 1.0/(mcSteps*N*N), 1.0/(mcSteps*mcSteps*N*N) 

    #It looks like the calculation time heavily depends on the Temperature
    #shuffle the values to get the work more equaly splitted
    #np.random.shuffle isn't supported by Numba, but you can use a Python callback
    with nb.objmode(ind='int32[::1]'): 
        ind = np.arange(nt)
        np.random.shuffle(ind)

    ind_rev=np.argsort(ind)
    T=T[ind]

    for tt in nb.prange(nt):

        E1 = M1 = E2 = M2 = 0
        config = initialstate(N)
        iT=1.0/T[tt]; iT2=iT*iT;
        p = 1 - np.exp(-2*J*iT)

        for i in range(eqSteps):           # equilibrate
            config=wolff(config,p,N)       # Monte Carlo moves

        for i in range(mcSteps):
            config=wolff(config,p,N)            
            Ene = calcEnergy(config,H)     # calculate the energy
            Mag = abs(calcMag(config,H))   # calculate the magnetisation

            E1 = E1 + Ene
            M1 = M1 + Mag
            M2 = M2 + Mag*Mag 
            E2 = E2 + Ene*Ene


        E[tt] = n1*E1
        M[tt] = n1*M1
        C[tt] = (n1*E2 - n2*E1*E1)*iT2
        X[tt] = (n1*M2 - n2*M1*M1)*iT

        #print ("Temp:",T[tt],":", E[tt], M[tt],C[tt],X[tt])

        #undo the shuffling
    return E[ind_rev],M[ind_rev],C[ind_rev],X[ind_rev]

#def control():
####################################
N  = 64       #N X N spin field
J  = 1
H  = 0
nt = 50
eqSteps = 150
mcSteps = 20

#############################################No change rquired here
T  = np.linspace(1.33, 4.8, nt) 

#You can measure the compilation time separately, it doesn't make sense to mix 
#runtime and compialtion time together.
#If the compilation time gets relevant it also make sense to use `cache=True`
E,M,C,X=run_simulation(N,eqSteps,mcSteps,T,J,H)

%timeit E,M,C,X=run_simulation(N,eqSteps,mcSteps,T,J,H)
1.17 s ± 74.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

#without parallelization
2.1 s ± 44 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

#without compilation
~130 s
...