Nov 28, 2021 - Cs

9월 어느날, 왜 CS를 면접 때 물어보는지 정말로 깨달았다. 여러 문제를 접하면서 지식의 파편은 많이 가지고 있다. 하지만 구조화 되지 않았고 서로 연결되지 않아 어느 순간부터 답답함을 느끼기 시작했다.

일단 컴퓨터는 어렵다기보단 방대하다. 양 자체가 많아서 암만 천재라 해도 단기간에 할 수가 없을 것이다. 시중 강의의 도움을 받으면 좋겠지만 내용을 보면 내 업무에 적용하기엔 다소 애매한 것 같다. 느낌상 면접을 위한 내용이 주를 이뤄 타겟이 다르다는 인상을 받았다. 결국 내가 직접 정리하는 것이 가장 좋아보인다.

앞으로 CS 시리즈를 차곡차곡 작성할 계획이다. 지난 기억 속의 문제 상황과 CS 지식을 연결하여 데이터화하는 시도다. 적당한 깊이로 지식을 습득해 문제 해결 능력이 상승하는 것, 그리고 완전히 practical한 글이 되는 것을 목표로 한다.

Aug 14, 2021 - 간만의 일기

새로운 일터에서는 매주마다 TIL스러운 일기를 남기고 있다. 처음엔 참 쓸 것도 많고 한장 한장 늘어가는 재미가 있었다. 이제 18주가 넘어가니 슬슬 회사에선 익숙한 것들이 대부분이고… 신규 msa 서비스 일정이 다가오고 있어서 새로운 지식을 접할 여유가 없어지고 있다.

개발 관련 글을 올릴 때는 항상 세상에 잘 없는 내용만 쓰려고 하고 일기를 쓸 때는 과한 내용이 없는지 검열을 철저히 하다보니 점점 블로그도 일처럼 느껴지고 있다.

운동도 1일1회 가벼운 warmup 이라도 꼭 하려는 무거운 책임감 같은 것이 생겼다. 내 몸에 관리가 이제 생존을 위한 필수 활동이다. 이제 부모님을 볼 때면 늙으면 저렇게 변하는구나 싶고 앞으로 몇년을 더 함께 할 수 있을까 걱정이 든다.

이러한 모습은 앞으로 뭐 하고 살까 막막했던 10년 전의 내가 바라던 성숙한 모습이 아니었을까 생각이 든다. 비록 오늘 하루 재미있지 않고, 맡은 바 책임감과 부담감을 느끼고, 몸이 늙어감을 실감할 지언정. 오늘 내가 무엇을 하였다고, 이것이 더 좋은 판단이라고, 앞으로 나는 어떠할 것이라고, 이제는 내가 답을 스스로 찾을 수 있다.

그 때의 나보다 몸은 안 좋을지라도 마음만큼은 더 강력해졌으며 앞으로도 그러할 것이라고 스스로 믿는다. 신기하게도 이 믿음은 만들려고 해서 만든 것도 아니고 흔들릴 것 같아도 흔들리지 않는다. 그저 처음부터 그 자리에 있던 것처럼, 마치 영원히 그 자리에 있을 것처럼, 그렇게 있다.

Jul 14, 2021 - Why Numpy Fast Or Slow

추천 모델 연산에 행렬 곱셈이 많이 쓰이는데 참 신기하게도 사소한 코드에도 성능이 달라진다. 정확한 원리를 알고 시스템을 개선하고자 2개월에 걸쳐 틈틈히 정리해놓았다. 보통 ML Engineer라면 이러한 성능 optimize는 관심이 있을텐데 생각보다 잘 정리된 글을 못 봐서 직접 만들어버렸다 😎


원래 numeric computation에 적합한 언어로 C, C++, Fortran(가장 성능이 좋음)이 꼽혔고 Python은 적합한 언어가 아니었다. 이를 보완하고자 2005년 numpy가 공개되었다.

numpy는 scipy와도 관계가 깊은데 scipy는 검증된 scientific package를 wrapping한 것이고, 이에 사용되는 numeric object를 numpy에서 제공한다. https://www.scipy.org/scipylib/faq.html#how-can-scipy-be-fast-if-it-is-written-in-an-interpreted-language-like-python

numpy의 핵심은 ndarray(n-dimensional array) class와 numeric operation을 Cython 및 BLAS로 구현한 것이다. ndarray는 fixed size 객체이다. 따라서 append 동작은 new size copy를 생성하고 이전 size를 삭제한다. https://scipy-lectures.org/advanced/advanced_numpy/

array 저장 방식은 C order, F order가 있는데 각각 C와 Fortran의 구현체와 대응된다. C는 row-wise, F는 column-wise로 stride의 차이가 있다. tobytes()로 확인해보면 1차원 array의 index로 행/열을 구분함을 알 수 있다.

c = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]], dtype=np.int16, order='C')
assert c.strides == (6, 2)

f = np.array(x, order='F')
assert f.strides == (2, 6)

assert f.T.data.f_contiguous, 'transpose changed strides and c order'

LAPACK과 BLAS

LAPACK은 Linear Algebra PACKage으로 fortran으로 작성되었으며 linear system algorithm을 제공한다. LAPACK의 내부는 대부분 BLAS 호출로 이뤄져있다. hardware acceleration으로 분류된다.

BLAS는 Strassen algorithm처럼 복잡한 code를 사용한 것이 아니라 cpu cache optimize에 대한 것이다. https://stackoverflow.com/questions/1303182/how-does-blas-get-such-extreme-performance SIMD(single instruction multiple data) 개념을 구현한 SSE(Streaming SIMD Extensions), AVX(Advanced Vector Extensions) 기술을 사용한다. C code에서 이를 호출하면 #include <emmintrin.h> _mm_mul_pd와 같은 특수한 함수를 사용한다. http://www.mathematik.uni-ulm.de/~lehn/apfel/sghpc/gemm/ https://software.intel.com/sites/landingpage/IntrinsicsGuide/# 하지만 최근 GPU를 주로 사용하면서 AVX와 같은 CPU 최적화 기술은 성장둔화될 것으로 보인다.


matrix multiplication의 시간 복잡도는 n^3이지만 슈트라센 알고리즘은 n^2.807이다. numpy는 blas를 사용하며 multithread로 실행된다. blas는 mkl, openblas가 성능이 좋다. https://software.intel.com/content/www/us/en/develop/articles/numpyscipy-with-intel-mkl.html scipy api로 blas를 직접 호출할 수 있다.

>>> import numpy as np
>>> np.show_config()
blas_mkl_info:
  NOT AVAILABLE
blis_info:
  NOT AVAILABLE
openblas_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/usr/local/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
blas_opt_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/usr/local/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
lapack_mkl_info:
  NOT AVAILABLE
openblas_lapack_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/usr/local/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]
lapack_opt_info:
    libraries = ['openblas', 'openblas']
    library_dirs = ['/usr/local/lib']
    language = c
    define_macros = [('HAVE_CBLAS', None)]

별도 설정 없는 m1 mac에서 openblas linkage를 확인했다. intel cpu가 아니기 때문에 당연히 mkl은 안된다.


blas 직접 호출시의 성능

import numpy as np
from scipy.linalg.blas import dgemm, sgemm

a = np.random.rand(10000, 10000)

%timeit a@a
%timeit dgemm(1, a, a)
%timeit dgemm(1, a, a, trans_a=1, trans_b=1)

환경변수를 통해 multithread 개수를 지정할 수 있다.

OMP_NUM_THREADS: openmp,
OPENBLAS_NUM_THREADS: openblas,
MKL_NUM_THREADS: mkl,
VECLIB_MAXIMUM_THREADS: accelerate,
NUMEXPR_NUM_THREADS: numexpr
import os
os.environ["OMP_NUM_THREADS"] = "4" # export OMP_NUM_THREADS=4
os.environ["OPENBLAS_NUM_THREADS"] = "4" # export OPENBLAS_NUM_THREADS=4 
os.environ["MKL_NUM_THREADS"] = "6" # export MKL_NUM_THREADS=6
os.environ["VECLIB_MAXIMUM_THREADS"] = "4" # export VECLIB_MAXIMUM_THREADS=4
os.environ["NUMEXPR_NUM_THREADS"] = "6" # export NUMEXPR_NUM_THREADS=6

성능 실험을 해보자.

import os
os.environ["OPENBLAS_NUM_THREADS"] = "4"
import numpy as np
P = np.random.rand(10000, 500)
Q = np.random.rand(10000, 500)
%timeit P @ Q.T
1.78 s ± 161 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
import os
os.environ["OPENBLAS_NUM_THREADS"] = "1"
import numpy as np
P = np.random.rand(10000, 500)
Q = np.random.rand(10000, 500)
%timeit P @ Q.T
6.95 s ± 245 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

thread에 따른 성능 차이가 확연하다.


dtype을 기본적으로 np.float64(double)로 설정되는데 이를 np.float32(single)로 변경만 해도 성능 향상이 있다.

import numpy as np
P = np.random.rand(10000, 500)
Q = np.random.rand(10000, 500)
P, Q = np.asarray(P, dtype=np.float32), np.asarray(Q, dtype=np.float32)
%timeit P @ Q.T

237 ms ± 24.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

stride가 작으면 cpu에서 fetch를 적게 할 수 있어 성능이 좋다.

x = np.zeros((20000,))
y = np.zeros((20000*67,))[::67]
print(x.strides, y.strides)
# (8,) (536,)
%timeit x.sum()
%timeit y.sum()

6.83 µs ± 86.5 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
34.6 µs ± 1.59 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

numpy code를 어떻게 짜느냐에 따라 속도 차이가 큰데, 이는 temporary array를 만들면서 cache 사용을 제대로 하지 못하기 때문에 발생한다.

cache 사용에 따라 통상 2~4배 성능 차이가 있다. https://numexpr.readthedocs.io/projects/NumExpr3/en/latest/intro.html#how-it-works

cache 전략은 cache size만큼 chunk하기, 연산하는 data의 address를 가깝게 하기 (aligned data).

import numpy as np
P = np.random.rand(10000, 5000)
Q = np.random.rand(10000, 5000)
P, Q = np.asarray(P, dtype=np.float32), np.asarray(Q, dtype=np.float32)
import numexpr as ne
%timeit 3 * P + 2 * Q
%timeit ne.evaluate("3 * P + 2 * Q")

121 ms ± 990 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
42.4 ms ± 178 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

optimization을 위해 numpy를 numba, cython으로 바꾸기도 한다.

서로 성능은 비슷하나 numba는 numeric array 연산만 쉽고 다른 기능은 제한이 많으며 conda 환경을 권장한다. cython은 python code를 대부분 쓸 수 있고 robust하다. http://stephanhoyer.com/2015/04/09/numba-vs-cython-how-to-choose/

numpy + cython은 numpy C API를 써서 힘들다고는 한다. https://cython.readthedocs.io/en/latest/src/tutorial/numpy.html

하지만 실질적으로 MKL과 같은 kernel lib를 배제할순 없다. https://stackoverflow.com/questions/56920713/numpy-faster-than-numba-and-cython-how-to-improve-numba-code


distributed computing은 OpenMPI로 구현된다. https://mpi4py.readthedocs.io/en/stable/tutorial.html uber 분산 학습 시스템에서도 사용한다.

opencl은 cuda처럼 gpu programming


octave benchmark

[_,_,t,_]=speed("p*q'", 'p=rand(30000,500); q=rand(30000,500)');
mean(t)
ans =  2.7118

numpy benchmark

import numpy as np
p = np.random.rand(30000, 500)
q = np.random.rand(30000, 500)
Pf, Qf = np.asarray(p, dtype=np.float32, order='F'), np.asarray(q, dtype=np.float32, order='F')
Pc, Qc = np.asarray(p, dtype=np.float32, order='C'), np.asarray(q, dtype=np.float32, order='C')
%timeit Pf @ Qf.T
%timeit Pc @ Qc.T

# macbook pro 16 inch 2019
2.71 s ± 213 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
2.88 s ± 19 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# m5.16xlarge
2.11 s ± 827 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1.82 s ± 136 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

materials

https://developer.ibm.com/blogs/use-python-for-scientific-research/ https://developer.ibm.com/languages/python/articles/ba-accelerate-python/ https://users.ece.cmu.edu/~franzf/papers/gttse07.pdf

cython

https://cython.readthedocs.io/en/latest/src/userguide/parallelism.html https://github.com/pydata/bottleneck/issues/92 https://github.com/benfred/implicit/blob/master/implicit/nearest_neighbours.h https://numpy.org/doc/1.17/reference/c-api.html