- 전체
- Python 일반
- Python 수학
- Python 그래픽
- Python 자료구조
- Python 인공지능
- Python 인터넷
- Python SAGE
- wxPython
- TkInter
- iPython
- wxPython
- pyQT
- Jython
- django
- flask
- blender python scripting
- python for minecraft
- Python 데이터 분석
- Python RPA
- cython
- PyCharm
- pySide
- kivy (python)
cython [Cython] [Python] 고성능 Python을 위한 Cython 활용하기 3편
2024.06.10 17:42
[Cython] [Python] 고성능 Python을 위한 Cython 활용하기 3편
안녕하세요. Teus입니다.
지난 포스팅을 통해서Cython-Python 사이 매개변수를 Numpy로 만들고
이 Numpy배열을 보다 빠르게 활용할 수 있는 방법에 대해서 다뤘습니다.
이번시간에는 Cython 연재 마지막 시간으로
Cython을 활용해서 병렬처리를 하는 방법에 대해서 알아보도록 하겠습니다.
1. OpenMP

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

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의 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 로 표시된걸 볼 수 있습니다.
그리고 컴파일된 파일을 통해서 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
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
댓글 0
| 번호 | 제목 | 글쓴이 | 날짜 | 조회 수 |
|---|---|---|---|---|
| 3 |
wxpython_in_action_wxact.pdf
| 졸리운_곰 | 2014.10.14 | 7113 |
| 2 |
wxPython 2.8 Application Development Cookbook (2010).pdf
| 졸리운_곰 | 2014.10.14 | 2773 |
| 1 |
The wxPython tutorial.pdf
| 졸리운_곰 | 2014.10.14 | 2875 |

