# Copyright (c) ModelScope Contributors. All rights reserved.
# Code partially sourced from Hugging Face TRL

# fmt: off
# apply patch before importing trl, which may internally reference GuidedDecodingParams
try:
    import vllm
    try:
        from vllm.sampling_params import GuidedDecodingParams
    except ImportError:
        import vllm.sampling_params

        # removed in https://github.com/vllm-project/vllm/pull/22772
        vllm.sampling_params.GuidedDecodingParams = vllm.sampling_params.StructuredOutputsParams
except ImportError:
    pass
# fmt: on
import asyncio
import inspect
import multiprocessing
import os
import time
import torch
import torch.distributed.distributed_c10d as c10d
import traceback
import uvicorn
from aiohttp import ClientConnectorError
from collections.abc import Sequence
from contextlib import asynccontextmanager, contextmanager
from dataclasses import asdict
from fastapi import FastAPI
from itertools import chain
from multiprocessing import Pipe, Process
from multiprocessing.connection import Connection
from transformers.utils import is_torch_npu_available
from typing import Any, Dict, List, Optional, Union

from swift.arguments import RolloutArguments
from swift.infer_engine import GRPOVllmEngine, InferClient
from swift.infer_engine.protocol import (InitCommunicatorRequest, RequestConfig, RolloutInferRequest,
                                         UpdateWeightsRequest)
from swift.rlhf_trainers.utils import (VLLM_LORA_INT_ID, VLLM_LORA_NAME, VLLM_LORA_PATH, FlattenedTensorBucket,
                                       FlattenedTensorMetadata, TensorLoRARequest, UpdateAdapterRequest,
                                       UpdateFlattenedAdapterRequest, UpdateFlattenedParamsRequest,
                                       broadcast_tensor_for_vllm_weight_sync, check_vllm_version_ge, chunk_list,
                                       finish_vllm_weight_reload, patch_vllm_load_adapter,
                                       patch_vllm_moe_model_weight_loader, vllm_supports_lora_load_inplace)
from swift.rollout import RolloutScheduler, multi_turns
from swift.utils import (gc_collect, get_logger, get_physical_device_count, get_seed, ipc_collect, is_torch_rocm,
                         is_vllm_ascend_available, is_vllm_metax_available, synchronize)
from ..base import SwiftPipeline

try:
    if check_vllm_version_ge('0.11.1'):
        from vllm.utils.network_utils import get_open_port
    else:
        from vllm.utils import get_open_port
    from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator
    from vllm.distributed.parallel_state import get_world_group
    from vllm.distributed.utils import StatelessProcessGroup
    if is_vllm_ascend_available():
        from vllm_ascend.distributed.device_communicators.pyhccl import PyHcclCommunicator as PyNcclCommunicator  # noqa

    if is_vllm_metax_available():
        import vllm_metax.patch
except ImportError:
    pass
"""
This module defines the execution logic for `swift rollout`.
It adds weight synchronization logic based on `vLLMEngine`.

Usage:
    swift rollout \
        --model xxx \
        --vllm_tensor_parallel_size xxx \
        --vllm_data_parallel_size xxx \
        --vllm_use_async_engine true/false \
        --other_vllm_arguments

Note:
- Rollout is intended solely for GRPO training sampling.
- For inference or deployment, please use the `swift infer` or `swift deploy` commands.
"""

patch_vllm_load_adapter()


def _set_death_signal():
    """Ensure this process is killed when its parent exits.

    Prevents orphan vLLM TP worker processes from leaking GPU memory
    when the parent Ray actor dies unexpectedly.
    """
    import ctypes
    import platform
    import signal
    if platform.system() != 'Linux':
        return
    libc = ctypes.CDLL('libc.so.6')
    libc.prctl(1, signal.SIGKILL)
    if os.getppid() == 1:
        os.kill(os.getpid(), signal.SIGKILL)


def _patch_full_weight_reload_loader(model) -> None:
    if is_vllm_ascend_available():
        from swift.model.npu_patch.vllm_ascend_moe import configure_vllm_ascend_moe_preprocessed_weight_sync
        configure_vllm_ascend_moe_preprocessed_weight_sync(model)
    patch_vllm_moe_model_weight_loader(model)


class WeightSyncWorkerExtension:

    # The following attributes are initialized when `init_communicator` method is called.
    communicator = None  # Communicator for weight updates
    client_rank = None  # Source rank for broadcasting updated weights

    def __new__(cls, **kwargs):
        _set_death_signal()
        return super().__new__(cls)

    def init_communicator(self, host: str, port: int, world_size: int) -> None:
        """
        Initializes the weight update communicator using a stateless process group.

        This method creates a `StatelessProcessGroup` that allows external training processes to communicate with vLLM
        workers without interfering with the global torch distributed group.

        Args:
            host (`str`):
                Hostname or IP address of the master node.
            port (`int`):
                Port number to be used for communication.
            world_size (`int`):
                Total number of participating processes in the update group.
        """
        if self.communicator is not None:
            return

        parallel_config = getattr(getattr(self, 'vllm_config', None), 'parallel_config', None)
        dp_index = int(getattr(parallel_config, 'data_parallel_index', 0)) if parallel_config is not None else 0
        if dp_index > 0:
            dp_rank = dp_index
            tp_size = int(parallel_config.tensor_parallel_size)
        else:
            dp_rank = int(os.environ.get('SWIFT_ROLLOUT_DP_RANK', '0'))
            tp_size = int(os.environ.get('SWIFT_ROLLOUT_TP_RANK', '1'))
        rank = get_world_group().rank + dp_rank * tp_size

        # Create a stateless process group to manage communication between training processes and vLLM workers.
        # Initialize the NCCL-based communicator for weight synchronization.
        pg = StatelessProcessGroup.create(host=host, port=port, rank=rank, world_size=world_size)

        if is_vllm_ascend_available():
            # https://github.com/modelscope/ms-swift/issues/5920
            device = get_world_group().local_rank
            import torch_npu
            torch_npu.npu.set_device(device)
        else:
            device = self.device

        self.communicator = PyNcclCommunicator(pg, device=device)

        # The client process that sends updated weights has the highest rank (world_size - 1).
        self.client_rank = world_size - 1

    def get_state_keys(self) -> List[str]:
        """Return runtime model parameter names for exact weight-name mapping."""
        return list(dict(self.model_runner.model.named_parameters()).keys())

    def update_named_param(self, name: str, dtype: str, shape: Sequence[int]) -> None:
        """
        Receives updated weights from the client process and updates the named parameter in the model.

        Args:
            name (`str`):
                Name of the weight tensor being updated.
            dtype (`str`):
                Data type of the weight tensor as a string (e.g., `"torch.float32"`).
            shape (`Sequence[int]`):
                Shape of the weight tensor.
        """
        if self.communicator is None:
            raise RuntimeError('Communicator not initialized. Call `init_communicator` first.')

        dtype = getattr(torch, dtype.split('.')[-1])
        # Allocate memory for the incoming weight tensor on the correct device.
        weight = torch.empty(shape, dtype=dtype, device=self.communicator.device)

        # Use NCCL to broadcast the updated weights from the client (src) to all workers.
        broadcast_tensor_for_vllm_weight_sync(self.communicator, weight, src=self.client_rank)
        synchronize()
        self.communicator.group.barrier()

        # Patch MoE weight_loader if needed. This endpoint updates base weights
        # one by one, so use the same full-reload layout setup as flattened
        # base-weight sync before the final post-load processing request.
        _patch_full_weight_reload_loader(self.model_runner.model)

        # Load the received weights into the model.
        self.model_runner.model.load_weights(weights=[(name, weight)])

    def update_adapter_flattened_param(self, peft_config: Dict, metadatas: list[Dict]) -> None:
        """
        Receives and applies a flattened LoRA adapter to the model.
        """
        metadatas = [FlattenedTensorMetadata(**metadata) for metadata in metadatas]
        if self.communicator is None:
            raise RuntimeError('Communicator not initialized. Call `init_communicator` first.')

        total_bytes = metadatas[-1].end_idx
        flatten_tensor = torch.empty(total_bytes, dtype=torch.uint8, device=self.communicator.device)
        broadcast_tensor_for_vllm_weight_sync(self.communicator, flatten_tensor, src=self.client_rank)
        synchronize()
        self.communicator.group.barrier()

        named_params = FlattenedTensorBucket(metadata=metadatas, flattened_tensor=flatten_tensor).reconstruct_tensors()
        req_kw = dict(
            lora_name=VLLM_LORA_NAME,
            lora_int_id=VLLM_LORA_INT_ID,
            lora_path=VLLM_LORA_PATH,
            peft_config=peft_config,
            lora_tensors=named_params,
        )
        if vllm_supports_lora_load_inplace():
            req_kw['load_inplace'] = True
        else:
            self.remove_lora(VLLM_LORA_INT_ID)
        lora_request = TensorLoRARequest(**req_kw)
        self.add_lora(lora_request)

    def update_adapter_param(self, peft_config: Dict, lora_tensors_metadata: list[Dict]) -> None:
        """
        Receives and applies a LoRA adapter to the model without flattening.
        Each tensor is broadcast individually.

        Args:
            peft_config: PEFT configuration dictionary.
            lora_tensors_metadata: List of metadata dictionaries for each tensor.
        """
        if self.communicator is None:
            raise RuntimeError('Communicator not initialized. Call `init_communicator` first.')

        # Receive each tensor individually
        named_params = {}
        for metadata in lora_tensors_metadata:
            name = metadata['name']
            dtype = getattr(torch, metadata['dtype'].split('.')[-1])
            shape = tuple(metadata['shape'])
            tensor = torch.empty(shape, dtype=dtype, device=self.communicator.device)
            broadcast_tensor_for_vllm_weight_sync(self.communicator, tensor, src=self.client_rank)
            named_params[name] = tensor

        synchronize()
        self.communicator.group.barrier()

        req_kw = dict(
            lora_name=VLLM_LORA_NAME,
            lora_int_id=VLLM_LORA_INT_ID,
            lora_path=VLLM_LORA_PATH,
            peft_config=peft_config,
            lora_tensors=named_params,
        )
        if vllm_supports_lora_load_inplace():
            req_kw['load_inplace'] = True
        else:
            self.remove_lora(VLLM_LORA_INT_ID)
        lora_request = TensorLoRARequest(**req_kw)
        self.add_lora(lora_request)

    def update_flattened_params(self, metadatas: list[Dict]) -> None:
        """
        Receives updated flattened weights from the client process and updates the model parameters.

        Args:
            metadatas (list[Dict]): List of metadata dictionaries for the flattened tensors.
        """
        metadatas = [FlattenedTensorMetadata(**metadata) for metadata in metadatas]
        if self.communicator is None:
            raise RuntimeError('Communicator not initialized. Call `init_communicator` first.')

        total_bytes = metadatas[-1].end_idx
        flatten_tensor = torch.empty(total_bytes, dtype=torch.uint8, device=self.communicator.device)

        broadcast_tensor_for_vllm_weight_sync(self.communicator, flatten_tensor, src=self.client_rank)
        synchronize()
        self.communicator.group.barrier()

        named_params = FlattenedTensorBucket(metadata=metadatas, flattened_tensor=flatten_tensor).reconstruct_tensors()

        _patch_full_weight_reload_loader(self.model_runner.model)
        self.model_runner.model.load_weights(weights=list(named_params.items()))

    def process_weights_after_loading(self) -> None:
        """Re-run process_weights_after_loading once after ALL weight
        buckets have been loaded, so the kernel-format layout is rebuilt
        on complete weights rather than partial ones.

        Uses vLLM's built-in ``process_weights_after_loading`` when
        *model_config* and *target_device* are available (same as verl);
        falls back to FusedMoE-only path otherwise.
        """
        model_config = self.model_runner.model_config
        finish_vllm_weight_reload(self.model_runner.model, model_config=model_config, target_device=self.device)

    def close_communicator(self) -> None:
        """
        Closes the communicator when weight synchronization is no longer needed.

        This method deletes the NCCL communicator to release associated resources.
        """

        if self.communicator is not None:
            del self.communicator
            self.communicator = None  # Ensure attribute is reset to None
            self.client_rank = None  # Ensure attribute is reset to None

    # ------------------------------------------------------------------
    # ZMQ + CUDA-IPC / shared-memory bucketed weight sync
    # ------------------------------------------------------------------
    #
    # Counterpart of ``VllmServer.update_weights_ipc`` (see
    # ``swift/ray/megatron/rollout/vllm_server.py``).  Avoids the extra
    # NCCL broadcast hop of ``update_flattened_params`` and reuses the
    # sender's bucket buffer via CUDA IPC (same node, same device) or
    # shared memory (CPU / cross-device fallback).
    #
    # TP>1:
    #   Only the TP driver (rank 0 in the TP group) talks to the ZMQ
    #   socket; the IPC handle / shm name and every bucket metadata is
    #   broadcast via vLLM's TP cpu_group so all ranks can rebuild the
    #   same buffer and load their own TP shard.
    #
    # LoRA / chunked tensors / FP8-QAT re-packing:
    #   Intentionally NOT implemented here.  GRPO / GKD in swift Ray
    #   uses full-parameter sync; LoRA / QAT paths keep going through
    #   ``update_flattened_params`` + ``init_communicator``.

    @staticmethod
    def _ipc_handle_signature(handle) -> tuple | None:
        """Derive a stable signature for a CUDA IPC handle.

        Two handles map the same CUDA memory region when their inner
        storage-handle bytes and metadata match.  We hash only the
        picklable / comparable parts to detect reuse.
        """
        try:
            _, args = handle
        except Exception:
            return None
        sig = []
        for v in args:
            if isinstance(v, (bytes, bytearray)):
                sig.append(('bytes', bytes(v)))
            elif isinstance(v, (int, float, bool, str)) or v is None:
                sig.append(('scalar', v))
            else:
                try:
                    sig.append(('repr', repr(v)))
                except Exception:
                    return None
        return tuple(sig)

    def update_weights_from_ipc(
        self,
        use_shm: bool = False,
        zmq_handle: Optional[str] = None,
        timeout_s: int = 300,
        peft_config: Optional[Dict] = None,
        base_sync_done: bool = False,
    ) -> None:
        """Receive and load weights via ZMQ + CUDA-IPC / SHM.

        Called via ``collective_rpc('update_weights_from_ipc', kwargs=...)``
        from ``VllmServer.update_weights_ipc``.  The sender binds a ZMQ
        REQ socket on ``zmq_handle`` and sends
          1. a CUDA IPC reduce_tensor handle (or ``{name, size}`` for shm),
          2. per-bucket ``{bucket_meta, is_last}`` payloads,
        with a sync ``recv()`` on our side so the sender can safely reuse
        the shared buffer between buckets.

        IPC buffer reuse: when the sender reuses the same CUDA IPC handle
        across sync rounds (same ``BucketedWeightSender`` buffer), we skip
        ``rebuild_cuda_tensor`` and reuse the cached mapping.  This avoids
        accumulating IPC mappings that the CUDA driver releases lazily,
        which is the root cause of apparent GPU memory growth under
        frequent syncs.

        When ``peft_config`` is provided and ``base_sync_done`` is True,
        the received weights are loaded as a LoRA adapter via
        ``TensorLoRARequest`` instead of ``model.load_weights``.
        """
        if zmq_handle is None:
            raise ValueError('update_weights_from_ipc: zmq_handle is required')

        import gc as _gc
        import torch as _torch
        import torch.distributed as _dist
        import zmq
        from torch.multiprocessing.reductions import rebuild_cuda_tensor

        device = getattr(self, 'device', None)
        if device is None:
            local_rank = getattr(self, 'local_rank', 0)
            device = _torch.device(f'cuda:{local_rank}' if _torch.cuda.is_available() else 'cpu')
            self.device = device

        tp_rank = getattr(self, 'rank', 0)
        tp_size = 1
        try:
            tp_size = self.model_runner.parallel_config.tensor_parallel_size
        except Exception:  # noqa: BLE001
            pass
        is_driver = (tp_rank == 0)

        cpu_group = None
        broadcast_src = 0
        if tp_size > 1:
            from vllm.distributed import get_tp_group
            tp_coord = get_tp_group()
            cpu_group = tp_coord.cpu_group
            broadcast_src = tp_coord.ranks[0]

        def _broadcast_obj(obj):
            if cpu_group is None:
                return obj
            obj_list = [obj]
            _dist.broadcast_object_list(obj_list, src=broadcast_src, group=cpu_group)
            return obj_list[0]

        socket = None
        if is_driver:
            ctx = zmq.Context.instance()
            socket = ctx.socket(zmq.REP)
            socket.setsockopt(zmq.RCVTIMEO, timeout_s * 1000)
            socket.setsockopt(zmq.SNDTIMEO, timeout_s * 1000)
            socket.setsockopt(zmq.LINGER, 0)
            socket.connect(zmq_handle)

        # ── Step 1: receive + rebuild IPC handle (with reuse) ────────
        comm_metadata = socket.recv_pyobj() if is_driver else None
        comm_metadata = _broadcast_obj(comm_metadata)

        buffer = None
        shm = None
        if not use_shm:
            handle = comm_metadata
            handle_sig = self._ipc_handle_signature(handle)
            cached_buf = getattr(self, '_swift_ipc_buffer', None)
            cached_sig = getattr(self, '_swift_ipc_handle_signature', None)

            if cached_buf is not None and cached_sig == handle_sig:
                buffer = cached_buf
            else:
                if cached_buf is not None:
                    self._swift_ipc_buffer = None
                    self._swift_ipc_handle_signature = None
                    del cached_buf
                    _gc.collect()
                    ipc_collect()

                func, args = handle
                list_args = list(args)
                dev_idx = device.index if device.type == 'cuda' else 0
                list_args[6] = dev_idx
                buffer = func(*list_args) if callable(func) else rebuild_cuda_tensor(*list_args)
                assert buffer.dtype == _torch.uint8
                self._swift_ipc_buffer = buffer
                self._swift_ipc_handle_signature = handle_sig
        else:
            from multiprocessing import shared_memory
            shm = shared_memory.SharedMemory(name=comm_metadata['name'])
            buffer = _torch.frombuffer(shm.buf[:comm_metadata['size']], dtype=_torch.uint8)

        if is_driver:
            socket.send(b'')  # ready for buckets

        # ── Step 2: stream buckets and load_weights per bucket ──────
        is_lora_sync = (peft_config is not None and base_sync_done)
        all_lora_weights: Dict[str, Any] = {} if is_lora_sync else None

        if not is_lora_sync:
            _patch_full_weight_reload_loader(self.model_runner.model)

        while True:
            metadata = socket.recv_pyobj() if is_driver else None
            metadata = _broadcast_obj(metadata)

            bucket_meta = metadata['bucket_meta']
            entries = list(bucket_meta.values()) if isinstance(bucket_meta, dict) else list(bucket_meta)

            weights: List[tuple] = []
            for meta in entries:
                name = meta['name']
                dtype = meta['dtype']
                shape = meta['shape']
                shape = shape if isinstance(shape, _torch.Size) else _torch.Size(shape)
                offset = int(meta['offset'])
                size = int(dtype.itemsize * shape.numel())

                raw = buffer[offset:offset + size]
                tensor = raw.view(dtype=dtype).view(shape)
                if use_shm:
                    tensor = tensor.to(device)
                else:
                    tensor = tensor.clone()
                weights.append((name, tensor))

            if _torch.cuda.is_available():
                _torch.cuda.synchronize()

            if is_driver:
                socket.send(b'')  # bucket received

            if tp_size > 1:
                _dist.barrier(group=cpu_group)

            if is_lora_sync:
                for name, tensor in weights:
                    all_lora_weights[name] = tensor
            else:
                self.model_runner.model.load_weights(weights=weights)
            del weights
            if metadata.get('is_last'):
                break

        # Re-run process_weights_after_loading so the kernel-format
        # layout is rebuilt after the in-place reload (vLLM issue
        # #42821).  Skipped for LoRA sync because the adapter path
        # doesn't call ``load_weights``.
        if not is_lora_sync:
            model_config = self.model_runner.model_config
            finish_vllm_weight_reload(self.model_runner.model, model_config=model_config, target_device=self.device)

        if is_lora_sync and all_lora_weights:
            req_kw = dict(
                lora_name=VLLM_LORA_NAME,
                lora_int_id=VLLM_LORA_INT_ID,
                lora_path=VLLM_LORA_PATH,
                peft_config=peft_config,
                lora_tensors=all_lora_weights,
            )
            if vllm_supports_lora_load_inplace():
                req_kw['load_inplace'] = True
            else:
                self.remove_lora(VLLM_LORA_INT_ID)
            lora_request = TensorLoRARequest(**req_kw)
            self.add_lora(lora_request)
            del all_lora_weights

        if is_driver and socket is not None:
            socket.close()
        del buffer
        if shm is not None:
            try:
                shm.close()
            except BufferError:
                pass
            shm = None
        _gc.collect()
        ipc_collect()
        if _torch.cuda.is_available():
            _torch.cuda.empty_cache()


logger = get_logger()


def safe_set_start_method():
    if multiprocessing.get_start_method(allow_none=True) is None:
        multiprocessing.set_start_method('spawn')


def get_rollout_engine_type(args: RolloutArguments, engine: GRPOVllmEngine):
    if args.multi_turn_scheduler:
        if args.multi_turn_scheduler not in multi_turns:
            raise ValueError(f"Multi-turn scheduler '{args.multi_turn_scheduler}' not found in multi_turns.")
        scheduler_cls = multi_turns[args.multi_turn_scheduler]

        kwargs = {}
        if 'tokenizer' in list(inspect.signature(scheduler_cls.__init__).parameters):
            kwargs['tokenizer'] = engine.template.tokenizer
        # gym kwargs
        if args.use_gym_env:
            kwargs.update({
                'use_gym_env': args.use_gym_env,
                'gym_env': args.gym_env,
            })

        rollout_engine: RolloutScheduler = scheduler_cls(infer_engine=engine, max_turns=args.max_turns, **kwargs)
        if not rollout_engine:
            raise ValueError(f"Failed to initialize multi-turn scheduler '{args.multi_turn_scheduler}'.")
    else:
        rollout_engine = engine
    return rollout_engine


def _set_visible_devices_for_dp_rank(data_parallel_rank: int, tensor_parallel_size: int):

    def _get_device_env_var():
        if is_torch_npu_available():
            return 'ASCEND_RT_VISIBLE_DEVICES'
        return 'CUDA_VISIBLE_DEVICES'

    env_var = _get_device_env_var()
    current = os.environ.get(env_var)
    if current:
        all_devices = current.split(',')
    else:
        from swift.utils import get_device_count
        all_devices = [str(i) for i in range(get_device_count())]

    start = data_parallel_rank * tensor_parallel_size
    end = start + tensor_parallel_size
    selected = all_devices[start:end]

    # ROCm: do NOT shrink the visibility mask to ``selected``. Restricting
    # CUDA_VISIBLE_DEVICES (e.g. to "6,7") makes vLLM renumber those GPUs locally
    # as cuda:0,1, so the device ids the rollout uses/reports diverge from the
    # trainer's global numbering, and the cross-process RCCL weight-sync fails on
    # ROCm with "invalid device ordinal" (the trainer can't resolve the rollout's
    # GPUs). Instead keep EVERY physical GPU visible and only reorder the mask so
    # the selected devices come first: vLLM still lands on the intended physical
    # GPUs (cuda:0..tp-1 -> selected), while RCCL can resolve all peers (incl. the
    # trainer's GPUs) by PCI bus id, so train<->rollout communication works.
    if env_var == 'CUDA_VISIBLE_DEVICES' and is_torch_rocm():
        total = get_physical_device_count()
        sel_ints = []
        for x in selected:
            try:
                sel_ints.append(int(x))
            except ValueError:
                sel_ints = []
                break
        if sel_ints and all(0 <= i < total for i in sel_ints):
            rest = [i for i in range(total) if i not in sel_ints]
            reordered = ','.join(str(i) for i in (sel_ints + rest))
            os.environ['CUDA_VISIBLE_DEVICES'] = reordered
            os.environ['HIP_VISIBLE_DEVICES'] = reordered
            return

    os.environ[env_var] = ','.join(selected)


def llm_worker(args: RolloutArguments, data_parallel_rank: int, master_port: int, connection: Connection) -> None:
    try:
        args._import_external_plugins()
        _set_visible_devices_for_dp_rank(data_parallel_rank, args.vllm_tensor_parallel_size)
        os.environ['VLLM_DP_MASTER_PORT'] = str(master_port)
        os.environ['SWIFT_ROLLOUT_DP_RANK'] = str(data_parallel_rank)
        os.environ['SWIFT_ROLLOUT_TP_RANK'] = str(args.vllm_tensor_parallel_size)
        worker_seed = get_seed()
        engine = SwiftRolloutDeploy.get_infer_engine(args, template=args.get_template(), seed=worker_seed)
        rollout_engine = get_rollout_engine_type(args, engine)
    except Exception:
        connection.send({'status': 'error', 'error': traceback.format_exc()})
        return
    connection.send({'status': 'ready'})

    while True:
        # Wait for commands from the parent process
        try:
            command = connection.recv()
        except KeyboardInterrupt:
            engine.engine.collective_rpc(method='close_communicator')
            break

        # Handle commands
        if command['type'] in ['call', 'fire_and_forget']:
            method_name = command['method']
            args, kwargs = command.get('args', ()), command.get('kwargs', {})
            method = getattr(rollout_engine, method_name, None) or getattr(rollout_engine.engine, method_name, None)
            try:
                result = method(*args, **kwargs)
            except Exception:
                logger.error(f'Method execution failed: {method_name}\n{traceback.format_exc()}')
                result = None
            if command['type'] == 'call':
                connection.send(result)
        elif command['type'] == 'shutdown':
            break


async def async_llm_worker(args: RolloutArguments, data_parallel_rank: int, master_port: int,
                           connection: Connection) -> None:
    try:
        args._import_external_plugins()
        os.environ['SWIFT_ROLLOUT_DP_RANK'] = str(data_parallel_rank)
        os.environ['SWIFT_ROLLOUT_TP_RANK'] = str(args.vllm_tensor_parallel_size)
        worker_seed = get_seed()
        engine = SwiftRolloutDeploy.get_infer_engine(args, template=args.get_template(), seed=worker_seed)
        rollout_engine = get_rollout_engine_type(args, engine)
    except Exception:
        connection.send({'status': 'error', 'error': traceback.format_exc()})
        return
    connection.send({'status': 'ready'})

    loop = asyncio.get_running_loop()
    while True:
        try:
            command = await loop.run_in_executor(None, connection.recv)
        except KeyboardInterrupt:
            await engine.engine.collective_rpc(method='close_communicator')
            break

        # Handle commands
        if command['type'] in ['call', 'fire_and_forget']:
            method_name = command['method']
            args, kwargs = command.get('args', ()), command.get('kwargs', {})
            method = getattr(rollout_engine, method_name, None) or getattr(rollout_engine.engine, method_name, None)
            try:
                result = await method(*args, **kwargs)
            except Exception:
                logger.error(f'Method execution failed: {method_name}\n{traceback.format_exc()}')
                result = None

            if command['type'] == 'call':
                connection.send(result)
        elif command['type'] == 'shutdown':
            break


def llm_worker_entry(*args, **kwargs):
    asyncio.run(async_llm_worker(*args, **kwargs))


class SwiftRolloutDeploy(SwiftPipeline):
    args_class = RolloutArguments
    args: args_class

    def _register_rl_rollout_app(self):
        self.app.get('/health/')(self.health)
        self.app.get('/get_world_size/')(self.get_world_size)
        self.app.get('/get_model_state_keys/')(self.get_model_state_keys)
        self.app.post('/init_communicator/')(self.init_communicator)
        self.app.post('/update_named_param/')(self.update_named_param)
        self.app.post('/update_adapter_flattened_param/')(self.update_adapter_flattened_param)
        self.app.post('/update_adapter_param/')(self.update_adapter_param)
        self.app.post('/update_flattened_params/')(self.update_flattened_params)
        self.app.post('/process_weights_after_loading/')(self.process_weights_after_loading)
        self.app.post('/reset_prefix_cache/')(self.reset_prefix_cache)
        self.app.post('/reset_encoder_cache/')(self.reset_encoder_cache)
        self.app.post('/reset_mm_cache/')(self.reset_mm_cache)
        self.app.post('/close_communicator/')(self.close_communicator)
        self.app.post('/infer/', response_model=None)(self.infer)
        self.app.post('/get_engine_type/')(self.get_engine_type)

    def __init__(self, args: Optional[Union[List[str], RolloutArguments]] = None):
        super().__init__(args)
        self.use_gym_env = self.args.use_gym_env
        self.use_async_engine = self.args.vllm_use_async_engine
        self.num_connections = 1 if self.use_async_engine else self.args.vllm_data_parallel_size
        safe_set_start_method()
        self.app = FastAPI(lifespan=self.lifespan)
        self._register_rl_rollout_app()
        self.master_port = get_open_port()
        self.connections = []
        self.processes = []
        self._start_data_parallel_workers()

    def _start_data_parallel_workers(self):
        for data_parallel_rank in range(self.num_connections):
            parent_conn, child_conn = Pipe()
            worker_func = llm_worker_entry if self.use_async_engine else llm_worker
            process = Process(target=worker_func, args=(self.args, data_parallel_rank, self.master_port, child_conn))
            process.start()
            self.connections.append(parent_conn)
            self.processes.append(process)

    @asynccontextmanager
    async def lifespan(self, app: FastAPI):
        pending_connections = set(range(self.num_connections))

        while pending_connections:
            for idx in list(pending_connections):
                connection = self.connections[idx]
                if not connection.poll(timeout=0.1):
                    if not self.processes[idx].is_alive():
                        raise RuntimeError(f'Worker process {idx} exited unexpectedly during initialization. '
                                           'Check worker logs for details.')
                    continue
                msg = connection.recv()
                if isinstance(msg, dict) and msg.get('status') == 'error':
                    error_msg = msg.get('error', 'Unknown error')
                    raise RuntimeError(f'Worker process {idx} failed during initialization:\n{error_msg}')
                if isinstance(msg, dict) and msg.get('status') == 'ready':
                    pending_connections.discard(idx)

        yield

        # Wait for processes to terminate
        for process in self.processes:
            process.join(timeout=10)
            if process.is_alive():
                logger.warning(f'Process {process} is still alive after 10 seconds, attempting to terminate...')
                process.terminate()
                process.join()

    @staticmethod
    def get_infer_engine(args: RolloutArguments, template=None, **kwargs):
        kwargs.update({
            'model_id_or_path': args.model,
            'model_type': args.model_type,
            'revision': args.model_revision,
            'torch_dtype': args.torch_dtype,
            'template': template,
            'use_async_engine': args.vllm_use_async_engine,
            'max_lora_rank': args.vllm_max_lora_rank,
        })
        infer_backend = kwargs.pop('infer_backend', None) or args.infer_backend
        if infer_backend != 'vllm':
            infer_backend = 'vllm'
            logger.info('Currently, rollout only supports the vLLM backend. Set vLLM backend')
        kwargs.update(args.get_vllm_engine_kwargs())
        kwargs.update({'enable_lora': args.vllm_enable_lora})  # override
        # Important: Use processed_logprobs so temperature scaling affects the logprobs
        # This is required for correct importance sampling in rollout correction
        kwargs['logprobs_mode'] = 'processed_logprobs' if check_vllm_version_ge('0.10.2') else None
        # used for RL external rollout backend
        engine_kwargs = kwargs.get('engine_kwargs', {})
        # for RL rollout model weight sync
        engine_kwargs.update({'worker_extension_cls': 'swift.pipelines.infer.rollout.WeightSyncWorkerExtension'})

        load_format = engine_kwargs.pop('load_format', 'auto')
        kwargs['load_format'] = load_format

        if args.vllm_use_async_engine and args.vllm_data_parallel_size > 1:
            engine_kwargs['data_parallel_size'] = args.vllm_data_parallel_size
        kwargs['engine_kwargs'] = engine_kwargs

        return GRPOVllmEngine(**kwargs)

    async def health(self):
        """
        Health check endpoint to verify that the server is running.
        """
        return {'status': 'ok'}

    async def get_world_size(self):
        """
        Retrieves the world size from the LLM engine.

        Returns:
            `dict`:
                A dictionary containing the world size.

        Example response:
        ```json
        {"world_size": 8}
        ```
        """
        return {'world_size': self.args.vllm_tensor_parallel_size * self.args.vllm_data_parallel_size}

    async def get_model_state_keys(self):
        """Get runtime vLLM model parameter names from one worker group."""
        if not self.connections:
            return {'keys': []}
        kwargs = {'method': 'get_state_keys'}
        self.connections[0].send({'type': 'call', 'method': 'collective_rpc', 'kwargs': kwargs})
        result = self.connections[0].recv()

        keys = []
        if isinstance(result, list):
            if result and all(isinstance(x, str) for x in result):
                keys = result
            else:
                for item in result:
                    if isinstance(item, list) and item and all(isinstance(x, str) for x in item):
                        keys = item
                        break
        return {'keys': keys}

    async def init_communicator(self, request: InitCommunicatorRequest):
        """
        Initializes the communicator for synchronizing model weights between a client and multiple server
        workers.

        Args:
            request (`InitCommunicatorRequest`):
                - `host` (`str`): Hostname or IP address of the master node.
                - `port` (`int`): Port number to be used for communication.
                - `world_size` (`int`): Total number of participating processes in the group.
        """
        world_size = self.args.vllm_tensor_parallel_size * self.args.vllm_data_parallel_size + 1

        # The function init_communicator is called this way: init_communicator(host, port, world_size)
        # So with collective_rpc we need to call it this way:
        # llm.collective_rpc(method="init_communicator", args=(host, port, world_size))
        kwargs = {'method': 'init_communicator', 'args': (request.host, request.port, world_size)}
        for connection in self.connections:
            connection.send({'type': 'fire_and_forget', 'method': 'collective_rpc', 'kwargs': kwargs})

        return {'message': 'Request received, initializing communicator'}

    async def update_named_param(self, request: UpdateWeightsRequest):
        """
        Updates the model weights with the provided tensor.

        Once this endpoint is called, the client process should broadcast the updated weights to all server workers.

        Args:
            request (`UpdateWeightsRequest`):
                - `name` (`str`): Name of the weight tensor being updated.
                - `dtype` (`str`): Data type of the weight tensor (e.g., `"torch.float32"`).
                - `shape` (list of `int`): Shape of the weight

        """
        # The function update_named_param is called this way: update_named_param("name", torch.float32, (10, 10))
        # So with collective_rpc we need to call it this way:
        # llm.collective_rpc("update_named_param", args=("name", torch.float32, (10, 10)))
        kwargs = {'method': 'update_named_param', 'args': (request.name, request.dtype, tuple(request.shape))}
        for connection in self.connections:
            connection.send({'type': 'fire_and_forget', 'method': 'collective_rpc', 'kwargs': kwargs})

        return {'message': 'Request received, updating named parameter'}

    async def update_adapter_flattened_param(self, request: UpdateFlattenedAdapterRequest):
        peft_config = asdict(request.peft_config)
        metadatas = [
            metadata.model_dump() if hasattr(metadata, 'model_dump') else metadata.dict()
            for metadata in request.metadatas
        ]
        kwargs = {'method': 'update_adapter_flattened_param', 'args': (peft_config, metadatas)}
        for connection in self.connections:
            connection.send({'type': 'fire_and_forget', 'method': 'collective_rpc', 'kwargs': kwargs})

        return {'message': 'Request received, updating adapter parameter'}

    async def update_adapter_param(self, request: UpdateAdapterRequest):
        """
        Updates the LoRA adapter weights without flattening.
        Each tensor is broadcast individually.

        Args:
            request (UpdateAdapterRequest):
                - peft_config (LoraConfig): PEFT configuration for the adapter.
                - lora_tensors_metadata (List[FlattenedTensorMetadata]): Metadata for each tensor.
        """
        peft_config = asdict(request.peft_config)
        lora_tensors_metadata = [
            metadata.model_dump() if hasattr(metadata, 'model_dump') else metadata.dict()
            for metadata in request.lora_tensors_metadata
        ]
        kwargs = {'method': 'update_adapter_param', 'args': (peft_config, lora_tensors_metadata)}
        for connection in self.connections:
            connection.send({'type': 'fire_and_forget', 'method': 'collective_rpc', 'kwargs': kwargs})

        return {'message': 'Request received, updating adapter parameter (non-flattened)'}

    async def update_flattened_params(self, request: UpdateFlattenedParamsRequest):
        """
        Updates the model weights with flattened tensor data.

        Args:
            request (UpdateFlattenedParamsRequest):
                - metadatas (List[FlattenedTensorMetadata]): Metadata for the flattened tensors.

        """
        metadatas = [
            metadata.model_dump() if hasattr(metadata, 'model_dump') else metadata.dict()
            for metadata in request.metadatas
        ]
        kwargs = {'method': 'update_flattened_params', 'args': (metadatas, )}
        for connection in self.connections:
            connection.send({'type': 'fire_and_forget', 'method': 'collective_rpc', 'kwargs': kwargs})

        return {'message': 'Request received, updating flattened parameters'}

    async def process_weights_after_loading(self):
        """
        Triggers process_weights_after_loading on all workers.
        """
        kwargs = {'method': 'process_weights_after_loading', 'args': ()}
        for connection in self.connections:
            connection.send({'type': 'call', 'method': 'collective_rpc', 'kwargs': kwargs})
        # Wait for all workers to complete before returning
        loop = asyncio.get_running_loop()
        await asyncio.gather(*(loop.run_in_executor(None, connection.recv) for connection in self.connections))
        return {'message': 'Weights processed after loading'}

    async def reset_prefix_cache(self):
        """
        Resets the prefix cache for the model.
        """
        for connection in self.connections:
            connection.send({'type': 'call', 'method': 'reset_prefix_cache'})
        # Wait for and collect all results
        all_outputs = [connection.recv() for connection in self.connections]
        success = all(output for output in all_outputs)
        return {'message': 'Request received, resetting prefix cache status: ' + str(success)}

    async def reset_encoder_cache(self):
        """Resets the encoder cache (vision encoder embeddings) for the model."""
        for connection in self.connections:
            connection.send({'type': 'call', 'method': 'reset_encoder_cache'})
        all_outputs = [connection.recv() for connection in self.connections]
        success = all(output for output in all_outputs)
        return {'message': 'Request received, resetting encoder cache status: ' + str(success)}

    async def reset_mm_cache(self):
        """Resets the multimodal processor cache for the model."""
        for connection in self.connections:
            connection.send({'type': 'call', 'method': 'reset_mm_cache'})
        all_outputs = [connection.recv() for connection in self.connections]
        success = all(output for output in all_outputs)
        return {'message': 'Request received, resetting mm cache status: ' + str(success)}

    async def get_engine_type(self):
        """
        Return a dictionary describing the runtime engine configuration.

        The returned object contains three keys:
        - engine_type (str): Either 'AsyncLLMEngine' or 'LLMEngine', indicating
        whether the asynchronous or synchronous engine is in use.
        - use_gym_env (bool, optional): Present and True **only when**
        ``use_async_engine`` and ``use_gym_env`` are both True.
        - enable_multi_turn (bool): True if multi-turn scheduling is enabled
        via ``args.multi_turn_scheduler``, otherwise False.

        Returns
        -------
        dict
            A concise specification of the current engine setup.
        """
        enable_multi_turn = False
        if self.args.multi_turn_scheduler:
            enable_multi_turn = True
        use_gym_env = False
        if self.use_async_engine and self.use_gym_env:
            use_gym_env = True
        engine_type = 'AsyncLLMEngine' if self.use_async_engine else 'LLMEngine'
        enable_lora = self.args.vllm_enable_lora
        return {
            'engine_type': engine_type,
            'enable_multi_turn': enable_multi_turn,
            'use_gym_env': use_gym_env,
            'enable_lora': enable_lora,
        }

    async def close_communicator(self):
        """
        Closes the weight update group and cleans up associated resources.
        """
        kwargs = {'method': 'close_communicator'}
        for connection in self.connections:
            connection.send({'type': 'fire_and_forget', 'method': 'collective_rpc', 'kwargs': kwargs})
        return {'message': 'Request received, closing communicator'}

    async def infer(
        self,
        infer_requests: List[Union[Dict, RolloutInferRequest]],
        request_config: Optional[RequestConfig] = None,
        *,
        use_tqdm: Optional[bool] = None,
    ):
        chunked_infer_requests = chunk_list(infer_requests, self.num_connections)

        # Send the prompts to each worker
        for i, (connection, requests) in enumerate(zip(self.connections, chunked_infer_requests)):
            # When the number of prompts is less than data_parallel_size, some workers will receive empty prompts.
            # However, vLLM requires that we always send at least one prompt. So we send a placeholder prompt to comply
            # with vLLM's requirement, and we later ignore the result.
            if not requests:
                requests = [RolloutInferRequest(messages=[{'role': 'user', 'content': '<placeholder>'}])]
            kwargs = {'infer_requests': requests, 'request_config': request_config, 'use_tqdm': use_tqdm}
            method = 'infer' if not self.use_async_engine else 'async_infer'
            connection.send({'type': 'call', 'method': method, 'kwargs': kwargs})

        all_outputs = [connection.recv() for connection in self.connections]
        # Handle empty prompts (see above)
        all_outputs = [output for output, requests in zip(all_outputs, chunked_infer_requests) if requests]
        all_outputs = list(chain.from_iterable(all_outputs))  # from list of list to single list

        return all_outputs

    def run(self):
        args = self.args
        uvicorn.run(self.app, host=args.host, port=args.port, log_level=args.log_level)


def rollout_main(args: Optional[Union[List[str], RolloutArguments]] = None) -> None:
    SwiftRolloutDeploy(args).main()


def is_accessible(port: int):
    infer_client = InferClient(port=port)
    try:
        infer_client.get_model_list()
    except ClientConnectorError:
        return False
    return True


@contextmanager
def run_rollout(args: RolloutArguments, return_url: bool = False):
    if isinstance(args, RolloutArguments) and args.__class__.__name__ == 'RolloutArguments':
        deploy_args = args
    else:
        args_dict = asdict(args)
        parameters = inspect.signature(RolloutArguments).parameters
        for k in list(args_dict.keys()):
            if k not in parameters or args_dict[k] is None:
                args_dict.pop(k)
        deploy_args = RolloutArguments(**args_dict)

    mp = multiprocessing.get_context('spawn')
    process = mp.Process(target=rollout_main, args=(deploy_args, ))
    process.start()
    try:
        while not is_accessible(deploy_args.port):
            time.sleep(1)
        yield f'http://127.0.0.1:{deploy_args.port}/v1' if return_url else deploy_args.port
    finally:
        process.terminate()
        logger.info('The deployment process has been terminated.')
