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