시작하며
R 과제로 시작해서 Python과 pandas를 넘어 Golang, k8s로 넘어오기까지 매번 모르는 것이 너무나 많았고 번번히 구글링 땜빵만 해왔다.
이번 기회에 내 code가 컴퓨터에서 어떻게 돌아가는지의 개념을 기초부터 정리해본다.
내가 가장 오랫동안 본 top
에서부터 시작한다.
top
기본적인 설명은 자료가 많다. https://sabarada.tistory.com/146 가장 잘 보이는 cpu, memory 항목의 주요 개념과 구글링 쿼리를😅 알자.
cpu
머신러닝 학습을 빠르게 하려면 어떻게 해야하는지 항상 궁금했다.
cpu는 많이 쓸 수록 좋다.
python 프로세스는 cpu가 100%로 제한되므로 multiprocessing
을 통해 프로세스 N개로 나눠
띄울 수 있다. 여기서 spawn, fork 개념이 나온다.
numpy, sklean
와 같은 좋은 패키지는 cython nogil
로 100% 제한을 해제한다. https://github.com/scikit-learn/scikit-learn/blob/844b4be24d20fc42cc13b957374c718956a0db39/sklearn/decomposition/_cdnmf_fast.pyx
pytorch
에선 openmp
를 통해 제한을 푼다. https://github.com/pytorch/pytorch/blob/master/aten/src/ATen/ParallelOpenMP.h#L22
cpu의 기본 명령어는 데이터 읽기/쓰기, 가감승제, and, or 연산과 같이 간단하다. 아무리 복잡한 프로그램도 이런 cpu 명령의 조합이다.
별도로 matrix 연산 속도를 위한 SSE, MMX 명령어가 있고 이를 intel mkl 라이브러리에 구현한다.
numpy
의 성능은 mkl과 같은 blas에서 온다.
기본적으로 cpu에게 효율적으로 일을 시킬 수 있는 compiler가 중요하다. https://sungjjinkang.github.io/c++/computerscience/2021/03/22/SIMD.html
동일한 로직도 compiler가 잘하면 성능이 좋아지며 numba jit compiler
가 있는 이유다.
cpu의 효율은 cache(L1, L2)에 달려있다. 연산 성능만 따지면 cpu는 기가헤르츠 단위의 연산을 할 수 있지만
연산에 필요한 데이터를 가져오는 속도는 이를 못 받쳐준다. https://formulusblack.com/blog/compute-performance-distance-of-data-as-a-measure-of-latency/
따라서 cache 영역에 핏한 데이터로 가공해야 연산 성능이 오른다.
이러한 전략을 cache miss를 줄인다고 하며, numexpr
에서 볼 수 있다. https://numexpr.readthedocs.io/projects/NumExpr3/en/latest/intro.html#
memory 할당도 cpu의 일이므로 copy보다 inplace update가 더 빠르다.
cpu는 설계 단위로 보면 socket - core - thread 로 볼 수 있고, 데이터는 cache - memory 로 볼 수 있다.
socket은 cpu가 물리적으로 붙어있는 단위로 numa를 socket 단위로 잡는다.
물리코어는 2개의 논리코어를 가지며 cache를 공유한다.
m5.16xlarge는 2 socket 각 32 core 가운데 16 물리코어 구성이다.
top으로 cpu 개수를 확인할수 있고 cpu마다 논리코어인지 물리코어인지 확인한다. cat /sys/devices/system/cpu/cpu3/topology/core_id
논리코어를 끄면 chcpu -e 16,17,18,19,20...
openmp를 활용한 병렬처리 성능이 향상됨을 확인했다.
ml workload에서는 동일한 물리 코어의 cache를 경쟁하는 상황이 되어 hyper thread 성능이 떨어진다.
다른 방법으로 환경변수 OMP_NUM_THREADS;OMP_PROC_BIND;GOMP_CPU_AFFINITY
로 동일한 효과를 줄 수 있다.
memory
top
의 memory 관련 지표 가운데 buff/cache
와 Swap
을 제외한 수치는 단어 그대로 받아들이면 된다.
buff/cache
는 file read를 빠르게 하는 캐시다. 참조
그래서 메모리 사용이 증가하면 줄어든다.
Swap
은 virtual memory 용어로 disk를 활용해서 RAM보다 큰 데이터를 올리는 개념이다.
buffer/cache
에서 오랫동안 쓰이지 않은 데이터는 Swap
으로 밀려난다.
swapon --show
으로 보면 swap의 크기와 파일 위치를 확인할 수 있다.
loop_cnt = 9999
for i in range(loop_cnt):
fn = f"file.{i}"
s = "".join(["1234567890"] * 99999)
with open(fn, 'w') as f:
f.write(s)
import os
for i in range(loop_cnt):
os.remove(f"file.{i}")
buff/cache
값이 증가했다가 감소하는 것을 확인할 수 있다.
import numpy as np
array = np.zeros((99999, 99999))
VIRT
수치가 증가하는 것을 볼 수 있다.
array[:5000, :] = 1
RES
값이 상승한다.
2를 누르면 numa node view
로 전환된다. NUMA는 cpu socket 단위로 메모리를 TBW
process
/proc
top
의 데이터 소스이며 https://tldp.org/LDP/Linux-Filesystem-Hierarchy/html/proc.html os의 모든 process 정보를 가진다.
sysctl은 /proc을 수정하는 명령어. https://en.wikipedia.org/wiki/Procfs
/proc/$PID/maps
으로 heap, stack에 할당된 값을 확인할 수 있다.