# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""W4A4 MXFP4 (Microscaling FP4) online/offline quantization for diffusion transformers.

Architecture mirrors mxfp8_config.py:

  MXFPLinearMethodBase                    – platform-agnostic skeleton (imported from mxfp8_config)
    NPUMxfp4LinearMethod                  – NPU single-scale offline (W4A4 MXFP4)
      NPUMxfp4OnlineLinearMethod          – NPU single-scale online (BF16 → FP4)
    NPUMxfp4DualScaleLinearMethod         – NPU dual-scale offline (W4A4 MXFP4 DualScale)
      NPUMxfp4DualScaleOnlineLinearMethod – NPU dual-scale online (BF16 → FP4)

Quantization configs:

  DiffusionMXFP4Config            – single-scale online/offline (quant_method="mxfp4")
  DiffusionMXFP4DualScaleMixedConfig – dual-scale + per-layer BF16 fallback (quant_method="mxfp4_dualscale")
      Offline: ignored_layers from config.json routes interleaved BF16 layers
      Online:  num_bf16_fallback_layers leading blocks stay in BF16 (default 5)

Key differences from MXFP8:

  1. Precision: float4_e2m1fn_x2 (FP4 packed, 2 values per element).
     npu_dynamic_mx_quant(x) without dst_type defaults to float4_e2m1fn_x2.

  2. Weight layout: stored as (N, K) — NOT pre-transposed.
     FP4 uses a packed format; transposing a packed tensor is not safely contiguous.
     Transpose is done inline in _quant_matmul via layer.weight.transpose(0, 1).

  3. GEMM signature: npu_quant_matmul requires explicit
     x1_dtype=float4_e2m1fn_x2 and x2_dtype=float4_e2m1fn_x2.

  Scale layout: (N, S/2, 2) — same reshape as MXFP8, also NOT pre-transposed;
  transposed inline in _quant_matmul.

Dual-scale (W4A4_MXFP4_DUALSCALE):

  Two-level quantization: fine scale (per-32 K) + coarse scale (per-512 K) + per-channel
  activation pre-scale (mul_scale from calibration). Uses npu_dynamic_dual_level_mx_quant
  and npu_dual_level_quant_matmul. Weight stored in NZ hardware format via npu_format_cast.

  Checkpoint tensor shapes for dual-scale:
    weight            : (N, K)          float8_e4m3fn     – FP4 packed
    weight_scale      : (N, K//32)      uint8    – fine scale (float8_e8m0fnu bits)
    weight_dual_scale : (N, K//512, 1)  float32  – coarse scale (extra dim avoids shape assert)
    mul_scale         : (K,)            float32  – per-input-channel activation pre-scale

Reference: MindIE-SD W4A4MXFP4QuantLinear / W4A4MXFP4DualQuantLinear (mindiesd/quantization/layer.py).
"""

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Any

import torch
from torch.nn import Module
from vllm.logger import init_logger
from vllm.model_executor.layers.linear import (
    LinearBase,
    UnquantizedLinearMethod,
)
from vllm.model_executor.layers.quantization import QuantizationMethods
from vllm.model_executor.layers.quantization.base_config import (
    QuantizationConfig,
    QuantizeMethodBase,
)
from vllm.model_executor.layers.quantization.fp8 import _copy_missing_attrs
from vllm.model_executor.layers.quantization.utils.quant_utils import is_layer_skipped
from vllm.model_executor.model_loader.weight_utils import initialize_single_dummy_weight
from vllm.model_executor.parameter import ModelWeightParameter
from vllm.model_executor.utils import replace_parameter

from vllm_omni.platforms import current_omni_platform
from vllm_omni.quantization.mxfp8_config import (
    MXFPLinearMethodBase,
    _LazyWeightMixin,
)

if TYPE_CHECKING:
    from vllm.model_executor.models.utils import WeightsMapper

logger = init_logger(__name__)


# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------


class DiffusionMXFP4Config(QuantizationConfig):
    """W4A4 MXFP4 quantization config for diffusion transformers.

    Supports both online (BF16 checkpoint → quantize at load time) and offline
    (pre-quantized MXFP4 checkpoint) modes, mirroring DiffusionMXFP8Config.

    MX (microscaling) format: groups of 32 K-dimension elements share one
    float8_e8m0fnu exponent scale. Weight and activation are float4_e2m1fn_x2.
    """

    def __init__(
        self,
        is_checkpoint_mxfp4_serialized: bool = False,
        ignored_layers: list[str] | None = None,
    ) -> None:
        super().__init__()
        self.is_checkpoint_mxfp4_serialized = is_checkpoint_mxfp4_serialized
        self.ignored_layers = ignored_layers or []

    @classmethod
    def get_name(cls) -> QuantizationMethods:
        return "mxfp4"

    @classmethod
    def get_supported_act_dtypes(cls) -> list[torch.dtype]:
        return [torch.bfloat16, torch.float16]

    @classmethod
    def get_min_capability(cls) -> int:
        return 80

    @classmethod
    def get_config_filenames(cls) -> list[str]:
        return []

    def apply_vllm_mapper(self, hf_to_vllm_mapper: WeightsMapper) -> None:
        if self.ignored_layers:
            self.ignored_layers = hf_to_vllm_mapper.apply_list(self.ignored_layers)

    @classmethod
    def from_config(cls, config: dict[str, Any]) -> DiffusionMXFP4Config:
        is_serialized = cls.get_from_keys_or(config, ["is_checkpoint_mxfp4_serialized"], False)
        ignored_layers = cls.get_from_keys_or(config, ["ignored_layers"], None)
        if not ignored_layers:
            ignored_layers = cls.get_from_keys_or(config, ["modules_to_not_convert"], None)
        return cls(
            is_checkpoint_mxfp4_serialized=is_serialized,
            ignored_layers=ignored_layers,
        )

    def get_quant_method(
        self,
        layer: torch.nn.Module,
        prefix: str,
    ) -> QuantizeMethodBase | None:
        if isinstance(layer, LinearBase):
            if is_layer_skipped(
                prefix=prefix,
                ignored_layers=self.ignored_layers,
                fused_mapping=self.packed_modules_mapping,
            ):
                return UnquantizedLinearMethod()
            if current_omni_platform.is_npu():
                if self.is_checkpoint_mxfp4_serialized:
                    return NPUMxfp4LinearMethod(self)
                return NPUMxfp4OnlineLinearMethod(self)
            raise NotImplementedError(
                "DiffusionMXFP4Config (W4A4 MXFP4) is currently only supported "
                "on NPU (Ascend) platforms. CUDA support is not yet implemented."
            )
        return None


# ---------------------------------------------------------------------------
# NPU MXFP4 single-scale offline method (pre-quantized checkpoint)
# ---------------------------------------------------------------------------


class NPUMxfp4LinearMethod(MXFPLinearMethodBase):
    """NPU W4A4 MXFP4 offline linear method for pre-quantized checkpoints.

    Weight canonical layout after process_weights_after_loading:
      weight      : (N, K) in float4_e2m1fn_x2  — NOT pre-transposed (FP4 packed)
      weight_scale: (N, S/2, 2) in float8_e8m0fnu — NOT pre-transposed

    Both are transposed inline in _quant_matmul, unlike MXFP8 which pre-transposes.
    NPUMxfp4OnlineLinearMethod normalizes to the same layout so apply() is shared.
    """

    def __init__(self, quant_config: DiffusionMXFP4Config) -> None:
        self.quant_config = quant_config
        self.out_dtype = torch.get_default_dtype()

    def create_weights(
        self,
        layer: torch.nn.Module,
        input_size_per_partition: int,
        output_partition_sizes: list[int],
        input_size: int,
        output_size: int,
        params_dtype: torch.dtype,
        **extra_weight_attrs,
    ) -> None:
        output_size_per_partition = sum(output_partition_sizes)
        weight_loader = extra_weight_attrs.get("weight_loader")

        layer.logical_widths = output_partition_sizes
        layer.input_size_per_partition = input_size_per_partition
        layer.output_size_per_partition = output_size_per_partition
        layer.orig_dtype = params_dtype
        layer.weight_block_size = None

        # BF16 placeholder; cast to float4_e2m1fn_x2 in process_weights.
        layer.register_parameter(
            "weight",
            ModelWeightParameter(
                data=torch.empty(output_size_per_partition, input_size_per_partition, dtype=params_dtype),
                input_dim=1,
                output_dim=0,
                weight_loader=weight_loader,
            ),
        )

        # Scale stored as uint8 in safetensors (float8_e8m0fnu is same bit width).
        # Using uint8 avoids a lossy float32 round-trip when loading the checkpoint.
        num_groups = (input_size_per_partition + 31) // 32
        layer.register_parameter(
            "weight_scale",
            ModelWeightParameter(
                data=torch.empty(output_size_per_partition, num_groups, dtype=torch.uint8),
                input_dim=1,
                output_dim=0,
                weight_loader=weight_loader,
            ),
        )

    def process_weights_after_loading(self, layer: Module) -> None:
        if getattr(layer, "_already_called_process_weights_after_loading", False):
            return

        import torch_npu

        # NPU: cast to float4_e2m1fn_x2. Weight stays (N, K) — no pre-transpose.
        w = layer.weight
        if w.dtype != torch_npu.float4_e2m1fn_x2:
            w = torch_npu.npu_dtype_cast(w.npu(), torch_npu.float4_e2m1fn_x2)

        # Scale: checkpoint stores uint8 bytes that ARE float8_e8m0fnu bits.
        # Only convert if neither uint8 nor the target NPU dtype already.
        # (N, K_groups) → (N, K_groups/2, 2). Not pre-transposed; done inline in _quant_matmul.
        s = layer.weight_scale.data
        if s.dtype not in (torch.uint8, torch_npu.float8_e8m0fnu):
            s = s.to(torch_npu.float8_e8m0fnu)
        N, K_groups = s.shape
        if K_groups % 2 == 1:
            s = torch.cat([s, torch.zeros(N, 1, dtype=s.dtype, device=s.device)], dim=1)
        s = s.reshape(N, -1, 2).contiguous()

        replace_parameter(layer, "weight", w)
        replace_parameter(layer, "weight_scale", s)
        layer._already_called_process_weights_after_loading = True

    # --- NPU MXFP4 ops — shared with online path via inheritance ---

    def _quantize_activation(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
        import torch_npu

        # No dst_type: npu_dynamic_mx_quant defaults to float4_e2m1fn_x2.
        return torch_npu.npu_dynamic_mx_quant(x)

    def _quant_matmul(
        self,
        x_q: torch.Tensor,
        x_scale: torch.Tensor,
        layer: torch.nn.Module,
        bias: torch.Tensor | None,
        ori_dtype: torch.dtype,
    ) -> torch.Tensor:
        import torch_npu

        if bias is not None and bias.dtype != torch.float32:
            bias = bias.to(torch.float32)
        # FP4 differences vs FP8:
        #   weight (N,K) transposed inline → (K,N); scale (N,S/2,2) transposed inline → (S/2,N,2).
        #   x1_dtype / x2_dtype required — FP4 dtype not inferred from tensor dtype.
        return torch_npu.npu_quant_matmul(
            x_q,
            layer.weight.transpose(0, 1),  # (K, N) inline
            layer.weight_scale.transpose(0, 1),  # (S/2, N, 2) inline
            scale_dtype=torch_npu.float8_e8m0fnu,
            x1_dtype=torch_npu.float4_e2m1fn_x2,
            x2_dtype=torch_npu.float4_e2m1fn_x2,
            pertoken_scale=x_scale,
            pertoken_scale_dtype=torch_npu.float8_e8m0fnu,
            bias=bias,
            output_dtype=ori_dtype,
            group_sizes=[1, 1, 32],
        )


# ---------------------------------------------------------------------------
# NPU MXFP4 single-scale online method (BF16 checkpoint → quantize at load time)
# ---------------------------------------------------------------------------


class NPUMxfp4OnlineLinearMethod(_LazyWeightMixin, NPUMxfp4LinearMethod):
    """NPU W4A4 MXFP4 online linear method.

    MRO: NPUMxfp4OnlineLinearMethod → _LazyWeightMixin → NPUMxfp4LinearMethod
         → MXFPLinearMethodBase → LinearMethodBase

      create_weights  : _LazyWeightMixin          (meta device + patched loader)
      process_weights : NPUMxfp4OnlineLinearMethod (BF16 → FP4 + normalize)
      apply / ops     : NPUMxfp4LinearMethod / MXFPLinearMethodBase (shared)
    """

    def process_weights_after_loading(self, layer: Module) -> None:
        if getattr(layer, "_already_called_process_weights_after_loading", False):
            return

        import torch_npu

        if layer.weight.device == torch.device("meta"):
            weight = ModelWeightParameter(
                data=torch.empty_like(layer.weight, device=layer._load_device),
                input_dim=1,
                output_dim=0,
                weight_loader=layer.weight.weight_loader,
            )
            _copy_missing_attrs(layer.weight, weight)
            layer.register_parameter("weight", weight)
            initialize_single_dummy_weight(layer.weight)

        # NPU: quantize BF16/FP16 (N, K) → FP4. No dst_type → float4_e2m1fn_x2.
        weight_fp4, weight_scale_raw = torch_npu.npu_dynamic_mx_quant(layer.weight)

        # Weight stays (N, K) — no pre-transpose for FP4 packed format.
        # Scale: (N, S) → (N, S/2, 2). Not pre-transposed; done inline in _quant_matmul.
        weight_scale = weight_scale_raw.reshape(weight_scale_raw.shape[0], -1, 2).contiguous()

        replace_parameter(layer, "weight", weight_fp4)
        replace_parameter(layer, "weight_scale", weight_scale)
        layer._already_called_process_weights_after_loading = True


# ---------------------------------------------------------------------------
# NPU MXFP4 dual-scale offline method (W4A4_MXFP4_DUALSCALE checkpoint)
# ---------------------------------------------------------------------------


class NPUMxfp4DualScaleLinearMethod(MXFPLinearMethodBase):
    """NPU W4A4 MXFP4 dual-scale offline method for pre-quantized checkpoints.

    Checkpoint tensors and their canonical post-load shapes:
      weight            : (N, K) float8_e4m3fn       – FP4 packed (2 values per byte)
      weight_scale      : (N, K//32) uint8  – fine scale (float8_e8m0fnu bits); reshaped to (N, K//64, 2)
      weight_dual_scale : (N, K//512, 1) float32 – coarse scale; transposed to (K//512, N)
      mul_scale         : (K,) float32      – per-input-channel activation pre-scale (from calibration)

    Forward pass:
      x_q, l0, l1 = npu_dynamic_dual_level_mx_quant(x, smooth_scale=mul_scale)
      out          = npu_dual_level_quant_matmul(x_q, weight, l0, weight_dual_scale, l1, weight_scale)

    Reference: MindIE-SD W4A4MXFP4DualQuantLinear.
    """

    def __init__(self, quant_config: Any) -> None:
        self.quant_config = quant_config
        self.out_dtype = torch.get_default_dtype()

    def create_weights(
        self,
        layer: torch.nn.Module,
        input_size_per_partition: int,
        output_partition_sizes: list[int],
        input_size: int,
        output_size: int,
        params_dtype: torch.dtype,
        **extra_weight_attrs,
    ) -> None:
        output_size_per_partition = sum(output_partition_sizes)
        weight_loader = extra_weight_attrs.get("weight_loader")

        layer.logical_widths = output_partition_sizes
        layer.input_size_per_partition = input_size_per_partition
        layer.output_size_per_partition = output_size_per_partition
        layer.orig_dtype = params_dtype
        layer.weight_block_size = None

        # FP4 packed: 2 values per float8_e4m3fn byte → checkpoint stores as float8_e4m3fn; register as BF16
        layer.register_parameter(
            "weight",
            ModelWeightParameter(
                data=torch.empty(output_size_per_partition, input_size_per_partition, dtype=params_dtype),
                input_dim=1,
                output_dim=0,
                weight_loader=weight_loader,
            ),
        )

        # Fine scale: one uint8 exponent (float8_e8m0fnu bit pattern) per group of 32 K elements.
        # input_dim=1: RowParallelLinear weight_loader shards along the K-group dimension (dim 1)
        # so each rank receives only its (K/TP)//32 groups; ColumnParallelLinear only uses
        # output_dim=0 for sharding and leaves dim 1 intact.
        num_groups_fine = (input_size_per_partition + 31) // 32
        layer.register_parameter(
            "weight_scale",
            ModelWeightParameter(
                data=torch.empty(output_size_per_partition, num_groups_fine, dtype=torch.uint8),
                input_dim=1,
                output_dim=0,
                weight_loader=weight_loader,
            ),
        )

        # Coarse scale: one float32 per group of 512 K elements.
        # Shape (N, K_coarse, 1) matches checkpoint layout exactly, avoiding the
        # shape-mismatch assert in linear.py:1344.
        # input_dim=1: same TP sharding rationale as weight_scale above.
        num_groups_coarse = (input_size_per_partition + 511) // 512
        layer.register_parameter(
            "weight_dual_scale",
            ModelWeightParameter(
                data=torch.empty(output_size_per_partition, num_groups_coarse, 1, dtype=torch.float32),
                input_dim=1,
                output_dim=0,
                weight_loader=weight_loader,
            ),
        )

        # mul_scale is a float32 calibration tensor; register as float32
        # to avoid precision loss from an implicit BF16 cast during weight loading.
        # input_dim=0: RowParallelLinear shards the 1-D per-input-channel tensor along dim 0
        # so each rank receives only its K/TP channels; ColumnParallelLinear leaves it intact.
        layer.register_parameter(
            "mul_scale",
            ModelWeightParameter(
                data=torch.empty(input_size_per_partition, dtype=torch.float32),
                input_dim=0,
                output_dim=None,
                weight_loader=weight_loader,
            ),
        )
        setattr(layer.mul_scale, "ignore_warning", True)

    def process_weights_after_loading(self, layer: Module) -> None:
        if getattr(layer, "_already_called_process_weights_after_loading", False):
            return

        import torch_npu

        # float8_e4m3fn (FP4 packed) → float4_e2m1fn_x2 → NZ hardware format (format ID 29).
        # NZ layout is required by npu_dual_level_quant_matmul for FP4 weight matrices.
        w = torch_npu.npu_dtype_cast(layer.weight.data.npu(), torch_npu.float4_e2m1fn_x2)
        w = torch_npu.npu_format_cast(w.view(torch.int8), 29, customize_dtype=torch.int8)

        # Fine scale: (N, K//32) uint8 → cast to float8_e8m0fnu → (N, K//64, 2).
        # The cast reinterprets the stored uint8 bit-patterns as float8_e8m0fnu exponents,
        # which is required for npu_dual_level_quant_matmul to compute correct scale values.
        s = layer.weight_scale.data.npu()
        s = s.reshape(s.shape[0], -1, 2).contiguous()

        # Coarse scale: (N, K//512, 1) → squeeze → (N, K//512) → transpose → (K//512, N).
        ds = layer.weight_dual_scale.data.squeeze(-1).transpose(0, 1).npu().contiguous()

        ms = layer.mul_scale.to(torch.bfloat16).data.view(-1).npu().contiguous()

        replace_parameter(layer, "weight", w)
        replace_parameter(layer, "weight_scale", s)
        replace_parameter(layer, "weight_dual_scale", ds)
        replace_parameter(layer, "mul_scale", ms)
        layer._already_called_process_weights_after_loading = True

    def _apply_inner(
        self,
        layer: torch.nn.Module,
        x: torch.Tensor,
        bias: torch.Tensor | None,
        ori_dtype: torch.dtype,
    ) -> torch.Tensor:
        """Dual-scale inner loop: dtype guard → 3-tuple quantize (mul_scale as smooth) → matmul.

        Overrides the default single-scale _apply_inner from MXFPLinearMethodBase.
        apply() in the base class handles reshape/unreshape; this method is not
        responsible for that.
        """
        if ori_dtype not in (torch.bfloat16, torch.float16):
            x = x.to(torch.bfloat16)
        x_q, l0_scale, l1_scale = self._quantize_activation(x, layer.mul_scale)
        return self._quant_matmul(x_q, l0_scale, l1_scale, layer, bias, ori_dtype)

    def _quantize_activation(  # type: ignore[override]
        self,
        x: torch.Tensor,
        smooth_scale: torch.Tensor,
    ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
        import torch_npu

        return torch_npu.npu_dynamic_dual_level_mx_quant(x, smooth_scale=smooth_scale)

    def _quant_matmul(  # type: ignore[override]
        self,
        x_q: torch.Tensor,
        l0_scale: torch.Tensor,
        l1_scale: torch.Tensor,
        layer: torch.nn.Module,
        bias: torch.Tensor | None,
        ori_dtype: torch.dtype,
    ) -> torch.Tensor:
        import torch_npu

        if bias is not None and bias.dtype != torch.float32:
            bias = bias.to(torch.float32)
        # weight_scale is (N, K//64, 2) float8_e8m0fnu — operator expects output-major layout.
        # weight_dual_scale is (K//512, N) float32 — transposed to K-major in process_weights.
        return torch_npu.npu_dual_level_quant_matmul(
            x_q,
            layer.weight,
            l0_scale,
            layer.weight_dual_scale,
            l1_scale,
            layer.weight_scale,
            bias=bias,
            output_dtype=ori_dtype,
        )


# ---------------------------------------------------------------------------
# NPU MXFP4 dual-scale online method (BF16 checkpoint → quantize at load time)
# ---------------------------------------------------------------------------


class NPUMxfp4DualScaleOnlineLinearMethod(_LazyWeightMixin, NPUMxfp4DualScaleLinearMethod):
    """NPU W4A4 MXFP4 dual-scale online method: quantises BF16 weights at load time.

    MRO: NPUMxfp4DualScaleOnlineLinearMethod → _LazyWeightMixin
         → NPUMxfp4DualScaleLinearMethod → MXFPLinearMethodBase

      create_weights        : _LazyWeightMixin              (meta device + patched loader)
      process_weights       : NPUMxfp4DualScaleOnlineLinearMethod (BF16 → FP4 + dual scales)
      apply / _quant_matmul : NPUMxfp4DualScaleLinearMethod (shared with offline path)
    """

    def process_weights_after_loading(self, layer: Module) -> None:
        if getattr(layer, "_already_called_process_weights_after_loading", False):
            return

        import torch_npu

        if layer.weight.device == torch.device("meta"):
            weight = ModelWeightParameter(
                data=torch.empty_like(layer.weight, device=layer._load_device),
                input_dim=1,
                output_dim=0,
                weight_loader=layer.weight.weight_loader,
            )
            _copy_missing_attrs(layer.weight, weight)
            layer.register_parameter("weight", weight)
            initialize_single_dummy_weight(layer.weight)

        # Quantize BF16 weight → FP4 + dual-level scales (no smooth pre-scale for online).
        # Returns: (weight_fp4, l0_scale[coarse per-512], l1_scale[fine per-32])
        weight_fp4, w_l0_scale, w_l1_scale = torch_npu.npu_dynamic_dual_level_mx_quant(
            layer.weight.data.npu(), smooth_scale=None
        )

        # NZ hardware format for the FP4 weight (same as offline path).
        w = torch_npu.npu_format_cast(weight_fp4.view(torch.int8), 29, customize_dtype=torch.int8)

        # Fine scale (l1): (N, K//32) → (N, K//64, 2). Dtype from op output (float8_e8m0fnu).
        s = w_l1_scale.reshape(w_l1_scale.shape[0], -1, 2).contiguous()

        # Coarse scale (l0): (N, K_coarse) → (K_coarse, N). Dtype from op output.
        ds = w_l0_scale.reshape(w_l0_scale.shape[0], -1).transpose(0, 1).contiguous()

        # No calibration available: identity pre-scale (no smooth quantization effect).
        ms = torch.ones(layer.input_size_per_partition, dtype=torch.bfloat16, device="npu")

        replace_parameter(layer, "weight", w)
        replace_parameter(layer, "weight_scale", s)
        replace_parameter(layer, "weight_dual_scale", ds)
        replace_parameter(layer, "mul_scale", ms)
        layer._already_called_process_weights_after_loading = True


# ---------------------------------------------------------------------------
# Block-index helper (shared by DiffusionMXFP4DualScaleMixedConfig)
# ---------------------------------------------------------------------------

_BLOCK_IDX_RE = re.compile(r"^blocks\.(\d+)\.")


def _parse_block_idx(prefix: str) -> int | None:
    """Extract block index from prefix like 'blocks.5.attn1.to_q'."""
    m = _BLOCK_IDX_RE.match(prefix)
    return int(m.group(1)) if m else None


# ---------------------------------------------------------------------------
# Config: MXFP4 DualScale + per-layer BF16 fallback
# ---------------------------------------------------------------------------


class DiffusionMXFP4DualScaleMixedConfig(QuantizationConfig):
    """W4A4 MXFP4 DualScale with per-layer BF16 fallback for diffusion transformers.

    Sensitive layers fall back to BF16 (original weights) while all other linear
    layers use W4A4 MXFP4 DualScale.  BF16 fallback layers may be interleaved
    anywhere in the transformer.

    Offline mode (is_checkpoint_serialized=True):
        Layers whose prefix appears in ignored_layers → UnquantizedLinearMethod (BF16)
        All other linear layers → NPUMxfp4DualScaleLinearMethod

        ignored_layers is injected into transformer/config.json by the merge script
        and contains the prefixes of all non-MXFP4 linear layers.

    Online mode (is_checkpoint_serialized=False):
        Layer routing applies two rules in priority order:
          1. ignored_layers (explicit per-layer BF16 override, user-supplied) → BF16
          2. Blocks 0 .. num_bf16_fallback_layers-1 (coarse leading-block rule) → BF16
          3. All other linear layers → NPUMxfp4DualScaleOnlineLinearMethod

        num_bf16_fallback_layers defaults to 5 when not specified.
        Set ignored_layers to pin arbitrary interleaved layers to BF16 without
        needing an offline checkpoint (useful for accuracy debugging).
        Layers outside "blocks.N.*" (condition_embedder etc.) always use online MXFP4
        unless they appear in ignored_layers.

    Config injected by merge_mxfp4_dualscale_checkpoint.py:
        {
            "quant_method": "mxfp4_dualscale",
            "is_checkpoint_serialized": true,
            "ignored_layers": ["blocks.0.attn1.to_q", ...]
        }
    """

    def __init__(
        self,
        is_checkpoint_serialized: bool = False,
        ignored_layers: list[str] | None = None,
        num_bf16_fallback_layers: int = 5,
    ) -> None:
        super().__init__()
        self.is_checkpoint_serialized = is_checkpoint_serialized
        self.ignored_layers = ignored_layers or []
        self.num_bf16_fallback_layers = num_bf16_fallback_layers

    @classmethod
    def get_name(cls) -> QuantizationMethods:
        return "mxfp4_dualscale"

    @classmethod
    def get_supported_act_dtypes(cls) -> list[torch.dtype]:
        return [torch.bfloat16, torch.float16]

    @classmethod
    def get_min_capability(cls) -> int:
        return 80

    @classmethod
    def get_config_filenames(cls) -> list[str]:
        return []

    def apply_vllm_mapper(self, hf_to_vllm_mapper: WeightsMapper) -> None:
        if self.ignored_layers:
            self.ignored_layers = hf_to_vllm_mapper.apply_list(self.ignored_layers)

    @classmethod
    def from_config(cls, config: dict[str, Any]) -> DiffusionMXFP4DualScaleMixedConfig:
        is_serialized = cls.get_from_keys_or(config, ["is_checkpoint_serialized"], False)
        ignored_layers = cls.get_from_keys_or(config, ["ignored_layers"], None)
        if not ignored_layers:
            ignored_layers = cls.get_from_keys_or(config, ["modules_to_not_convert"], None)
        num_bf16_fallback_layers = cls.get_from_keys_or(config, ["num_bf16_fallback_layers"], 5)
        return cls(
            is_checkpoint_serialized=is_serialized,
            ignored_layers=ignored_layers,
            num_bf16_fallback_layers=num_bf16_fallback_layers,
        )

    def get_quant_method(
        self,
        layer: torch.nn.Module,
        prefix: str,
    ) -> QuantizeMethodBase | None:
        if not isinstance(layer, LinearBase):
            return None

        if self.is_checkpoint_serialized:
            # Offline: ignored_layers lists interleaved BF16 fallback layer prefixes.
            if is_layer_skipped(
                prefix=prefix,
                ignored_layers=self.ignored_layers,
                fused_mapping=self.packed_modules_mapping,
            ):
                return UnquantizedLinearMethod()
            if not current_omni_platform.is_npu():
                raise NotImplementedError(
                    "DiffusionMXFP4DualScaleMixedConfig is currently only supported on NPU (Ascend) platforms."
                )
            return NPUMxfp4DualScaleLinearMethod(self)

        # Online: explicit ignored_layers take priority (user-specified per-layer BF16 override),
        # then fall back to the coarse leading-block rule.
        if is_layer_skipped(
            prefix=prefix,
            ignored_layers=self.ignored_layers,
            fused_mapping=self.packed_modules_mapping,
        ):
            return UnquantizedLinearMethod()
        block_idx = _parse_block_idx(prefix)
        if block_idx is not None and block_idx < self.num_bf16_fallback_layers:
            return UnquantizedLinearMethod()

        if not current_omni_platform.is_npu():
            raise NotImplementedError(
                "DiffusionMXFP4DualScaleMixedConfig is currently only supported on NPU (Ascend) platforms."
            )
        return NPUMxfp4DualScaleOnlineLinearMethod(self)
