Skip to content
Published on

Ray 2.56의 레이블 로컬리티 스케줄링 — 배치 그룹이 노드가 아니라 NVLink 랙을 보기 시작했다

공유하기
Authors

들어가며 — 노드 경계가 더는 성능 경계가 아니다

분산 스케줄러에는 오래된 암묵적 가정이 하나 있습니다. "빠른 인터커넥트는 노드 안에서 끝난다." 8-GPU HGX 서버 시대에는 이 가정이 대체로 참이었습니다 — NVLink는 노드 내부에서 GPU들을 묶고, 노드 사이는 InfiniBand나 이더넷으로 떨어집니다. 그래서 스케줄러가 "같은 노드에 몰아넣기"만 할 줄 알면 빠른 통신은 자동으로 따라왔습니다.

NVIDIA의 랙 스케일 시스템이 이 가정을 깼습니다. GB300 NVL72는 NVIDIA 스펙 기준(벤더 자체 수치) Blackwell Ultra GPU 72개와 Grace CPU 36개를 랙 하나에서 NVLink 도메인 하나로 묶고, NVLink 대역폭을 130 TB/s로 제시합니다. 즉 이제 "빠른 인터커넥트"의 경계가 노드가 아니라 랙입니다. 노드 단위로만 생각하는 스케줄러에게는 보이지 않는 경계입니다.

2026년 6월 29일 나온 Ray 2.56.0은 이 지형 변화에 대한 Ray 코어의 첫 응답을 릴리스 하이라이트로 실었습니다 — GPU 도메인을 인지하는 배치 그룹, 공식 명칭으로는 레이블 로컬리티 스케줄링(label locality scheduling)입니다. 이 글은 이 기능이 무엇을 풀고, 정확히 어떻게 동작하고, 알파 딱지가 붙은 이유가 무엇인지를 릴리스 노트·문서·소스 코드에서 확인한 내용으로 정리합니다.

배치 그룹 복습 — 네 가지 전략은 전부 노드 단위다

Ray의 배치 그룹(placement group)은 자원 번들 묶음을 원자적으로 예약하는 갱 스케줄링 프리미티브입니다. "GPU 4개·CPU 2개짜리 번들 18개를 전부 확보되거나 아니면 하나도 잡지 마라" 같은 요구를 표현합니다. 멀티노드 학습이나 분산 추론처럼 워커 일부만 뜨면 의미가 없는 워크로드의 기본 도구이고, Ray Serve의 모델 서빙이나 분산 학습 스택 아래에서도 이게 돌고 있습니다.

배치 전략은 네 가지입니다 — PACK(한 노드에 몰기 선호), STRICT_PACK(한 노드 강제), SPREAD(노드마다 분산 선호), STRICT_SPREAD(노드마다 분산 강제). 여기서 주목할 단어는 전략 이름이 아니라 "노드"입니다. 네 전략 모두 배치의 단위가 노드입니다. 노드보다 큰 토폴로지 — 랙, NVLink 도메인 — 는 이 어휘에 존재하지 않습니다.

문제 — NVL 랙에서 STRICT_PACK은 불가능하고 PACK은 무심하다

Ray 2.56.0 문서가 드는 예를 그대로 옮기면 이렇습니다. 랙 2개, 랙마다 노드 18개, 노드마다 GPU 4개·CPU 2개인 클러스터에서, 번들 18개를 한 랙 안에 배치하고 싶습니다.

from ray.util.placement_group import placement_group

# 시도 1: STRICT_PACK -> 번들 18개를 '노드 하나'에 넣으려 함.
# 노드에는 GPU가 4개뿐이므로 이 배치 그룹은 영원히 pending.
pg = placement_group([{"GPU": 4, "CPU": 2}] * 18, strategy="STRICT_PACK")

# 시도 2: PACK -> 노드들에 나눠 담지만 '랙'이라는 개념이 없어서
# 번들이 두 랙에 걸쳐도 스케줄러는 아무 불만이 없음.
pg = placement_group([{"GPU": 4, "CPU": 2}] * 18, strategy="PACK")

두 랙에 걸치면 무슨 일이 생기냐면 — 걸친 지점의 통신이 NVLink 도메인 밖으로, 즉 랙 간 네트워크로 떨어집니다. 도메인 안에 있으라고 산 하드웨어를 스케줄러가 반쯤 무효화하는 셈입니다.

물론 우회는 있었습니다. 노드마다 커스텀 라벨을 달고 정적 라벨 셀렉터로 고정하는 방법입니다 — 예컨대 모든 번들에 rack-1 라벨을 요구하는 식으로. 문서는 이 우회의 한계를 두 가지로 명시합니다. 첫째, 셀렉터가 정적이라 장애에 대응하지 못합니다 — rack-1의 노드가 전부 죽으면 배치 그룹이 다른 랙으로 자동으로 옮겨 갈 수 없습니다. 둘째, 정작 원하는 건 "아무 랙이나 하나"인데 특정 랙을 손으로 지정해야 하고, 랙이 많아지면 이 관리가 그대로 짐이 됩니다.

Ray 2.56의 답 — 노드 전략 위에 도메인 레벨 레이어를 얹는다

레이블 로컬리티 스케줄링은 기존 노드 단위 전략을 대체하지 않고, 그 위에 도메인 레벨 스케줄링 레이어를 하나 얹습니다. 문서 기준으로 동작은 세 단계입니다.

  1. 후보 노드들을 ray.io/gpu-domain 라벨 값으로 묶는다 — 같은 값이면 같은 도메인.
  2. 모든 번들을 수용할 수 있는 도메인 하나를 고른다.
  3. 그 도메인 안에서, 사용자가 지정한 노드 레벨 전략을 그대로 적용한다.

도메인 레벨에서는 STRICT_PACK만 지원합니다 — 배치 그룹 전체가 도메인 하나 안에 들어가거나, 아니면 스케줄되지 않습니다. 발동 조건은 배치 그룹의 모든 번들이 bundle_label_selector에 GB200 또는 GB300 가속기 타입을 요구하는 것입니다.

import ray
from ray.util.placement_group import placement_group

bundles = [{"GPU": 4, "CPU": 2}] * 18
label_selector = [{"ray.io/accelerator-type": "GB300"}] * 18

pg = placement_group(
    bundles=bundles,
    bundle_label_selector=label_selector,
)

ray.get(pg.ready())  # 18개 번들 전부가 같은 ray.io/gpu-domain 안에 잡힘

발동 조건이 얼마나 좁은지는 2.56.0 태그의 소스에서 그대로 확인됩니다 — 트리거는 문자 그대로 GB200과 GB300 두 값의 하드코딩된 집합이고, 번들 하나라도 이 셀렉터를 달았는데 나머지가 같은 값이 아니면 ValueError를 던집니다. H100이든 B200이든 다른 가속기 타입을 적으면 도메인 레이어 없이 평범한 정적 라벨 셀렉터로만 동작합니다.

여기서 운영자가 반드시 알아야 할 비대칭이 하나 있습니다. 두 라벨의 출처가 다릅니다.

  • ray.io/accelerator-type은 Ray가 자동으로 답니다 — 라벨 문서 기준 기본 노드 라벨은 ray.io/node-id와 ray.io/accelerator-type 둘이고, 후자는 NVML이 보고하는 GPU 이름에서 유도됩니다(CPU 노드에는 안 붙습니다).
  • ray.io/gpu-domain은 Ray가 자동으로 달지 않습니다. 문서가 명시적으로 적어 둔 부분입니다 — 노드가 어느 랙에 속하는지는 운영자가 채워야 합니다.
# 노드가 속한 NVLink 도메인(랙)을 직접 라벨로 지정해야 한다
ray start --labels="ray.io/gpu-domain=rack-1"

즉 "도메인 인지"의 절반은 프로비저닝 파이프라인의 몫입니다. 노드 부팅 시점에 랙 식별자를 얻어 이 라벨에 적어 넣는 자동화가 없다면 이 기능은 시작조차 못 합니다. 참고로 오토스케일러 쪽은 준비가 되어 있습니다 — 도메인 제약과 라벨 셀렉터를 오토스케일러 프로토콜로 전달하는 변경(#61614, #62487)이 같은 릴리스에 들어가서, 스케일업 판단도 도메인 정보를 받아서 합니다.

장애 시맨틱 — 문서가 정직하게 적어 둔 부분

이 기능의 존재 이유 절반이 장애 대응입니다(정적 셀렉터는 랙이 죽으면 끝이었으니까). 문서가 명시한 시맨틱은 두 갈래입니다.

부분 장애 — 도메인 안의 일부 노드가 죽으면, Ray는 잃은 번들을 같은 도메인의 살아 있는 노드로 재스케줄합니다. 남은 번들 위의 액터·태스크는 계속 돕니다. 단, 같은 도메인 안에 자원이 모자라면 그 번들들은 infeasible 상태로 큐에 남아 같은 도메인에 자원이 날 때까지 기다립니다. 다른 도메인으로 옮기고 싶다면 방법은 하나뿐입니다 — remove_placement_group으로 지우고 새로 만드는 것. 그리고 이 호출은 그 배치 그룹의 번들을 쓰던 액터와 태스크를 전부 강제 종료하며 재시작해 주지 않습니다. 복구 자동화는 사용자 몫입니다.

전체 장애 — 도메인의 노드가 전부 죽으면, Ray가 도메인 할당을 지우고 배치 그룹 전체를 다른 도메인으로 재스케줄합니다. 정적 라벨 셀렉터가 못 하던 게 정확히 이것입니다.

어디에 잡혔는지는 확인할 수 있습니다. 대시보드 배치 그룹 테이블에 Label Domain 컬럼이 생겼고(#62533), 상태 API로도 보입니다.

ray list placement-groups --detail
# 출력 일부 (2.56.0 문서 예시를 요약)
- placement_group_id: 237f47c3235ac1a96ad423c3f74501000000
  name: gpu-domain-pg
  state: CREATED
  label_domain_key: ray.io/gpu-domain
  label_domain_assignments:
    ray.io/gpu-domain: rack-2

레이블 로컬리티를 안 쓰는 배치 그룹에서는 label_domain_key가 빈 문자열입니다.

알파는 알파다 — 한계 목록

문서 스스로 알파라고 경고하는 기능이고, 제약이 뚜렷합니다. 확인된 것만 나열합니다.

  • 트리거가 GB200·GB300 전용입니다. 소스에 하드코딩된 두 값이고, 문서도 이 두 가속기 타입만 지원한다고 명시합니다. 기능의 시작점이 된 PR #61442(2026년 4월 3일 머지)도 목적을 숨기지 않습니다 — NVML 도메인(GPU 도메인으로 추상화)을 위한 것이고, 우선은 GB300 랙을 겨냥한다고 적혀 있습니다.
  • 도메인 레벨 전략은 STRICT_PACK 하나입니다. 문서는 향후 도메인 레벨 전략 추가와 임의 라벨 도메인 지원 계획을 언급하지만, 현재는 계획일 뿐입니다.
  • ray.io/gpu-domain 라벨은 수동입니다. 랙 토폴로지를 라벨로 옮겨 적는 자동화를 직접 만들어야 하고, 이 라벨이 틀리면 스케줄러는 틀린 토폴로지를 믿습니다.
  • 부분 장애 시 같은 도메인 대기 시맨틱은 양날입니다. 도메인 안 복구가 목표라면 올바른 기본값이지만, "어느 도메인이든 빨리 다시 뜨는 것"이 더 중요한 워크로드라면 infeasible 큐에서 무기한 기다리는 쪽이 손해고, 탈출구는 액터 전멸을 감수하는 재생성뿐입니다.
  • 갓 만들어진 코드 경로입니다. 같은 릴리스 사이클에 레이블 도메인 배치 그룹이 infeasible 큐에 갇히는 버그 수정(#62483, 4월 25일 머지)이 이미 들어갔습니다. 알파 딱지는 장식이 아닙니다.
  • 성능 수치는 공개된 것이 없습니다. 릴리스 노트에도, 문서에도, PR에도 이 기능의 벤치마크는 없습니다. 애초에 속도 최적화가 아니라 배치 시맨틱 기능이고, 이득의 크기는 "도메인 안 NVLink 대역폭 대 랙 간 네트워크 대역폭 차이 × 당신 워크로드의 노드 간 통신량"으로 결정됩니다. 그 숫자는 당신 클러스터에서 재야 합니다.

이 기능이 놓인 자리 — 라벨로 수렴하는 Ray 스케줄링

레이블 로컬리티는 고립된 기능이 아니라, Ray 스케줄링이 라벨 셀렉터 위로 재편되는 흐름의 최신 단계입니다. 확인되는 타임라인은 이렇습니다 — 라벨 스케줄링 자체는 2.49(2025년 8월)부터 베타 기능으로 들어왔고(문서는 "KubeRay에서 스케줄링을 제어하는 용도"라고 소개하지만 ray start --labels 같은 일반 경로도 함께 문서화되어 있습니다), 2.50부터는 오토스케일러 v2를 켠 동적 클러스터라면 라벨 요구를 채우기 위해 지정된 워커 그룹에서 노드를 늘릴 줄 알게 됐고, 2.53은 Ray Train의 ScalingConfig에 label_selector를 달았습니다. 그리고 2.56은 Ray 내부 라이브러리들이 쓰던 NodeAffinitySchedulingStrategy(soft=False)를 ray.io/node-id 라벨 셀렉터로 교체하는 작업(#54940)과 함께, 라벨 위에 도메인 개념을 얹는 이번 기능을 실었습니다. 특수 목적 스케줄링 전략들을 하나씩 라벨 어휘로 흡수하는 방향입니다. TPU 쪽은 같은 문제를 SlicePlacementGroup이라는 별도 경로로 풀어 왔는데, 이번 릴리스에서 여기에도 명시적 bundle_label_selector 지원이 붙었습니다(#63171).

이 방향 자체는 Ray만의 것이 아닙니다. Kubernetes도 v1.36의 Workload·PodGroup API로 갱 스케줄링과 토폴로지 인지를 코어 스케줄러로 끌어들이는 중입니다. 랙 스케일 하드웨어가 스케줄러들에게 같은 숙제를 내고 있고, 다들 "노드보다 큰 단위"라는 답안을 쓰기 시작한 셈입니다.

언제 쓰고, 언제 무시해도 되나

쓸 이유가 있는 경우는 조건이 명확합니다 — GB200·GB300 NVL 랙이 있고, 클러스터에 도메인이 둘 이상 있고, 배치 그룹 하나가 노드 여러 개에 걸치는 워크로드(멀티노드 학습, 노드를 넘는 텐서/전문가 병렬 추론)를 돌릴 때. 이때 이 기능이 주는 건 두 가지입니다. 도메인 걸침 방지, 그리고 도메인을 손으로 지정하지 않아도 되는 것(전체 장애 시 자동 이사 포함).

반대로 다음 경우엔 무시해도 됩니다.

  • 워크로드가 노드 하나에 들어간다 — STRICT_PACK이 이미 답입니다.
  • 멀티노드 NVLink 도메인이 없다 — 8-GPU 서버 클러스터에서는 노드 경계가 곧 도메인 경계라 기존 전략으로 충분하고, 애초에 GB200·GB300이 아니면 트리거가 발동하지 않습니다.
  • 클러스터 전체가 도메인 하나다 — 고를 도메인이 하나뿐이면 도메인 선택 레이어가 해 줄 일이 없습니다.
  • 장애 시 "같은 도메인 대기"보다 "아무 데서나 빨리 재기동"이 중요하다 — 이 기능의 부분 장애 시맨틱이 오히려 발목을 잡을 수 있습니다. 재생성 자동화를 갖추고 쓰든가, 아직은 거리를 두는 게 낫습니다.

마치며

정리하면 이렇습니다. Ray의 배치 그룹은 지금까지 노드 단위 어휘만 갖고 있었고, NVL72류 랙 스케일 시스템은 그 어휘로 표현할 수 없는 경계 — 멀티노드 NVLink 도메인 — 를 만들었습니다. Ray 2.56.0의 레이블 로컬리티 스케줄링은 같은 ray.io/gpu-domain 라벨을 가진 노드 묶음을 도메인으로 정의하고, 배치 그룹 전체를 도메인 하나에 STRICT_PACK하는 레이어를 노드 전략 위에 얹습니다. 정적 라벨 셀렉터와 달리 도메인 전체 장애 시 자동으로 다른 도메인으로 옮겨 갑니다.

동시에 이 기능은 알파의 정의에 충실합니다. GB200·GB300 하드코딩 트리거, 도메인 레벨 STRICT_PACK 단일 전략, 수동 gpu-domain 라벨, 파괴적인 도메인 강제 이동 경로, 그리고 공개된 성능 수치 없음. NVL 랙을 실제로 운영한다면 지금 검증을 시작할 가치가 있고, 아니라면 — 이 기능이 당신에게 의미가 생기는 날은 하드웨어 구매 주문서가 먼저 알려 줄 겁니다.

참고 자료