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.
Files changed (332) 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 +130 -0
  7. nvmath/_internal/threadsafe.py +106 -0
  8. nvmath/_internal/utils.py +43 -0
  9. nvmath/_internal/workspace.py +490 -0
  10. nvmath/_utils.py +147 -0
  11. nvmath/bindings/__init__.py +60 -0
  12. nvmath/bindings/_internal/__init__.pxd +0 -0
  13. nvmath/bindings/_internal/__init__.py +0 -0
  14. nvmath/bindings/_internal/common_types.pxd +31 -0
  15. nvmath/bindings/_internal/cublas.cp314t-win_amd64.pyd +0 -0
  16. nvmath/bindings/_internal/cublas.pxd +530 -0
  17. nvmath/bindings/_internal/cublasLt.cp314t-win_amd64.pyd +0 -0
  18. nvmath/bindings/_internal/cublasLt.pxd +59 -0
  19. nvmath/bindings/_internal/cublasMp.pxd +52 -0
  20. nvmath/bindings/_internal/cudss.cp314t-win_amd64.pyd +0 -0
  21. nvmath/bindings/_internal/cudss.pxd +54 -0
  22. nvmath/bindings/_internal/cufft.cp314t-win_amd64.pyd +0 -0
  23. nvmath/bindings/_internal/cufft.pxd +70 -0
  24. nvmath/bindings/_internal/cufftMp.pxd +77 -0
  25. nvmath/bindings/_internal/curand.cp314t-win_amd64.pyd +0 -0
  26. nvmath/bindings/_internal/curand.pxd +42 -0
  27. nvmath/bindings/_internal/cusolver.cp314t-win_amd64.pyd +0 -0
  28. nvmath/bindings/_internal/cusolver.pxd +15 -0
  29. nvmath/bindings/_internal/cusolverDn.cp314t-win_amd64.pyd +0 -0
  30. nvmath/bindings/_internal/cusolverDn.pxd +406 -0
  31. nvmath/bindings/_internal/cusolverMp.pxd +71 -0
  32. nvmath/bindings/_internal/cusolverSp.cp314t-win_amd64.pyd +0 -0
  33. nvmath/bindings/_internal/cusolverSp.pxd +75 -0
  34. nvmath/bindings/_internal/cusparse.cp314t-win_amd64.pyd +0 -0
  35. nvmath/bindings/_internal/cusparse.pxd +471 -0
  36. nvmath/bindings/_internal/cusparseLt.cp314t-win_amd64.pyd +0 -0
  37. nvmath/bindings/_internal/cusparseLt.pxd +48 -0
  38. nvmath/bindings/_internal/cutensor.cp314t-win_amd64.pyd +0 -0
  39. nvmath/bindings/_internal/cutensor.pxd +58 -0
  40. nvmath/bindings/_internal/mathdx.cp314t-win_amd64.pyd +0 -0
  41. nvmath/bindings/_internal/mathdx.pxd +116 -0
  42. nvmath/bindings/_internal/nvshmem.pxd +29 -0
  43. nvmath/bindings/_internal/utils.cp314t-win_amd64.pyd +0 -0
  44. nvmath/bindings/_internal/utils.pxd +174 -0
  45. nvmath/bindings/_internal/utils.pyi +10 -0
  46. nvmath/bindings/cublas.cp314t-win_amd64.pyd +0 -0
  47. nvmath/bindings/cublas.pxd +558 -0
  48. nvmath/bindings/cublas.pyi +812 -0
  49. nvmath/bindings/cublasLt.cp314t-win_amd64.pyd +0 -0
  50. nvmath/bindings/cublasLt.pxd +109 -0
  51. nvmath/bindings/cublasLt.pyi +1461 -0
  52. nvmath/bindings/cublasMp.pxd +85 -0
  53. nvmath/bindings/cublasMp.pyi +267 -0
  54. nvmath/bindings/cudss.cp314t-win_amd64.pyd +0 -0
  55. nvmath/bindings/cudss.pxd +98 -0
  56. nvmath/bindings/cudss.pyi +443 -0
  57. nvmath/bindings/cufft.cp314t-win_amd64.pyd +0 -0
  58. nvmath/bindings/cufft.pxd +118 -0
  59. nvmath/bindings/cufft.pyi +301 -0
  60. nvmath/bindings/cufftMp.pxd +124 -0
  61. nvmath/bindings/cufftMp.pyi +326 -0
  62. nvmath/bindings/curand.cp314t-win_amd64.pyd +0 -0
  63. nvmath/bindings/curand.pxd +71 -0
  64. nvmath/bindings/curand.pyi +189 -0
  65. nvmath/bindings/cusolver.cp314t-win_amd64.pyd +0 -0
  66. nvmath/bindings/cusolver.pxd +62 -0
  67. nvmath/bindings/cusolver.pyi +320 -0
  68. nvmath/bindings/cusolverDn.cp314t-win_amd64.pyd +0 -0
  69. nvmath/bindings/cusolverDn.pxd +430 -0
  70. nvmath/bindings/cusolverDn.pyi +422 -0
  71. nvmath/bindings/cusolverMp.pxd +98 -0
  72. nvmath/bindings/cusolverMp.pyi +114 -0
  73. nvmath/bindings/cusolverSp.cp314t-win_amd64.pyd +0 -0
  74. nvmath/bindings/cusolverSp.pxd +95 -0
  75. nvmath/bindings/cusolverSp.pyi +70 -0
  76. nvmath/bindings/cusparse.cp314t-win_amd64.pyd +0 -0
  77. nvmath/bindings/cusparse.pxd +546 -0
  78. nvmath/bindings/cusparse.pyi +1017 -0
  79. nvmath/bindings/cusparseLt.cp314t-win_amd64.pyd +0 -0
  80. nvmath/bindings/cusparseLt.pxd +99 -0
  81. nvmath/bindings/cusparseLt.pyi +252 -0
  82. nvmath/bindings/cutensor.cp314t-win_amd64.pyd +0 -0
  83. nvmath/bindings/cutensor.pxd +98 -0
  84. nvmath/bindings/cutensor.pyi +324 -0
  85. nvmath/bindings/cycublas.cp314t-win_amd64.pyd +0 -0
  86. nvmath/bindings/cycublas.pxd +664 -0
  87. nvmath/bindings/cycublasLt.cp314t-win_amd64.pyd +0 -0
  88. nvmath/bindings/cycublasLt.pxd +1045 -0
  89. nvmath/bindings/cycublasMp.pxd +171 -0
  90. nvmath/bindings/cycudss.cp314t-win_amd64.pyd +0 -0
  91. nvmath/bindings/cycudss.pxd +277 -0
  92. nvmath/bindings/cycufft.cp314t-win_amd64.pyd +0 -0
  93. nvmath/bindings/cycufft.pxd +333 -0
  94. nvmath/bindings/cycufftMp.pxd +342 -0
  95. nvmath/bindings/cycurand.cp314t-win_amd64.pyd +0 -0
  96. nvmath/bindings/cycurand.pxd +141 -0
  97. nvmath/bindings/cycusolver.cp314t-win_amd64.pyd +0 -0
  98. nvmath/bindings/cycusolver.pxd +137 -0
  99. nvmath/bindings/cycusolverDn.cp314t-win_amd64.pyd +0 -0
  100. nvmath/bindings/cycusolverDn.pxd +443 -0
  101. nvmath/bindings/cycusolverMp.pxd +107 -0
  102. nvmath/bindings/cycusolverSp.cp314t-win_amd64.pyd +0 -0
  103. nvmath/bindings/cycusolverSp.pxd +93 -0
  104. nvmath/bindings/cycusparse.cp314t-win_amd64.pyd +0 -0
  105. nvmath/bindings/cycusparse.pxd +679 -0
  106. nvmath/bindings/cycusparseLt.cp314t-win_amd64.pyd +0 -0
  107. nvmath/bindings/cycusparseLt.pxd +135 -0
  108. nvmath/bindings/cycutensor.cp314t-win_amd64.pyd +0 -0
  109. nvmath/bindings/cycutensor.pxd +189 -0
  110. nvmath/bindings/cymathdx.cp314t-win_amd64.pyd +0 -0
  111. nvmath/bindings/cymathdx.pxd +552 -0
  112. nvmath/bindings/cynvshmem.pxd +118 -0
  113. nvmath/bindings/mathdx.cp314t-win_amd64.pyd +0 -0
  114. nvmath/bindings/mathdx.pxd +182 -0
  115. nvmath/bindings/mathdx.pyi +1562 -0
  116. nvmath/bindings/nvpl/__init__.pxd +0 -0
  117. nvmath/bindings/nvpl/__init__.py +13 -0
  118. nvmath/bindings/nvpl/_internal/__init__.pxd +0 -0
  119. nvmath/bindings/nvpl/_internal/__init__.py +0 -0
  120. nvmath/bindings/nvpl/_internal/blas.cp314t-win_amd64.pyd +0 -0
  121. nvmath/bindings/nvpl/_internal/blas.pxd +237 -0
  122. nvmath/bindings/nvpl/_internal/fft.cp314t-win_amd64.pyd +0 -0
  123. nvmath/bindings/nvpl/_internal/fft.pxd +36 -0
  124. nvmath/bindings/nvpl/blas.cp314t-win_amd64.pyd +0 -0
  125. nvmath/bindings/nvpl/blas.pxd +131 -0
  126. nvmath/bindings/nvpl/blas.pyi +168 -0
  127. nvmath/bindings/nvpl/cyblas.cp314t-win_amd64.pyd +0 -0
  128. nvmath/bindings/nvpl/cyblas.pxd +280 -0
  129. nvmath/bindings/nvpl/cyfft.cp314t-win_amd64.pyd +0 -0
  130. nvmath/bindings/nvpl/cyfft.pxd +93 -0
  131. nvmath/bindings/nvpl/fft.cp314t-win_amd64.pyd +0 -0
  132. nvmath/bindings/nvpl/fft.pxd +100 -0
  133. nvmath/bindings/nvpl/fft.pyi +168 -0
  134. nvmath/bindings/nvshmem.pxd +54 -0
  135. nvmath/bindings/nvshmem.pyi +191 -0
  136. nvmath/device/__init__.py +38 -0
  137. nvmath/device/_deprecated.py +33 -0
  138. nvmath/device/common.py +315 -0
  139. nvmath/device/common_backend.py +131 -0
  140. nvmath/device/common_cuda.py +201 -0
  141. nvmath/device/common_numba.py +300 -0
  142. nvmath/device/common_numba_cuda_mlir.py +202 -0
  143. nvmath/device/common_opaque_tensor.py +201 -0
  144. nvmath/device/cublasdx.py +1606 -0
  145. nvmath/device/cublasdx_backend.py +860 -0
  146. nvmath/device/cublasdx_numba.py +1534 -0
  147. nvmath/device/cublasdx_numba_cuda_mlir.py +208 -0
  148. nvmath/device/cufftdx.py +373 -0
  149. nvmath/device/cufftdx_backend.py +220 -0
  150. nvmath/device/cufftdx_numba.py +140 -0
  151. nvmath/device/cufftdx_numba_cuda_mlir.py +79 -0
  152. nvmath/device/curand_kernel.py +9147 -0
  153. nvmath/device/cusolverdx.py +2708 -0
  154. nvmath/device/cusolverdx_backend.py +440 -0
  155. nvmath/device/cusolverdx_numba.py +567 -0
  156. nvmath/device/cusolverdx_numba_cuda_mlir.py +604 -0
  157. nvmath/device/cusolverdx_overload_backend.py +1029 -0
  158. nvmath/device/llvm_array.py +29 -0
  159. nvmath/device/random.py +441 -0
  160. nvmath/device/random_helpers.py +23 -0
  161. nvmath/device/random_states.py +187 -0
  162. nvmath/device/types.py +138 -0
  163. nvmath/device/vector_types_numba.py +259 -0
  164. nvmath/distributed/__init__.py +200 -0
  165. nvmath/distributed/_internal/__init__.py +0 -0
  166. nvmath/distributed/_internal/nccl.py +86 -0
  167. nvmath/distributed/_internal/nvshmem.py +307 -0
  168. nvmath/distributed/_internal/symmetric_memory.py +35 -0
  169. nvmath/distributed/_internal/tensor_ifc.py +70 -0
  170. nvmath/distributed/_internal/tensor_ifc_cupy.py +68 -0
  171. nvmath/distributed/_internal/tensor_ifc_host_device.py +172 -0
  172. nvmath/distributed/_internal/tensor_ifc_numpy.py +46 -0
  173. nvmath/distributed/_internal/tensor_ifc_torch.py +162 -0
  174. nvmath/distributed/_internal/tensor_wrapper.py +81 -0
  175. nvmath/distributed/_utils.py +167 -0
  176. nvmath/distributed/distribution/__init__.py +30 -0
  177. nvmath/distributed/distribution/_configuration.py +39 -0
  178. nvmath/distributed/distribution/distributions.py +1024 -0
  179. nvmath/distributed/distribution/redistribute.py +1284 -0
  180. nvmath/distributed/fft/__init__.py +7 -0
  181. nvmath/distributed/fft/_configuration.py +82 -0
  182. nvmath/distributed/fft/fft.py +2742 -0
  183. nvmath/distributed/linalg/__init__.py +22 -0
  184. nvmath/distributed/linalg/_internal/__init__.py +3 -0
  185. nvmath/distributed/linalg/_internal/epilog_protocol.py +586 -0
  186. nvmath/distributed/linalg/_internal/matmul_desc_ifc.py +28 -0
  187. nvmath/distributed/linalg/advanced/__init__.py +8 -0
  188. nvmath/distributed/linalg/advanced/_configuration.py +171 -0
  189. nvmath/distributed/linalg/advanced/matmulmod.py +3573 -0
  190. nvmath/distributed/linalg/generic/__init__.py +8 -0
  191. nvmath/distributed/linalg/generic/_caching.py +66 -0
  192. nvmath/distributed/linalg/generic/_configuration.py +61 -0
  193. nvmath/distributed/linalg/generic/_factorization.py +172 -0
  194. nvmath/distributed/linalg/generic/_initialization.py +966 -0
  195. nvmath/distributed/linalg/generic/_problem_spec.py +511 -0
  196. nvmath/distributed/linalg/generic/solvermod.py +1368 -0
  197. nvmath/distributed/process_group.py +408 -0
  198. nvmath/fft/__init__.py +7 -0
  199. nvmath/fft/_configuration.py +189 -0
  200. nvmath/fft/_exec_utils.py +82 -0
  201. nvmath/fft/_helpers.py +237 -0
  202. nvmath/fft/fft.py +3122 -0
  203. nvmath/internal/__init__.pxd +3 -0
  204. nvmath/internal/__init__.py +10 -0
  205. nvmath/internal/_bindings.cp314t-win_amd64.pyd +0 -0
  206. nvmath/internal/_bindings.pxd +18 -0
  207. nvmath/internal/_device_utils.py +45 -0
  208. nvmath/internal/_layout/__init__.pxd +3 -0
  209. nvmath/internal/_layout/__init__.py +7 -0
  210. nvmath/internal/_layout/_layout.cp314t-win_amd64.pyd +0 -0
  211. nvmath/internal/_layout/_layout.pxd +1303 -0
  212. nvmath/internal/_layout/_layout.pyi +1145 -0
  213. nvmath/internal/enum_utils.py +142 -0
  214. nvmath/internal/formatters.py +87 -0
  215. nvmath/internal/mem_limit.py +51 -0
  216. nvmath/internal/memory.cp314t-win_amd64.pyd +0 -0
  217. nvmath/internal/memory.pxd +13 -0
  218. nvmath/internal/memory.pyi +50 -0
  219. nvmath/internal/ndbuffer/__init__.pxd +3 -0
  220. nvmath/internal/ndbuffer/__init__.py +9 -0
  221. nvmath/internal/ndbuffer/_copy_kernel.cp314t-win_amd64.pyd +0 -0
  222. nvmath/internal/ndbuffer/_copy_kernel.pxd +10 -0
  223. nvmath/internal/ndbuffer/_jit.cp314t-win_amd64.pyd +0 -0
  224. nvmath/internal/ndbuffer/_jit.pxd +7 -0
  225. nvmath/internal/ndbuffer/_ndbuffer.cp314t-win_amd64.pyd +0 -0
  226. nvmath/internal/ndbuffer/_ndbuffer.pxd +40 -0
  227. nvmath/internal/ndbuffer/_ndbuffer.pyi +463 -0
  228. nvmath/internal/ndbuffer/copy_kernel/args.h +34 -0
  229. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/array_view.h +52 -0
  230. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/elementwise.h +68 -0
  231. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/grid_indexer.h +69 -0
  232. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/transposed.h +242 -0
  233. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/type_utils.h +39 -0
  234. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/utils.h +132 -0
  235. nvmath/internal/ndbuffer/copy_kernel/copy_kernel_impl/vec.h +159 -0
  236. nvmath/internal/ndbuffer/copy_kernel/elementwise.h +53 -0
  237. nvmath/internal/ndbuffer/copy_kernel/transposed.h +58 -0
  238. nvmath/internal/package_ifc.py +168 -0
  239. nvmath/internal/package_ifc_cuda.py +57 -0
  240. nvmath/internal/package_ifc_cupy.py +67 -0
  241. nvmath/internal/package_ifc_torch.py +69 -0
  242. nvmath/internal/package_wrapper.py +14 -0
  243. nvmath/internal/tensor_ifc.py +179 -0
  244. nvmath/internal/tensor_ifc_cupy.py +234 -0
  245. nvmath/internal/tensor_ifc_ndbuffer.py +147 -0
  246. nvmath/internal/tensor_ifc_numpy.py +184 -0
  247. nvmath/internal/tensor_ifc_torch.py +178 -0
  248. nvmath/internal/tensor_wrapper.py +160 -0
  249. nvmath/internal/typemaps.py +113 -0
  250. nvmath/internal/utils.py +805 -0
  251. nvmath/linalg/__init__.py +56 -0
  252. nvmath/linalg/_internal/__init__.py +3 -0
  253. nvmath/linalg/_internal/algo_cap_ifc.py +82 -0
  254. nvmath/linalg/_internal/algo_config_ifc.py +43 -0
  255. nvmath/linalg/_internal/batch.py +234 -0
  256. nvmath/linalg/_internal/enum_to_tuples.py +64 -0
  257. nvmath/linalg/_internal/epilog_protocol.py +766 -0
  258. nvmath/linalg/_internal/layout.py +624 -0
  259. nvmath/linalg/_internal/matmul_desc_ifc.py +28 -0
  260. nvmath/linalg/_internal/matmul_pref_ifc.py +27 -0
  261. nvmath/linalg/_internal/matrix_layout_ifc.py +26 -0
  262. nvmath/linalg/_internal/solver_utils.py +432 -0
  263. nvmath/linalg/_internal/typemaps.py +144 -0
  264. nvmath/linalg/_internal/utils.py +157 -0
  265. nvmath/linalg/advanced/__init__.py +8 -0
  266. nvmath/linalg/advanced/_algorithmmod.py +170 -0
  267. nvmath/linalg/advanced/_configuration.py +351 -0
  268. nvmath/linalg/advanced/helpers/__init__.py +5 -0
  269. nvmath/linalg/advanced/helpers/matmul.py +1316 -0
  270. nvmath/linalg/advanced/matmulmod.py +3734 -0
  271. nvmath/linalg/generic/__init__.py +53 -0
  272. nvmath/linalg/generic/_configuration/__init__.py +39 -0
  273. nvmath/linalg/generic/_configuration/layout.py +263 -0
  274. nvmath/linalg/generic/_configuration/match.py +734 -0
  275. nvmath/linalg/generic/_configuration/qualifiers.py +493 -0
  276. nvmath/linalg/generic/_configuration/solver_configuration.py +59 -0
  277. nvmath/linalg/generic/_configuration/wrap.py +217 -0
  278. nvmath/linalg/generic/_dtype.py +15 -0
  279. nvmath/linalg/generic/matmulmod.py +2094 -0
  280. nvmath/linalg/generic/solvermod.py +1301 -0
  281. nvmath/memory.py +279 -0
  282. nvmath/sparse/__init__.py +38 -0
  283. nvmath/sparse/_internal/__init__.py +21 -0
  284. nvmath/sparse/_internal/common_utils.py +147 -0
  285. nvmath/sparse/_internal/cudss_config_ifc.py +702 -0
  286. nvmath/sparse/_internal/cudss_data_ifc.py +399 -0
  287. nvmath/sparse/_internal/cudss_utils.py +506 -0
  288. nvmath/sparse/_internal/cusparse_utils.py +382 -0
  289. nvmath/sparse/_internal/sparse_bsc_ifc.py +303 -0
  290. nvmath/sparse/_internal/sparse_bsr_ifc.py +305 -0
  291. nvmath/sparse/_internal/sparse_coo_ifc.py +256 -0
  292. nvmath/sparse/_internal/sparse_csc_ifc.py +268 -0
  293. nvmath/sparse/_internal/sparse_csr_ifc.py +288 -0
  294. nvmath/sparse/_internal/sparse_dia_ifc.py +242 -0
  295. nvmath/sparse/_internal/sparse_format_helpers.py +601 -0
  296. nvmath/sparse/_internal/sparse_tensor_ifc.py +133 -0
  297. nvmath/sparse/_internal/sparse_ust_ifc.py +141 -0
  298. nvmath/sparse/_internal/utils.py +56 -0
  299. nvmath/sparse/advanced/__init__.py +7 -0
  300. nvmath/sparse/advanced/_configuration.py +227 -0
  301. nvmath/sparse/advanced/direct_solver.py +2069 -0
  302. nvmath/sparse/generic/__init__.py +7 -0
  303. nvmath/sparse/generic/_configuration.py +129 -0
  304. nvmath/sparse/generic/_helpers.py +137 -0
  305. nvmath/sparse/generic/_thunks.py +21 -0
  306. nvmath/sparse/generic/matmulmod.py +2353 -0
  307. nvmath/sparse/ust/__init__.py +7 -0
  308. nvmath/sparse/ust/_converters.py +422 -0
  309. nvmath/sparse/ust/_cpp.py +28 -0
  310. nvmath/sparse/ust/_drawer.py +565 -0
  311. nvmath/sparse/ust/_emitter.py +1033 -0
  312. nvmath/sparse/ust/_jit.py +188 -0
  313. nvmath/sparse/ust/_kernel.py +282 -0
  314. nvmath/sparse/ust/_utils.py +149 -0
  315. nvmath/sparse/ust/interfaces/__init__.py +0 -0
  316. nvmath/sparse/ust/interfaces/torch_interface.py +476 -0
  317. nvmath/sparse/ust/tensor.py +1016 -0
  318. nvmath/sparse/ust/tensor_format.py +957 -0
  319. nvmath/tensor/__init__.py +6 -0
  320. nvmath/tensor/_configuration.py +120 -0
  321. nvmath/tensor/_internal/__init__.py +3 -0
  322. nvmath/tensor/_internal/cutensor_config_ifc.py +279 -0
  323. nvmath/tensor/_internal/cutensor_utils.py +230 -0
  324. nvmath/tensor/_internal/data.py +43 -0
  325. nvmath/tensor/_internal/einsum_parser.py +444 -0
  326. nvmath/tensor/_internal/typemaps.py +96 -0
  327. nvmath/tensor/contract.py +1900 -0
  328. nvmath_python-1.0.0.dist-info/METADATA +134 -0
  329. nvmath_python-1.0.0.dist-info/RECORD +332 -0
  330. nvmath_python-1.0.0.dist-info/WHEEL +5 -0
  331. nvmath_python-1.0.0.dist-info/licenses/LICENSE +177 -0
  332. 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
+ )