# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo

# SPDX-License-Identifier: Apache-2.0
# Adapted from rocm/vllm: https://github.com/ROCm/vllm/blob/v0.7.3%2Brocm/vllm/platforms/rocm.py
"""
This file is a platform abstraction for ROCm GPUs,
adjusted to match the structure and interface of `cuda.py`.
"""

import types
from functools import lru_cache
from typing import Any

import torch
import torch.nn as nn
import torch.nn.functional as F

import sglang.multimodal_gen.envs as envs
from sglang.multimodal_gen.runtime.platforms.interface import (
    AttentionBackendEnum,
    DeviceCapability,
    Platform,
    PlatformEnum,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger

logger = init_logger(__name__)


# ROCm uses the same torch.cuda interface
class RocmPlatform(Platform):
    _enum = PlatformEnum.ROCM
    device_name: str = "rocm"
    device_type: str = "cuda"  # torch uses 'cuda' backend string
    dispatch_key: str = "CUDA"
    device_control_env_var: str = "CUDA_VISIBLE_DEVICES"

    @classmethod
    def get_local_torch_device(cls) -> torch.device:
        return torch.device(f"cuda:{envs.LOCAL_RANK}")

    @classmethod
    def get_device_capability(cls, device_id: int = 0) -> DeviceCapability:
        major, minor = torch.cuda.get_device_capability(device_id)
        return DeviceCapability(major=major, minor=minor)

    @classmethod
    def get_device_name(cls, device_id: int = 0) -> str:
        return str(torch.cuda.get_device_name(device_id))

    @classmethod
    @lru_cache(maxsize=1)
    def get_device_total_memory(cls, device_id: int = 0) -> int:
        return torch.cuda.get_device_properties(device_id).total_memory

    @classmethod
    def is_async_output_supported(cls, enforce_eager: bool | None) -> bool:
        if enforce_eager:
            logger.warning(
                "To see benefits of async output processing, enable CUDA graph. "
                "Since enforce-eager is enabled, async output processor cannot be used"
            )
            return False
        return True

    @classmethod
    def log_warnings(cls) -> None:
        pass  # ROCm-specific warnings can be added here

    @classmethod
    def get_current_memory_usage(cls, device: torch.device | None = None) -> float:
        torch.cuda.reset_peak_memory_stats(device)
        return float(torch.cuda.max_memory_allocated(device))

    @classmethod
    def get_available_gpu_memory(
        cls,
        device_id: int | None = None,
        distributed: bool = False,
        empty_cache: bool = True,
        cpu_group: Any = None,
    ) -> float:
        if empty_cache:
            torch.cuda.empty_cache()

        if device_id is None:
            device_id = torch.cuda.current_device()

        free_gpu_memory, _ = torch.cuda.mem_get_info(device_id)

        if distributed:
            import torch.distributed as dist

            tensor = torch.tensor(free_gpu_memory, dtype=torch.float32, device="cuda")
            dist.all_reduce(tensor, op=dist.ReduceOp.MIN, group=cpu_group)
            free_gpu_memory = float(tensor.item())

        return free_gpu_memory / (1 << 30)

    @classmethod
    def get_attn_backend_cls_str(
        cls,
        selected_backend: AttentionBackendEnum | None,
        head_size: int,
        dtype: torch.dtype,
    ) -> str:
        if selected_backend == AttentionBackendEnum.TORCH_SDPA:
            logger.info("Using Torch SDPA backend.")
            return "sglang.multimodal_gen.runtime.layers.attention.backends.sdpa.SDPABackend"

        elif selected_backend in (AttentionBackendEnum.FA, None):
            pass

        elif selected_backend == AttentionBackendEnum.AITER:
            if dtype not in (torch.float16, torch.bfloat16):
                logger.warning(
                    "AITer backend works best with fp16/bf16 inputs but got dtype=%s. "
                    "Proceeding with AITer anyway.",
                    dtype,
                )
            logger.info("Using AITer backend on ROCm.")
            return "sglang.multimodal_gen.runtime.layers.attention.backends.aiter.AITerBackend"

        elif selected_backend == AttentionBackendEnum.AITER_SAGE:
            if dtype in (torch.float16, torch.bfloat16):
                logger.info("Using AITER Sage backend on ROCm.")
                return "sglang.multimodal_gen.runtime.layers.attention.backends.aiter_sage.AITERSageBackend"
            else:
                logger.warning(
                    "AITER Sage backend only supports bf16/fp16 inputs but got dtype=%s.",
                    dtype,
                )

        elif selected_backend in (
            AttentionBackendEnum.SLIDING_TILE_ATTN,
            AttentionBackendEnum.SAGE_ATTN,
        ):
            raise ValueError(
                f"{selected_backend.name} is not supported on {cls.device_name}."
            )
        elif selected_backend:
            raise ValueError(
                f"Invalid attention backend for {cls.device_name}: {selected_backend}"
            )

        target_backend = AttentionBackendEnum.FA
        if dtype not in (torch.float16, torch.bfloat16):
            logger.info(
                "Cannot use FlashAttention backend for dtype other than "
                "torch.float16 or torch.bfloat16."
            )
            target_backend = AttentionBackendEnum.TORCH_SDPA

        if target_backend == AttentionBackendEnum.FA:
            try:
                import flash_attn  # noqa: F401

                from sglang.kernels.ops.attention.flash_attention_v3 import (
                    _is_fa3_supported,
                )
                from sglang.multimodal_gen.runtime.layers.attention.backends.flash_attn import (  # noqa: F401
                    FlashAttentionBackend,
                )

                if not _is_fa3_supported():
                    logger.info(
                        "FlashAttention backend now dispatches through FA3 "
                        "(CUDA-only). Using Torch SDPA backend on ROCm."
                    )
                    target_backend = AttentionBackendEnum.TORCH_SDPA

                if target_backend == AttentionBackendEnum.FA:
                    supported_sizes = FlashAttentionBackend.get_supported_head_sizes()
                    if head_size not in supported_sizes:
                        logger.info(
                            "Cannot use FlashAttention-2 backend for head size %d.",
                            head_size,
                        )
                        target_backend = AttentionBackendEnum.TORCH_SDPA
            except ImportError:
                logger.info(
                    "Cannot use FlashAttention backend because the "
                    "flash_attn package is not found. "
                    "Make sure that flash_attn was built and installed "
                    "(on by default)."
                )
                target_backend = AttentionBackendEnum.TORCH_SDPA

        if target_backend == AttentionBackendEnum.TORCH_SDPA:
            logger.info("Using Torch SDPA backend.")

            return "sglang.multimodal_gen.runtime.layers.attention.backends.sdpa.SDPABackend"

        logger.info("Using Flash Attention backend.")

        return "sglang.multimodal_gen.runtime.layers.attention.backends.flash_attn.FlashAttentionBackend"

    @classmethod
    def get_device_communicator_cls(cls) -> str:
        return "sglang.multimodal_gen.runtime.distributed.device_communicators.cuda_communicator.CudaCommunicator"  # works for ROCm too

    @classmethod
    def optimize_vae(cls, vae: torch.nn.Module) -> torch.nn.Module:
        """Apply ROCm-specific optimizations to VAE.

        - Enable MIOpen benchmark mode so that the best convolution algorithm
          is selected for each distinct input shape (benefits Conv3d-heavy VAE
          decode).
        - Replace nn.GroupNorm with AITer GroupNorm when available.
        - Replace CausalConv3d (3x3x3) with temporal-unfolded batched Conv2D.
        """
        if envs.SGLANG_USE_ROCM_CUDNN_BENCHMARK and not torch.backends.cudnn.benchmark:
            torch.backends.cudnn.benchmark = True
            logger.info(
                "Enabled cudnn.benchmark (MIOpen auto-tuning) for VAE conv layers"
            )

        if envs.SGLANG_USE_ROCM_VAE:
            try:
                from aiter.ops.groupnorm import GroupNorm as AiterGroupNorm

                count = cls._replace_groupnorm(vae, AiterGroupNorm)
                if count > 0:
                    logger.info(
                        "Replaced %d nn.GroupNorm modules with AITer GroupNorm in VAE",
                        count,
                    )
            except Exception:
                logger.warning(
                    "Failed to apply AITer GroupNorm to VAE.",
                    exc_info=True,
                )

        use_bf16 = envs.SGLANG_USE_ROCM_VAE_CONV2D_BF16
        use_conv2d = envs.SGLANG_USE_ROCM_VAE_CONV2D or use_bf16
        if use_conv2d:
            count = cls._replace_conv3d_with_conv2d(vae, use_bf16=use_bf16)
            if count > 0:
                mode = "BF16" if use_bf16 else "same dtype"
                logger.info(
                    "Replaced %d CausalConv3d modules with batched Conv2D "
                    "(compute=%s) in VAE",
                    count,
                    mode,
                )

        return vae

    @staticmethod
    def _replace_groupnorm(module: torch.nn.Module, aiter_gn_cls: type) -> int:
        count = 0
        for name, child in module.named_children():
            if isinstance(child, torch.nn.GroupNorm) and child.affine:
                replacement = aiter_gn_cls(
                    num_groups=child.num_groups,
                    num_channels=child.num_channels,
                    eps=child.eps,
                    affine=True,
                    device=child.weight.device,
                    dtype=child.weight.dtype,
                )
                replacement.weight = child.weight
                replacement.bias = child.bias
                setattr(module, name, replacement)
                count += 1
            else:
                count += RocmPlatform._replace_groupnorm(child, aiter_gn_cls)
        return count

    @staticmethod
    def _conv3d_as_batched_conv2d(
        x_padded: torch.Tensor,
        weight_2d: torch.Tensor,
        bias: torch.Tensor | None,
        stride: tuple[int, ...],
        kt: int,
        compute_bf16: bool = False,
    ) -> torch.Tensor:
        """Replace F.conv3d with temporal-unfolded batched Conv2D.

        ``x_padded`` must already be spatially/temporally padded so that
        ``F.conv3d(x_padded, weight, bias, stride, padding=0)`` would produce
        the correct output.  This routine unfolds along the temporal axis,
        reshapes into a batch of 2-D frames, runs ``F.conv2d``, and folds the
        result back.

        *weight_2d* is the pre-transformed 2-D kernel
        ``[C_out, Kt*C_in, Kh, Kw]``, cached at patch time to avoid
        redundant permute/reshape on every forward call.

        When *compute_bf16* is True the convolution is executed in BF16 and
        the output is cast back to the original dtype.
        """
        orig_dtype = x_padded.dtype
        N, C_in, T, H, W = x_padded.shape
        C_out = weight_2d.shape[0]
        stride_t, stride_h, stride_w = stride

        T_out = (T - kt) // stride_t + 1

        # (N, C_in, T, H, W) -> (N, T_out, Kt, C_in, H, W) -> (N*T_out, Kt*C_in, H, W)
        unfolded = x_padded.unfold(2, kt, stride_t)
        unfolded = unfolded.permute(0, 2, 5, 1, 3, 4).reshape(
            N * T_out, kt * C_in, H, W
        )

        w = weight_2d
        if compute_bf16 and orig_dtype != torch.bfloat16:
            unfolded = unfolded.to(torch.bfloat16)
            w = w.to(torch.bfloat16)
            b = bias.to(torch.bfloat16) if bias is not None else None
        else:
            b = bias

        out = F.conv2d(unfolded, w, b, stride=(stride_h, stride_w))

        if compute_bf16 and orig_dtype != torch.bfloat16:
            out = out.to(orig_dtype)

        _, _, H_out, W_out = out.shape
        return out.reshape(N, T_out, C_out, H_out, W_out).permute(0, 2, 1, 3, 4)

    @staticmethod
    def _replace_conv3d_with_conv2d(
        module: torch.nn.Module, use_bf16: bool = False
    ) -> int:
        """Walk *module* and patch every CausalConv3d that has a 3-D kernel.

        A ``CausalConv3d`` is identified as any ``nn.Conv3d`` subclass that
        carries a ``_padding`` attribute (set by the Wan / diffusers causal
        conv wrapper).  Only modules whose kernel is truly 3-D (Kt>1, Kh>1,
        Kw>1) are replaced; pointwise or 1-D-temporal convolutions are left
        untouched.  Modules with non-default ``groups`` or ``dilation`` are
        skipped as the 2-D decomposition assumes groups=1 and dilation=1.
        """
        patched = 0
        skipped = 0
        for _name, child in module.named_modules():
            if not isinstance(child, nn.Conv3d):
                continue
            if not hasattr(child, "_padding"):
                continue
            kt, kh, kw = child.kernel_size
            if kt <= 1 or kh <= 1 or kw <= 1:
                skipped += 1
                continue
            if child.groups != 1 or any(d != 1 for d in child.dilation):
                skipped += 1
                continue

            padding = child._padding
            stride = child.stride

            # Pre-compute the 2-D weight: [C_out, C_in, Kt, Kh, Kw]
            # -> [C_out, Kt*C_in, Kh, Kw]  (cached as a buffer)
            weight_2d = (
                child.weight.data.permute(0, 2, 1, 3, 4)
                .reshape(child.out_channels, kt * child.in_channels, kh, kw)
                .contiguous()
            )
            child.register_buffer("_weight_2d", weight_2d)

            def _patched_forward(
                self,
                x,
                cache_x=None,
                *,
                _padding=padding,
                _stride=stride,
                _kt=kt,
                _bf16=use_bf16,
            ):
                pad = list(_padding)
                if cache_x is not None and _padding[4] > 0:
                    cache_x = cache_x.to(x.device)
                    x = torch.cat([cache_x, x], dim=2)
                    pad[4] -= cache_x.shape[2]
                x = F.pad(x, pad)
                x = x.to(self.weight.dtype)
                return RocmPlatform._conv3d_as_batched_conv2d(
                    x,
                    self._weight_2d,
                    self.bias,
                    _stride,
                    _kt,
                    compute_bf16=_bf16,
                )

            child.forward = types.MethodType(_patched_forward, child)
            patched += 1

        logger.info(
            "Conv3D→Conv2D: patched %d CausalConv3d (3D kernel, compute=%s), "
            "skipped %d (1D/pointwise/grouped)",
            patched,
            "BF16" if use_bf16 else "same dtype",
            skipped,
        )
        return patched

    @classmethod
    def enable_dit_layerwise_offload_for_wan_by_default(cls) -> bool:
        """ROCm performs better without DIT layerwise offload on Wan."""
        return False
