본문으로 건너뛰기

[Ray] Actor Pool Map Operator 스케줄러 오버헤드 57% 감소

PR 링크: ray-project/ray#61591 상태: Merged | 변경: +11 / -13

들어가며

Ray Data 파이프라인에서 500개 이상의 액터를 사용할 때 스케줄러의 tick당 오버헤드가 병목이 됩니다. update_resource_usageselect_actors가 매 tick마다 모든 액터를 순회하는데, 프로파일링 결과 protobuf enum 재해석, 중복 dict lookup, 불필요한 함수 호출 등 Python 오버헤드가 지배적이었습니다.

핵심 코드 분석

Protobuf enum 상수를 모듈 레벨에서 캐싱

Before:

if actor_state in (None, gcs_pb2.ActorTableData.ActorState.DEAD):
    return running_actor_state
elif actor_state != gcs_pb2.ActorTableData.ActorState.ALIVE:
    assert actor_state == gcs_pb2.ActorTableData.ActorState.RESTARTING

After:

_ACTOR_STATE_DEAD = gcs_pb2.ActorTableData.ActorState.DEAD
_ACTOR_STATE_ALIVE = gcs_pb2.ActorTableData.ActorState.ALIVE
_ACTOR_STATE_RESTARTING = gcs_pb2.ActorTableData.ActorState.RESTARTING

if actor_state in (None, _ACTOR_STATE_DEAD):
    return running_actor_state
elif actor_state != _ACTOR_STATE_ALIVE:
    assert actor_state == _ACTOR_STATE_RESTARTING

Protobuf의 EnumTypeWrapper.__getattr__는 매 접근마다 문자열 룩업을 수행합니다. 모듈 로드 시 1회만 해석하면 1000개 액터 기준 약 5.3s에서 0.6s로 감소합니다.

_rank_actors에서 dict 조회 1회로 통합

Before:

ranks = [
    (
        locs_priorities.get(
            self._running_actors[actor].actor_location, INT32_MAX
        ),
        self._running_actors[actor].num_tasks_in_flight,
    )
    for actor in actors
]

After:

return [
    (
        locs_priorities.get(state.actor_location, INT32_MAX),
        state.num_tasks_in_flight,
    )
    for state in (self._running_actors[actor] for actor in actors)
]

액터당 self._running_actors[actor] 조회가 2회에서 1회로 줄어들고, 각 조회마다 발생하는 ActorHandle.__hash__ 비용이 절반이 됩니다.

왜 이게 좋은가

프로파일링 결과 (1000 액터 기준):

함수 Before After 감소
update_resource_usage 11.83s 5.06s 57%
refresh_actor_state 11.46s 4.54s 60%
select_actors 6.82s 2.76s 60%
_rank_actors 3.18s 1.25s 61%
  • 총 코드 변경은 +11/-13줄로 매우 작지만 효과는 극적입니다
  • Python의 동적 속성 조회, dict hashing, 함수 호출 오버헤드를 이해하고 있어야 가능한 최적화입니다
  • 핫 루프에서 상수를 로컬 변수로 끌어올리는 것은 CPython 성능 최적화의 기본 기법입니다

참고 자료

댓글

관련 포스트

PR Analysis 의 다른글