[Cython] [Python] 고성능 Python을 위한 Cython 활용하기 3편

안녕하세요. Teus입니다.

 

지난 포스팅을 통해서Cython-Python 사이 매개변수를 Numpy로 만들고

이 Numpy배열을 보다 빠르게 활용할 수 있는 방법에 대해서 다뤘습니다.

 

이번시간에는 Cython 연재 마지막 시간으로

Cython을 활용해서 병렬처리를 하는 방법에 대해서 알아보도록 하겠습니다.

 

1. OpenMP

image.png

 

뜬금없이 OpenMP가 등장해서 당황할 수 있습니다.

 

image.png

OpenMP는 Open Multi-Processing의 약자로

 

C/C++ 언어에서 Macro를 정의하는 "#"연산자를 사용해서 코드를 작성합니다.

 

Wikipedia에 있는 예제를 보시면

//출처 : https://ko.wikipedia.org/wiki/OpenMP
//openMP를 사용하기 위해서 omp.h 헤더파일을 import
 #include <omp.h>
 #include <stdio.h>
 #include <stdlib.h>

 int main (int argc, char *argv[]) {
   int th_id, nthreads;
   //progma omp라는 문장을 추가하여 해당 이하 구문을 다수의 Thread를 만들어 실행시킴
   #pragma omp parallel private(th_id)
   {
     th_id = omp_get_thread_num();
     printf("Hello World : 스레드 %d\n", th_id);
     #pragma omp barrier
     if ( th_id == 0 ) {
       nthreads = omp_get_num_threads();
       printf("모두 %d 개의 스레드가 있습니다\n",nthreads);
     }
   }
   return EXIT_SUCCESS;
 }

단순하게 #pragma를 추가하여 특정 scope를 병렬처리할 수가 있습니다.

 

이때 반복문 역시 병렬처리가 가능하며, embarrassingly parallel한 작업을 효율적으로 처리할 수 있습니다.

//출처 : https://learn.microsoft.com/ko-kr/cpp/parallel/openmp/a-examples?view=msvc-170
#pragma omp parallel for
    for (i=1; i<n; i++)
        b[i] = (a[i] + a[i-1]) / 2.0;

(OpenMP만 가지고도 책한권이 있는 수준이라... OpenMP는 이만큼만 소개하고 가겠습니다!)

 

2. Cython의 병렬처리

위에서 OpenMP를 언급한 이유를 다들 예상 하셨을 거라고 생각합니다.

그렇죠. Cython에서는 OpenMP를 활용해서 반복문은 병렬처리 하게 됩니다.

어차피 CPython Level에서 MultiThread를 사용하는 것이기 때문에

 

Python의 GIL의 속박에서 벗어날 수가 있습니다.

Cython을 활용한 병렬처리 공식문서

 

Cython 공식 홈페이지에 있는 예제를 한번 보도록 하겠습니다.

#cython의 prange를 사용한 openmp 활용
from cython.parallel cimport prange
#cython코드에서 cython wrapper를 사용하기 위한 임포트
cimport cython
from libc.math cimport sin

import numpy as np

@cython.boundscheck(False)
@cython.wraparound(False)
def do_sine(double[:,:] input):
    cdef double[:,:] output = np.empty_like(input)
    cdef Py_ssize_t i, j

    for i in prange(input.shape[0], nogil=True):
        for j in range(input.shape[1]):
            output[i, j] = sin(input[i, j])
    return np.asarray(output)
    
"""
compile후 나오는 C언어 코드에서 병렬처리 된 부분
#pragma omp parallel
{
    #pragma omp for firstprivate(i) lastprivate(i) lastprivate(j)
    for (__pyx_t_8 = 0; __pyx_t_8 < __pyx_t_9; __pyx_t_8++){
        i = __pyx_t_8;
        /* body goes here */
    }
"""

생각보다 단순하게 병렬처리가 가능한것을 볼 수 있습니다.

기본적으로 range를 사용하던 부분을 range -> prange로 치환하게 되며 이때 gil없이 사용하기 위해 nogile = True 로 표시된걸 볼 수 있습니다.

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

그리고 컴파일된 파일을 통해서 CPython 내부적으로 OpenMP의 pragma omp for 를 사용해서 embrassingly parallel이 진행되는것을 볼 수 있습니다.

 

3. 좀더 Detail한 예제로

이번에는 numpy Array와 filter를 받고, 2d Convolution을 하는 함수를 예제로 보겠습니다.

%%cython --annotate
import numpy as np
cimport numpy as np
np.import_array()
DTYPE = np.int
ctypedef np.int_t DTYPE_t


from cython.parallel cimport prange
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def my_conv2d(np.ndarray[DTYPE_t, ndim = 2] mat, np.ndarray[DTYPE_t, ndim = 2] f):
    cdef int mat_h = mat.shape[1]
    cdef int mat_v = mat.shape[0]
    cdef int f_h = f.shape[1]
    cdef int f_v = f.shape[0]
    cdef DTYPE_t temp_val
    
    cdef int ret_mat_h = mat_h-f_h+1
    cdef int ret_mat_v = mat_v-f_v+1
    
    cdef int row, col, i_row, i_col, temp_row, temp_col;
    
    cdef np.ndarray[DTYPE_t, ndim = 2] ret = np.zeros([ret_mat_v,ret_mat_h], dtype = DTYPE)    
    for row in prange(ret_mat_v, nogil = True):
        for col in range(ret_mat_h):
            temp_val = 0
            for i_row in range(f_v):
                for i_col in range(f_h):                    
                    temp_row = row+i_row
                    temp_col = col+i_col                    
                    temp_val += (mat[temp_row, temp_col] * f[i_row, i_col])
            ret[row,col] = temp_val
            
    return ret
import time
import numpy as np
import copy
N = 1000
input = np.array([copy.deepcopy([np.random.randint(0, 10) for i in range(1000)]) for k in range(1000)])
filter = np.array([copy.deepcopy([np.random.randint(0, 10) for i in range(3)]) for k in range(3)])
st = time.perf_counter()
normal = my_conv2d(input, filter)
print(f"time cfunc : {time.perf_counter() - st}")

row단위로 병렬처리를 하기 위해서 4중 반복문의 최외곽에 prange를 사용한것을 볼 수 있습니다.

 

해당 코드를 실행시켜 보면...

Error compiling Cython file:
------------------------------------------------------------
...
                for i_col in range(f_h):                    
                    temp_row = row+i_row
                    temp_col = col+i_col                    
                    #temp_val = temp_val + (mat[temp_row, temp_col] * f[i_row, i_col])
                    temp_val += (mat[temp_row, temp_col] * f[i_row, i_col])
            ret[row,col] = temp_val
                          ^
------------------------------------------------------------
ipython\cython\_cython_magic_066a197a2f983a6a606f576c0c7491ec.pyx:34:27: Cannot read reduction variable in loop body

이유를 알 수 없는 에러가 발생합니다.

비슷한 Error가 난 다른 사례를 보면, python의 += 연산자를 사용할 경우 openmp로 전환하는데 문제가 발생하는 것을 알수가 있습니다(출처)

때문에 해당 위치말고, += 연산자를 풀어서 accumlate -> add연산으로 바꿔줘야만 합니다.

#temp_val += (mat[temp_row, temp_col] * f[i_row, i_col]) -->
temp_val = temp_val + (mat[temp_row, temp_col] * f[i_row, i_col]) 

변경후 실행하면, 정상적으로 compile이 되고 실행할 수가 있습니다.

주의. nogil 모드이기 때문에 python 인터프리터가 관여하게 될 경우 error가 발생할 수 있습니다.

이때 --annotate를 통해서 살펴보면, CPython에서 __Pyx_FastGIL_Remember()와 __Pyx_FastGIL_Forget()틍 통해서 최외곽 반복문에 OPENMP를 적용하는것을 알 수가 있습니다.

// for row in prange(ret_mat_v, nogile = True)의 CPython 변환 코드
{
      #ifdef WITH_THREAD
      PyThreadState *_save;
      Py_UNBLOCK_THREADS
      __Pyx_FastGIL_Remember();
      #endif
      /*try:*/ {
        __pyx_t_6 = __pyx_v_ret_mat_v;
        if ((1 == 0)) abort();
        {
            #if ((defined(__APPLE__) || defined(__OSX__)) && (defined(__GNUC__) && (__GNUC__ > 2 || (__GNUC__ == 2 && (__GNUC_MINOR__ > 95)))))
                #undef likely
                #undef unlikely
                #define likely(x)   (x)
                #define unlikely(x) (x)
            #endif
            __pyx_t_8 = (__pyx_t_6 - 0 + 1 - 1/abs(1)) / 1;
            if (__pyx_t_8 > 0)
            {
                #ifdef _OPENMP
                #pragma omp parallel
                #endif /* _OPENMP */
                {
                    #ifdef _OPENMP
                    #pragma omp for lastprivate(__pyx_v_col) lastprivate(__pyx_v_i_col) lastprivate(__pyx_v_i_row) firstprivate(__pyx_v_row) lastprivate(__pyx_v_row) lastprivate(__pyx_v_temp_col) lastprivate(__pyx_v_temp_row) lastprivate(__pyx_v_temp_val)
                    #endif /* _OPENMP */
                    for (__pyx_t_7 = 0; __pyx_t_7 < __pyx_t_8; __pyx_t_7++){
                        {
                            __pyx_v_row = (int)(0 + 1 * __pyx_t_7);
                            /* Initialize private variables to invalid values */
                            __pyx_v_col = ((int)0xbad0bad0);
                            __pyx_v_i_col = ((int)0xbad0bad0);
                            __pyx_v_i_row = ((int)0xbad0bad0);
                            __pyx_v_temp_col = ((int)0xbad0bad0);
                            __pyx_v_temp_row = ((int)0xbad0bad0);
                            __pyx_v_temp_val = ((__pyx_t_46_cython_magic_13dc7bc2b38a451e5c2b88564a61cb58_DTYPE_t)0xbad0bad0);
/* … */
      /*finally:*/ {
        /*normal exit:*/{
          #ifdef WITH_THREAD
          __Pyx_FastGIL_Forget();
          Py_BLOCK_THREADS
          #endif
          goto __pyx_L5;
        }
        __pyx_L5:;
      }
  }

특이한 점으로, loop 내부에서 사용한 value들의 경우 lastprivate로 처리하여 invalid value를 할당한 뒤 type casting 하는것을 볼 수 있습니다.

 

4. Compile

openmp를 사용하기 위해서는 setup.py 파일에 별도의 compile linker arg 추가가 필요합니다.

#setup.py
from distutils.core import setup
from distutils.extension import Extension
import numpy as np
import sys
#compile에 사용된 linker, compile 매개변수 정의
#os 종류에 따라 명령어가 살짝 차이남
if sys.platform.startswith("win"):
    openmp_arg = '/openmp'
else:
    openmp_arg = '-fopenmp'

ext_modules = [Extension(
      "cy_conv2d_parallel",
      ["cy_conv2d_parallel.pyx"], 
      #compile간 사용할 매개변수 추가
        extra_compile_args=[openmp_arg],
        extra_link_args=[openmp_arg],
      )]

from Cython.Build import cythonize
setup(
    ext_modules=cythonize(
        ext_modules,
        compiler_directives={"language_level": "3"},
    ),
    include_dirs=[np.get_include()]
) 

이렇게 설정 후 cmd창에서 python setup.py build_ext --inplace를 실행하면

cy_conv2d_parallel.c -> cy_conv2d_parallel.pyd 파일로 변환되게 됩니다.

 

5. Performance

이제 병렬처리 되지 않은 conv_2d와 병렬처리된 conv_2d_parallel의 performance를 비교해 보겠습니다.

import cy_conv2d
import cy_conv2d_parallel
import numpy as np
import copy
input = np.array([copy.deepcopy([np.random.randint(0, 10) for i in range(1000)]) for k in range(1000)])
filter = np.array([copy.deepcopy([np.random.randint(0, 10) for i in range(3)]) for k in range(3)])
st = time.perf_counter()
normal = cy_conv2d_parallel.my_conv2d(input, filter)
print(f"time cfunc with parallel : {time.perf_counter() - st}")

st = time.perf_counter()
normal = cy_conv2d.my_conv2d(input, filter)
print(f"time cfunc : {time.perf_counter() - st}")
'''
time cfunc with parallel : 0.01012540000010631
time cfunc : 0.17689960000279825
'''

최대 8배 빨라질 것으로 예상 하였으나(8코어) 그 이상으로 빨라진 것을 확인할 수 있습니다.

 

이 외에도 단순하게 Thread를 추가하여 별도의 작업을 실행할 수 있으나, 해당 내용은 이번 포스팅에서는 다루지 않을 예정입니다.

(궁금하신 분들은 Cython의 Parallelism 으로..)

 

이제 어떻게 Cython을 통해서 고성능 파이썬을 구현할지 감들이 오셨을거라고 생각합니다.

 

다들 빠른코드로 빠른 퇴근하시기 바랍니다!

 

 

[출처] https://devocean.sk.com/blog/techBoardDetail.do?ID=164733

 

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
15 [python 데이터분석] [jupyter] 주피터 노트북에 이미지 삽입 file 졸리운_곰 2025.09.06 526
14 [python 데이터분석] [Python] Streamlit 사용법 (python 데이터분석 웹 만들기) file 졸리운_곰 2024.12.22 462
13 [python 데이터분석] Anaconda : Error while loading conda entry point: conda-libmamba-solver (libarchive.so.19: cannot open shared object file: No such file or directory) 졸리운_곰 2024.12.14 520
12 [python 데이터분석] Anaconda | Conda update 반영 안됨(update 후에도 버전 변경 없음) file 졸리운_곰 2024.11.18 431
11 [python 데이터분석] Keeping Anaconda Up To Date 졸리운_곰 2024.05.30 552
10 [python 데이터 분석] 국내 경제 100대 통계지표 졸리운_곰 2024.02.18 672
9 [python 데이터 분석] Python 에서 R언어 패키지 호출 : Calling R From Python With rpy2 file 졸리운_곰 2024.01.28 676
8 [python 데이터 분석] 파이썬을 활용한 코스피, 달러 환율정보 수집부터 차트 시각화까지 file 졸리운_곰 2023.12.11 735
7 [Python 데이터분석][pandas] [Python pandas] DataFrame의 문자열 칼럼을 숫자형으로 바꾸기 : pd.to_numeric(), DataFrame.astype() file 졸리운_곰 2023.12.09 357
6 [Python 데이터분석] [Python 환경설정] VS code 설치 및 Anaconda와 연동하기 file 졸리운_곰 2023.03.17 527
5 [Python 데이터분석][python 데이터분석 프로덕션] [Python] Docker를 사용한 Dash 웹앱 생성 file 졸리운_곰 2021.12.10 403
4 [Python 데이터분석] [pandas] 공공데이터(csv) 활용시 한글 깨짐 현상 해결 file 졸리운_곰 2021.09.30 592
3 [Python 데이터분석] 공공데이터포털::공휴일 데이터 조회 (REST API) file 졸리운_곰 2021.09.30 358
2 [Python 데이터 분석] pandas의 to_csv()를 사용해서 csv 파일로 저장하기(save 하기) 졸리운_곰 2021.09.29 603
1 [Python 데이터 분석] 데이터 과학을 단순하게 만드는 3가지 Python 패키지 file 졸리운_곰 2021.09.24 513
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED