# Copyright 2025 Bytedance Ltd. and/or its affiliates
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from contextlib import nullcontext
from typing import Any, Optional

import torch.distributed as dist
from torch.distributed import ProcessGroup


_DATA_PARALLEL_GROUP = None

_ULYSSES_SEQUENCE_PARALLEL_GROUP = {"default": None}
_ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP = {"default": None}
_ULYSSES_GROUP_KEY = "default"

_CONTEXT_PARALLEL_GROUP = None

_UNIFIED_SEQUENCE_PARALLEL_GROUP = None
_UNIFIED_SEQUENCE_PARALLEL_CPU_GROUP = None


# ------------------------------ Data Parallel ------------------------------ #
def set_data_parallel_group(group: dist.ProcessGroup):
    """
    Set data parallel process group.
    """
    global _DATA_PARALLEL_GROUP
    _DATA_PARALLEL_GROUP = group


def get_data_parallel_group() -> Optional[dist.ProcessGroup]:
    """
    Get data parallel process group.
    """
    global _DATA_PARALLEL_GROUP
    return _DATA_PARALLEL_GROUP


def get_data_parallel_rank() -> Optional[dist.ProcessGroup]:
    """
    Get data parallel rank.
    """
    group = get_data_parallel_group()
    return dist.get_rank(group)


def get_data_parallel_world_size() -> Optional[dist.ProcessGroup]:
    """
    Get data parallel world_size.
    """
    group = get_data_parallel_group()
    return dist.get_world_size(group)


# ----------------------------- Ulysses Parallel ---------------------------- #
def set_ulysses_sequence_parallel_group(group: dist.ProcessGroup, group_key: str = "default"):
    """
    Set ulysses sequence parallel process group.
    """
    global _ULYSSES_SEQUENCE_PARALLEL_GROUP
    _ULYSSES_SEQUENCE_PARALLEL_GROUP[group_key] = group


def set_ulysses_sequence_parallel_cpu_group(group: dist.ProcessGroup, group_key: str = "default"):
    """
    Set ulysses sequence parallel process group.
    """
    global _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP
    _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP[group_key] = group


def set_ulysses_sequence_parallel_group_key(group_key: str = "default"):
    """
    Set ulysses sequence parallel process group key.
    """
    global _ULYSSES_GROUP_KEY
    _ULYSSES_GROUP_KEY = group_key


def get_ulysses_sequence_parallel_group_key() -> str:
    """
    Get ulysses sequence parallel group key.
    """
    global _ULYSSES_GROUP_KEY
    return _ULYSSES_GROUP_KEY


def get_ulysses_sequence_parallel_group() -> Optional[dist.ProcessGroup]:
    """
    Get ulysses sequence parallel process group.
    """
    global _ULYSSES_SEQUENCE_PARALLEL_GROUP
    group_key = get_ulysses_sequence_parallel_group_key()
    if group_key not in _ULYSSES_SEQUENCE_PARALLEL_GROUP:
        raise RuntimeError(f"Unknown key {group_key} in Ulysses sequence parallel group!")
    return _ULYSSES_SEQUENCE_PARALLEL_GROUP[group_key]


def get_ulysses_sequence_parallel_cpu_group() -> Optional[dist.ProcessGroup]:
    """
    Get ulysses sequence parallel CPU process group.
    """
    global _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP
    group_key = get_ulysses_sequence_parallel_group_key()
    if group_key not in _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP:
        raise RuntimeError(f"Unknown key {group_key} in Ulysses sequence parallel group!")
    return _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP[group_key]


def get_ulysses_sequence_parallel_group_by_key(group_key: str = "default") -> Optional[dist.ProcessGroup]:
    """
    Get ulysses sequence parallel process group.
    """
    global _ULYSSES_SEQUENCE_PARALLEL_GROUP
    if group_key not in _ULYSSES_SEQUENCE_PARALLEL_GROUP:
        raise RuntimeError(f"Unknown key {group_key} in Ulysses sequence parallel group!")
    return _ULYSSES_SEQUENCE_PARALLEL_GROUP[group_key]


def get_ulysses_sequence_parallel_cpu_group_by_key(group_key: str = "default") -> Optional[dist.ProcessGroup]:
    """
    Get ulysses sequence parallel CPU process group.
    """
    global _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP
    if group_key not in _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP:
        raise RuntimeError(f"Unknown key {group_key} in Ulysses sequence parallel group!")
    return _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP[group_key]


def get_ulysses_sequence_parallel_rank(group: ProcessGroup = None) -> int:
    """
    Get ulysses sequence parallel rank.
    """
    group = get_ulysses_sequence_parallel_group() if group is None else group
    return dist.get_rank(group) if group else 0


def get_ulysses_sequence_parallel_world_size(group: ProcessGroup = None) -> int:
    """
    Get ulysses sequence parallel world size.
    """
    group = get_ulysses_sequence_parallel_group() if group is None else group
    return dist.get_world_size(group) if group else 1


# ----------------------------- Context Parallel ---------------------------- #


def set_context_parallel_group(cp_group: dist.ProcessGroup):
    """
    Set context parallel process group.
    """
    global _CONTEXT_PARALLEL_GROUP
    _CONTEXT_PARALLEL_GROUP = cp_group


def get_context_parallel_group(check_initialized=True):
    """Get the context parallel group the caller rank belongs to."""
    global _CONTEXT_PARALLEL_GROUP
    if check_initialized:
        assert _CONTEXT_PARALLEL_GROUP is not None, "context parallel group is not initialized"
    return _CONTEXT_PARALLEL_GROUP


def get_context_parallel_rank():
    """Return my rank for the context parallel group."""

    if dist.is_available() and dist.is_initialized():
        return dist.get_rank(group=get_context_parallel_group())
    else:
        return 0


def get_context_parallel_world_size():
    """Return world size for the context parallel group."""
    if dist.is_available() and dist.is_initialized():
        return dist.get_world_size(group=get_context_parallel_group())
    else:
        return 0


# ----------------------------- Unified Parallel ---------------------------- #
def set_unified_sequence_parallel_group(group: dist.ProcessGroup):
    """
    Set unified sequence parallel process group.
    """
    global _UNIFIED_SEQUENCE_PARALLEL_GROUP
    _UNIFIED_SEQUENCE_PARALLEL_GROUP = group


def set_unified_sequence_parallel_cpu_group(group: dist.ProcessGroup):
    """
    Set unified sequence parallel process group.
    """
    global _UNIFIED_SEQUENCE_PARALLEL_CPU_GROUP
    _UNIFIED_SEQUENCE_PARALLEL_CPU_GROUP = group


def get_unified_sequence_parallel_group() -> Optional[dist.ProcessGroup]:
    """
    Get unified sequence parallel process group.
    """
    global _UNIFIED_SEQUENCE_PARALLEL_GROUP
    return _UNIFIED_SEQUENCE_PARALLEL_GROUP


def get_unified_sequence_parallel_cpu_group() -> Optional[dist.ProcessGroup]:
    """
    Get unified sequence parallel CPU process group.
    """
    global _UNIFIED_SEQUENCE_PARALLEL_CPU_GROUP
    return _UNIFIED_SEQUENCE_PARALLEL_CPU_GROUP


def get_unified_sequence_parallel_rank() -> int:
    """
    Get unified sequence parallel rank.
    """
    group = get_unified_sequence_parallel_group()
    return dist.get_rank(group) if group else 0


def get_unified_sequence_parallel_world_size() -> int:
    """
    Get unified sequence parallel world size.
    """
    group = get_unified_sequence_parallel_group()
    return dist.get_world_size(group) if group else 1


# ------------------------------- Initialize ------------------------------- #
def init_sequence_parallel(
    ulysses_size: int = 1, sep_dp: bool = False, ulysses_group_key: str = "default", cp_size: int = 1
):
    """
    Initialize unified sequence parallel.
    """
    global _CONTEXT_PARALLEL_GROUP
    global _ULYSSES_SEQUENCE_PARALLEL_GROUP
    global _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP

    set_ulysses_sequence_parallel_group(group=None, group_key="default")
    set_ulysses_sequence_parallel_cpu_group(group=None, group_key="default")

    if ulysses_size == 1 and cp_size == 1:
        return

    assert dist.is_initialized()
    world_size = dist.get_world_size()
    rank = dist.get_rank()
    unified_sp_size = ulysses_size * cp_size
    assert world_size % unified_sp_size == 0
    data_parallel_size = world_size // unified_sp_size

    if cp_size > 1:
        assert _CONTEXT_PARALLEL_GROUP is None, "Context parallel group has already been initialized!"
    if ulysses_size:
        assert (ulysses_group_key == "default" and _ULYSSES_SEQUENCE_PARALLEL_GROUP[ulysses_group_key] is None) or (
            ulysses_group_key != "default" and ulysses_group_key not in _ULYSSES_SEQUENCE_PARALLEL_GROUP
        ), f"Ulysses sequence parallel group ({ulysses_group_key}) has already been initialized!"
        assert (
            ulysses_group_key == "default" and _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP[ulysses_group_key] is None
        ) or (ulysses_group_key != "default" and ulysses_group_key not in _ULYSSES_SEQUENCE_PARALLEL_CPU_GROUP), (
            f"Ulysses sequence parallel ({ulysses_group_key}) group has already been initialized!"
        )

    for i in range(data_parallel_size):
        # build ulysses group
        if ulysses_size > 1:
            for j in range(cp_size):
                start_rank = i * unified_sp_size + j * ulysses_size
                end_rank = start_rank + ulysses_size
                ulysses_ranks = range(start_rank, end_rank)
                ulysses_group = dist.new_group(ulysses_ranks)
                ulysses_cpu_group = dist.new_group(ulysses_ranks, backend="gloo")
                if rank in ulysses_ranks:
                    set_ulysses_sequence_parallel_group(group=ulysses_group, group_key=ulysses_group_key)
                    set_ulysses_sequence_parallel_cpu_group(group=ulysses_cpu_group, group_key=ulysses_group_key)

        # build cp group
        if cp_size > 1:
            for j in range(ulysses_size):
                cp_global_ranks = range(i * unified_sp_size + j, (i + 1) * unified_sp_size, ulysses_size)
                cp_group = dist.new_group(cp_global_ranks)
                if rank in cp_global_ranks:
                    set_context_parallel_group(cp_group=cp_group)

        # build unified sp group
        unified_sp_ranks = range(i * unified_sp_size, (i + 1) * unified_sp_size)
        sp_group = dist.new_group(unified_sp_ranks)
        sp_cpu_group = dist.new_group(unified_sp_ranks, backend="gloo")
        if rank in unified_sp_ranks:
            set_unified_sequence_parallel_group(group=sp_group)
            set_unified_sequence_parallel_cpu_group(group=sp_cpu_group)

    if sep_dp:
        for j in range(unified_sp_size):
            dp_ranks = range(j, world_size, unified_sp_size)
            dp_group = dist.new_group(dp_ranks)
            if rank in dp_ranks:
                set_data_parallel_group(dp_group)


class UlyssesGroupKeyManager:
    def __init__(self, group_key: str):
        self.group_key = group_key

    def __enter__(self):
        set_ulysses_sequence_parallel_group_key(group_key=self.group_key)

    def __exit__(self, *args: Any):
        set_ulysses_sequence_parallel_group_key(group_key="default")


def is_ulysses_sequence_parallel_initialized() -> bool:
    """
    Check if ulysses sequence parallel is initialized.
    """
    return get_ulysses_sequence_parallel_group() is not None


def is_context_parallel_initialized() -> bool:
    """
    Check if ulysses sequence parallel is initialized.
    """
    return get_context_parallel_group() is not None


def get_ulysses_group_key_context(group_key: str = "default"):
    if not isinstance(group_key, str):
        raise RuntimeError(f"A Ulysses group key must be specified, now get: {group_key}")

    if group_key != "default":
        return UlyssesGroupKeyManager(group_key)
    else:
        return nullcontext()
