|
27 | 27 | import logging |
28 | 28 | import os |
29 | 29 | import threading |
30 | | -from typing import Any, List, Optional |
| 30 | +from pathlib import Path |
| 31 | +from typing import Any, Dict, List, Optional |
31 | 32 |
|
32 | 33 | import ray |
33 | 34 |
|
|
50 | 51 | _BOOTSTRAP_CACHE_VERSION = -1 |
51 | 52 |
|
52 | 53 |
|
| 54 | +def _config_get(config: Any, key: str, default: Any = None) -> Any: |
| 55 | + if isinstance(config, dict): |
| 56 | + return config.get(key, default) |
| 57 | + return getattr(config, key, default) |
| 58 | + |
| 59 | + |
53 | 60 | # --------------------------------------------------------------------------- |
54 | 61 | # RLix hooks — real implementation injected into async_grpo_train |
55 | 62 | # --------------------------------------------------------------------------- |
@@ -593,26 +600,237 @@ def run(self) -> None: |
593 | 600 | def _setup_nemo_rl_objects(self) -> tuple: |
594 | 601 | """Create NeMo RL runtime objects from pipeline_config. |
595 | 602 |
|
596 | | - In the full implementation this mirrors examples/run_grpo.py: |
597 | | - - Create Policy (Megatron backend) on shared PG from F12. |
598 | | - - Create VllmGeneration on shared PG from F12. |
599 | | - - Build dataloader, tokenizer, loss_fn, checkpointer. |
600 | | - - Return them for async_grpo_train. |
| 603 | + Mirrors ``examples/run_grpo.py`` through tokenizer, generation config, |
| 604 | + response data, and ``grpo.setup()``. The only RLix-specific difference is |
| 605 | + that training and inference clusters are injected as shared-PG backed |
| 606 | + ``RLixVirtualClusterAdapter`` instances instead of letting NeMo RL create |
| 607 | + standalone ``RayVirtualCluster`` placement groups. |
| 608 | + """ |
| 609 | + from omegaconf import OmegaConf |
| 610 | + |
| 611 | + from nemo_rl.algorithms.grpo import setup as grpo_setup |
| 612 | + from nemo_rl.algorithms.utils import get_tokenizer |
| 613 | + from nemo_rl.data.utils import setup_response_data |
| 614 | + from nemo_rl.models.generation import configure_generation_config |
| 615 | + from nemo_rl.utils.config import ( |
| 616 | + load_config, |
| 617 | + parse_hydra_overrides, |
| 618 | + register_omegaconf_resolvers, |
| 619 | + ) |
| 620 | + from nemo_rl.utils.logger import get_next_experiment_dir |
601 | 621 |
|
602 | | - Feature 12 dependency: Policy and VllmGeneration must be initialized |
603 | | - on placement groups obtained from RollResourceManagerProxy (shared PG), |
604 | | - not via RayVirtualCluster.create() which would conflict with ROLL workers |
605 | | - in mixed-deployment mode. |
| 622 | + nemo_config_path = self._resolve_nemo_config_path() |
| 623 | + register_omegaconf_resolvers() |
| 624 | + cfg = load_config(nemo_config_path) |
606 | 625 |
|
607 | | - Raises: |
608 | | - NotImplementedError: Until Feature 12 (shared PG) is implemented |
609 | | - and wired into this method. |
610 | | - """ |
611 | | - raise NotImplementedError( |
612 | | - "_setup_nemo_rl_objects requires Feature 12 (shared PlacementGroup) " |
613 | | - "to be implemented. In the meantime, call async_grpo_train directly " |
614 | | - "from your training script and pass rlix_hooks=NemoRLRLixHooks(pipeline)." |
| 626 | + overrides = _config_get(self._pipeline_config, "nemo_config_overrides", None) |
| 627 | + if overrides: |
| 628 | + cfg = parse_hydra_overrides(cfg, list(overrides)) |
| 629 | + |
| 630 | + master_config = OmegaConf.to_container(cfg, resolve=True) |
| 631 | + if not isinstance(master_config, dict): |
| 632 | + raise RuntimeError( |
| 633 | + f"NeMo config {nemo_config_path!s} did not resolve to a dict" |
| 634 | + ) |
| 635 | + |
| 636 | + logger.info("[%s] Loaded NeMo RL config from %s", self._pipeline_id, nemo_config_path) |
| 637 | + |
| 638 | + if bool(_config_get(self._pipeline_config, "nemo_increment_log_dir", True)): |
| 639 | + master_config["logger"]["log_dir"] = get_next_experiment_dir( |
| 640 | + master_config["logger"]["log_dir"] |
| 641 | + ) |
| 642 | + |
| 643 | + tokenizer = get_tokenizer(master_config["policy"]["tokenizer"]) |
| 644 | + if master_config["policy"]["generation"] is None: |
| 645 | + raise RuntimeError("NeMo RL GRPO requires policy.generation config") |
| 646 | + has_refit_draft_weights = bool(master_config["policy"]["draft"]["enabled"]) |
| 647 | + master_config["policy"]["generation"] = configure_generation_config( |
| 648 | + master_config["policy"]["generation"], |
| 649 | + tokenizer, |
| 650 | + has_refit_draft_weights=has_refit_draft_weights, |
| 651 | + ) |
| 652 | + |
| 653 | + dataset, val_dataset, task_to_env, val_task_to_env = setup_response_data( |
| 654 | + tokenizer, |
| 655 | + master_config["data"], |
| 656 | + master_config["env"], |
| 657 | + ) |
| 658 | + |
| 659 | + train_device_mapping = self._resolve_device_mapping( |
| 660 | + master_config, "train_device_mapping" |
| 661 | + ) |
| 662 | + infer_device_mapping = self._resolve_device_mapping( |
| 663 | + master_config, "infer_device_mapping" |
| 664 | + ) |
| 665 | + train_cluster = self._make_rlix_virtual_cluster( |
| 666 | + name=f"{self._pipeline_id}_nemo_train", |
| 667 | + device_mapping=train_device_mapping, |
| 668 | + max_colocated_worker_groups=1, |
| 669 | + sorted_bundle_indices=train_device_mapping, |
615 | 670 | ) |
| 671 | + infer_cluster = self._make_rlix_virtual_cluster( |
| 672 | + name=f"{self._pipeline_id}_nemo_infer", |
| 673 | + device_mapping=infer_device_mapping, |
| 674 | + max_colocated_worker_groups=1, |
| 675 | + sorted_bundle_indices=None, |
| 676 | + ) |
| 677 | + |
| 678 | + ( |
| 679 | + policy, |
| 680 | + policy_generation, |
| 681 | + _clusters, |
| 682 | + dataloader, |
| 683 | + val_dataloader, |
| 684 | + loss_fn, |
| 685 | + nemo_logger, |
| 686 | + checkpointer, |
| 687 | + grpo_save_state, |
| 688 | + master_config, |
| 689 | + ) = grpo_setup( |
| 690 | + master_config, |
| 691 | + tokenizer, |
| 692 | + dataset, |
| 693 | + val_dataset, |
| 694 | + external_train_cluster=train_cluster, |
| 695 | + external_inference_cluster=infer_cluster, |
| 696 | + ) |
| 697 | + |
| 698 | + if policy_generation is not None: |
| 699 | + setattr(policy_generation, "_rlix_device_mapping", list(infer_device_mapping)) |
| 700 | + |
| 701 | + self._policy = policy |
| 702 | + self._policy_generation = policy_generation |
| 703 | + if self._model_update_service is None: |
| 704 | + self._create_model_update_service() |
| 705 | + |
| 706 | + async_cfg = master_config["grpo"]["async_grpo"] |
| 707 | + return ( |
| 708 | + policy, |
| 709 | + policy_generation, |
| 710 | + dataloader, |
| 711 | + val_dataloader, |
| 712 | + tokenizer, |
| 713 | + loss_fn, |
| 714 | + task_to_env, |
| 715 | + val_task_to_env, |
| 716 | + nemo_logger, |
| 717 | + checkpointer, |
| 718 | + grpo_save_state, |
| 719 | + master_config, |
| 720 | + int(async_cfg["max_trajectory_age_steps"]), |
| 721 | + ) |
| 722 | + |
| 723 | + def _resolve_nemo_config_path(self) -> Path: |
| 724 | + raw_path = ( |
| 725 | + _config_get(self._pipeline_config, "nemo_config_path") |
| 726 | + or _config_get(self._pipeline_config, "nemo_rl_config_path") |
| 727 | + or _config_get(self._pipeline_config, "config") |
| 728 | + ) |
| 729 | + if not raw_path: |
| 730 | + raise RuntimeError( |
| 731 | + "NemoRLFullFinetunePipeline requires pipeline_config.nemo_config_path" |
| 732 | + ) |
| 733 | + path = Path(str(raw_path)).expanduser() |
| 734 | + if not path.is_absolute(): |
| 735 | + path = Path.cwd() / path |
| 736 | + if not path.exists(): |
| 737 | + raise FileNotFoundError(f"NeMo RL config not found: {path}") |
| 738 | + return path |
| 739 | + |
| 740 | + def _resolve_device_mapping(self, master_config: Dict[str, Any], key: str) -> List[int]: |
| 741 | + explicit = _config_get(self._pipeline_config, key) |
| 742 | + if explicit is None: |
| 743 | + explicit = ( |
| 744 | + master_config.get("rlix", {}).get(key) |
| 745 | + if isinstance(master_config.get("rlix"), dict) |
| 746 | + else None |
| 747 | + ) |
| 748 | + if explicit is None: |
| 749 | + raise RuntimeError( |
| 750 | + f"Missing {key}; provide pipeline_config.{key} or " |
| 751 | + f"nemo_config.rlix.{key}" |
| 752 | + ) |
| 753 | + mapping = [int(x) for x in explicit] |
| 754 | + if not mapping: |
| 755 | + raise RuntimeError(f"{key} must be non-empty") |
| 756 | + return mapping |
| 757 | + |
| 758 | + def _make_rlix_virtual_cluster( |
| 759 | + self, |
| 760 | + *, |
| 761 | + name: str, |
| 762 | + device_mapping: List[int], |
| 763 | + max_colocated_worker_groups: int, |
| 764 | + sorted_bundle_indices: Optional[List[int]], |
| 765 | + ) -> Any: |
| 766 | + from rlix.pipeline.nemo_rl_virtual_cluster_adapter import RLixVirtualClusterAdapter |
| 767 | + |
| 768 | + pg_alloc = self._allocate_shared_pg(device_mapping=device_mapping) |
| 769 | + placement_groups = self._extract_placement_groups(pg_alloc) |
| 770 | + bundle_ct_per_node_list = self._extract_bundle_counts( |
| 771 | + pg_alloc=pg_alloc, |
| 772 | + placement_groups=placement_groups, |
| 773 | + device_mapping=device_mapping, |
| 774 | + ) |
| 775 | + return RLixVirtualClusterAdapter( |
| 776 | + placement_groups=placement_groups, |
| 777 | + bundle_ct_per_node_list=bundle_ct_per_node_list, |
| 778 | + num_gpus_per_node=int(_config_get(self._pipeline_config, "num_gpus_per_node", 1)), |
| 779 | + use_gpus=True, |
| 780 | + max_colocated_worker_groups=max_colocated_worker_groups, |
| 781 | + name=name, |
| 782 | + sorted_bundle_indices=sorted_bundle_indices, |
| 783 | + device_mapping=device_mapping, |
| 784 | + ) |
| 785 | + |
| 786 | + def _allocate_shared_pg(self, *, device_mapping: List[int]) -> Any: |
| 787 | + from roll.distributed.scheduler.resource_manager import RollResourceManagerProxy |
| 788 | + |
| 789 | + proxy = RollResourceManagerProxy( |
| 790 | + num_gpus_per_node=int(_config_get(self._pipeline_config, "num_gpus_per_node", 1)) |
| 791 | + ) |
| 792 | + if hasattr(proxy, "allocate_placement_group"): |
| 793 | + return proxy.allocate_placement_group( |
| 794 | + world_size=len(device_mapping), |
| 795 | + device_mapping=list(device_mapping), |
| 796 | + ) |
| 797 | + |
| 798 | + if sorted(device_mapping) != list(range(len(device_mapping))): |
| 799 | + raise RuntimeError( |
| 800 | + "RollResourceManagerProxy has no allocate_placement_group(); " |
| 801 | + "fallback node2pg mode only supports contiguous zero-based " |
| 802 | + f"device mappings, got {device_mapping!r}" |
| 803 | + ) |
| 804 | + return proxy |
| 805 | + |
| 806 | + def _extract_placement_groups(self, pg_alloc: Any) -> List[Any]: |
| 807 | + for attr in ("placement_groups", "pgs", "node_placement_groups"): |
| 808 | + value = getattr(pg_alloc, attr, None) |
| 809 | + if value: |
| 810 | + return list(value.values()) if isinstance(value, dict) else list(value) |
| 811 | + node2pg = getattr(pg_alloc, "node2pg", None) |
| 812 | + if node2pg: |
| 813 | + return [node2pg[k] for k in sorted(node2pg)] |
| 814 | + if isinstance(pg_alloc, (list, tuple)): |
| 815 | + return list(pg_alloc) |
| 816 | + raise RuntimeError( |
| 817 | + "Unable to extract placement groups from RollResourceManagerProxy allocation" |
| 818 | + ) |
| 819 | + |
| 820 | + def _extract_bundle_counts( |
| 821 | + self, |
| 822 | + *, |
| 823 | + pg_alloc: Any, |
| 824 | + placement_groups: List[Any], |
| 825 | + device_mapping: List[int], |
| 826 | + ) -> List[int]: |
| 827 | + for attr in ("bundle_ct_per_node_list", "bundle_counts", "workers_per_node"): |
| 828 | + value = getattr(pg_alloc, attr, None) |
| 829 | + if value: |
| 830 | + return [int(x) for x in value] |
| 831 | + if len(placement_groups) == 1: |
| 832 | + return [len(device_mapping)] |
| 833 | + return [int(getattr(pg, "bundle_count")) for pg in placement_groups] |
616 | 834 |
|
617 | 835 | # ------------------------------------------------------------------ |
618 | 836 | # Phase helpers — stubs for other Features |
|
0 commit comments