시작하며

R 과제로 시작해서 Python과 pandas를 넘어 Golang, k8s로 넘어오기까지 매번 모르는 것이 너무나 많았고 번번히 구글링 땜빵만 해왔다. 이번 기회에 내 code가 컴퓨터에서 어떻게 돌아가는지의 개념을 기초부터 정리해본다. 내가 가장 오랫동안 본 top에서부터 시작한다.

img.png

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/cacheSwap을 제외한 수치는 단어 그대로 받아들이면 된다.

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에 할당된 값을 확인할 수 있다.