| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import os |
| import subprocess |
| import time |
| from unittest.mock import MagicMock, patch |
|
|
| import ray |
| import torch |
| from ray.util import placement_group_table |
| from ray.util.scheduling_strategies import NodeAffinitySchedulingStrategy |
|
|
| from verl.single_controller.ray import RayResourcePool, SubRayResourcePool |
| from verl.workers.rollout.replica import RolloutMode |
| from verl.workers.rollout.trtllm_rollout.trtllm_async_server import TRTLLMHttpServer, TRTLLMReplica |
|
|
|
|
| class TestTRTLLMReplica: |
| def test_placement_group_with_sub_ray_resource_pool(self): |
| """ |
| Scenario: SubRayResourcePool, 1 node, 8 GPUs, TP=4, replica_rank=1 |
| SubRayResourcePool pre-assigns start_bundle_index=4 for replica 1. |
| Expected: Replica 1 gets bundles [4, 5, 6, 7] |
| """ |
| with patch("verl.workers.rollout.trtllm_rollout.trtllm_async_server.ray"): |
| mock_config = MagicMock() |
| mock_config.tensor_model_parallel_size = 4 |
| mock_config.data_parallel_size = 1 |
| mock_config.pipeline_model_parallel_size = 1 |
|
|
| replica = TRTLLMReplica( |
| replica_rank=1, |
| config=mock_config, |
| model_config=MagicMock(), |
| gpus_per_node=8, |
| ) |
|
|
| mock_pg = MagicMock() |
| mock_pg.bundle_count = 8 |
|
|
| resource_pool = SubRayResourcePool( |
| placement_groups=[mock_pg], |
| start_bundle_index=4, |
| subgroup_world_size=4, |
| ) |
|
|
| replica.resource_pool = resource_pool |
| replica.world_size = 4 |
|
|
| pgs, bundle_indices = replica.get_pgs_and_bundle_indices() |
|
|
| assert len(pgs) == 1 |
| assert pgs[0] == mock_pg |
| assert len(bundle_indices) == 1 |
| assert bundle_indices[0] == [4, 5, 6, 7] |
|
|
| def test_placement_group_with_ray_resource_pool(self): |
| """ |
| Scenario: RayResourcePool, 1 node, 8 GPUs, TP=2, replica_rank=1 |
| RayResourcePool calculates: local_bundle_index = world_size * replica_rank = 2 * 1 = 2 |
| Expected: Replica 1 gets bundles [2, 3] |
| """ |
| with patch("verl.workers.rollout.trtllm_rollout.trtllm_async_server.ray"): |
| mock_config = MagicMock() |
| mock_config.tensor_model_parallel_size = 2 |
| mock_config.data_parallel_size = 1 |
| mock_config.pipeline_model_parallel_size = 1 |
|
|
| replica = TRTLLMReplica( |
| replica_rank=1, |
| config=mock_config, |
| model_config=MagicMock(), |
| gpus_per_node=8, |
| ) |
|
|
| mock_pg = MagicMock() |
| mock_pg.bundle_count = 8 |
|
|
| resource_pool = RayResourcePool( |
| process_on_nodes=[8], |
| use_gpu=True, |
| max_colocate_count=1, |
| name_prefix="test_rollout", |
| ) |
| resource_pool.pgs = [mock_pg] |
|
|
| replica.resource_pool = resource_pool |
| replica.world_size = 2 |
|
|
| pgs, bundle_indices = replica.get_pgs_and_bundle_indices() |
|
|
| assert len(pgs) == 1 |
| assert pgs[0] == mock_pg |
| assert len(bundle_indices) == 1 |
| assert bundle_indices[0] == [2, 3] |
|
|
|
|
| class TestTRTLLMHttpServer: |
| @staticmethod |
| def _build_rollout_config(*, response_length: int | None = None, free_cache_engine: bool = False): |
| from hydra import compose, initialize_config_dir |
|
|
| config_dir = os.path.abspath("verl/verl/trainer/config") |
| if not os.path.exists(config_dir): |
| config_dir = os.path.abspath("verl/trainer/config") |
|
|
| with initialize_config_dir(config_dir=config_dir, version_base=None): |
| config = compose(config_name="ppo_trainer") |
|
|
| config.trainer.n_gpus_per_node = 1 |
| config.trainer.nnodes = 1 |
| model_root = os.path.expanduser(os.getenv("TRTLLM_TEST_MODEL_PATH_ROOT", "~/models")) |
| config.actor_rollout_ref.model.path = os.path.join(model_root, "Qwen/Qwen2.5-0.5B-Instruct") |
| config.actor_rollout_ref.rollout.name = "trtllm" |
| config.actor_rollout_ref.rollout.mode = "async" |
| config.actor_rollout_ref.rollout.tensor_model_parallel_size = 1 |
| if response_length is not None: |
| config.actor_rollout_ref.rollout.response_length = response_length |
| if free_cache_engine: |
| config.actor_rollout_ref.rollout.free_cache_engine = True |
|
|
| return config.actor_rollout_ref.rollout, config.actor_rollout_ref.model |
|
|
| @staticmethod |
| def _create_server(rollout_config, model_config, *, name: str): |
| resource_pool = RayResourcePool( |
| process_on_nodes=[1], |
| use_gpu=True, |
| max_colocate_count=1, |
| name_prefix="test_rollout", |
| ) |
| pgs = resource_pool.get_placement_groups() |
| bundle_indices = [[0]] |
|
|
| pg_data = placement_group_table(pgs[0]) |
| node_id = pg_data["bundles_to_node_id"][bundle_indices[0][0]] |
|
|
| return TRTLLMHttpServer.options( |
| scheduling_strategy=NodeAffinitySchedulingStrategy( |
| node_id=node_id, |
| soft=False, |
| ), |
| runtime_env={"env_vars": {"RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES": "1"}}, |
| name=name, |
| ).remote( |
| config=rollout_config, |
| model_config=model_config, |
| is_reward_model=False, |
| rollout_mode=RolloutMode.COLOCATED, |
| workers=[], |
| replica_rank=0, |
| max_colocate_count=1, |
| pgs=pgs, |
| bundle_indices=bundle_indices, |
| ) |
|
|
| def test_async_generate(self): |
| """Test TRT-LLM generate method with real model.""" |
| try: |
| os.environ.setdefault("TLLM_RAY_FORCE_LOCAL_CLUSTER", "1") |
| ray.init(address="local", ignore_reinit_error=True, include_dashboard=False) |
|
|
| rollout_config, model_config = self._build_rollout_config(response_length=50) |
|
|
| server = self._create_server( |
| rollout_config, |
| model_config, |
| name="trtllm_server_test_generate", |
| ) |
|
|
| ray.get(server.launch_server.remote()) |
|
|
| |
| prompt_ids = [1, 2, 3, 4, 5] |
| sampling_params = { |
| "temperature": 1.0, |
| "top_k": 0, |
| "logprobs": 1, |
| } |
| request_id = "test_request_1" |
|
|
| result = ray.get(server.generate.remote(prompt_ids, sampling_params, request_id)) |
|
|
| print(f"Result: {result}") |
| |
| assert hasattr(result, "token_ids"), "Result should have token_ids attribute" |
| assert hasattr(result, "log_probs"), "Result should have log_probs attribute" |
| assert isinstance(result.token_ids, list), "token_ids should be a list" |
| assert len(result.token_ids) > 0, "Generated tokens should not be empty" |
|
|
| |
| assert result.log_probs is not None, "log_probs should not be None when requested" |
| assert len(result.log_probs) == len(result.token_ids), "log_probs length should match token_ids" |
|
|
| print(f"Generated {len(result.token_ids)} tokens") |
| print(f"Token IDs: {result.token_ids[:10]}...") |
| print(f"Log probs: {result.log_probs[:10]}...") |
|
|
| finally: |
| ray.shutdown() |
| subprocess.run(["ray", "stop"], capture_output=True) |
|
|
| def test_async_memory_management(self): |
| """Test TRT-LLM async memory management (sleep) reduces memory usage.""" |
| try: |
| os.environ.setdefault("TLLM_RAY_FORCE_LOCAL_CLUSTER", "1") |
| ray.init(address="local", ignore_reinit_error=True, include_dashboard=False) |
|
|
| rollout_config, model_config = self._build_rollout_config(free_cache_engine=True) |
|
|
| server = self._create_server( |
| rollout_config, |
| model_config, |
| name="trtllm_server_test_0", |
| ) |
|
|
| ray.get(server.launch_server.remote()) |
| device_ids = ray.get(server.report_device_ids.remote()) |
| print(f"TRTLLM device UUIDs: {device_ids}") |
|
|
| def _uuid_to_device_index(device_uuid: str) -> int | None: |
| for idx in range(torch.cuda.device_count()): |
| props = torch.cuda.get_device_properties(idx) |
| uuid = getattr(props, "uuid", None) |
| if uuid is None: |
| |
| return 0 |
| if isinstance(uuid, bytes): |
| uuid_str = uuid.decode("utf-8", errors="ignore") |
| else: |
| uuid_str = str(uuid) |
| if uuid_str == device_uuid or uuid_str in device_uuid: |
| print(f"Mapped device UUID {device_uuid} to torch device index {idx}") |
| return idx |
| return 0 |
|
|
| def get_gpu_memory_mb_for_device(device_uuid: str) -> float: |
| device_index = _uuid_to_device_index(device_uuid) |
| prev_device = torch.cuda.current_device() |
| torch.cuda.set_device(device_index) |
| mem_free, mem_total = torch.cuda.mem_get_info() |
| torch.cuda.set_device(prev_device) |
| return (mem_total - mem_free) / (1024**2) |
|
|
| baseline_memory_mb = get_gpu_memory_mb_for_device(device_ids[0]) |
| print(f" Baseline memory: {baseline_memory_mb:.2f} MB") |
|
|
| ray.get(server.sleep.remote()) |
| time.sleep(2) |
|
|
| sleep_memory_mb = get_gpu_memory_mb_for_device(device_ids[0]) |
| memory_freed_mb = baseline_memory_mb - sleep_memory_mb |
| print(f" Memory after sleep: {sleep_memory_mb:.2f} MB") |
| print(f" Memory freed: {memory_freed_mb:.2f} MB") |
|
|
| assert memory_freed_mb >= baseline_memory_mb * 0.6, ( |
| f"Expected sleep() to free >=60% of baseline memory. " |
| f"Baseline: {baseline_memory_mb:.2f} MB, freed: {memory_freed_mb:.2f} MB." |
| ) |
|
|
| finally: |
| ray.shutdown() |
| subprocess.run(["ray", "stop"], capture_output=True) |
|
|