Files
2026-07-13 13:17:40 +08:00

211 lines
7.1 KiB
Python

import json
import logging
import os
import threading
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Any, Dict, Optional
import xgboost
from packaging.version import Version
from xgboost import RabitTracker
from xgboost.collective import CommunicatorContext
import ray
from ray.train._internal.base_worker_group import BaseWorkerGroup
from ray.train.backend import Backend, BackendConfig
from ray.train.v2._internal.util import TrainingFramework
logger = logging.getLogger(__name__)
@dataclass
class XGBoostConfig(BackendConfig):
"""Configuration for xgboost collective communication setup.
Ray Train will set up the necessary coordinator processes and environment
variables for your workers to communicate with each other.
Additional configuration options can be passed into the
`xgboost.collective.CommunicatorContext` that wraps your own `xgboost.train` code.
See the `xgboost.collective` module for more information:
https://github.com/dmlc/xgboost/blob/master/python-package/xgboost/collective.py
Args:
xgboost_communicator: The backend to use for collective communication for
distributed xgboost training. For now, only "rabit" is supported.
"""
xgboost_communicator: str = "rabit"
@property
def train_func_context(self):
@contextmanager
def collective_communication_context():
with CommunicatorContext(**_get_xgboost_args()):
yield
return collective_communication_context
@property
def framework(self):
return TrainingFramework.XGBOOST
@property
def backend_cls(self):
if self.xgboost_communicator == "rabit":
return (
_XGBoostRabitBackend
if Version(xgboost.__version__) >= Version("2.1.0")
else _XGBoostRabitBackend_pre_xgb210
)
raise NotImplementedError(f"Unsupported backend: {self.xgboost_communicator}")
def to_dict(self) -> Dict[str, Any]:
return {"xgboost_communicator": self.xgboost_communicator}
class _XGBoostRabitBackend(Backend):
def __init__(self):
self._tracker: Optional[RabitTracker] = None
self._wait_thread: Optional[threading.Thread] = None
def _setup_xgboost_distributed_backend(self, worker_group: BaseWorkerGroup):
# Set up the rabit tracker on the Train driver.
num_workers = len(worker_group)
rabit_args = {"n_workers": num_workers}
train_driver_ip = ray.util.get_node_ip_address()
# NOTE: sortby="task" is needed to ensure that the xgboost worker ranks
# align with Ray Train worker ranks.
# The worker ranks will be sorted by `dmlc_task_id`,
# which is defined below.
self._tracker = RabitTracker(
n_workers=num_workers, host_ip=train_driver_ip, sortby="task"
)
self._tracker.start()
# The RabitTracker is started in a separate thread, and the
# `wait_for` method must be called for `worker_args` to return.
self._wait_thread = threading.Thread(target=self._tracker.wait_for, daemon=True)
self._wait_thread.start()
rabit_args.update(self._tracker.worker_args())
start_log = (
"RabitTracker coordinator started with parameters:\n"
f"{json.dumps(rabit_args, indent=2)}"
)
logger.debug(start_log)
def set_xgboost_communicator_args(args):
import ray.train
args["dmlc_task_id"] = (
f"[xgboost.ray-rank={ray.train.get_context().get_world_rank():08}]:"
f"{ray.get_runtime_context().get_actor_id()}"
)
_set_xgboost_args(args)
worker_group.execute(set_xgboost_communicator_args, rabit_args)
def on_training_start(
self, worker_group: BaseWorkerGroup, backend_config: XGBoostConfig
):
assert backend_config.xgboost_communicator == "rabit"
self._setup_xgboost_distributed_backend(worker_group)
def on_shutdown(self, worker_group: BaseWorkerGroup, backend_config: XGBoostConfig):
timeout = 5
if self._wait_thread is not None:
self._wait_thread.join(timeout=timeout)
if self._wait_thread.is_alive():
logger.warning(
"During shutdown, the RabitTracker thread failed to join "
f"within {timeout} seconds. "
"The process will still be terminated as part of Ray actor cleanup."
)
class _XGBoostRabitBackend_pre_xgb210(Backend):
def __init__(self):
self._tracker: Optional[RabitTracker] = None
def _setup_xgboost_distributed_backend(self, worker_group: BaseWorkerGroup):
# Set up the rabit tracker on the Train driver.
num_workers = len(worker_group)
rabit_args = {"DMLC_NUM_WORKER": num_workers}
train_driver_ip = ray.util.get_node_ip_address()
# NOTE: sortby="task" is needed to ensure that the xgboost worker ranks
# align with Ray Train worker ranks.
# The worker ranks will be sorted by `DMLC_TASK_ID`,
# which is defined below.
self._tracker = RabitTracker(
n_workers=num_workers, host_ip=train_driver_ip, sortby="task"
)
self._tracker.start(n_workers=num_workers)
worker_args = self._tracker.worker_envs()
rabit_args.update(worker_args)
start_log = (
"RabitTracker coordinator started with parameters:\n"
f"{json.dumps(rabit_args, indent=2)}"
)
logger.debug(start_log)
def set_xgboost_env_vars():
import ray.train
for k, v in rabit_args.items():
os.environ[k] = str(v)
# Ranks are assigned in increasing order of the worker's task id.
# This task id will be sorted by increasing world rank.
os.environ["DMLC_TASK_ID"] = (
f"[xgboost.ray-rank={ray.train.get_context().get_world_rank():08}]:"
f"{ray.get_runtime_context().get_actor_id()}"
)
worker_group.execute(set_xgboost_env_vars)
def on_training_start(
self, worker_group: BaseWorkerGroup, backend_config: XGBoostConfig
):
assert backend_config.xgboost_communicator == "rabit"
self._setup_xgboost_distributed_backend(worker_group)
def on_shutdown(self, worker_group: BaseWorkerGroup, backend_config: XGBoostConfig):
if not self._tracker:
return
timeout = 5
self._tracker.thread.join(timeout=timeout)
if self._tracker.thread.is_alive():
logger.warning(
"During shutdown, the RabitTracker thread failed to join "
f"within {timeout} seconds. "
"The process will still be terminated as part of Ray actor cleanup."
)
_xgboost_args: dict = {}
_xgboost_args_lock = threading.Lock()
def _set_xgboost_args(args):
with _xgboost_args_lock:
global _xgboost_args
_xgboost_args = args
def _get_xgboost_args() -> dict:
with _xgboost_args_lock:
return _xgboost_args