Stage 1 · Ray 기본과 로컬 설치 — Task · ObjectRef · raylet/Worker
한눈에 보기
Ray는 “멀티프로세싱을 더 잘 만든 것”으로 오해받기 쉽지만, 실제로는 한 노드가 곧 완전한 분산 클러스터 라는 확연히 다른 전제에서 출발합니다. 이 첫 단계에서는 Ray를 한 대의 노트북에 설치하고, @ray.remote Task와 ObjectRef로 GIL을 초월한 CPU 병렬화를 직접 체험합니다.
이번 포스트에서 다루는 핵심 질문은 세 가지입니다.
- Ray를 로컬에 설치하고 기동하는 방법과, “한 노드 = 클러스터”는 무슨 뜻인가?
@ray.remoteTask와 ObjectRef,ray.get/put/wait는 어떻게 동작하는가?- raylet(스케줄러)과 Worker(실행 프로세스)는 어떻게 다르고, 오브젝트 스토어는 어떻게 동작하는가?
Ray 설치와 기동
설치 — ray[default]
Ray는 파이썬 패키지입니다. 권장 설치 형태는 ray[default] (Core + 대시보드 + 클러스터 런처)입니다.
pip install -U "ray[default]"
상황에 따라 추가 extras를 붙일 수 있습니다.
pip install -U "ray[data,train,tune,serve]" # ML 라이브러리 통합 권장
pip install -U "ray[all]" # 전부 (무거우니 비권장)
버전·플랫폼 참고: 2026년 8월 기준 최신은 2.58.0입니다. 공식 지원 플랫폼은 Linux(x86_64·aarch64) 와 Apple Silicon(M1~M4) macOS입니다. Windows는 베타이며 파이썬 3.12까지만 지원하고 다중 노드는 검증되지 않았습니다. 파이썬은 3.10 이상을 씁니다. (Pydantic은 v1이 2.56부터 지원 중단되므로 v2로 업그레이드하세요.)
기동 — ray.init() 한 번이 곧 클러스터
Ray를 시작하는 가장 간단한 방법은 코드에서 ray.init()을 호출하는 것입니다.
import ray
ray.init() # 자동 감지: 없으면 로컬 클러스터 시작
print(ray.cluster_resources()) # 사용 가능한 자원 확인
핵심 사고방식: ray.init()은 사실 “클러스터를 하나 기동한다”는 뜻입니다. GCS(Global Control Store, 메타데이터), head raylet, 워커 프로세스들, 오브젝트 스토어, 대시보드가 모두 한 노드에 공존합니다. 노드 수가 1개일 뿐, 구조는 진짜 클러스터와 동일합니다.
자원을 명시적으로 제어하고 싶다면:
ray.init(num_cpus=4) # 감지된 코어 수를 덮어씀 (Jupyter 등과 headroom 확보)
ray.init(num_gpus=0) # GPU 0개 선언 → GPU 요구 태스크가 깔끔하게 실패
ray.init(address="auto") # 기존 클러스터에 연결, 없으면 오류
local_mode=True는 더 이상 지원되지 않습니다. 예전에는 로컬에서 직렬 실행하던 옵션이었는데, Ray 분산 디버거로 대체되었습니다.num_cpus와 소규모 클러스터를 쓰는 것이 지금의 방식입니다.
CLI 기동 — ray start --head
터미널에서 head 노드를 띄우고 코드에서 연결할 수도 있습니다.
ray start --head --num-cpus 4 # head 노드 기동 (기본 GCS 포트 6379, 코어 수 지정)
# 이후 python/노트북에서 ray.init(address="auto") 로 연결
ray status # 노드·자원 상태 확인
ray stop # 모두 종료
대시보드 — http://127.0.0.1:8265
ray[default]를 설치하면 브라우저에서 대시보드를 엽니다. 클러스터 상태, 실행 중인 Task/Actor, 작업(Job), 오브젝트, 리소스 타임라인, 메모리·메트릭을 실시간으로 봅니다. 이 시리즈 내내 디버깅의 눈입니다.
오브젝트 스토어 주의점 (로컬): 오브젝트 스토어는 기본적으로 RAM의 30% 를 씁니다. Linux는
/dev/shm(공유 메모리)에 두지만, macOS는/tmp(디스크)에 두어 느립니다. 용량을 넘치면 객체가 디스크로 스필(spill) 되며 유실되지는 않습니다. 큰 결과를 만질 때는 이 점을 염두에 두세요.
Task와 ObjectRef
@ray.remote — 함수를 원격 실행으로
@ray.remote 데코레이터를 붙이면 함수가 Ray Task가 됩니다. .remote()로 호출하면 즉시 ObjectRef(결과에 대한 미래 참조)를 반환하고, 실행은 백그라운드에서 진행됩니다.
import ray
ray.init(num_cpus=4)
@ray.remote
def square(x: int) -> int:
return x * x
# 4개의 Task를 병렬로 제출 (각각 num_cpus=1 기본)
futures = [square.remote(i) for i in range(4)]
print(futures) # [ObjectRef(...), ...] — 즉시 반환
print(ray.get(futures)) # [0, 1, 4, 9] — 모두 준비될 때까지 블록
Task= 실행 단위. 기본num_cpus=1인 독립 스케줄링 단위이며, 4코어 머신에서는num_cpus=1태스크가 최대 4개 병렬 실행됩니다.ObjectRef= future. 함수의 인자로 ref를 넘기면 Ray가 자동으로 값으로 풀어줍니다. Task B가 Task A의 결과 ref를 인자로 받으면, A가 끝날 때까지 B는 자동으로 대기합니다(의존성 해소).
ray.get / ray.put / ray.wait
import ray
ray.init()
# put: 값을 스토어에 올리고 ObjectRef 획득 (큰 객체를 반복 전달할 때 유리)
big = list(range(10_000))
ref = ray.put(big)
@ray.remote
def process(data):
return sum(data)
print(ray.get(process.remote(ref))) # ref를 인자로 → 자동 해소
# wait: num_returns개가 준비되는 대로 (준비된 refs, 남은 refs) 반환 → 스트리밍
results = [slow_task.remote(i) for i in range(20)]
while results:
ready, results = ray.wait(results, num_returns=5, timeout=1.0)
for r in ready:
print("처리 완료:", ray.get(r))
ray.put: 객체를 스토어에 미리 올려 놓아 인자 전달 시 복사를 아끼고 지연 전달을 가능하게 합니다.ray.get(list): ref 리스트 전체가 준비될 때까지 블록 후 값 리스트 반환.ray.wait(refs, num_returns=N): N개가 준비되는 대로 (준비된 것, 남은 것)을 돌려줍니다. 결과가 부분적으로 도착하는 스트리밍 처리의 핵심 도구입니다.
Task 옵션 — 자원 제어
@ray.remote(num_cpus=4, num_gpus=0) # 자원 요구량 명시
def heavy():
return "heavy work"
# 또는 호출 시점에
heavy.options(num_cpus=4).remote()
@ray.remote(max_retries=3, retry_exceptions=True) # 내결함성
def flaky():
return "ok"
num_cpus/num_gpus: 스케줄링에 필요한 자원. 병렬 상한을 정합니다.max_retries/retry_exceptions: 시스템/앱 오류 시 재시도.num_returns=3: 한 Task가 여러 객체를 반환.
CPU 병렬화 데모 — GIL 초월
Concurrency 시리즈에서 보았듯, 파이썬은 GIL 때문에 순수 CPU 작업을 스레드로는 병렬화하지 못합니다. Ray는 multiprocessing처럼 프로세스로 실행하므로 GIL을 우회합니다.
import time, ray
from concurrent.futures import ProcessPoolExecutor
ray.init(num_cpus=8)
@ray.remote
def cpu_work(n: int) -> int:
total = 0
for i in range(n):
total += i ** 2
return total
NS = [1_000_000] * 8
# Ray로 병렬 실행
t0 = time.perf_counter()
results = ray.get([cpu_work.remote(n) for n in NS])
print(f"Ray: {time.perf_counter() - t0:.3f}s", results[:1])
# ProcessPoolExecutor로 동일 작업
t0 = time.perf_counter()
with ProcessPoolExecutor(max_workers=8) as ex:
list(ex.map(cpu_work, NS))
print(f"multiprocessing: {time.perf_counter() - t0:.3f}s")
두 방식 모두 코어 수만큼 진짜 병렬로 돕니다. Ray가 추가로 주는 것은 오브젝트 스토어·의존성 자동 해소·내결함성·ray.wait 스트리밍입니다 — 단순 루프라면 ProcessPoolExecutor가 더 가볍습니다.
raylet vs Worker — 실행 구조
제일 헷갈리는 구분입니다. 정리하면:
| raylet | Worker | |
|---|---|---|
| 무엇인가 | 노드당 하나의 C++ 프로세스 | Task/Actor 코드가 도는 파이썬 프로세스 |
| 하는 일 | 스케줄링, 자원 관리, 오브젝트 스토어 관리 | 실제 함수/메서드 실행 |
| 수명 | 노드가 살아있는 동안 | 워커 풀에 속해 재사용 |
- Task는 워커 풀을 공유합니다. 기본 1 CPU당 1 워커이며, raylet이 idle 워커에 작업을 넘기고, 없으면 새 파이썬 프로세스를 띄웁니다. 워커는 여러 Task에 재사용됩니다.
- Actor는 전용 워커를 독점합니다(2단계).
- 오브젝트 스토어는 노드의 공유 메모리로, 반환값·
ray.put객체가 여기 살고 결과가 스필할 수 있습니다.
ray status와 대시보드로 워커 수와 자원 사용을 직접 확인해 보세요.
Summary
- Ray는 한 노드 = 완전한 클러스터 전제에서 출발합니다.
ray.init()한 번이 GCS·raylet·워커·오브젝트 스토어·대시보드를 한 머신에 띄웁니다. @ray.remoteTask는 프로세스로 실행되어 GIL을 우회하고, ObjectRef future +ray.get/put/wait로 결과를 관리합니다.- raylet(스케줄러) 과 Worker(실행 프로세스) 는 다릅니다. Task는 워커 풀(1 CPU당 1)을 공유하고, Actor는 워커를 독점합니다.
- 오브젝트 스토어는 RAM 30%를 쓰고 넘치면 디스크로 스필됩니다(macOS는 디스크 백업이라 느림).
다음 학습 (Next Learning)
- Stage 2 · Actor와 동시성 — 상태를 가진 분산 객체(Actor)와 Job 내부의 in-process 동시성(
max_concurrency)을 배웁니다. - Python GIL — Ray의 프로세스 실행이 왜 GIL을 우회하는지 그 원인을 다시 확인.
- Stage 3 · Ray Data 파이프라인 — Task를 넘어 block 단위의 데이터 파이프라인과 샤딩을 다룹니다.