CPU란 무엇인가

CPU는 Central Processing Unit의 약자로, '중앙 처리 장치'라고도 불립니다. 인간으로 따지면 두뇌에 해당하는 역할을 하는데요. 컴퓨터의 두뇌로서 모든 명령을 처리하고 중요한 계산을 하는 역할을 합니다. 사용자가 "1+2를 계산해라"라는 명령을 내렸을 때, 이 명령을 받아 계산을 실행하고 "3"이라는 결과값을 도출해 내는 게 CPU가 하는 일이죠.

저는 올해 초 맥북 프로를 구매했는데요. 칩(SoC: System on a Chip)으로는 M3 Pro를 골랐습니다. M3 Pro에는 12코어 CPU(6개의 고성능 코어, 6개의 고효율 코어)와 18코어 GPU가 포함되어 있습니다. 여기서 말하는 '12코어'란 CPU 내부에 12개의 처리 유닛(코어)가 있다는 뜻인데요. 쉽게 설명하자면, 컴퓨터가 여러 가지 작업을 동시에 할 때 이 코어들이 나눠서 일을 처리하는 것이죠. 각 코어는 독립적으로 작업을 처리할 수 있기 때문에, 코어가 많을수록 한 번에 더 많은 작업을 할 수 있어요. 예를 들어서, 제가 구매한 M3 Pro보다 비싼 모델인 M3 Max의 경우 16코어 CPU와 40코어 GPU가 탑재되어 있기 때문에 더욱 성능이 좋습니다.

이렇게 코어가 많을수록 CPU가 '병렬 처리'라는 것을 잘 해낼 수 있어서, 컴퓨터는 멀티태스킹이나 고사양 작업을 잘 해낼 수 있게 됩니다. 병렬 처리는 여러 작업을 독립적으로, 동시에 처리하는 능력을 의미하는데요. 병렬 처리를 활용하면 작업을 빠르게, 효율적으로 완수할 수 있어요. 

그렇지만 병렬 처리가 항상 100% 반드시 효율적인 것은 아닙니다. 작업을 병렬로 나누는 과정에서 오히려 시간이 더 소모될 수도 있고요. 소프트웨어가 병렬 처리를 지원하지 않으면 모든 코어를 제대로 활용하지 못할 수 있습니다. 따라서 CPU 자원을 잘 활용할 수 있도록 프로그램을 구성하는 것이 무척 중요하다고 볼 수 있겠습니다.


CPU와 GPU의 차이

CPU는 마치 스포츠카와 같습니다. 한두 사람을 아주 빠르게 운송할 수 있죠. 반면에 GPU는 고속버스와 같습니다. 속도는 스포츠카에 미치지 못하지만, 한 번에 훨씬 더 많은 사람을 태울 수 있어요. 만약 같은 단위 시간 동안 운송할 수 있는 사람의 수가 차량의 성능에 해당한다면, 고속버스에 해당하는 GPU의 성능이 더 좋다고 볼 수 있는 것이죠.

그러나 CPU와 GPU는 마치 스포츠카와 고속버스처럼 그 용도와 목적, 성질이 무척 다릅니다. GPU는 CPU보다 병렬 처리를 수십 배 잘해낼 수 있지만 하나의 작업을 빠르게 실행하는 속도는 느리고, CPU는 OS를 실행하는 데 필요한 여러 가지 기능을 다양하게 탑재하고 있는 등, 차이점이 많아요. 따라서 내가 실행하고자 하는 작업이 CPU와 GPU - 둘 중 무엇에 더 최적화 되어 있는지 잘 판단하고 알맞은 것을 선택하는 것이 중요합니다.


CPU 집약적 task

AI 엔지니어로서 머신 러닝, 딥러닝 모델을 많이 다뤄온 저에게는 항상 GPU에 대한 고민이 먼저 이루어지곤 했습니다. 딥러닝 모델은 대량의 데이터와 수십억 개의 파라미터를 다루고, 행렬 곱셈(Matrix Multification/Dot Product) 같은 대규모 수학 연산을 자주 하기 때문에, 많은 메모리 대역폭과 고성능 연산 능력을 가지고 있는 GPU의 병렬 처리가 필수적으로 요구됩니다. 그래서 GPU 자원이 턱없이 부족한 환경에서 공부를 해오던 저에게는 좋은 사양의 GPU에 대한 갈증이 항상 있어 왔습니다.

그런데 이번에 처음으로 CPU 집약적 task, CPU의 병렬 처리에 대한 고민을 하게 되었어요. 저는 현재 맡은 업무에서 빅데이터셋의 품질 관리를 위해 문서간의 '편집 유사도(Edit Similarity / Levenshtein Distance)'를 계산하는 알고리즘과 코드를 작성하고 있는데요. 전체 과정에서 논리적이고 순차적인 연산이 매우 중요하기 때문에,이는 GPU보다 CPU를 잘 활용해야 하는 task에 해당했습니다. 또한 해당 과제는 가지고 있는 데이터 내의 모든 문서쌍에 대해 편집 유사도를 계산해야 하는 O(n * m)의 시간 복잡도를 가진 작업으로, 엄청난 연산량이 요구되는 만큼 CPU의 병렬 처리가 반드시 필요한 상황이었습니다.


CPU 병렬 처리를 지원하는 라이브러리

파이썬에는 CPU의 병렬 처리를 지원하는 다양한 라이브러리가 존재합니다. 저는 그 중에서 아래와 같이 4개의 라이브러리를 선택, 직접 비교해 보았습니다.

[1] Multiprocessing

  • Python의 표준 라이브러리로 추가 설치 없이 사용할 수 있음
  • CPU의 여러 코어를 활용해 병렬 프로세싱을 쉽게 구현할 수 있음. 각 프로세스는 독립된 메모리 공간을 사용하기 때문에, 안전한 병렬 처리가 가능함. 단순한 병렬 작업을 수행할 때, 특히 CPU 집약적인 작업에 적합함
  • docs : https://docs.python.org/ko/3/library/multiprocessing.html
from multiprocessing import Pool

def f(x):
    return x*x

if __name__ == '__main__':
    with Pool(5) as p:
        print(p.map(f, [1, 2, 3]))
        
# [1, 4, 9]

[2] Ray

  • Ray는 기본적으로 CPU 자원을 병렬로 활용할 수 있도록 설계되어 있으며, 여러 CPU 코어를 이용해 동시에 여러 작업을 실행할 수 있게 해 줌.CPU, GPU 자원을 자동으로 관리하고 최적화된 성능을 제공.
  • 기존 코드를 수정할 필요 없이 @ray.remote 데코레이터를 사용해 매우 쉽게 병렬 작업을 처리할 수 있어 사용이 편리함
  • 로컬 환경뿐만 아니라 클러스터 환경에서 대규모 데이터 처리를 효율적으로 분산 실행할 수 있음
  •  docs : https://docs.ray.io/en/latest/ray-core/walkthrough.html?_gl=1*1e3lt4l*_ga*ODU4NzkwMDUxLjE3MjY3OTQxMjE.*_up*MQ..
# Define the square task.
@ray.remote
def square(x):
    return x * x

# Launch four parallel square tasks.
futures = [square.remote(i) for i in range(4)]

# Retrieve results.
print(ray.get(futures))
# -> [0, 1, 4, 9]

[3] Dask

  • 대규모 데이터 분석과 병렬 처리를 위한 동적 작업 스케줄러를 제공하는 라이브러리
  • Pandas, NumPy와 호환이 되므로 PandasNumPy 작업을 확장할 필요가 있을 때 매우 유용함
  • docs : https://docs.dask.org/en/stable/
import dask.array as da

x = da.random.random((10000, 10000), chunks=(1000, 1000))
result = x.mean().compute()  # Dask로 병렬 처리
print(result)

[4] ProcessPoolExecutor

  • Python의 concurrent.futures 모듈에 있는 고급 병렬 처리 라이브러리로, 다중 프로세스를 쉽게 관리할 수 있는 기능을 제공
  • multiprocessing과 유사하지만 더 높은 수준의 API를 제공해 프로세스를 관리하고 병렬 처리를 쉽게 수행
  • docs : https://docs.python.org/3/library/concurrent.futures.html
from concurrent.futures import ProcessPoolExecutor

def square(x):
    return x * x

with ProcessPoolExecutor() as executor:
    futures = [executor.submit(square, i) for i in range(5)]
    results = [f.result() for f in futures]
    print(results)  # [0, 1, 4, 9, 16]

처리 결과 비교

처리 결과, 제게 주어진 task에서 가장 좋은 성능을 보인 라이브러리는 Ray였습니다.

기존 작업 (병렬 처리 X) 약 26분
Ray 약 6분 (성능 약 77% 향상)
MultiProcessing 약 10분 (성능 약 61.54% 향상)
Dask 기존 시간(26분)을 초과하여 중단
ProcessPoolExecutor 기존 시간(26분)을 초과하여 중단

위와 같은 실험 결과를 토대로 저는 주어진 과제에 레이를 활용하기로 결정했습니다. 실제로 해당 결과를 공유한 이후MultiProcessing보다 레이가 더 빠르다니, 흥미로운 실험 결과라는 코멘트를 받기도 하였습니다 :)

물론 모든 작업에서 반드시 위와 같은 결과를 보이는 것이 절대 아니기 때문에, 과제에 따라 실험을 통해 적합한 라이브러리를 잘 취사 선택하는 것이 중요하겠습니다.


자원 확인

nproc
#128

리눅스 명령어로 확인한 결과 제 서버가 가지고 있는 CPU 코어는 총 128개로 확인이 되었습니다.

ray.init(include_dashboard=True)

 

ray의 init함수를 쓸 때 대시보드 설정을 True로 할당하면, 브라우저에서 localhost:8265에 접속해 대시보드를 볼 수 있습니다. (저는 회사 서버 + 도커 컨테이너를 쓰고 있어서 이 부분은 복잡해서 pass)

import ray

ray.init()

# 현재 사용 가능한 리소스 보기
available_resources = ray.available_resources()
print(available_resources)

위와 같이 available_resources 함수를 실행하면, 사용 중인 CPU와 GPU 자원에 대한 정보가 출력됩니다.

(예) {'accelerator_type:G': 1.0, 'node:__internal_head__': 1.0, 'CPU': 128.0, 'memory': 1058863122432.0, 'object_store_memory': 10000000000.0, 'GPU': 6.0, 'node:172.17.0.8': 1.0}

  • 'CPU': 128.0 -> 이 시스템에 128개의 CPU 코어가 사용 가능하다는 것을 나타냅니다. nproc 명령어에서 확인한 128개의 코어 수와 일치하네요. Ray는 이 128개의 코어를 병렬 처리에 사용합니다.
  • 'memory': 1058863122432.0 -> 이 값은 총 사용 가능한 메모리를 바이트 단위로 나타내고, 제게 주어진 시스템에는 약 1TB의 메모리가 사용 가능하네요.
  • 'object_store_memory': 10000000000.0 -> 이 메모리는 Ray에서 객체를 저장하는 데 사용됩니다.  10GB가 설정되어 있네요.

이상으로 CPU의 병렬 처리에 대한 간략한 포스팅을 마치도록 하겠습니다. 주어진 리소스를 잘 파악하고 100% 활용할 수 있도록 늘 고민하는 엔지니어가 될 수 있도록 오늘도 내일도 열심히 공부하고 노력하겠습니다 :)

감사합니다!

+ Recent posts