Files
modelscope--ms-swift/swift/ray/megatron/pipeline.py
T
wehub-resource-sync a203934033
Lint test / lint (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:34:58 +08:00

410 lines
16 KiB
Python

# Copyright (c) ModelScope Contributors. All rights reserved.
import importlib
import os
import ray
from contextlib import contextmanager
from typing import Any, Dict, List, Optional
from swift.utils import get_logger
from .base_trainer import BaseRayTrainer
from .driver_utils import (build_dataset_from_dict, compute_iter_params, estimate_dp_size, merge_group_dict,
parse_ray_yaml)
logger = get_logger()
_TRAINER_REGISTRY: Dict[str, Dict[str, Any]] = {
'grpo': {
'trainer': 'swift.ray.megatron.grpo_trainer.GRPOTrainer',
'loss': 'swift.ray.megatron.loss.grpo.GRPOLoss',
},
'gkd': {
'trainer': 'swift.ray.megatron.gkd_trainer.GKDTrainer',
'loss': 'swift.ray.megatron.loss.gkd.GKDLoss',
},
}
def register_ray_trainer(
rlhf_type: str,
trainer: str,
loss: Optional[str] = None,
):
"""Register a custom algorithm for the Ray pipeline.
Args:
rlhf_type: Algorithm identifier (e.g. ``'grpo'``).
trainer: Dotted path to the driver-side trainer class.
The class must accept ``(worker_groups, rollout_replicas)``
and expose ``set_data_info()`` / ``train()`` methods.
Example: ``'swift.ray.megatron.grpo_trainer.GRPOTrainer'``
loss: Dotted path to a ``Loss`` subclass that defines
``forward_step`` + ``loss_func``.
Pass ``None`` to use the internal trainer's forward_step.
"""
_TRAINER_REGISTRY[rlhf_type] = {'trainer': trainer, 'loss': loss}
class MegatronRayPipeline:
def __init__(self, config_path: str):
self.ray_config, group_configs, shared_config = parse_ray_yaml(config_path)
shared_config['use_ray'] = True
self.rlhf_type = self.ray_config.rlhf_type
if self.rlhf_type not in _TRAINER_REGISTRY:
raise ValueError('Unknown rlhf_type %r. Available: %s' % (self.rlhf_type, list(_TRAINER_REGISTRY)))
self.group_cfgs: Dict[str, Dict[str, Any]] = {
g: merge_group_dict(shared_config, gd)
for g, gd in group_configs.items()
}
self.shared_cfg = {k: v for k, v in shared_config.items() if v is not None}
self._entry = _TRAINER_REGISTRY[self.rlhf_type]
self.resource_pool_manager = None
self.worker_groups: Dict[str, Any] = {}
self.rollout_replicas: List[Any] = []
self.teacher_replicas: List[Any] = []
def init(self) -> None:
# Initialize Ray, create resource pools, spawn workers and replicas.
self._data_info = self._build_dataset()
self._compute_train_iters()
ray.init(ignore_reinit_error=True)
self._create_pools()
self._init_worker_groups()
with self._colocate_offload_ctx():
self._init_rollout_replicas()
self._init_teacher_replicas()
self._driver_trainer: BaseRayTrainer = self._create_trainer()
self._driver_trainer.set_data_info(self._data_info)
def train(self) -> Any:
"""Run the training loop. Requires ``init()`` to have been called."""
if not hasattr(self, '_driver_trainer'):
raise RuntimeError('MegatronRayPipeline.train(): call init() first')
return self._driver_trainer.train()
def run(self) -> Any:
"""Convenience: ``init()`` + ``train()`` + ``shutdown()``."""
self.init()
try:
return self.train()
finally:
self._shutdown()
def _build_dataset(self) -> Dict[str, Any]:
# Merge train group config (tuner_type, lora_rank, etc.) into shared cfg so that
# _check_teacher can detect LoRA self-distillation (teacher_model == model + lora).
cfg = {**self.shared_cfg, **self.group_cfgs.get('train', {})}
data_info = build_dataset_from_dict(cfg)
return data_info
def _compute_train_iters(self):
train_cfg = self.group_cfgs.get('train')
gpus = self.group_gpus.get('train', 0)
assert train_cfg is not None and gpus > 0
dp_size = estimate_dp_size(train_cfg, gpus)
iter_params = compute_iter_params(self._data_info, dp_size)
train_iters = iter_params.get('train_iters')
assert train_iters is not None and train_iters > 0
self.group_cfgs['train']['train_iters'] = train_iters
def _create_pools(self):
from .resource_pool import ResourcePool, ResourcePoolManager
colocated_sets = {frozenset(g) for g in self.colocate_groups}
pool_mapping: Dict[str, ResourcePool] = {}
assigned: set = set()
for colocated in colocated_sets:
# Colocated roles share one GPU set, so they must request the same number
# of gpus. Validate explicitly (and avoid relying on frozenset order).
gpus_by_role = {g: self.group_gpus.get(g, 0) for g in colocated}
distinct = set(gpus_by_role.values())
if distinct == {0}:
continue
if len(distinct) > 1:
raise ValueError(f'Colocated roles must request the same number of gpus, but got '
f'{gpus_by_role}. Set an equal `gpus` for all roles in {sorted(colocated)}.')
gpus = distinct.pop()
pon = self.ray_config.gpus_as_process_on_nodes(gpus)
shared = ResourcePool(pon, max_colocate_count=len(colocated))
for g in colocated:
pool_mapping[g] = shared
assigned.add(g)
for name, gpus in self.group_gpus.items():
if name in assigned or gpus <= 0:
continue
pon = self.ray_config.gpus_as_process_on_nodes(gpus)
pool_mapping[name] = ResourcePool(pon)
self.resource_pool_manager = ResourcePoolManager(pool_mapping)
self.resource_pool_manager.create_all()
def _is_rollout_hybrid(self) -> bool:
"""True if rollout shares its pool with train (HYBRID mode)."""
return any('rollout' in cg and 'train' in cg for cg in self.colocate_groups)
def _init_worker_groups(self):
self._validate_grpo_train_batch_params()
self._spawn_train_group('train')
train_wg = self.worker_groups['train']
padding_vals = train_wg.broadcast('get_padding_to')
self._data_info['_padding_to'] = next((v for v in padding_vals if v is not None), None)
def _validate_grpo_train_batch_params(self) -> None:
"""Early validation of GRPO batch params before spawning workers.
Resolves generation_batch_size / steps_per_generation, then validates
DP alignment — all via static helpers in RLHFMegatronArgumentsMixin.
"""
if self.rlhf_type != 'grpo':
return
train_cfg = self.group_cfgs.get('train')
train_gpus = self.group_gpus.get('train', 0)
if not train_cfg or train_gpus <= 0:
return
cfg = dict(train_cfg)
global_batch_size = cfg.get('global_batch_size')
if global_batch_size <= 0:
return
micro_batch_size = cfg.get('micro_batch_size', 1)
num_generations = cfg.get('num_generations', 8)
from swift.megatron.arguments.megatron_args import RLHFMegatronArgumentsMixin
generation_batch_size, _ = RLHFMegatronArgumentsMixin.resolve_generation_batch_size(
cfg.get('generation_batch_size'), cfg.get('steps_per_generation'), global_batch_size, num_generations)
dp_size = estimate_dp_size(cfg, train_gpus)
RLHFMegatronArgumentsMixin.validate_batch_dp_alignment(generation_batch_size, num_generations, dp_size,
micro_batch_size, train_gpus)
def _spawn_train_group(self, role: str) -> None:
from .megatron_worker import MegatronWorker
from .worker_group import WorkerGroup
pool = self.resource_pool_manager.get_pool(role)
cfg = dict(self.group_cfgs.get(role, {}))
cfg.setdefault('rlhf_type', self.ray_config.rlhf_type)
worker_cls = ray.remote(num_gpus=0)(MegatronWorker)
wg = WorkerGroup.from_pool(role, pool, worker_cls=worker_cls)
loss_cls = self._entry.get('loss')
rollout_config = self._build_rollout_config_for_workers() if self._is_rollout_hybrid() else None
wg.broadcast('init_actor', cfg, loss_cls_path=loss_cls, rollout_config=rollout_config)
wg.build_dispatch_info(worker_cls=MegatronWorker)
self.worker_groups[role] = wg
logger.info('MegatronWorker group [%s] on %d GPUs', role, pool.world_size)
@contextmanager
def _colocate_offload_ctx(self):
"""Offload train workers during vLLM init (colocate only)."""
need = self._is_rollout_hybrid() and bool(self.shared_cfg.get('offload_model', True))
colocated_wgs = [
wg for role, wg in self.worker_groups.items() if need and any(role in g for g in self.colocate_groups)
]
for wg in colocated_wgs:
wg.broadcast('offload_to_cpu')
try:
yield
finally:
for wg in colocated_wgs:
wg.broadcast('reload_to_gpu')
def _init_rollout_replicas(self) -> None:
rollout_gpus = self.group_gpus.get('rollout', 0)
if rollout_gpus <= 0:
self.rollout_replicas = []
return
from .rollout.replica import RolloutReplica
rollout_cfg = self._with_router_replay_rollout_config(self.group_cfgs.get('rollout', {}))
is_hybrid = self._is_rollout_hybrid()
pool = self.resource_pool_manager.get_pool('train' if is_hybrid else 'rollout')
template_kwargs = self._get_template_kwargs_for_rollout()
self.rollout_replicas = RolloutReplica.create_replicas(
rollout_cfg=rollout_cfg,
rollout_gpus=rollout_gpus,
pool=pool,
is_hybrid=is_hybrid,
sleep_level=self.ray_config.sleep_level,
template_kwargs=template_kwargs,
)
def _init_teacher_replicas(self) -> None:
teacher_gpus = self.group_gpus.get('teacher', 0)
if teacher_gpus <= 0:
self.teacher_replicas = []
return
from .rollout.replica import RolloutReplica
teacher_cfg = self.group_cfgs.get('teacher', {})
pool = self.resource_pool_manager.get_pool('teacher')
template_kwargs = self._get_template_kwargs_for_rollout()
args = self._data_info.get('_driver_args')
if args is not None:
base_len = template_kwargs.get('max_length') or getattr(args, 'max_length')
template_kwargs = dict(template_kwargs)
template_kwargs['max_length'] = base_len + args.max_completion_length
self.teacher_replicas = RolloutReplica.create_replicas(
rollout_cfg=teacher_cfg,
rollout_gpus=teacher_gpus,
pool=pool,
is_hybrid=False,
sleep_level=0,
template_kwargs=template_kwargs,
actor_name_prefix='swift_teacher_server',
)
def _with_router_replay_rollout_config(self, rollout_cfg: Dict[str, Any]) -> Dict[str, Any]:
cfg = dict(rollout_cfg or {})
args = self._data_info.get('_driver_args')
router_mode = getattr(args, 'router_replay_mode', 'disabled') if args is not None else 'disabled'
if router_mode != 'R3':
return cfg
from swift.rlhf_trainers.utils import check_vllm_version_ge
if not check_vllm_version_ge('0.14.0'):
raise ValueError('router_replay_mode=R3 requires vLLM>=0.14.0 to return routed_experts.')
engine_kwargs = dict(cfg.get('vllm_engine_kwargs') or {})
engine_kwargs.setdefault('enable_return_routed_experts', True)
# https://github.com/vllm-project/vllm/pull/39917
import vllm
from packaging import version
vllm_version = vllm.__version__
if vllm_version is not None and version.parse('0.21.0rc1') <= version.parse(vllm_version) <= version.parse(
'0.21.0'):
engine_kwargs.setdefault('async_scheduling', False)
cfg['vllm_engine_kwargs'] = engine_kwargs
return cfg
def _get_template_kwargs_for_rollout(self) -> Dict[str, Any]:
"""Extract template config for vLLM alignment (padding_free=False, sp=1)."""
args = self._data_info.get('_driver_args')
if args is None:
return {}
kwargs = args.get_template_kwargs()
kwargs['padding_free'] = False
kwargs['sequence_parallel_size'] = 1
return kwargs
def _build_rollout_config_for_workers(self) -> Optional[Dict[str, Any]]:
"""Build rollout config dict for MegatronWorker._init_rollout_adapter.
Returns None if no rollout GPUs are configured.
"""
rollout_gpus = self.group_gpus.get('rollout', 0)
if rollout_gpus <= 0:
return None
rollout_cfg = self.group_cfgs.get('rollout', {})
bucket_mb = int(os.environ.get('SWIFT_RAY_WEIGHT_BUCKET_MB', '2048'))
return {
'rollout_tp_size': rollout_cfg.get('vllm_tensor_parallel_size', 1),
'rollout_dp_size': rollout_cfg.get('vllm_data_parallel_size', 1),
'bucket_size_mb': bucket_mb,
}
def _create_trainer(self):
cls_path = self._entry['trainer']
mod_path, cls_name = cls_path.rsplit('.', 1)
mod = importlib.import_module(mod_path)
trainer_cls = getattr(mod, cls_name)
weight_sync_mode = self._get_weight_sync_mode()
sleep_level = self._resolve_sleep_level()
return trainer_cls(
self.worker_groups,
self.rollout_replicas,
weight_sync_mode=weight_sync_mode,
sleep_level=sleep_level,
teacher_replicas=self.teacher_replicas)
def _resolve_sleep_level(self) -> int:
"""Colocate: honor user config; separate: force 0."""
user_level = self.ray_config.sleep_level
if self._is_rollout_hybrid():
return user_level
if user_level != 0:
logger.warning('sleep_level=%d ignored in separate mode (vLLM stays resident). '
'Overriding to 0.', user_level)
return 0
def _get_weight_sync_mode(self) -> str:
"""Colocate: naive (IPC); separate: nccl (broadcast)."""
if self._is_rollout_hybrid():
return 'naive'
return 'nccl'
@property
def group_gpus(self) -> Dict[str, int]:
return self.ray_config.group_gpus
@property
def colocate_groups(self) -> List[List[str]]:
return self.ray_config.colocate_groups
def _shutdown(self):
"""Best-effort teardown — each step swallows exceptions so a
failure in one stage does not skip the remaining cleanup."""
for replica in self.rollout_replicas:
try:
replica.shutdown()
except Exception as e: # noqa: BLE001
logger.warning('RolloutReplica shutdown failed: %s', e)
self.rollout_replicas = []
for replica in self.teacher_replicas:
try:
replica.shutdown()
except Exception as e: # noqa: BLE001
logger.warning('TeacherReplica shutdown failed: %s', e)
self.teacher_replicas = []
seen: set = set()
for wg in self.worker_groups.values():
if id(wg) not in seen:
seen.add(id(wg))
try:
wg.shutdown()
except Exception as e: # noqa: BLE001
logger.warning('WorkerGroup shutdown failed: %s', e)
self.worker_groups.clear()
if self.resource_pool_manager is not None:
try:
self.resource_pool_manager.destroy_all()
except Exception as e: # noqa: BLE001
logger.warning('destroy_all placement groups failed: %s', e)
ray.shutdown()
def main():
import sys
argv = sys.argv[1:]
config_path = None
for i, arg in enumerate(argv):
if arg == '--config' and i + 1 < len(argv):
config_path = argv[i + 1]
break
if config_path is None:
raise ValueError('Usage: python -m swift.ray.megatron.pipeline --config <yaml>')
return MegatronRayPipeline(config_path).run()
if __name__ == '__main__':
main()