nvmath-python 1.0.0__cp314-cp314t-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nvmath/__init__.pxd +0 -0
- nvmath/__init__.py +45 -0
- nvmath/_internal/__init__.py +0 -0
- nvmath/_internal/attribute_ifc_factory.py +330 -0
- nvmath/_internal/layout.py +70 -0
- nvmath/_internal/templates.py +130 -0
- nvmath/_internal/threadsafe.py +106 -0
- nvmath/_internal/utils.py +43 -0
- nvmath/_internal/workspace.py +490 -0
- nvmath/_utils.py +147 -0
- nvmath/bindings/__init__.py +60 -0
- nvmath/bindings/_internal/__init__.pxd +0 -0
- nvmath/bindings/_internal/__init__.py +0 -0
- nvmath/bindings/_internal/common_types.pxd +31 -0
- nvmath/bindings/_internal/cublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cublas.pxd +530 -0
- nvmath/bindings/_internal/cublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cublasLt.pxd +59 -0
- nvmath/bindings/_internal/cublasMp.pxd +52 -0
- nvmath/bindings/_internal/cudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cudss.pxd +54 -0
- nvmath/bindings/_internal/cufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cufft.pxd +70 -0
- nvmath/bindings/_internal/cufftMp.pxd +77 -0
- nvmath/bindings/_internal/curand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/curand.pxd +42 -0
- nvmath/bindings/_internal/cusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolver.pxd +15 -0
- nvmath/bindings/_internal/cusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolverDn.pxd +406 -0
- nvmath/bindings/_internal/cusolverMp.pxd +71 -0
- nvmath/bindings/_internal/cusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusolverSp.pxd +75 -0
- nvmath/bindings/_internal/cusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusparse.pxd +471 -0
- nvmath/bindings/_internal/cusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cusparseLt.pxd +48 -0
- nvmath/bindings/_internal/cutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/cutensor.pxd +58 -0
- nvmath/bindings/_internal/mathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/mathdx.pxd +116 -0
- nvmath/bindings/_internal/nvshmem.pxd +29 -0
- nvmath/bindings/_internal/utils.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/_internal/utils.pxd +174 -0
- nvmath/bindings/_internal/utils.pyi +10 -0
- nvmath/bindings/cublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cublas.pxd +558 -0
- nvmath/bindings/cublas.pyi +812 -0
- nvmath/bindings/cublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cublasLt.pxd +109 -0
- nvmath/bindings/cublasLt.pyi +1461 -0
- nvmath/bindings/cublasMp.pxd +85 -0
- nvmath/bindings/cublasMp.pyi +267 -0
- nvmath/bindings/cudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cudss.pxd +98 -0
- nvmath/bindings/cudss.pyi +443 -0
- nvmath/bindings/cufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cufft.pxd +118 -0
- nvmath/bindings/cufft.pyi +301 -0
- nvmath/bindings/cufftMp.pxd +124 -0
- nvmath/bindings/cufftMp.pyi +326 -0
- nvmath/bindings/curand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/curand.pxd +71 -0
- nvmath/bindings/curand.pyi +189 -0
- nvmath/bindings/cusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolver.pxd +62 -0
- nvmath/bindings/cusolver.pyi +320 -0
- nvmath/bindings/cusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolverDn.pxd +430 -0
- nvmath/bindings/cusolverDn.pyi +422 -0
- nvmath/bindings/cusolverMp.pxd +98 -0
- nvmath/bindings/cusolverMp.pyi +114 -0
- nvmath/bindings/cusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusolverSp.pxd +95 -0
- nvmath/bindings/cusolverSp.pyi +70 -0
- nvmath/bindings/cusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusparse.pxd +546 -0
- nvmath/bindings/cusparse.pyi +1017 -0
- nvmath/bindings/cusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cusparseLt.pxd +99 -0
- nvmath/bindings/cusparseLt.pyi +252 -0
- nvmath/bindings/cutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cutensor.pxd +98 -0
- nvmath/bindings/cutensor.pyi +324 -0
- nvmath/bindings/cycublas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycublas.pxd +664 -0
- nvmath/bindings/cycublasLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycublasLt.pxd +1045 -0
- nvmath/bindings/cycublasMp.pxd +171 -0
- nvmath/bindings/cycudss.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycudss.pxd +277 -0
- nvmath/bindings/cycufft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycufft.pxd +333 -0
- nvmath/bindings/cycufftMp.pxd +342 -0
- nvmath/bindings/cycurand.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycurand.pxd +141 -0
- nvmath/bindings/cycusolver.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolver.pxd +137 -0
- nvmath/bindings/cycusolverDn.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolverDn.pxd +443 -0
- nvmath/bindings/cycusolverMp.pxd +107 -0
- nvmath/bindings/cycusolverSp.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusolverSp.pxd +93 -0
- nvmath/bindings/cycusparse.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusparse.pxd +679 -0
- nvmath/bindings/cycusparseLt.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycusparseLt.pxd +135 -0
- nvmath/bindings/cycutensor.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cycutensor.pxd +189 -0
- nvmath/bindings/cymathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/cymathdx.pxd +552 -0
- nvmath/bindings/cynvshmem.pxd +118 -0
- nvmath/bindings/mathdx.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/mathdx.pxd +182 -0
- nvmath/bindings/mathdx.pyi +1562 -0
- nvmath/bindings/nvpl/__init__.pxd +0 -0
- nvmath/bindings/nvpl/__init__.py +13 -0
- nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
- nvmath/bindings/nvpl/_internal/__init__.py +0 -0
- nvmath/bindings/nvpl/_internal/blas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/_internal/blas.pxd +237 -0
- nvmath/bindings/nvpl/_internal/fft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
- nvmath/bindings/nvpl/blas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/blas.pxd +131 -0
- nvmath/bindings/nvpl/blas.pyi +168 -0
- nvmath/bindings/nvpl/cyblas.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/cyblas.pxd +280 -0
- nvmath/bindings/nvpl/cyfft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/cyfft.pxd +93 -0
- nvmath/bindings/nvpl/fft.cp314t-win_amd64.pyd +0 -0
- nvmath/bindings/nvpl/fft.pxd +100 -0
- nvmath/bindings/nvpl/fft.pyi +168 -0
- nvmath/bindings/nvshmem.pxd +54 -0
- nvmath/bindings/nvshmem.pyi +191 -0
- nvmath/device/__init__.py +38 -0
- nvmath/device/_deprecated.py +33 -0
- nvmath/device/common.py +315 -0
- nvmath/device/common_backend.py +131 -0
- nvmath/device/common_cuda.py +201 -0
- nvmath/device/common_numba.py +300 -0
- nvmath/device/common_numba_cuda_mlir.py +202 -0
- nvmath/device/common_opaque_tensor.py +201 -0
- nvmath/device/cublasdx.py +1606 -0
- nvmath/device/cublasdx_backend.py +860 -0
- nvmath/device/cublasdx_numba.py +1534 -0
- nvmath/device/cublasdx_numba_cuda_mlir.py +208 -0
- nvmath/device/cufftdx.py +373 -0
- nvmath/device/cufftdx_backend.py +220 -0
- nvmath/device/cufftdx_numba.py +140 -0
- nvmath/device/cufftdx_numba_cuda_mlir.py +79 -0
- nvmath/device/curand_kernel.py +9147 -0
- nvmath/device/cusolverdx.py +2708 -0
- nvmath/device/cusolverdx_backend.py +440 -0
- nvmath/device/cusolverdx_numba.py +567 -0
- nvmath/device/cusolverdx_numba_cuda_mlir.py +604 -0
- nvmath/device/cusolverdx_overload_backend.py +1029 -0
- nvmath/device/llvm_array.py +29 -0
- nvmath/device/random.py +441 -0
- nvmath/device/random_helpers.py +23 -0
- nvmath/device/random_states.py +187 -0
- nvmath/device/types.py +138 -0
- nvmath/device/vector_types_numba.py +259 -0
- nvmath/distributed/__init__.py +200 -0
- nvmath/distributed/_internal/__init__.py +0 -0
- nvmath/distributed/_internal/nccl.py +86 -0
- nvmath/distributed/_internal/nvshmem.py +307 -0
- nvmath/distributed/_internal/symmetric_memory.py +35 -0
- nvmath/distributed/_internal/tensor_ifc.py +70 -0
- nvmath/distributed/_internal/tensor_ifc_cupy.py +68 -0
- nvmath/distributed/_internal/tensor_ifc_host_device.py +172 -0
- nvmath/distributed/_internal/tensor_ifc_numpy.py +46 -0
- nvmath/distributed/_internal/tensor_ifc_torch.py +162 -0
- nvmath/distributed/_internal/tensor_wrapper.py +81 -0
- nvmath/distributed/_utils.py +167 -0
- nvmath/distributed/distribution/__init__.py +30 -0
- nvmath/distributed/distribution/_configuration.py +39 -0
- nvmath/distributed/distribution/distributions.py +1024 -0
- nvmath/distributed/distribution/redistribute.py +1284 -0
- nvmath/distributed/fft/__init__.py +7 -0
- nvmath/distributed/fft/_configuration.py +82 -0
- nvmath/distributed/fft/fft.py +2742 -0
- nvmath/distributed/linalg/__init__.py +22 -0
- nvmath/distributed/linalg/_internal/__init__.py +3 -0
- nvmath/distributed/linalg/_internal/epilog_protocol.py +586 -0
- nvmath/distributed/linalg/_internal/matmul_desc_ifc.py +28 -0
- nvmath/distributed/linalg/advanced/__init__.py +8 -0
- nvmath/distributed/linalg/advanced/_configuration.py +171 -0
- nvmath/distributed/linalg/advanced/matmulmod.py +3573 -0
- nvmath/distributed/linalg/generic/__init__.py +8 -0
- nvmath/distributed/linalg/generic/_caching.py +66 -0
- nvmath/distributed/linalg/generic/_configuration.py +61 -0
- nvmath/distributed/linalg/generic/_factorization.py +172 -0
- nvmath/distributed/linalg/generic/_initialization.py +966 -0
- nvmath/distributed/linalg/generic/_problem_spec.py +511 -0
- nvmath/distributed/linalg/generic/solvermod.py +1368 -0
- nvmath/distributed/process_group.py +408 -0
- nvmath/fft/__init__.py +7 -0
- nvmath/fft/_configuration.py +189 -0
- nvmath/fft/_exec_utils.py +82 -0
- nvmath/fft/_helpers.py +237 -0
- nvmath/fft/fft.py +3122 -0
- nvmath/internal/__init__.pxd +3 -0
- nvmath/internal/__init__.py +10 -0
- nvmath/internal/_bindings.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/_bindings.pxd +18 -0
- nvmath/internal/_device_utils.py +45 -0
- nvmath/internal/_layout/__init__.pxd +3 -0
- nvmath/internal/_layout/__init__.py +7 -0
- nvmath/internal/_layout/_layout.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/_layout/_layout.pxd +1303 -0
- nvmath/internal/_layout/_layout.pyi +1145 -0
- nvmath/internal/enum_utils.py +142 -0
- nvmath/internal/formatters.py +87 -0
- nvmath/internal/mem_limit.py +51 -0
- nvmath/internal/memory.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/memory.pxd +13 -0
- nvmath/internal/memory.pyi +50 -0
- nvmath/internal/ndbuffer/__init__.pxd +3 -0
- nvmath/internal/ndbuffer/__init__.py +9 -0
- nvmath/internal/ndbuffer/_copy_kernel.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_copy_kernel.pxd +10 -0
- nvmath/internal/ndbuffer/_jit.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_jit.pxd +7 -0
- nvmath/internal/ndbuffer/_ndbuffer.cp314t-win_amd64.pyd +0 -0
- nvmath/internal/ndbuffer/_ndbuffer.pxd +40 -0
- nvmath/internal/ndbuffer/_ndbuffer.pyi +463 -0
- nvmath/internal/ndbuffer/copy_kernel/args.h +34 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/array_view.h +52 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/elementwise.h +68 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/grid_indexer.h +69 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/transposed.h +242 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/type_utils.h +39 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/utils.h +132 -0
- nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/vec.h +159 -0
- nvmath/internal/ndbuffer/copy_kernel/elementwise.h +53 -0
- nvmath/internal/ndbuffer/copy_kernel/transposed.h +58 -0
- nvmath/internal/package_ifc.py +168 -0
- nvmath/internal/package_ifc_cuda.py +57 -0
- nvmath/internal/package_ifc_cupy.py +67 -0
- nvmath/internal/package_ifc_torch.py +69 -0
- nvmath/internal/package_wrapper.py +14 -0
- nvmath/internal/tensor_ifc.py +179 -0
- nvmath/internal/tensor_ifc_cupy.py +234 -0
- nvmath/internal/tensor_ifc_ndbuffer.py +147 -0
- nvmath/internal/tensor_ifc_numpy.py +184 -0
- nvmath/internal/tensor_ifc_torch.py +178 -0
- nvmath/internal/tensor_wrapper.py +160 -0
- nvmath/internal/typemaps.py +113 -0
- nvmath/internal/utils.py +805 -0
- nvmath/linalg/__init__.py +56 -0
- nvmath/linalg/_internal/__init__.py +3 -0
- nvmath/linalg/_internal/algo_cap_ifc.py +82 -0
- nvmath/linalg/_internal/algo_config_ifc.py +43 -0
- nvmath/linalg/_internal/batch.py +234 -0
- nvmath/linalg/_internal/enum_to_tuples.py +64 -0
- nvmath/linalg/_internal/epilog_protocol.py +766 -0
- nvmath/linalg/_internal/layout.py +624 -0
- nvmath/linalg/_internal/matmul_desc_ifc.py +28 -0
- nvmath/linalg/_internal/matmul_pref_ifc.py +27 -0
- nvmath/linalg/_internal/matrix_layout_ifc.py +26 -0
- nvmath/linalg/_internal/solver_utils.py +432 -0
- nvmath/linalg/_internal/typemaps.py +144 -0
- nvmath/linalg/_internal/utils.py +157 -0
- nvmath/linalg/advanced/__init__.py +8 -0
- nvmath/linalg/advanced/_algorithmmod.py +170 -0
- nvmath/linalg/advanced/_configuration.py +351 -0
- nvmath/linalg/advanced/helpers/__init__.py +5 -0
- nvmath/linalg/advanced/helpers/matmul.py +1316 -0
- nvmath/linalg/advanced/matmulmod.py +3734 -0
- nvmath/linalg/generic/__init__.py +53 -0
- nvmath/linalg/generic/_configuration/__init__.py +39 -0
- nvmath/linalg/generic/_configuration/layout.py +263 -0
- nvmath/linalg/generic/_configuration/match.py +734 -0
- nvmath/linalg/generic/_configuration/qualifiers.py +493 -0
- nvmath/linalg/generic/_configuration/solver_configuration.py +59 -0
- nvmath/linalg/generic/_configuration/wrap.py +217 -0
- nvmath/linalg/generic/_dtype.py +15 -0
- nvmath/linalg/generic/matmulmod.py +2094 -0
- nvmath/linalg/generic/solvermod.py +1301 -0
- nvmath/memory.py +279 -0
- nvmath/sparse/__init__.py +38 -0
- nvmath/sparse/_internal/__init__.py +21 -0
- nvmath/sparse/_internal/common_utils.py +147 -0
- nvmath/sparse/_internal/cudss_config_ifc.py +702 -0
- nvmath/sparse/_internal/cudss_data_ifc.py +399 -0
- nvmath/sparse/_internal/cudss_utils.py +506 -0
- nvmath/sparse/_internal/cusparse_utils.py +382 -0
- nvmath/sparse/_internal/sparse_bsc_ifc.py +303 -0
- nvmath/sparse/_internal/sparse_bsr_ifc.py +305 -0
- nvmath/sparse/_internal/sparse_coo_ifc.py +256 -0
- nvmath/sparse/_internal/sparse_csc_ifc.py +268 -0
- nvmath/sparse/_internal/sparse_csr_ifc.py +288 -0
- nvmath/sparse/_internal/sparse_dia_ifc.py +242 -0
- nvmath/sparse/_internal/sparse_format_helpers.py +601 -0
- nvmath/sparse/_internal/sparse_tensor_ifc.py +133 -0
- nvmath/sparse/_internal/sparse_ust_ifc.py +141 -0
- nvmath/sparse/_internal/utils.py +56 -0
- nvmath/sparse/advanced/__init__.py +7 -0
- nvmath/sparse/advanced/_configuration.py +227 -0
- nvmath/sparse/advanced/direct_solver.py +2069 -0
- nvmath/sparse/generic/__init__.py +7 -0
- nvmath/sparse/generic/_configuration.py +129 -0
- nvmath/sparse/generic/_helpers.py +137 -0
- nvmath/sparse/generic/_thunks.py +21 -0
- nvmath/sparse/generic/matmulmod.py +2353 -0
- nvmath/sparse/ust/__init__.py +7 -0
- nvmath/sparse/ust/_converters.py +422 -0
- nvmath/sparse/ust/_cpp.py +28 -0
- nvmath/sparse/ust/_drawer.py +565 -0
- nvmath/sparse/ust/_emitter.py +1033 -0
- nvmath/sparse/ust/_jit.py +188 -0
- nvmath/sparse/ust/_kernel.py +282 -0
- nvmath/sparse/ust/_utils.py +149 -0
- nvmath/sparse/ust/interfaces/__init__.py +0 -0
- nvmath/sparse/ust/interfaces/torch_interface.py +476 -0
- nvmath/sparse/ust/tensor.py +1016 -0
- nvmath/sparse/ust/tensor_format.py +957 -0
- nvmath/tensor/__init__.py +6 -0
- nvmath/tensor/_configuration.py +120 -0
- nvmath/tensor/_internal/__init__.py +3 -0
- nvmath/tensor/_internal/cutensor_config_ifc.py +279 -0
- nvmath/tensor/_internal/cutensor_utils.py +230 -0
- nvmath/tensor/_internal/data.py +43 -0
- nvmath/tensor/_internal/einsum_parser.py +444 -0
- nvmath/tensor/_internal/typemaps.py +96 -0
- nvmath/tensor/contract.py +1900 -0
- nvmath_python-1.0.0.dist-info/METADATA +134 -0
- nvmath_python-1.0.0.dist-info/RECORD +332 -0
- nvmath_python-1.0.0.dist-info/WHEEL +5 -0
- nvmath_python-1.0.0.dist-info/licenses/LICENSE +177 -0
- nvmath_python-1.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. ALL RIGHTS RESERVED.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import TYPE_CHECKING, Literal
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from nvmath.internal.tensor_ifc import TensorHolder
|
|
18
|
+
from nvmath.internal.tensor_wrapper import wrap_operand
|
|
19
|
+
from nvmath.internal.utils import create_empty_tensor, get_or_create_stream, infer_object_package
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"BlockScalingFormat",
|
|
23
|
+
"create_mxfp8_scale",
|
|
24
|
+
"invert_mxfp8_scale",
|
|
25
|
+
"apply_mxfp8_scale",
|
|
26
|
+
"unpack_fp4",
|
|
27
|
+
"quantize_to_fp4",
|
|
28
|
+
"get_block_scale_offset",
|
|
29
|
+
"to_block_scale",
|
|
30
|
+
"expand_block_scale",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
# ====================================================
|
|
34
|
+
# helper functions for FP8 and MXFP8
|
|
35
|
+
# ====================================================
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _c_strides(operand_shape: tuple[int, ...]) -> tuple[int, ...]:
|
|
39
|
+
ndim = len(operand_shape)
|
|
40
|
+
strides = [0] * ndim
|
|
41
|
+
stride = 1
|
|
42
|
+
for i in range(ndim - 1, -1, -1):
|
|
43
|
+
strides[i] = stride
|
|
44
|
+
stride *= operand_shape[i]
|
|
45
|
+
return tuple(strides)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validate_operand_ndim_for_block_scaling(operand_shape: tuple[int, ...]):
|
|
49
|
+
ndim = len(operand_shape)
|
|
50
|
+
if ndim < 2:
|
|
51
|
+
raise ValueError(f"Operand shape must be at least 2D, got {ndim}D with shape {operand_shape}")
|
|
52
|
+
return ndim
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _infer_blocked_axis(operand: TensorHolder):
|
|
56
|
+
# In general, strides are not reliable if corresponding extent is 1,
|
|
57
|
+
# but we expect (and check) operand last two extents to be divisible by
|
|
58
|
+
# 128 or 64.
|
|
59
|
+
return -1 if operand.strides[-2] > operand.strides[-1] else -2
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class BlockScalingFormat(str, Enum):
|
|
63
|
+
"""
|
|
64
|
+
Block scaling format for microscaling data types.
|
|
65
|
+
|
|
66
|
+
Passed as the ``block_scaling_format`` argument to
|
|
67
|
+
:func:`get_block_scale_offset`, :func:`to_block_scale`, and
|
|
68
|
+
:func:`expand_block_scale`.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
NVFP4 = "NVFP4"
|
|
72
|
+
MXFP8 = "MXFP8"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Per-format properties for microscaling (block-scaled) data types.
|
|
76
|
+
# Used by validation helpers and expand_block_scale to dispatch on
|
|
77
|
+
# block size, allowed dtypes, and scale interpretation without
|
|
78
|
+
# per-format if/elif branches. To add a new format, add an entry here.
|
|
79
|
+
_MICROSCALING_FORMAT_PROPERTIES: dict[BlockScalingFormat, dict] = {
|
|
80
|
+
BlockScalingFormat.NVFP4: {
|
|
81
|
+
"num_scalars_in_block": 16,
|
|
82
|
+
"operand_dtypes": ("float4_e2m1fn_x2",),
|
|
83
|
+
# NVFP4 scales are float8_e4m3fn values. uint8 is also accepted
|
|
84
|
+
# because the two dtypes have the same bit-width, and internally
|
|
85
|
+
# the scale bytes are reinterpreted as uint8 for the strided
|
|
86
|
+
# expansion anyway (see _expand_block_scale).
|
|
87
|
+
"scale_dtypes": ("float8_e4m3fn", "uint8"),
|
|
88
|
+
# Raw uint8 scale bytes are reinterpreted as float8_e4m3fn values.
|
|
89
|
+
"scale_interpretation": "float8_e4m3fn",
|
|
90
|
+
},
|
|
91
|
+
BlockScalingFormat.MXFP8: {
|
|
92
|
+
"num_scalars_in_block": 32,
|
|
93
|
+
"operand_dtypes": ("float8_e4m3fn", "float8_e5m2"),
|
|
94
|
+
"scale_dtypes": ("uint8",),
|
|
95
|
+
# Raw uint8 scale bytes are unsigned 8-bit biased exponents: 2^(value - 127).
|
|
96
|
+
"scale_interpretation": "ue8m0",
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
_COMPATIBLE_FORMATS_FOR_OPERAND_DTYPE: dict[str, set[BlockScalingFormat]] = {}
|
|
101
|
+
for _fmt, _props in _MICROSCALING_FORMAT_PROPERTIES.items():
|
|
102
|
+
for _dt in _props["operand_dtypes"]:
|
|
103
|
+
_COMPATIBLE_FORMATS_FOR_OPERAND_DTYPE.setdefault(_dt, set()).add(_fmt)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _validate_operand_dtype_block_scaling_format_compatibility(
|
|
107
|
+
operand_dtype: str,
|
|
108
|
+
block_scaling_format: BlockScalingFormat,
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Check that *block_scaling_format* is consistent with *operand_dtype*."""
|
|
111
|
+
compatible = _COMPATIBLE_FORMATS_FOR_OPERAND_DTYPE.get(operand_dtype)
|
|
112
|
+
if compatible is not None and block_scaling_format not in compatible:
|
|
113
|
+
compatible_str = {str(f) for f in compatible}
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"Operand dtype {operand_dtype} requires block_scaling_format in {compatible_str}, got '{block_scaling_format}'."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _validate_scale_dtype_block_scaling_format_compatibility(
|
|
120
|
+
scale_dtype: str,
|
|
121
|
+
block_scaling_format: BlockScalingFormat,
|
|
122
|
+
param_name: str = "scales",
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Raise :exc:`TypeError` if *scale_dtype* is not valid for *block_scaling_format*."""
|
|
125
|
+
props = _MICROSCALING_FORMAT_PROPERTIES[block_scaling_format]
|
|
126
|
+
if scale_dtype not in props["scale_dtypes"]:
|
|
127
|
+
allowed = " or ".join(f"torch.{d}" for d in props["scale_dtypes"])
|
|
128
|
+
raise TypeError(f"For {block_scaling_format}, {param_name} must have dtype {allowed}, got {scale_dtype}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _infer_operand_shape_axis(
|
|
132
|
+
operand_or_shape: torch.Tensor | tuple[int, ...],
|
|
133
|
+
axis: Literal[-1, -2] | None,
|
|
134
|
+
) -> tuple[int, tuple[int, ...], Literal[-1, -2], str | None]:
|
|
135
|
+
"""Resolve operand shape, ndim, axis, and (optionally) operand dtype.
|
|
136
|
+
|
|
137
|
+
Returns ``(ndim, operand_shape, axis, operand_dtype)`` where
|
|
138
|
+
*operand_dtype* is ``None`` when a plain shape tuple is passed.
|
|
139
|
+
"""
|
|
140
|
+
import torch
|
|
141
|
+
|
|
142
|
+
if isinstance(operand_or_shape, tuple):
|
|
143
|
+
ndim = _validate_operand_ndim_for_block_scaling(operand_or_shape)
|
|
144
|
+
if axis is None:
|
|
145
|
+
raise ValueError(f"Axis must be specified when operand shape is provided, got {axis}")
|
|
146
|
+
return ndim, operand_or_shape, axis, None
|
|
147
|
+
elif isinstance(operand_or_shape, torch.Tensor):
|
|
148
|
+
operand = wrap_operand(operand_or_shape)
|
|
149
|
+
operand_shape: tuple[int, ...] = operand.shape # type: ignore
|
|
150
|
+
ndim = _validate_operand_ndim_for_block_scaling(operand_shape)
|
|
151
|
+
|
|
152
|
+
# Infer blocked axis
|
|
153
|
+
inferred_axis = _infer_blocked_axis(operand)
|
|
154
|
+
if axis is None:
|
|
155
|
+
axis = inferred_axis
|
|
156
|
+
else:
|
|
157
|
+
if axis >= 0:
|
|
158
|
+
axis -= ndim
|
|
159
|
+
if axis != inferred_axis:
|
|
160
|
+
layout = "row-major" if inferred_axis == -1 else "col-major"
|
|
161
|
+
raise ValueError(f"Incorrect axis: {axis} for {layout} operand, expected {inferred_axis}.")
|
|
162
|
+
|
|
163
|
+
if operand.dtype == "float4_e2m1fn_x2":
|
|
164
|
+
operand_shape_list = list(operand_shape)
|
|
165
|
+
operand_shape_list[axis] *= 2
|
|
166
|
+
operand_shape = tuple(operand_shape_list)
|
|
167
|
+
return ndim, operand_shape, axis, operand.dtype
|
|
168
|
+
else:
|
|
169
|
+
raise ValueError(
|
|
170
|
+
f"Expected operand_shape to be a tuple or a torch.Tensor to infer the shape from, got {type(operand_or_shape)}."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _validate_shape_axes_block_scaling_format(
|
|
175
|
+
operand_or_shape: torch.Tensor | tuple[int, ...],
|
|
176
|
+
axis: Literal[-1, -2] | None,
|
|
177
|
+
block_scaling_format: BlockScalingFormat,
|
|
178
|
+
) -> tuple[tuple[int, ...], Literal[-1, -2], Literal[-1, -2], BlockScalingFormat, int]:
|
|
179
|
+
"""
|
|
180
|
+
Infers operand_shape and blocked/unblocked axes from ``operand_or_shape`` when a tensor
|
|
181
|
+
is passed; validates ``block_scaling_format`` and related ``num_scalars_in_block``.
|
|
182
|
+
|
|
183
|
+
The accepted operand_shape, axis, and block_scaling_format are validated, so that axis
|
|
184
|
+
is one of the last two dimensions of the operand, and operand's shape extents are
|
|
185
|
+
multiples of cuBLAS tile size and num_scalars_in_block.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
ndim, operand_shape, axis, operand_dtype = _infer_operand_shape_axis(operand_or_shape, axis)
|
|
189
|
+
|
|
190
|
+
if operand_dtype is not None:
|
|
191
|
+
_validate_operand_dtype_block_scaling_format_compatibility(operand_dtype, block_scaling_format)
|
|
192
|
+
|
|
193
|
+
neg_axis = axis if axis < 0 else axis - ndim
|
|
194
|
+
if neg_axis == -1:
|
|
195
|
+
unblocked_axis, blocked_axis = -2, -1 # type: tuple[Literal[-1, -2], Literal[-1, -2]]
|
|
196
|
+
elif neg_axis == -2:
|
|
197
|
+
unblocked_axis, blocked_axis = -1, -2
|
|
198
|
+
else:
|
|
199
|
+
raise ValueError(f"Axis must point to one of the last two dimensions of the operand, got {axis}")
|
|
200
|
+
|
|
201
|
+
num_scalars_in_block = _MICROSCALING_FORMAT_PROPERTIES[block_scaling_format]["num_scalars_in_block"]
|
|
202
|
+
|
|
203
|
+
if any(extent <= 0 for extent in operand_shape):
|
|
204
|
+
raise ValueError(f"Operand shape must have positive extents, got {operand_shape}")
|
|
205
|
+
|
|
206
|
+
tile_height = 128
|
|
207
|
+
tile_width = 4
|
|
208
|
+
scalars_per_tile_row = tile_width * num_scalars_in_block
|
|
209
|
+
unblocked_extent = operand_shape[unblocked_axis]
|
|
210
|
+
blocked_extent = operand_shape[blocked_axis]
|
|
211
|
+
|
|
212
|
+
# Validate dimension requirements for NVFP4/MXFP8 tiling
|
|
213
|
+
if unblocked_extent % tile_height != 0:
|
|
214
|
+
raise ValueError(
|
|
215
|
+
f"The extent at axis {unblocked_axis} must be a positive multiple of {tile_height} "
|
|
216
|
+
f"(cuBLASLt uses {tile_height}x{tile_width} scale tiles), "
|
|
217
|
+
f"got operand_shape[{unblocked_axis}] = {unblocked_extent}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
if blocked_extent % scalars_per_tile_row != 0:
|
|
221
|
+
raise ValueError(
|
|
222
|
+
f"The extent at axis {blocked_axis} must be a positive multiple of {scalars_per_tile_row} "
|
|
223
|
+
f"(cuBLASLt uses {tile_height}x{tile_width} "
|
|
224
|
+
f"scale tiles where each tile row covers {num_scalars_in_block} elements along blocked "
|
|
225
|
+
f"dimension: {tile_width} groups * {num_scalars_in_block} elements in a group), "
|
|
226
|
+
f"got operand_shape[{blocked_axis}] = {blocked_extent}"
|
|
227
|
+
)
|
|
228
|
+
return operand_shape, unblocked_axis, blocked_axis, block_scaling_format, num_scalars_in_block
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _scales_2d_matrix_tiled_layout(
|
|
232
|
+
operand_shape: tuple[int, ...],
|
|
233
|
+
unblocked_axis: int,
|
|
234
|
+
blocked_axis: int,
|
|
235
|
+
num_scalars_in_block: int,
|
|
236
|
+
broadcast_block: bool = True,
|
|
237
|
+
) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
|
|
238
|
+
"""
|
|
239
|
+
Returns logical 2D shape for a matrix of scales and corresponding 5D (or 6D when
|
|
240
|
+
broadcasting) tensor shape and strides that account for the tiling and interleaved
|
|
241
|
+
layout of the tile.
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
tile_width = 4
|
|
245
|
+
tile_col_shape = (4, 32)
|
|
246
|
+
tile_height = 128 # prod(tile_y)
|
|
247
|
+
|
|
248
|
+
unblocked_extent = operand_shape[unblocked_axis]
|
|
249
|
+
blocked_extent = operand_shape[blocked_axis]
|
|
250
|
+
|
|
251
|
+
num_blocks_in_a_row = blocked_extent // num_scalars_in_block
|
|
252
|
+
num_tiles_in_a_row = num_blocks_in_a_row // tile_width
|
|
253
|
+
num_tiles_in_col = unblocked_extent // tile_height
|
|
254
|
+
|
|
255
|
+
# For operand of shape (H, W), the scale tensor is logically
|
|
256
|
+
# a 2D matrix of shape (H, W // num_blocks_in_a_row) (if axis = -1)
|
|
257
|
+
# or (H // num_blocks_in_a_row, W) (if axis = -2).
|
|
258
|
+
# The scale matrix is tiled with tile of shape 128x4.
|
|
259
|
+
# The tile has interleaved layout that splits column into
|
|
260
|
+
# 4 groups of 32 elements, so actual tile shape is (4, 32, 4).
|
|
261
|
+
# To account for the interleaved layout,
|
|
262
|
+
# we need to split the 2D matrix into 5D tensor.
|
|
263
|
+
shape_unblocked = num_tiles_in_col, tile_col_shape[0], tile_col_shape[1]
|
|
264
|
+
squeezed_unblocked = unblocked_extent
|
|
265
|
+
assert squeezed_unblocked == math.prod(shape_unblocked)
|
|
266
|
+
shape_blocked = num_tiles_in_a_row, tile_width
|
|
267
|
+
squeezed_blocked = num_blocks_in_a_row
|
|
268
|
+
assert squeezed_blocked == math.prod(shape_blocked)
|
|
269
|
+
# In total, we get 5D tensor with following dense strides:
|
|
270
|
+
strides_unblocked = (
|
|
271
|
+
512 * num_tiles_in_a_row, # 1 * tile_size * num_tiles_in_a_row
|
|
272
|
+
4, # 1 * tile_width
|
|
273
|
+
16, # 1 * tile_width * tile_col_shape[0]
|
|
274
|
+
)
|
|
275
|
+
strides_blocked = (
|
|
276
|
+
512, # 1 * tile_width * tile_col_shape[0] * tile_col_shape[1] = tile_size
|
|
277
|
+
1, # fastest changing dim
|
|
278
|
+
)
|
|
279
|
+
# Broadcast every scale to the whole block if needed.
|
|
280
|
+
if broadcast_block:
|
|
281
|
+
shape_blocked = shape_blocked + (num_scalars_in_block,) # type: ignore
|
|
282
|
+
strides_blocked = strides_blocked + (0,) # type: ignore
|
|
283
|
+
squeezed_blocked = blocked_extent
|
|
284
|
+
assert math.prod(shape_blocked) == blocked_extent
|
|
285
|
+
# Finally, we combine the two parts into a 5D (or 6D when broadcasting) tensor.
|
|
286
|
+
if blocked_axis == -1:
|
|
287
|
+
shape = shape_unblocked + shape_blocked
|
|
288
|
+
strides = strides_unblocked + strides_blocked # type: ignore
|
|
289
|
+
logical_shape = (squeezed_unblocked, squeezed_blocked)
|
|
290
|
+
else:
|
|
291
|
+
assert blocked_axis == -2
|
|
292
|
+
shape = shape_blocked + shape_unblocked
|
|
293
|
+
strides = strides_blocked + strides_unblocked
|
|
294
|
+
logical_shape = (squeezed_blocked, squeezed_unblocked)
|
|
295
|
+
return shape, strides, logical_shape
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _scales_nd_matrix_tiled_layout(
|
|
299
|
+
operand_shape: tuple[int, ...],
|
|
300
|
+
unblocked_axis: Literal[-1, -2],
|
|
301
|
+
blocked_axis: Literal[-1, -2],
|
|
302
|
+
num_scalars_in_block: int,
|
|
303
|
+
broadcast_block: bool,
|
|
304
|
+
) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
|
|
305
|
+
"""
|
|
306
|
+
Returns logical 2D shape for a matrix of scales (or 3D to account for batching) and
|
|
307
|
+
corresponding (1 if batching) + 5D (+ 1 if broadcasting) tensor shape and strides that
|
|
308
|
+
account for the tiling and interleaved layout of the tile.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
matrix_shape, matrix_strides, matrix_logical_shape = _scales_2d_matrix_tiled_layout(
|
|
312
|
+
operand_shape, unblocked_axis, blocked_axis, num_scalars_in_block, broadcast_block
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
operand_ndim = len(operand_shape)
|
|
316
|
+
if operand_ndim >= 2:
|
|
317
|
+
batch_shape = operand_shape[:-2]
|
|
318
|
+
batch_size = math.prod(batch_shape)
|
|
319
|
+
batch_stride = math.prod(operand_shape[-2:]) // num_scalars_in_block
|
|
320
|
+
matrix_shape = (batch_size, *matrix_shape)
|
|
321
|
+
matrix_strides = (batch_stride, *matrix_strides)
|
|
322
|
+
matrix_logical_shape = batch_shape + matrix_logical_shape
|
|
323
|
+
|
|
324
|
+
return matrix_shape, matrix_strides, matrix_logical_shape
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _validate_tensor(x, where, tensor_name="tensor", dtype=None):
|
|
328
|
+
"""
|
|
329
|
+
Validate the tensor package and dtype.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
x: wrapped tensor object
|
|
333
|
+
|
|
334
|
+
where: name of the function that is performing the validation
|
|
335
|
+
|
|
336
|
+
tensor_name: name of the tensor to use in the error messages
|
|
337
|
+
|
|
338
|
+
dtype: if not None, check that the object dtype matches the specified dtype
|
|
339
|
+
|
|
340
|
+
"""
|
|
341
|
+
package = infer_object_package(x)
|
|
342
|
+
if package != "torch":
|
|
343
|
+
raise ValueError(
|
|
344
|
+
f"Only torch.Tensor is currently supported by function '{where}'; the "
|
|
345
|
+
f"specified {tensor_name} belongs to '{package}' package."
|
|
346
|
+
)
|
|
347
|
+
x = wrap_operand(x)
|
|
348
|
+
if dtype is not None and x.dtype != dtype:
|
|
349
|
+
raise ValueError(
|
|
350
|
+
f"The function '{where}' requires the specified {tensor_name} to have dtype "
|
|
351
|
+
f"'{dtype}', whereas it has dtype '{x.dtype}'."
|
|
352
|
+
)
|
|
353
|
+
return x
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _validate_mxfp8_scale(mx_scales, where, x=None):
|
|
357
|
+
if x is not None:
|
|
358
|
+
x = _validate_tensor(x, where)
|
|
359
|
+
mx_scales = _validate_tensor(mx_scales, where, tensor_name="mx_scales tensor", dtype="uint8")
|
|
360
|
+
|
|
361
|
+
if x is None:
|
|
362
|
+
return mx_scales
|
|
363
|
+
|
|
364
|
+
if mx_scales.shape != (x.size // 32,):
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"The shape of mx_scales {mx_scales.shape} is not compatible with a tensor of shape {x.shape}. "
|
|
367
|
+
f"The expected mx_scales shape is {(x.size // 32,)}."
|
|
368
|
+
)
|
|
369
|
+
return mx_scales, x
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def create_mxfp8_scale(x, exponent, stream=None):
|
|
373
|
+
"""
|
|
374
|
+
.. experimental:: function
|
|
375
|
+
|
|
376
|
+
Create MXFP8 block scale with the same value for the whole tensor ``x``.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
x: The tensor to create the block scale for
|
|
380
|
+
|
|
381
|
+
exponent: An integer from [-127, 128] range. Effective scale will be ``2^exponent``.
|
|
382
|
+
|
|
383
|
+
stream: Optional stream to create the block scale on.
|
|
384
|
+
Defaults to the stream of ``x``.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
An MXFP8 block scale factors tensor (1D, cuBLAS-compatible interleaved layout) to be
|
|
388
|
+
used with MXFP8 computations.
|
|
389
|
+
"""
|
|
390
|
+
x = _validate_tensor(x, "create_mxfp8_scale")
|
|
391
|
+
|
|
392
|
+
if not -127 <= exponent <= 128:
|
|
393
|
+
raise ValueError("The exponent should be an integer from [-127, 128] range.")
|
|
394
|
+
|
|
395
|
+
stream_holder = None if x.device_id == "cpu" else get_or_create_stream(x.device_id, stream, x.name)
|
|
396
|
+
mx_scales = create_empty_tensor(
|
|
397
|
+
x.__class__, (x.size // 32,), "uint8", device_id=x.device_id, stream_holder=stream_holder, verify_strides=False
|
|
398
|
+
)
|
|
399
|
+
mx_scales.tensor[:] = exponent + 127
|
|
400
|
+
return mx_scales.tensor
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def invert_mxfp8_scale(mx_scales):
|
|
404
|
+
"""
|
|
405
|
+
.. experimental:: function
|
|
406
|
+
|
|
407
|
+
Compute a reciprocal of MXFP8 block scale.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
mx_scales: MXFP8 block scale tensor (UE8M0 format).
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
Tensor of the same shape as ``mx_scales`` with exponents replaced by the
|
|
414
|
+
reciprocals.
|
|
415
|
+
"""
|
|
416
|
+
_validate_mxfp8_scale(mx_scales, "invert_mxfp8_scale")
|
|
417
|
+
|
|
418
|
+
ret = mx_scales.clone()
|
|
419
|
+
ret[ret == 255] = 254 # Prevent the overflow
|
|
420
|
+
# remove the bias (127), negate, add the bias back: -(scale - 127) + 127
|
|
421
|
+
ret[:] = 254 - ret
|
|
422
|
+
return ret
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _idx_batch_offset(
|
|
426
|
+
operand_shape: tuple[int, ...], index: tuple[int, ...] | tuple[torch.Tensor, ...], num_scalars_in_block: int
|
|
427
|
+
) -> int | torch.Tensor:
|
|
428
|
+
ndim = len(operand_shape)
|
|
429
|
+
if ndim == 2:
|
|
430
|
+
return 0
|
|
431
|
+
|
|
432
|
+
assert ndim > 2
|
|
433
|
+
batch_strides = _c_strides(operand_shape)[:-2]
|
|
434
|
+
batch_index = index[:-2]
|
|
435
|
+
batch_offset = sum(i * stride for i, stride in zip(batch_index, batch_strides, strict=True)) # type: ignore
|
|
436
|
+
batch_offset //= num_scalars_in_block
|
|
437
|
+
return batch_offset
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def get_block_scale_offset(
|
|
441
|
+
index: tuple[int, ...] | tuple[torch.Tensor, ...],
|
|
442
|
+
operand_or_shape: torch.Tensor | tuple[int, ...],
|
|
443
|
+
block_scaling_format: BlockScalingFormat,
|
|
444
|
+
*,
|
|
445
|
+
axis: Literal[-1, -2] | None = None,
|
|
446
|
+
) -> int | torch.Tensor:
|
|
447
|
+
"""
|
|
448
|
+
.. experimental:: function
|
|
449
|
+
|
|
450
|
+
Computes offset of a block scale factor in the 1D interleaved scales tensor.
|
|
451
|
+
|
|
452
|
+
Matmul (cuBLAS) expects scale factors in specific `interleaved layout
|
|
453
|
+
<https://docs.nvidia.com/cuda/cublas/index.html#d-block-scaling-factors-layout>`_.
|
|
454
|
+
|
|
455
|
+
This function aims to abstract away the interleaved layout details, offering indexing
|
|
456
|
+
that more directly corresponds to the operand's shape.
|
|
457
|
+
|
|
458
|
+
Example:
|
|
459
|
+
Suppose that you are doing an NVFP4 matmul ``a @ b`` with ``a`` of shape (M=128,
|
|
460
|
+
K=128). For matrix ``a``, a single scale is applied to consecutive 16 elements
|
|
461
|
+
blocks in a row (axis=-1). Therefore, to find the scale applied to ``a[y, x]``, we
|
|
462
|
+
first need to adjust the x index to the index of the 16-element block it belongs to,
|
|
463
|
+
which is ``block_idx = x // 16``. Then, calling: ``get_block_scale_offset((y,
|
|
464
|
+
block_idx), a, BlockScalingFormat.NVFP4)`` will return the offset of the scale
|
|
465
|
+
applied to ``a[y, x]`` (and all other elements in the same 16-element block).
|
|
466
|
+
|
|
467
|
+
The schematic below shows matrix ``a`` with the 16-element blocks annotated.
|
|
468
|
+
Asterisks mark two target blocks:
|
|
469
|
+
|
|
470
|
+
- elements in ``a`` at indices from (5, 32) to (5, 47), correspond to the same block
|
|
471
|
+
(K-group 2) and map to the same offset ``get_block_scale_offset((5, 2), a,
|
|
472
|
+
BlockScalingFormat.NVFP4) == 82``
|
|
473
|
+
- elements in ``a`` at indices from (5, 80) to (5, 95), correspond to the same block
|
|
474
|
+
(K-group 5) and map to the same offset ``get_block_scale_offset((5, 5), a,
|
|
475
|
+
BlockScalingFormat.NVFP4) == 593``
|
|
476
|
+
|
|
477
|
+
.. code-block:: text
|
|
478
|
+
|
|
479
|
+
| K-grp 0 | K-grp 1 | K-grp 2 | K-grp 3 | K-grp 4 | K-grp 5 | ...
|
|
480
|
+
| [0..15] | [16..31] | [32..47] | [48..63] | [64..79] | [80..95] | ...
|
|
481
|
+
+----------+----------+----------+----------+----------+----------+---
|
|
482
|
+
row 0 | | | | | | |
|
|
483
|
+
... | | | | | | |
|
|
484
|
+
row 5 | | | * | | | * |
|
|
485
|
+
... | | | | | | |
|
|
486
|
+
row127| | | | | | |
|
|
487
|
+
+----------+----------+----------+----------+----------+----------+---
|
|
488
|
+
(5,2) (5,5)
|
|
489
|
+
|
|
490
|
+
Note:
|
|
491
|
+
As far as computing the block scale offset, the only difference between MXFP8 and
|
|
492
|
+
NVFP4 is the number of elements in a block (32 for MXFP8, 16 for NVFP4).
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
index: A tuple of indices with length equal to ``len(operand_shape)``. Can be:
|
|
496
|
+
|
|
497
|
+
- A tuple of integers for single-element query, e.g., ``(10, 20)``
|
|
498
|
+
- A tuple of tensors for batch query, e.g., ``(xs, ys)`` where ``xs`` and ``ys``
|
|
499
|
+
are tensors of the same shape
|
|
500
|
+
|
|
501
|
+
operand_or_shape: Operand tensor (that the scales apply to) or
|
|
502
|
+
the operand's logical (non-packed, non-blocked) shape.
|
|
503
|
+
|
|
504
|
+
block_scaling_format: The block scaling format of the operand:
|
|
505
|
+
:attr:`BlockScalingFormat.NVFP4` or :attr:`BlockScalingFormat.MXFP8`.
|
|
506
|
+
Internally, it is validated to be consistent with the operand dtype, and a
|
|
507
|
+
:exc:`ValueError` is raised if not.
|
|
508
|
+
|
|
509
|
+
axis: The blocked dimension of the operand tensor.
|
|
510
|
+
For example, for NVFP4/MXFP8 matmul, A is blocked in rows (``axis = -1``), and B
|
|
511
|
+
is blocked in columns (``axis = -2``). Depending on ``operand_or_shape``:
|
|
512
|
+
|
|
513
|
+
- if a *shape* is passed to ``operand_or_shape``, then ``axis`` is required
|
|
514
|
+
- if an *operand* is passed to ``operand_or_shape``, then ``axis`` can be
|
|
515
|
+
omitted and the blocked dimension is inferred from the operand's layout.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
An integer (if ``index`` contains integers) or a tensor of integers (if ``index``
|
|
519
|
+
contains tensors), indicating the offset(s) to the MXFP8/NVFP4 block scale
|
|
520
|
+
factor(s). The returned offset points to a block scale factor that is applied to:
|
|
521
|
+
|
|
522
|
+
- for axis == -2: ``operand[*index[-2:],
|
|
523
|
+
block_size*index[-2]:block_size*(index[-2]+1), index[-1]]``.
|
|
524
|
+
- for axis == -1: ``operand[*index[-2:], index[-1],
|
|
525
|
+
block_size*index[-1]:block_size*(index[-1]+1)]``.
|
|
526
|
+
|
|
527
|
+
where the block size is 32 for MXFP8 and 16 for NVFP4.
|
|
528
|
+
|
|
529
|
+
Note:
|
|
530
|
+
In typical use-cases, there should be no need to manually modify MXFP8 scales. The
|
|
531
|
+
scales returned as ``"d_out_scale"`` by one matmul, can be directly reused as input
|
|
532
|
+
scales for another matmul.
|
|
533
|
+
|
|
534
|
+
Hint:
|
|
535
|
+
- To apply the interleaved scales (e.g. as returned by matmul's ``d_out_scale``) to
|
|
536
|
+
the operand, use :func:`apply_mxfp8_scale` instead.
|
|
537
|
+
- To specify scales as ND tensor and copy them to cuBLAS-compatible interleaved
|
|
538
|
+
layout, use :func:`to_block_scale` instead.
|
|
539
|
+
|
|
540
|
+
"""
|
|
541
|
+
# infer operand_shape, unblocked_axis, blocked_axis, num_scalars_in_block
|
|
542
|
+
# validate operand_shape's dim and extents for divisibility by cuBLAS tile
|
|
543
|
+
# and num_scalars_in_block
|
|
544
|
+
operand_shape, unblocked_axis, blocked_axis, _, num_scalars_in_block = _validate_shape_axes_block_scaling_format(
|
|
545
|
+
operand_or_shape, axis, block_scaling_format
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
# We need to validate index against the operand_shape
|
|
549
|
+
ndim = len(operand_shape)
|
|
550
|
+
if len(index) != ndim:
|
|
551
|
+
raise ValueError("Index length must match the number of dimensions of the operand.")
|
|
552
|
+
|
|
553
|
+
blocked_dim = operand_shape[blocked_axis]
|
|
554
|
+
unblocked_idx = index[unblocked_axis]
|
|
555
|
+
blocked_group_idx = index[blocked_axis]
|
|
556
|
+
|
|
557
|
+
# Tile dimensions
|
|
558
|
+
TILE_OUTER = 128
|
|
559
|
+
TILE_INNER_GROUPS = 4
|
|
560
|
+
|
|
561
|
+
# Determine tile coordinates
|
|
562
|
+
sf_outer = unblocked_idx // TILE_OUTER
|
|
563
|
+
sf_inner = (blocked_group_idx // TILE_INNER_GROUPS) * TILE_INNER_GROUPS
|
|
564
|
+
sf_inner_dim = blocked_dim // num_scalars_in_block
|
|
565
|
+
|
|
566
|
+
# Compute tile offset (global layout)
|
|
567
|
+
# see https://docs.nvidia.com/cuda/cublas/index.html#d-block-scaling-factors-layout
|
|
568
|
+
tile_offset = (sf_inner + sf_outer * sf_inner_dim) * 128
|
|
569
|
+
|
|
570
|
+
# Compute intra-tile offset
|
|
571
|
+
intra_outer = unblocked_idx % TILE_OUTER
|
|
572
|
+
intra_inner = blocked_group_idx % TILE_INNER_GROUPS
|
|
573
|
+
intra_offset = (intra_outer % 32) * 16 + (intra_outer // 32) * 4 + intra_inner
|
|
574
|
+
|
|
575
|
+
batch_offset = _idx_batch_offset(operand_shape, index, num_scalars_in_block)
|
|
576
|
+
|
|
577
|
+
return batch_offset + tile_offset + intra_offset
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _expand_block_scale(
|
|
581
|
+
scales_1d: torch.Tensor,
|
|
582
|
+
operand_or_shape: torch.Tensor | tuple[int, ...],
|
|
583
|
+
block_scaling_format: BlockScalingFormat,
|
|
584
|
+
axis: Literal[-1, -2] | None,
|
|
585
|
+
device: Literal["cuda", "cpu"] | None = None,
|
|
586
|
+
) -> torch.Tensor:
|
|
587
|
+
"""
|
|
588
|
+
For a documentation, see the public function :func:`expand_block_scale`.
|
|
589
|
+
This one does most of the work but does not handle conversion of the expanded
|
|
590
|
+
scales to output dtype, instead it returns scales as uint8.
|
|
591
|
+
"""
|
|
592
|
+
import torch
|
|
593
|
+
|
|
594
|
+
if not isinstance(scales_1d, torch.Tensor):
|
|
595
|
+
raise TypeError(f"scales_1d must be a torch.Tensor, got {type(scales_1d)}")
|
|
596
|
+
|
|
597
|
+
scales_wrapped = wrap_operand(scales_1d)
|
|
598
|
+
scales_ndim = len(scales_wrapped.shape)
|
|
599
|
+
|
|
600
|
+
if scales_ndim != 1:
|
|
601
|
+
raise ValueError(f"scales_1d must be a 1D tensor, got {scales_ndim}D tensor with shape {scales_wrapped.shape}")
|
|
602
|
+
|
|
603
|
+
if scales_wrapped.strides[0] != 1:
|
|
604
|
+
raise ValueError(f"scales_1d must be 1D contiguous tensor, got non-unit stride: {scales_wrapped.strides}")
|
|
605
|
+
|
|
606
|
+
operand_shape, unblocked_axis, blocked_axis, block_scaling_format, num_scalars_in_block = (
|
|
607
|
+
_validate_shape_axes_block_scaling_format(operand_or_shape, axis, block_scaling_format)
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
_validate_scale_dtype_block_scaling_format_compatibility(scales_wrapped.dtype, block_scaling_format, "scales_1d")
|
|
611
|
+
|
|
612
|
+
expected_num_scales = math.prod(operand_shape) // num_scalars_in_block
|
|
613
|
+
num_scales = scales_wrapped.shape[0]
|
|
614
|
+
if num_scales != expected_num_scales:
|
|
615
|
+
raise ValueError(
|
|
616
|
+
f"The scale tensor must have {expected_num_scales} elements: "
|
|
617
|
+
f"{expected_num_scales} = math.prod({operand_shape}) // {num_scalars_in_block},"
|
|
618
|
+
f" got: {num_scales}."
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
matrix_shape, matrix_strides, matrix_logical_shape = _scales_nd_matrix_tiled_layout(
|
|
622
|
+
operand_shape, unblocked_axis, blocked_axis, num_scalars_in_block, True
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
if device is not None and device not in ("cuda", "cpu"):
|
|
626
|
+
raise ValueError(f"device must be 'cuda', 'cpu', or None, got '{device}'")
|
|
627
|
+
target_device = torch.device(device) if device is not None else scales_wrapped.tensor.device
|
|
628
|
+
|
|
629
|
+
scales_on_device = scales_wrapped.tensor.to(target_device)
|
|
630
|
+
scales_on_device = scales_on_device.view(torch.uint8)
|
|
631
|
+
|
|
632
|
+
expanded = torch.as_strided(
|
|
633
|
+
scales_on_device,
|
|
634
|
+
size=matrix_shape,
|
|
635
|
+
stride=matrix_strides,
|
|
636
|
+
).reshape(matrix_logical_shape)
|
|
637
|
+
return expanded
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _convert_to_output_dtype(
|
|
641
|
+
tensor: torch.Tensor,
|
|
642
|
+
output_dtype: Literal["smallest"] | torch.dtype,
|
|
643
|
+
) -> torch.Tensor:
|
|
644
|
+
import torch
|
|
645
|
+
|
|
646
|
+
def _float_rank(dtype):
|
|
647
|
+
if dtype == torch.float16:
|
|
648
|
+
return 0
|
|
649
|
+
elif dtype == torch.float32:
|
|
650
|
+
return 1
|
|
651
|
+
else:
|
|
652
|
+
return 2
|
|
653
|
+
|
|
654
|
+
if output_dtype == tensor.dtype:
|
|
655
|
+
return tensor
|
|
656
|
+
|
|
657
|
+
smallest_dtype = _smallest_dtype_that_fits(tensor)
|
|
658
|
+
|
|
659
|
+
if output_dtype == "smallest":
|
|
660
|
+
return tensor.type(smallest_dtype)
|
|
661
|
+
else:
|
|
662
|
+
if _float_rank(output_dtype) < _float_rank(smallest_dtype):
|
|
663
|
+
raise ValueError(
|
|
664
|
+
f"Result requires at least {smallest_dtype}; requested {output_dtype} would overflow or underflow."
|
|
665
|
+
)
|
|
666
|
+
return tensor.type(output_dtype)
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _convert_uint8_ue8m0_scale_to_float64(mx_scales: torch.Tensor) -> torch.Tensor:
|
|
670
|
+
import torch
|
|
671
|
+
|
|
672
|
+
assert mx_scales.dtype == torch.uint8
|
|
673
|
+
# Use float64 for scale computation to be safe always,
|
|
674
|
+
# this way we avoid overflow for corner case
|
|
675
|
+
# when exponent is 128 (2^128 overflows float32)
|
|
676
|
+
return 2 ** (mx_scales.type(torch.float64) - 127)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def _smallest_dtype_that_fits(out_64):
|
|
680
|
+
"""
|
|
681
|
+
(Private) Return the smallest torch dtype that can
|
|
682
|
+
hold the values in out_64 without overflow or underflow.
|
|
683
|
+
"""
|
|
684
|
+
import torch
|
|
685
|
+
|
|
686
|
+
nonzero = out_64[out_64 != 0]
|
|
687
|
+
if nonzero.numel() == 0:
|
|
688
|
+
min_abs = 0
|
|
689
|
+
max_abs = 0
|
|
690
|
+
else:
|
|
691
|
+
abs_vals = torch.abs(nonzero)
|
|
692
|
+
min_abs = torch.min(abs_vals).item()
|
|
693
|
+
max_abs = torch.max(abs_vals).item()
|
|
694
|
+
|
|
695
|
+
finfo16 = torch.finfo(torch.float16)
|
|
696
|
+
finfo32 = torch.finfo(torch.float32)
|
|
697
|
+
|
|
698
|
+
if max_abs <= finfo16.max and (min_abs >= finfo16.tiny or min_abs == 0):
|
|
699
|
+
return torch.float16
|
|
700
|
+
if max_abs <= finfo32.max and (min_abs >= finfo32.tiny or min_abs == 0):
|
|
701
|
+
return torch.float32
|
|
702
|
+
return torch.float64
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def apply_mxfp8_scale(
|
|
706
|
+
x: torch.Tensor,
|
|
707
|
+
scales_1d: torch.Tensor,
|
|
708
|
+
output_dtype: Literal["smallest"] | torch.dtype = "smallest",
|
|
709
|
+
) -> torch.Tensor:
|
|
710
|
+
"""
|
|
711
|
+
.. experimental:: function
|
|
712
|
+
|
|
713
|
+
Apply MXFP8 block scale factors to a tensor ``x``.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
x: The tensor to which the scaling should be applied.
|
|
717
|
+
Currently it must be a ``torch.Tensor``.
|
|
718
|
+
|
|
719
|
+
scales_1d: The block scale factors (stored in cuBLAS-compatible interleaved layout)
|
|
720
|
+
to apply. Its shape must be compatible with ``x``, and currently it must also be
|
|
721
|
+
a ``torch.Tensor``.
|
|
722
|
+
|
|
723
|
+
output_dtype: Output dtype. If provided, must be a floating-point
|
|
724
|
+
``torch.dtype`` (float16, float32, or float64) and must be at least as wide as
|
|
725
|
+
the smallest dtype that can represent the result, or :exc:`ValueError` is
|
|
726
|
+
raised. If 'smallest' (default), the smallest dtype that can represent the
|
|
727
|
+
result is automatically chosen.
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
A tensor with values of ``x`` with scales applied, in the chosen or provided dtype.
|
|
731
|
+
|
|
732
|
+
Raises:
|
|
733
|
+
ValueError: When the result will over/underflow the requested dtype.
|
|
734
|
+
|
|
735
|
+
Behavior:
|
|
736
|
+
The operation is computed in float64. Then, the function determines the smallest
|
|
737
|
+
dtype (float16, float32, or float64) that can represent the result without overflow
|
|
738
|
+
or underflow. If ``output_dtype`` was passed, it must be at least as wide as that
|
|
739
|
+
minimum otherwise :exc:`ValueError` is raised; if ``output_dtype='smallest'``, that
|
|
740
|
+
minimum is used. The result is finally cast to the chosen dtype and returned.
|
|
741
|
+
|
|
742
|
+
Note:
|
|
743
|
+
This function is not intended for production usage due to its relatively low
|
|
744
|
+
performance and high memory consumption. Prefer
|
|
745
|
+
:attr:`~nvmath.linalg.advanced.MatmulOptions.result_type` to request non-FP8 output.
|
|
746
|
+
"""
|
|
747
|
+
import torch
|
|
748
|
+
|
|
749
|
+
if output_dtype != "smallest" and output_dtype not in (torch.float16, torch.float32, torch.float64):
|
|
750
|
+
raise TypeError("output_dtype must be 'smallest' or one of torch.float16, torch.float32, torch.float64.")
|
|
751
|
+
|
|
752
|
+
# Use float64 for scale computation to be safe always,
|
|
753
|
+
# this way we avoid overflow for corner case
|
|
754
|
+
# when exponent is 128 (2^128 overflows float32)
|
|
755
|
+
expanded_scales = _expand_block_scale(scales_1d, x, BlockScalingFormat.MXFP8, axis=None)
|
|
756
|
+
expanded_scales = _convert_uint8_ue8m0_scale_to_float64(expanded_scales)
|
|
757
|
+
# Explicitly cast x to float64, promotion is not guaranteed, see:
|
|
758
|
+
# https://docs.pytorch.org/docs/stable/tensor_attributes.html#type-promotion-doc,
|
|
759
|
+
# "Promotion for shell dtypes is not defined".
|
|
760
|
+
out_64 = x.type(torch.float64) * expanded_scales
|
|
761
|
+
return _convert_to_output_dtype(out_64, output_dtype)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
# ====================================================
|
|
765
|
+
# helper functions for FP4 E2M1 encoding/decoding
|
|
766
|
+
# ====================================================
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
_FP4_DECODE_VALUES = (0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, -0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0)
|
|
770
|
+
_FP4_BOUNDARIES = [0.25, 0.75, 1.25, 1.75, 2.5, 3.5, 5.0]
|
|
771
|
+
_FP4_MAG_CODES = [0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7]
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _get_fp4_lookup_table(device):
|
|
775
|
+
"""
|
|
776
|
+
(Private) Create FP4 lookup table as
|
|
777
|
+
a torch tensor on the specified device.
|
|
778
|
+
|
|
779
|
+
Args:
|
|
780
|
+
device: The device to create the lookup table on.
|
|
781
|
+
|
|
782
|
+
Returns:
|
|
783
|
+
A torch tensor with dtype torch.float32 and shape (16,)
|
|
784
|
+
on the specified device.
|
|
785
|
+
"""
|
|
786
|
+
import torch
|
|
787
|
+
|
|
788
|
+
return torch.tensor(
|
|
789
|
+
_FP4_DECODE_VALUES,
|
|
790
|
+
dtype=torch.float32,
|
|
791
|
+
device=device,
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _quantize_to_fp4_codes_bucketize(x: torch.Tensor, device) -> torch.Tensor:
|
|
796
|
+
"""
|
|
797
|
+
Quantize float32 tensor to nearest FP4 value using torch.bucketize on the 8
|
|
798
|
+
distinct FP4 magnitudes.
|
|
799
|
+
"""
|
|
800
|
+
import torch
|
|
801
|
+
|
|
802
|
+
if x.dtype != torch.float32:
|
|
803
|
+
raise ValueError(f"x must be float32, got {x.dtype}")
|
|
804
|
+
|
|
805
|
+
boundaries = torch.tensor(_FP4_BOUNDARIES, dtype=torch.float32, device=device)
|
|
806
|
+
mag_codes = torch.tensor(_FP4_MAG_CODES, dtype=torch.uint8, device=device)
|
|
807
|
+
|
|
808
|
+
# Example: x = [1.2, -2.3, 0.1]
|
|
809
|
+
|
|
810
|
+
# sign: True where negative (including -0.0). [False, True, False]
|
|
811
|
+
sign = torch.signbit(x)
|
|
812
|
+
|
|
813
|
+
# mag: absolute value. [1.2, 2.3, 0.1]
|
|
814
|
+
mag = x.abs()
|
|
815
|
+
|
|
816
|
+
# Binary-search each magnitude into the boundary bins:
|
|
817
|
+
# https://docs.pytorch.org/docs/stable/generated/torch.bucketize.html
|
|
818
|
+
# boundaries = [0.25, 0.75, 1.25, 1.75, 2.5, 3.5, 5.0]
|
|
819
|
+
# Default (right=False): boundary[i-1] < value <= boundary[i]
|
|
820
|
+
# 1.2: 0.75 < 1.2 <= 1.25 -> bucket 2
|
|
821
|
+
# 2.3: 1.75 < 2.3 <= 2.5 -> bucket 4
|
|
822
|
+
# 0.1: 0.1 <= 0.25 -> bucket 0
|
|
823
|
+
# Result: [2, 4, 0]
|
|
824
|
+
bucket = torch.bucketize(mag, boundaries)
|
|
825
|
+
|
|
826
|
+
# Map bucket index to the 3-bit magnitude code (lower 3 bits of FP4).
|
|
827
|
+
# mag_codes = [0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7]
|
|
828
|
+
# bucket [2, 4, 0] -> code [0x2, 0x4, 0x0]
|
|
829
|
+
# These represent magnitudes [1.0, 2.0, 0.0].
|
|
830
|
+
code = mag_codes[bucket]
|
|
831
|
+
|
|
832
|
+
# Set bit 3 (the FP4 sign bit) for negative values.
|
|
833
|
+
# sign.to(torch.uint8): [F, T, F] -> [0, 1, 0]
|
|
834
|
+
# << 3: shift into bit 3: [0x0, 0x8, 0x0]
|
|
835
|
+
# OR with magnitude code:
|
|
836
|
+
# code [0x2, 0x4, 0x0] | [0x0, 0x8, 0x0] = [0x2, 0xC, 0x0]
|
|
837
|
+
# Final FP4 values: [1.0, -2.0, 0.0]
|
|
838
|
+
code = code | (sign.to(torch.uint8) << 3)
|
|
839
|
+
return code
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def quantize_to_fp4(
|
|
843
|
+
x: torch.Tensor,
|
|
844
|
+
axis: Literal[-1, -2],
|
|
845
|
+
) -> torch.Tensor:
|
|
846
|
+
"""
|
|
847
|
+
.. experimental:: function
|
|
848
|
+
|
|
849
|
+
Quantize a torch tensor to ``torch.float4_e2m1fn_x2`` dtype.
|
|
850
|
+
|
|
851
|
+
The function supports 1D, 2D, and higher-dimensional input torch tensors with dtype
|
|
852
|
+
float32. It quantizes each float32 value to the nearest representable FP4 value and
|
|
853
|
+
packs two 4-bit codes per byte, halving the packed dimension. The packing direction is
|
|
854
|
+
controlled by ``axis``:
|
|
855
|
+
|
|
856
|
+
- ``axis=-1``: Packs consecutive elements along the last dimension. Input shape ``(...,
|
|
857
|
+
Q)`` produces output ``(..., Q//2)`` with row-major layout (last stride = 1).
|
|
858
|
+
|
|
859
|
+
- ``axis=-2``: Packs consecutive elements along the second-to-last dimension. Input
|
|
860
|
+
shape ``(..., P, Q)`` produces output ``(..., P//2, Q)`` with column-major layout
|
|
861
|
+
(second-to-last stride = 1).
|
|
862
|
+
|
|
863
|
+
Args:
|
|
864
|
+
x: Torch tensor with dtype float32 (1D, 2D, or higher-dimensional).
|
|
865
|
+
|
|
866
|
+
axis: The axis along which to pack. Must be ``-1`` (last dimension)
|
|
867
|
+
or ``-2`` (second-to-last dimension).
|
|
868
|
+
|
|
869
|
+
Returns:
|
|
870
|
+
Torch tensor with dtype ``torch.float4_e2m1fn_x2`` on the same device as the input.
|
|
871
|
+
|
|
872
|
+
.. important::
|
|
873
|
+
The packed dimension must have even size.
|
|
874
|
+
|
|
875
|
+
Note:
|
|
876
|
+
This helper quantizes a single tensor and is suitable for understanding how packing
|
|
877
|
+
for ``torch.float4_e2m1fn_x2`` works in practice and/or for experimenting with FP4
|
|
878
|
+
GEMMs outside of typical deep-learning workflows. It is not fully optimized for
|
|
879
|
+
performance but should be adequate for most common use cases. For production
|
|
880
|
+
whole-model quantization, consider tools such as `torchao
|
|
881
|
+
<https://github.com/pytorch/ao>`_ or `bitsandbytes
|
|
882
|
+
<https://github.com/bitsandbytes-foundation/bitsandbytes>`_.
|
|
883
|
+
|
|
884
|
+
.. seealso::
|
|
885
|
+
:func:`unpack_fp4` — decode packed FP4 values back to float32.
|
|
886
|
+
"""
|
|
887
|
+
import torch
|
|
888
|
+
|
|
889
|
+
# preconditions
|
|
890
|
+
# -------------
|
|
891
|
+
if not isinstance(x, torch.Tensor):
|
|
892
|
+
raise TypeError(f"x must be a torch.Tensor, got {type(x)}")
|
|
893
|
+
if x.dtype != torch.float32:
|
|
894
|
+
raise ValueError(f"x must be float32, got {x.dtype}")
|
|
895
|
+
if axis not in (-1, -2):
|
|
896
|
+
raise ValueError(f"axis must be -1 or -2, got {axis}")
|
|
897
|
+
if x.ndim < 1:
|
|
898
|
+
raise ValueError(f"x must be at least 1D, got {x.ndim}D")
|
|
899
|
+
if x.ndim == 1 and axis != -1:
|
|
900
|
+
raise ValueError(f"axis must be -1 for 1D tensors, got {axis}")
|
|
901
|
+
|
|
902
|
+
if (packed_dim := x.shape[axis]) % 2 != 0:
|
|
903
|
+
raise ValueError(f"Packed dimension must be even, got {packed_dim}")
|
|
904
|
+
|
|
905
|
+
device = x.device
|
|
906
|
+
codes = _quantize_to_fp4_codes_bucketize(x, device)
|
|
907
|
+
|
|
908
|
+
if axis == -1:
|
|
909
|
+
low = codes[..., 0::2]
|
|
910
|
+
high = codes[..., 1::2]
|
|
911
|
+
packed = low | (high << 4)
|
|
912
|
+
return packed.view(torch.float4_e2m1fn_x2)
|
|
913
|
+
|
|
914
|
+
# axis == -2: column-wise packing
|
|
915
|
+
low = codes[..., 0::2, :]
|
|
916
|
+
high = codes[..., 1::2, :]
|
|
917
|
+
packed_codes = low | (high << 4)
|
|
918
|
+
# Transpose so columns become contiguous rows, force a physical copy,
|
|
919
|
+
# reinterpret as fp4x2 (requires stride[-1]==1), then transpose back.
|
|
920
|
+
# The result has the original (…, rows, cols) shape with column-major
|
|
921
|
+
# memory layout, as requested by the caller.
|
|
922
|
+
result = packed_codes.mT.contiguous().view(torch.float4_e2m1fn_x2).mT
|
|
923
|
+
return result
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _decode_fp4_1d_tensor_to_float32(fp4_tensor: torch.Tensor) -> torch.Tensor:
|
|
927
|
+
"""
|
|
928
|
+
(Private) Decode a 1D FP4 tensor to float32.
|
|
929
|
+
|
|
930
|
+
Args:
|
|
931
|
+
fp4_tensor: 1D FP4 tensor with shape (K//2,)
|
|
932
|
+
|
|
933
|
+
Returns:
|
|
934
|
+
Float32 tensor with unpacked shape (K,) and contiguous layout.
|
|
935
|
+
"""
|
|
936
|
+
import torch
|
|
937
|
+
|
|
938
|
+
assert fp4_tensor.ndim == 1, f"fp4_tensor must be 1D, got {fp4_tensor.ndim}D"
|
|
939
|
+
assert fp4_tensor.dtype == torch.float4_e2m1fn_x2, f"fp4_tensor must be float4_e2m1fn_x2, got {fp4_tensor.dtype}"
|
|
940
|
+
|
|
941
|
+
# Validate stride (must be contiguous)
|
|
942
|
+
stride = fp4_tensor.stride()
|
|
943
|
+
assert stride[0] == 1, f"1D FP4 tensor must be contiguous (stride=1), but got stride={stride}"
|
|
944
|
+
|
|
945
|
+
# Create FP4 lookup table
|
|
946
|
+
fp4_lookup = _get_fp4_lookup_table(fp4_tensor.device)
|
|
947
|
+
|
|
948
|
+
# View as uint8 and extract 4-bit FP4 codes [0..15] from each byte.
|
|
949
|
+
# low needs & 0xF to mask off bits [4..7]; high doesn't because
|
|
950
|
+
# >> 4 on uint8 already zeros bits [4..7].
|
|
951
|
+
fp4_as_uint8 = fp4_tensor.view(torch.uint8)
|
|
952
|
+
low_code = (fp4_as_uint8 & 0xF).int()
|
|
953
|
+
high_code = (fp4_as_uint8 >> 4).int()
|
|
954
|
+
|
|
955
|
+
# Map codes to float values via lookup table
|
|
956
|
+
vals_low = fp4_lookup[low_code]
|
|
957
|
+
vals_high = fp4_lookup[high_code]
|
|
958
|
+
|
|
959
|
+
# Interleave: (K//2,) -> (K,)
|
|
960
|
+
# stack along last dim pairs [low_i, high_i], reshape flattens to
|
|
961
|
+
# [low0, high0, low1, high1, ...].
|
|
962
|
+
return torch.stack([vals_low, vals_high], dim=-1).reshape(-1)
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
def _decode_fp4_2d_plus_tensor_to_float32(fp4_tensor: torch.Tensor, row_wise_packing: bool) -> torch.Tensor:
|
|
966
|
+
"""
|
|
967
|
+
(Private) Decode a 2-D or higher FP4 tensor to float32.
|
|
968
|
+
|
|
969
|
+
Args:
|
|
970
|
+
fp4_tensor: FP4 tensor with shape (..., M, K//2) for row-wise
|
|
971
|
+
packing or (..., K//2, N) for column-wise packing, where ... represents zero or
|
|
972
|
+
more batch dimensions.
|
|
973
|
+
|
|
974
|
+
row_wise_packing: If True, uses row-wise packing (trailing
|
|
975
|
+
dimension is packed). If False, uses column-wise packing (second-to-last
|
|
976
|
+
dimension is packed). Determined by the caller from tensor strides.
|
|
977
|
+
|
|
978
|
+
Returns:
|
|
979
|
+
Float32 tensor with unpacked shape (..., M, K) for row-wise or (..., K, N) for
|
|
980
|
+
column-wise packing.
|
|
981
|
+
"""
|
|
982
|
+
import torch
|
|
983
|
+
|
|
984
|
+
assert fp4_tensor.ndim >= 2, f"fp4_tensor must be at least 2D, got {fp4_tensor.ndim}D"
|
|
985
|
+
assert fp4_tensor.dtype == torch.float4_e2m1fn_x2, f"fp4_tensor must be float4_e2m1fn_x2, got {fp4_tensor.dtype}"
|
|
986
|
+
|
|
987
|
+
batch_shape = fp4_tensor.shape[:-2]
|
|
988
|
+
device = fp4_tensor.device
|
|
989
|
+
|
|
990
|
+
# Create FP4 lookup table
|
|
991
|
+
fp4_lookup = _get_fp4_lookup_table(device)
|
|
992
|
+
|
|
993
|
+
# View as uint8 and extract 4-bit values (vectorized across all dims).
|
|
994
|
+
# low needs & 0xF to mask off bits [4..7]; high doesn't because
|
|
995
|
+
# >> 4 on uint8 already zeros bits [4..7].
|
|
996
|
+
fp4_as_uint8 = fp4_tensor.view(torch.uint8)
|
|
997
|
+
low_code = (fp4_as_uint8 & 0xF).int()
|
|
998
|
+
high_code = (fp4_as_uint8 >> 4).int()
|
|
999
|
+
|
|
1000
|
+
# Lookup decoded values (vectorized)
|
|
1001
|
+
vals_low = fp4_lookup[low_code]
|
|
1002
|
+
vals_high = fp4_lookup[high_code]
|
|
1003
|
+
|
|
1004
|
+
if row_wise_packing:
|
|
1005
|
+
# Row-wise: (..., M, K//2) -> (..., M, K) with row-major layout
|
|
1006
|
+
# Expand last dimension by 2
|
|
1007
|
+
expanded_shape = batch_shape + (fp4_tensor.shape[-2], fp4_tensor.shape[-1] * 2)
|
|
1008
|
+
result = torch.zeros(expanded_shape, dtype=torch.float32, device=device)
|
|
1009
|
+
result[..., 0::2] = vals_low
|
|
1010
|
+
result[..., 1::2] = vals_high
|
|
1011
|
+
else:
|
|
1012
|
+
# Column-wise: (..., K//2, N) -> (..., K, N) with column-major layout
|
|
1013
|
+
# Expand second-to-last dimension by 2
|
|
1014
|
+
rows, cols = fp4_tensor.shape[-2:]
|
|
1015
|
+
expanded_shape = batch_shape + (rows * 2, cols)
|
|
1016
|
+
|
|
1017
|
+
# Create column-major layout for batched tensors
|
|
1018
|
+
matrix_size = (rows * 2) * cols
|
|
1019
|
+
batch_size = int(np.prod(batch_shape)) if batch_shape else 1
|
|
1020
|
+
storage = torch.zeros(batch_size * matrix_size, dtype=torch.float32, device=device)
|
|
1021
|
+
# Stride: batch elements contiguous, then column-major per matrix
|
|
1022
|
+
output_stride = (matrix_size,) + (1, rows * 2)
|
|
1023
|
+
result = torch.as_strided(storage, size=(batch_size,) + (rows * 2, cols), stride=output_stride)
|
|
1024
|
+
# Flatten batch dims in vals_low/vals_high to match result shape
|
|
1025
|
+
vals_low_flat = vals_low.reshape(batch_size, rows, cols)
|
|
1026
|
+
vals_high_flat = vals_high.reshape(batch_size, rows, cols)
|
|
1027
|
+
result[:, 0::2, :] = vals_low_flat
|
|
1028
|
+
result[:, 1::2, :] = vals_high_flat
|
|
1029
|
+
result = result.reshape(expanded_shape)
|
|
1030
|
+
|
|
1031
|
+
return result
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def unpack_fp4(fp4_tensor: torch.Tensor, axis: Literal[-1, -2]) -> torch.Tensor:
|
|
1035
|
+
"""
|
|
1036
|
+
.. experimental:: function
|
|
1037
|
+
|
|
1038
|
+
Unpack an N-D torch tensor with dtype ``torch.float4_e2m1fn_x2`` to float32.
|
|
1039
|
+
|
|
1040
|
+
Since each byte stores two FP4 values, the output tensor has one dimension doubled along
|
|
1041
|
+
``axis``.
|
|
1042
|
+
|
|
1043
|
+
- ``axis=-1``: The last dimension is the packed axis. Input shape ``(..., Q)`` with
|
|
1044
|
+
row-major layout produces output ``(..., 2*Q)``.
|
|
1045
|
+
- ``axis=-2``: The second-to-last dimension is the packed axis. Input shape ``(..., P,
|
|
1046
|
+
Q)`` with column-major layout produces output ``(..., 2*P, Q)``.
|
|
1047
|
+
|
|
1048
|
+
Args:
|
|
1049
|
+
fp4_tensor: FP4 tensor with dtype ``torch.float4_e2m1fn_x2``.
|
|
1050
|
+
|
|
1051
|
+
axis: The axis along which the tensor was packed. Must be ``-1``
|
|
1052
|
+
(last dimension) or ``-2`` (second-to-last dimension).
|
|
1053
|
+
|
|
1054
|
+
Returns:
|
|
1055
|
+
A torch tensor with dtype float32 with the unpacked shape on the same device as the
|
|
1056
|
+
input.
|
|
1057
|
+
|
|
1058
|
+
.. seealso::
|
|
1059
|
+
:func:`quantize_to_fp4` — quantize and pack float32 values to FP4.
|
|
1060
|
+
"""
|
|
1061
|
+
import torch
|
|
1062
|
+
|
|
1063
|
+
if not isinstance(fp4_tensor, torch.Tensor):
|
|
1064
|
+
raise TypeError(f"fp4_tensor must be a torch.Tensor, got {type(fp4_tensor)}")
|
|
1065
|
+
if fp4_tensor.ndim < 1:
|
|
1066
|
+
raise ValueError(f"fp4_tensor must be at least 1D, got {fp4_tensor.ndim}D")
|
|
1067
|
+
if fp4_tensor.dtype != torch.float4_e2m1fn_x2:
|
|
1068
|
+
raise ValueError(f"fp4_tensor must be float4_e2m1fn_x2, got {fp4_tensor.dtype}")
|
|
1069
|
+
if axis not in (-1, -2):
|
|
1070
|
+
raise ValueError(f"axis must be -1 or -2, got {axis}")
|
|
1071
|
+
if fp4_tensor.ndim == 1 and axis != -1:
|
|
1072
|
+
raise ValueError(f"axis must be -1 for 1D tensors, got {axis}")
|
|
1073
|
+
|
|
1074
|
+
row_wise_packing = axis == -1
|
|
1075
|
+
|
|
1076
|
+
if fp4_tensor.ndim == 1:
|
|
1077
|
+
return _decode_fp4_1d_tensor_to_float32(fp4_tensor)
|
|
1078
|
+
return _decode_fp4_2d_plus_tensor_to_float32(fp4_tensor, row_wise_packing)
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
def to_block_scale(
|
|
1082
|
+
scale_tensor: torch.Tensor,
|
|
1083
|
+
operand_or_shape: torch.Tensor | tuple[int, ...],
|
|
1084
|
+
block_scaling_format: BlockScalingFormat,
|
|
1085
|
+
*,
|
|
1086
|
+
axis: Literal[-1, -2] | None = None,
|
|
1087
|
+
out: torch.Tensor | None = None,
|
|
1088
|
+
) -> torch.Tensor:
|
|
1089
|
+
"""
|
|
1090
|
+
.. experimental:: function
|
|
1091
|
+
|
|
1092
|
+
Copy ND scale tensor to flat tensor accounting for the tiled layout required by
|
|
1093
|
+
cuBLASLt.
|
|
1094
|
+
|
|
1095
|
+
Matmul (cuBLAS) expects scale factors in specific `interleaved layout
|
|
1096
|
+
<https://docs.nvidia.com/cuda/cublas/index.html#d-block-scaling-factors-layout>`_.
|
|
1097
|
+
|
|
1098
|
+
This function aims to abstract away the interleaved layout details, offering a way to
|
|
1099
|
+
specify scales as ND tensor with shape corresponding to the operand's shape and copy
|
|
1100
|
+
them to cuBLAS-compatible interleaved layout.
|
|
1101
|
+
|
|
1102
|
+
Example:
|
|
1103
|
+
Suppose that you are doing an NVFP4 matmul ``a @ b`` with ``a`` of shape (M=128,
|
|
1104
|
+
K=128). For matrix ``a``, a single scale is applied to consecutive 16 elements
|
|
1105
|
+
blocks in a row (axis=-1). You can specify the block scales as a ND tensor with
|
|
1106
|
+
shape ``(M, K // 16)`` such that scale from ``scale_tensor[i, j]`` will be applied
|
|
1107
|
+
to the block of elements ``a[i, j*16:j*16+16]`` and then call
|
|
1108
|
+
``to_block_scale(scale_tensor, a, BlockScalingFormat.NVFP4)``, which will return a
|
|
1109
|
+
1D interleaved scale tensor that can be passed as quantization scales for the
|
|
1110
|
+
matmul.
|
|
1111
|
+
|
|
1112
|
+
Note:
|
|
1113
|
+
As far as computing the block scale offset, the only difference between MXFP8 and
|
|
1114
|
+
NVFP4 is the number of elements in a block (32 for MXFP8, 16 for NVFP4).
|
|
1115
|
+
|
|
1116
|
+
Args:
|
|
1117
|
+
scale_tensor: ND scale tensor with dtype:
|
|
1118
|
+
|
|
1119
|
+
- for NVFP4: ``torch.float8_e4m3fn`` or ``torch.uint8`` (interpreted as
|
|
1120
|
+
``torch.float8_e4m3fn``)
|
|
1121
|
+
- for MXFP8: ``torch.uint8`` (interpreted as ``UE8M0``)
|
|
1122
|
+
|
|
1123
|
+
operand_or_shape: Operand tensor (that the scales apply to) or
|
|
1124
|
+
the operand's logical (non-packed, non-blocked) shape.
|
|
1125
|
+
|
|
1126
|
+
block_scaling_format: The block scaling format of the operand:
|
|
1127
|
+
:attr:`BlockScalingFormat.NVFP4` or :attr:`BlockScalingFormat.MXFP8`.
|
|
1128
|
+
Internally, it is validated to be consistent with the operand dtype, and a
|
|
1129
|
+
:exc:`ValueError` is raised if not.
|
|
1130
|
+
|
|
1131
|
+
axis: The blocked dimension of the operand tensor.
|
|
1132
|
+
For example, for NVFP4/MXFP8 matmul, A is blocked in rows (``axis = -1``), and B
|
|
1133
|
+
is blocked in columns (``axis = -2``). Depending on ``operand_or_shape``:
|
|
1134
|
+
|
|
1135
|
+
- if a *shape* is passed to ``operand_or_shape``, then ``axis`` is required
|
|
1136
|
+
- if an *operand* is passed to ``operand_or_shape``, then ``axis`` can be
|
|
1137
|
+
omitted and the blocked dimension is inferred from the operand's layout.
|
|
1138
|
+
|
|
1139
|
+
out: Output tensor to copy the scales to. If ``None``, a new tensor is created.
|
|
1140
|
+
|
|
1141
|
+
Returns:
|
|
1142
|
+
Flat ``out`` tensor containing the scales copied to match cuBLAS-compatible
|
|
1143
|
+
interleaved layout. The `out` dtype is the same as the `scale_tensor` dtype.
|
|
1144
|
+
"""
|
|
1145
|
+
import torch
|
|
1146
|
+
|
|
1147
|
+
if not isinstance(scale_tensor, torch.Tensor):
|
|
1148
|
+
raise TypeError(f"scale_tensor must be a torch.Tensor, got {type(scale_tensor)}")
|
|
1149
|
+
|
|
1150
|
+
scale_wrapped = wrap_operand(scale_tensor)
|
|
1151
|
+
num_scales = scale_wrapped.size
|
|
1152
|
+
|
|
1153
|
+
operand_shape, unblocked_axis, blocked_axis, block_scaling_format, num_scalars_in_block = (
|
|
1154
|
+
_validate_shape_axes_block_scaling_format(operand_or_shape, axis, block_scaling_format)
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
_validate_scale_dtype_block_scaling_format_compatibility(scale_wrapped.dtype, block_scaling_format, "scale_tensor")
|
|
1158
|
+
|
|
1159
|
+
num_scales = scale_wrapped.size
|
|
1160
|
+
expected_num_scales = math.prod(operand_shape) // num_scalars_in_block
|
|
1161
|
+
if num_scales != expected_num_scales:
|
|
1162
|
+
raise ValueError(
|
|
1163
|
+
f"For operand of shape {operand_shape}, and block_scaling_format {block_scaling_format}, "
|
|
1164
|
+
f"the scale_tensor must have shape {expected_num_scales}, got {num_scales} (shape:{scale_wrapped.shape})."
|
|
1165
|
+
)
|
|
1166
|
+
|
|
1167
|
+
if out is None:
|
|
1168
|
+
out = torch.empty(
|
|
1169
|
+
num_scales,
|
|
1170
|
+
device=scale_wrapped.tensor.device,
|
|
1171
|
+
dtype=scale_wrapped.tensor.dtype,
|
|
1172
|
+
)
|
|
1173
|
+
else:
|
|
1174
|
+
if out.ndim != 1:
|
|
1175
|
+
raise ValueError(f"out must be a 1D tensor, got {out.ndim}D tensor with shape {out.shape}")
|
|
1176
|
+
if out.shape[0] != num_scales:
|
|
1177
|
+
raise ValueError(
|
|
1178
|
+
f"Flat scale tensor (out) and ND scale_tensor must have the same number "
|
|
1179
|
+
f"of elements, got {out.shape[0]} and {num_scales}"
|
|
1180
|
+
)
|
|
1181
|
+
if out.dtype != scale_wrapped.tensor.dtype:
|
|
1182
|
+
raise ValueError(
|
|
1183
|
+
f"Flat scale tensor (out) and ND scale_tensor must "
|
|
1184
|
+
f"have the same dtype, got {out.dtype} and {scale_wrapped.dtype}"
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
matrix_shape, matrix_strides, matrix_logical_shape = _scales_nd_matrix_tiled_layout(
|
|
1188
|
+
operand_shape, unblocked_axis, blocked_axis, num_scalars_in_block, False
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
if scale_wrapped.shape != matrix_logical_shape:
|
|
1192
|
+
raise ValueError(
|
|
1193
|
+
f"For operand of shape {operand_shape}, block_scaling_format {block_scaling_format}, "
|
|
1194
|
+
f"blocked along axis {axis}, "
|
|
1195
|
+
f"the scale_tensor must have shape {matrix_logical_shape}, got {scale_wrapped.shape}."
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
torch.as_strided(
|
|
1199
|
+
out,
|
|
1200
|
+
size=matrix_shape,
|
|
1201
|
+
stride=matrix_strides,
|
|
1202
|
+
).copy_(scale_wrapped.tensor.view(matrix_shape))
|
|
1203
|
+
return out
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def expand_block_scale(
|
|
1207
|
+
scales_1d: torch.Tensor,
|
|
1208
|
+
operand_or_shape: torch.Tensor | tuple[int, ...],
|
|
1209
|
+
block_scaling_format: BlockScalingFormat,
|
|
1210
|
+
*,
|
|
1211
|
+
axis: Literal[-1, -2] | None = None,
|
|
1212
|
+
output_dtype: Literal["smallest"] | torch.dtype = "smallest",
|
|
1213
|
+
device: Literal["cuda", "cpu"] | None = None,
|
|
1214
|
+
) -> torch.Tensor:
|
|
1215
|
+
"""
|
|
1216
|
+
.. experimental:: function
|
|
1217
|
+
|
|
1218
|
+
Expand NVFP4/MXFP8 block scales from 1D cuBLAS-compatible interleaved array to the full
|
|
1219
|
+
operand shape.
|
|
1220
|
+
|
|
1221
|
+
Matmul (cuBLAS) expects and returns the block scale factors in specific `interleaved
|
|
1222
|
+
layout
|
|
1223
|
+
<https://docs.nvidia.com/cuda/cublas/index.html#d-block-scaling-factors-layout>`_.
|
|
1224
|
+
|
|
1225
|
+
This function takes that 1D interleaved scale array (either provided as input or
|
|
1226
|
+
returned by cuBLASLt for NVFP4/MXFP8 output) and expands it to a full ND tensor with
|
|
1227
|
+
shape ``operand_or_shape`` where each element gets its corresponding scale value. This
|
|
1228
|
+
can be useful, for example, to manually dequantize the result of a matmul, by
|
|
1229
|
+
elementwise multiplication of the expanded scales with the result.
|
|
1230
|
+
|
|
1231
|
+
Args:
|
|
1232
|
+
scales_1d: 1D tensor of scale values with dtype:
|
|
1233
|
+
|
|
1234
|
+
- for NVFP4: ``torch.float8_e4m3fn``, or ``torch.uint8`` (interpreted as
|
|
1235
|
+
``torch.float8_e4m3fn``)
|
|
1236
|
+
- for MXFP8: ``torch.uint8``, interpreted as exponent (``UE8M0``)
|
|
1237
|
+
|
|
1238
|
+
The scales are expected to be stored in cuBLAS-compatible interleaved layout
|
|
1239
|
+
(e.g. as returned by matmul's ``d_out_scale``). The number of elements in the
|
|
1240
|
+
tensor must be equal to the number of elements in the operand tensor, divided by
|
|
1241
|
+
the number of elements in a block (for NVFP4: 16, for MXFP8: 32).
|
|
1242
|
+
|
|
1243
|
+
operand_or_shape: Operand tensor or its logical (non-packed, non-blocked) shape.
|
|
1244
|
+
The scales are expanded to match this shape.
|
|
1245
|
+
|
|
1246
|
+
block_scaling_format: The block scaling format of the operand:
|
|
1247
|
+
:attr:`BlockScalingFormat.NVFP4` or :attr:`BlockScalingFormat.MXFP8`.
|
|
1248
|
+
Internally, it is validated to be consistent with the operand dtype, and a
|
|
1249
|
+
:exc:`ValueError` is raised if not.
|
|
1250
|
+
|
|
1251
|
+
axis: The blocked dimension of the operand tensor.
|
|
1252
|
+
For example, for NVFP4/MXFP8 matmul, A is blocked in rows (``axis = -1``), and B
|
|
1253
|
+
is blocked in columns (``axis = -2``). Depending on ``operand_or_shape``:
|
|
1254
|
+
|
|
1255
|
+
- if a *shape* is passed to ``operand_or_shape``, then ``axis`` is required
|
|
1256
|
+
- if an *operand* is passed to ``operand_or_shape``, then ``axis`` can be
|
|
1257
|
+
omitted and the blocked dimension is inferred from the operand's layout.
|
|
1258
|
+
|
|
1259
|
+
output_dtype: Output dtype.
|
|
1260
|
+
If provided, must be a torch's dtype:
|
|
1261
|
+
|
|
1262
|
+
- for NVFP4: ``float8_e4m3fn``, ``float16``, ``float32``, or ``float64``
|
|
1263
|
+
- for MXFP8: ``uint8`` (exponent ``UE8M0``), ``float16``, ``float32``, or
|
|
1264
|
+
``float64``
|
|
1265
|
+
|
|
1266
|
+
It must be wide enough to represent the result, or :exc:`ValueError` is raised.
|
|
1267
|
+
If 'smallest' (default), the smallest of accepted dtypes that can represent the
|
|
1268
|
+
result is automatically chosen (for MXFP8: ``uint8`` interpreted as exponent
|
|
1269
|
+
(``UE8M0``), for NVFP4: ``float8_e4m3fn``).
|
|
1270
|
+
|
|
1271
|
+
device: Device for the output tensor. When ``None`` (default), the
|
|
1272
|
+
device is inferred from ``scales_1d``. When specified, must be ``"cuda"`` or
|
|
1273
|
+
``"cpu"``.
|
|
1274
|
+
|
|
1275
|
+
Returns:
|
|
1276
|
+
Tensor with shape ``operand_or_shape`` (and dtype as specified by ``output_dtype``)
|
|
1277
|
+
containing expanded scales, on the target device. Each element contains the scale
|
|
1278
|
+
value that applies to the corresponding position in the FP4/FP8 matrix.
|
|
1279
|
+
|
|
1280
|
+
Note:
|
|
1281
|
+
For computing a single scale index rather than expanding all scales, use
|
|
1282
|
+
:func:`get_block_scale_offset` instead.
|
|
1283
|
+
"""
|
|
1284
|
+
import torch
|
|
1285
|
+
|
|
1286
|
+
_COMMON_EXPAND_OUTPUT_DTYPES = (torch.float16, torch.float32, torch.float64)
|
|
1287
|
+
|
|
1288
|
+
expanded = _expand_block_scale(scales_1d, operand_or_shape, block_scaling_format, axis, device)
|
|
1289
|
+
assert expanded.dtype == torch.uint8
|
|
1290
|
+
|
|
1291
|
+
scale_interpretation = _MICROSCALING_FORMAT_PROPERTIES[block_scaling_format]["scale_interpretation"]
|
|
1292
|
+
|
|
1293
|
+
if scale_interpretation == "float8_e4m3fn":
|
|
1294
|
+
expanded = expanded.view(torch.float8_e4m3fn)
|
|
1295
|
+
if output_dtype == "smallest" or output_dtype == torch.float8_e4m3fn:
|
|
1296
|
+
return expanded
|
|
1297
|
+
elif output_dtype in _COMMON_EXPAND_OUTPUT_DTYPES:
|
|
1298
|
+
return expanded.type(output_dtype)
|
|
1299
|
+
else:
|
|
1300
|
+
supported_dtypes_str = ", ".join([str(dt) for dt in (torch.float8_e4m3fn,) + _COMMON_EXPAND_OUTPUT_DTYPES])
|
|
1301
|
+
raise TypeError(f"output_dtype must be 'smallest' or one of {supported_dtypes_str}. Got {output_dtype}")
|
|
1302
|
+
elif scale_interpretation == "ue8m0":
|
|
1303
|
+
if output_dtype == "smallest" or output_dtype == torch.uint8:
|
|
1304
|
+
return expanded
|
|
1305
|
+
elif output_dtype in _COMMON_EXPAND_OUTPUT_DTYPES:
|
|
1306
|
+
# TODO: This could be optimized - we can assess the exponents
|
|
1307
|
+
# keeping uint8 type and directly convert to output dtype.
|
|
1308
|
+
expanded = _convert_uint8_ue8m0_scale_to_float64(expanded)
|
|
1309
|
+
return _convert_to_output_dtype(expanded, output_dtype)
|
|
1310
|
+
else:
|
|
1311
|
+
supported_dtypes_str = ", ".join([str(dt) for dt in (torch.uint8,) + _COMMON_EXPAND_OUTPUT_DTYPES])
|
|
1312
|
+
raise TypeError(f"output_dtype must be 'smallest' or one of {supported_dtypes_str}. Got {output_dtype}")
|
|
1313
|
+
else:
|
|
1314
|
+
raise AssertionError(
|
|
1315
|
+
f"Unknown scale_interpretation '{scale_interpretation}' for block_scaling_format '{block_scaling_format}'"
|
|
1316
|
+
)
|