Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / components / quantization / ops / npu_fake_w4a8.py: 37%
70 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""NPU operators used by the fake-QAT W4A8 grouped-linear strategy.
17The native W4A8 strategy is intentionally not used here. Fake W4A8 keeps the
18master parameter in its source dtype, quantizes it to MXFP4 for every forward,
19decodes that result to an MXFP8 operand, and executes the regular all-MXFP8
20grouped-matmul contract. This mirrors the QAT path used by the original
21grouped W4A8 implementation while keeping the native Packed-MXFP4 storage
22and WeightNz adapter isolated in ``npu_w4a8.py``.
23"""
25from functools import lru_cache
26from types import ModuleType
27from typing import Callable, Optional
29import torch # pylint: disable=forbidden-backend-import
31from hyper_parallel.components.quantization.config import (
32 GMM_SPLIT_SCALE,
33 GROUP_LIST_OFFSETS,
34 MX_BLOCK_SIZE_32,
35)
36from hyper_parallel.components.quantization.ops.npu_w4a8 import W4A8CapabilityError
39_A5_DEVICE_MARKERS = ("Ascend950", "Ascend910_95")
42class FakeW4A8CapabilityError(W4A8CapabilityError):
43 """Report an unavailable fake-W4A8 NPU operator contract."""
46class FakeW4A8NpuOps:
47 """Adapt MX quantization, MXFP4 conversion, and MXFP8 grouped matmul."""
49 def __init__(self, torch_npu_module: ModuleType) -> None:
50 """Validate and retain the active torch_npu operator module."""
51 required = (
52 "npu_dynamic_mx_quant",
53 "npu_grouped_dynamic_mx_quant",
54 "npu_grouped_matmul",
55 "float4_e2m1fn_x2",
56 "float8_e4m3fn",
57 "float8_e8m0fnu",
58 )
59 missing = [name for name in required if not hasattr(torch_npu_module, name)]
60 if missing:
61 version = getattr(torch_npu_module, "__version__", "unknown")
62 raise FakeW4A8CapabilityError(
63 "The active torch_npu stack does not provide fake W4A8 operators; "
64 f"missing {missing}, torch_npu={version}."
65 )
66 npu_handle = getattr(torch_npu_module, "npu", None)
67 try:
68 device_name = (
69 npu_handle.get_device_name()
70 if npu_handle is not None and hasattr(npu_handle, "get_device_name")
71 else ""
72 )
73 except (RuntimeError, TypeError, OSError):
74 device_name = ""
75 if not any(marker in device_name for marker in _A5_DEVICE_MARKERS):
76 raise FakeW4A8CapabilityError(
77 "Fake W4A8 requires an Ascend 950/A5 runtime, "
78 f"got {device_name or 'unknown'}."
79 )
80 self._torch_npu = torch_npu_module
82 @property
83 def activation_dtype(self) -> torch.dtype:
84 """Return the MXFP8 activation dtype exposed by torch_npu."""
85 return self._torch_npu.float8_e4m3fn
87 @property
88 def weight_dtype(self) -> torch.dtype:
89 """Return the packed MXFP4 weight dtype exposed by torch_npu."""
90 return self._torch_npu.float4_e2m1fn_x2
92 @property
93 def e8m0_dtype(self) -> torch.dtype:
94 """Return the E8M0 scale dtype used by the grouped-matmul API."""
95 return self._torch_npu.float8_e8m0fnu
97 def dynamic_mx_quant(
98 self,
99 tensor: torch.Tensor,
100 *,
101 axis: int,
102 block_size: int,
103 quant_dtype: torch.dtype,
104 round_mode: Optional[str] = None,
105 scale_alg: Optional[int] = None,
106 ) -> tuple[torch.Tensor, torch.Tensor]:
107 """Quantize a tensor with the requested dynamic-MX orientation."""
108 kwargs = {"axis": axis, "dst_type": quant_dtype, "block_size": block_size}
109 if round_mode is not None:
110 kwargs["round_mode"] = round_mode
111 if scale_alg is not None:
112 kwargs["scale_alg"] = scale_alg
113 return self._torch_npu.npu_dynamic_mx_quant(tensor, **kwargs)
115 def convert_packed_mxfp4_to_block_mxfp8(
116 self,
117 packed_mxfp4: torch.Tensor,
118 mxfp4_scale: torch.Tensor,
119 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
120 """Run CANN's converter and return MXFP8 data with two axis scales."""
122 return _get_cann_mx_to_block_mx_quant_op()(
123 packed_mxfp4,
124 mxfp4_scale,
125 dst_type=self.activation_dtype,
126 x_type=self.weight_dtype,
127 )
129 def grouped_dynamic_mx_quant(
130 self,
131 tensor: torch.Tensor,
132 group_list: torch.Tensor,
133 *,
134 block_size: int,
135 quant_dtype: torch.dtype,
136 ) -> tuple[torch.Tensor, torch.Tensor]:
137 """Quantize grouped activation ranges with block-32 MX metadata."""
138 if block_size != MX_BLOCK_SIZE_32:
139 raise ValueError("npu_grouped_dynamic_mx_quant is only valid for block_size=32.")
140 return self._torch_npu.npu_grouped_dynamic_mx_quant(
141 tensor,
142 group_list.to(torch.int32),
143 round_mode="rint",
144 dst_type=quant_dtype,
145 blocksize=MX_BLOCK_SIZE_32,
146 )
148 def grouped_matmul(
149 self,
150 left: torch.Tensor,
151 right: torch.Tensor,
152 right_scale: torch.Tensor,
153 *,
154 left_scale: torch.Tensor,
155 group_list: torch.Tensor,
156 group_type: int,
157 block_size: int,
158 output_dtype: torch.dtype,
159 ) -> torch.Tensor:
160 """Execute one grouped matrix multiplication with fake-MX operands."""
161 if block_size != MX_BLOCK_SIZE_32:
162 raise ValueError("Fake W4A8 supports only block_size=32.")
163 kwargs = {
164 "bias": [],
165 "scale": [right_scale],
166 "per_token_scale": [left_scale],
167 "group_list": group_list.to(torch.int64),
168 "group_type": group_type,
169 "output_dtype": output_dtype,
170 "group_list_type": GROUP_LIST_OFFSETS,
171 "split_item": GMM_SPLIT_SCALE,
172 "scale_dtype": self.e8m0_dtype,
173 "per_token_scale_dtype": self.e8m0_dtype,
174 }
175 return self._torch_npu.npu_grouped_matmul([left], [right], **kwargs)[0]
178@lru_cache(maxsize=1)
179def _get_fake_w4a8_npu_ops() -> FakeW4A8NpuOps:
180 """Resolve the process-local fake-W4A8 NPU adapter."""
182 try:
183 import torch_npu # pylint: disable=C0415
184 except ImportError as exc:
185 raise FakeW4A8CapabilityError(
186 "Fake W4A8 requires torch_npu, but it is not importable."
187 ) from exc
188 return FakeW4A8NpuOps(torch_npu)
191@lru_cache(maxsize=1)
192def _get_cann_mx_to_block_mx_quant_op() -> Callable[
193 ..., tuple[torch.Tensor, torch.Tensor, torch.Tensor]
194]:
195 """Load the CANN operator behind the project-level conversion method."""
197 try:
198 from cann_ops_nn import ( # pylint: disable=C0415
199 mx_to_block_mx_quant as cann_mx_to_block_mx_quant,
200 )
201 except (ImportError, OSError) as exc:
202 raise FakeW4A8CapabilityError(
203 "Fake W4A8 block_size=32 requires CANN 9.2 with "
204 "cann_ops_nn.mx_to_block_mx_quant importable."
205 ) from exc
206 return cann_mx_to_block_mx_quant
209def validate_fake_w4a8_gmm_runtime(block_size: int) -> None:
210 """Validate fake-W4A8 operators and the block-32 CANN converter."""
212 _get_fake_w4a8_npu_ops()
213 if block_size != MX_BLOCK_SIZE_32:
214 raise ValueError("Fake W4A8 supports only block_size=32.")
215 _get_cann_mx_to_block_mx_quant_op()
218__all__ = [
219 "FakeW4A8CapabilityError",
220 "FakeW4A8NpuOps",
221 "validate_fake_w4a8_gmm_runtime",
222]