Stage 1 · Ray 기본과 로컬 설치 — Task · ObjectRef · raylet/Worker

Ray 기본 — raylet이 ObjectRef로 Task를 워커 풀에 분배하는 구조 Driver (내 코드) @ray.remote · .remote() raylet (스케줄러) 자원 · 데이터 로컬리티 Task 제출 오브젝트 스토어 메모리 / 디스크 스필 Worker 1 (프로세스) Worker 2 (프로세스) Worker 3 (프로세스) 결과 ObjRef Task는 워커 풀을 공유 (1 CPU당 1 워커)
Ray 기본 구조 — 내 코드(Driver)가 `@ray.remote` 함수를 호출하면 곧바로 비동기 **ObjectRef**를 받고, **raylet**(스케줄러)이 이를 자원과 데이터 위치를 고려해 워커 풀(1 CPU당 1 프로세스)에 분배합니다. 결과는 **오브젝트 스토어**에 저장되어 `ray.get`으로 가져옵니다.

한눈에 보기

Ray는 “멀티프로세싱을 더 잘 만든 것”으로 오해받기 쉽지만, 실제로는 한 노드가 곧 완전한 분산 클러스터 라는 확연히 다른 전제에서 출발합니다. 이 첫 단계에서는 Ray를 한 대의 노트북에 설치하고, @ray.remote Task와 ObjectRef로 GIL을 초월한 CPU 병렬화를 직접 체험합니다.

이번 포스트에서 다루는 핵심 질문은 세 가지입니다.

  1. Ray를 로컬에 설치하고 기동하는 방법과, “한 노드 = 클러스터”는 무슨 뜻인가?
  2. @ray.remote Task와 ObjectRef, ray.get/put/wait는 어떻게 동작하는가?
  3. 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.remote Task는 프로세스로 실행되어 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 단위의 데이터 파이프라인과 샤딩을 다룹니다.