utils.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. """
  2. Copyright (c) 2025 by the b12x authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. """
  13. import ctypes
  14. import functools
  15. import importlib.util
  16. from typing import Union, Tuple
  17. import cuda.bindings.driver as cuda
  18. import cutlass
  19. import cutlass._mlir.dialects.cute as _cute_ir
  20. import cutlass.cute as cute
  21. import torch
  22. from cutlass._mlir import ir
  23. from cutlass.cutlass_dsl import dsl_user_op
  24. from cutlass.cute.typing import AddressSpace, Numeric, Pointer, Type
  25. def ceil_div(a: int, b: int) -> int:
  26. """Ceiling division."""
  27. return (a + b - 1) // b
  28. def is_cute_dsl_available() -> bool:
  29. return (
  30. importlib.util.find_spec("cutlass") is not None
  31. and importlib.util.find_spec("cutlass.cute") is not None
  32. )
  33. # MX-FP6 (W6A6/W6A8) uses sf_vec_size=32 and m16n8k32 MMA (see b12x._lib.fp6).
  34. MXFP6_SF_VEC_SIZE = 32
  35. MXFP6_MMA_K = 32
  36. MXFP6_SF_DTYPE_STR = "float8_e8m0fnu"
  37. MXFP6_AB_DTYPE_STRINGS = frozenset({"float6_e3m2fn", "float6_e2m3fn"})
  38. def get_cutlass_dtype(dtype: str) -> cutlass.dtype:
  39. dtype_map = {
  40. "float16": cutlass.Float16,
  41. "bfloat16": cutlass.BFloat16,
  42. "float32": cutlass.Float32,
  43. "float8_e5m2": cutlass.Float8E5M2,
  44. "float8_e4m3fn": cutlass.Float8E4M3FN,
  45. "float8_e8m0fnu": cutlass.Float8E8M0FNU,
  46. "float4_e2m1fn": cutlass.Float4E2M1FN,
  47. "float6_e3m2fn": cutlass.Float6E3M2FN,
  48. "float6_e2m3fn": cutlass.Float6E2M3FN,
  49. }
  50. try:
  51. return dtype_map[dtype]
  52. except KeyError as exc:
  53. raise KeyError(f"unsupported cutlass dtype string: {dtype!r}") from exc
  54. def cutlass_to_torch_dtype(cutlass_dtype):
  55. """Return the Torch dtype used to store tensors for a CUTLASS element type.
  56. MX-FP6 element types (``Float6E3M2FN`` / ``Float6E2M3FN``) map to ``torch.uint8``
  57. because PyTorch has no packed FP6 view. Kernels receive the logical element type via
  58. compile-time ``_gptr`` / pointer element-type injection (same pattern as NVFP4/dlpack).
  59. """
  60. torch_dtype = getattr(torch, cutlass_dtype.__name__.lower(), None)
  61. torch_type_map = {
  62. cutlass.TFloat32: torch.float32,
  63. cutlass.Float32: torch.float32,
  64. cutlass.Float16: torch.float16,
  65. cutlass.BFloat16: torch.bfloat16,
  66. cutlass.Float8E5M2: torch.float8_e5m2,
  67. cutlass.Float8E4M3FN: torch.float8_e4m3fn,
  68. cutlass.Float8E8M0FNU: torch.float8_e8m0fnu,
  69. cutlass.Float8E4M3B11FNUZ: torch.float8_e4m3fnuz,
  70. cutlass.Float4E2M1FN: torch.float4_e2m1fn_x2, # FP4 packed (2 values per byte)
  71. cutlass.Float6E3M2FN: torch.uint8,
  72. cutlass.Float6E2M3FN: torch.uint8,
  73. }
  74. if torch_dtype is None:
  75. torch_dtype = torch_type_map.get(cutlass_dtype)
  76. if torch_dtype is None:
  77. raise TypeError(f"{cutlass_dtype} is not supported by torch")
  78. return torch_dtype
  79. def is_mxfp6_ab_dtype(ab_dtype) -> bool:
  80. """True when ``ab_dtype`` is an MX-FP6 operand element type."""
  81. return ab_dtype in (cutlass.Float6E3M2FN, cutlass.Float6E2M3FN)
  82. def is_mxfp6_ab_dtype_string(dtype: str) -> bool:
  83. return dtype in MXFP6_AB_DTYPE_STRINGS
  84. def mxfp6_tile_k(sf_vec_size: int = MXFP6_SF_VEC_SIZE) -> int:
  85. """K tile size for one MX-FP6 pipeline stage (four ``m16n8k32`` MMA slices)."""
  86. if sf_vec_size != MXFP6_SF_VEC_SIZE:
  87. raise ValueError(f"MX-FP6 expects sf_vec_size={MXFP6_SF_VEC_SIZE}, got {sf_vec_size}")
  88. return sf_vec_size * 4
  89. def mxfp6_num_k_blocks(tile_k: int, mma_k: int = MXFP6_MMA_K) -> int:
  90. """Number of ``m16n8k32`` MMA K-blocks covered by ``tile_k``."""
  91. if tile_k % mma_k != 0:
  92. raise ValueError(f"tile_k={tile_k} must be divisible by mma_k={mma_k}")
  93. return tile_k // mma_k
  94. def mxfp6_packed_k_bytes(k: int) -> int:
  95. """Packed storage bytes along K (4 FP6 values per 3 bytes)."""
  96. if k % 4 != 0:
  97. raise ValueError(f"k must be divisible by 4 for MX-FP6 packing, got {k}")
  98. return (3 * k) // 4
  99. def mxfp6_logical_k_from_packed_bytes(packed_k_bytes: int) -> int:
  100. """Logical K element count from packed byte width along K."""
  101. if packed_k_bytes % 3 != 0:
  102. raise ValueError(
  103. f"packed_k_bytes must be divisible by 3, got {packed_k_bytes}"
  104. )
  105. return (packed_k_bytes * 4) // 3
  106. def mxfp6_tile_shape_mnk(
  107. tile_m: int,
  108. tile_n: int,
  109. sf_vec_size: int = MXFP6_SF_VEC_SIZE,
  110. ) -> Tuple[int, int, int]:
  111. """Return ``(tile_m, tile_n, tile_k)`` for MX-FP6 block-scaled kernels."""
  112. return tile_m, tile_n, mxfp6_tile_k(sf_vec_size)
  113. def verify_mxfp6_smem_tile_k(
  114. tile_k: int,
  115. sf_vec_size: int = MXFP6_SF_VEC_SIZE,
  116. ) -> int:
  117. """Check ``tile_k`` satisfies ``sm120_make_smem_layout_sfa/sfb`` divisibility for MX-FP6.
  118. Returns ``mma_nsf = tile_k // sf_vec_size`` (4 when ``tile_k=128``).
  119. """
  120. if sf_vec_size != MXFP6_SF_VEC_SIZE:
  121. raise ValueError(f"MX-FP6 expects sf_vec_size={MXFP6_SF_VEC_SIZE}, got {sf_vec_size}")
  122. if tile_k % sf_vec_size != 0:
  123. raise ValueError(f"tile_k={tile_k} must be divisible by sf_vec_size={sf_vec_size}")
  124. mma_nsf = tile_k // sf_vec_size
  125. blk_sf = 4
  126. if tile_k % (blk_sf * mma_nsf) != 0:
  127. raise ValueError(
  128. f"tile_k={tile_k} must be divisible by blk_sf*mma_nsf={blk_sf * mma_nsf} "
  129. f"for sm120 SF smem (mma_nsf={mma_nsf})"
  130. )
  131. return mma_nsf
  132. _num_sm_cache: dict[int, int] = {}
  133. def get_num_sm(device: torch.device) -> int:
  134. """Return the device SM count without exposing lru_cache to Dynamo."""
  135. index = device.index
  136. if index is None:
  137. index = torch.cuda.current_device()
  138. sm_count = _num_sm_cache.get(index)
  139. if sm_count is None:
  140. sm_count = torch.cuda.get_device_properties(index).multi_processor_count
  141. _num_sm_cache[index] = sm_count
  142. return sm_count
  143. @torch._dynamo.disable
  144. def current_cuda_stream() -> cuda.CUstream:
  145. """Return the current Torch CUDA stream as a CUDA driver stream handle."""
  146. return cuda.CUstream(torch.cuda.current_stream().cuda_stream)
  147. def cuda_stream_to_int(stream: object | None) -> int | None:
  148. """Return a raw CUDA stream handle, preserving None as a caller fallback."""
  149. if stream is None:
  150. return None
  151. cuda_stream = getattr(stream, "cuda_stream", None)
  152. if cuda_stream is not None:
  153. return int(cuda_stream)
  154. return int(stream)
  155. def cuda_stream_from_int_or_current(stream_int: int | None) -> cuda.CUstream:
  156. """Use a supplied raw CUDA stream handle, or fall back to Torch's current stream."""
  157. if stream_int is None:
  158. return current_cuda_stream()
  159. return cuda.CUstream(int(stream_int))
  160. # Cache for HardwareInfo - it's expensive to create on every call
  161. _hardware_info_cache: "cutlass.utils.HardwareInfo | None" = None
  162. def get_hardware_info() -> "cutlass.utils.HardwareInfo":
  163. """Get cached HardwareInfo singleton.
  164. HardwareInfo queries CUDA device capabilities, which can be expensive.
  165. This function caches the singleton to avoid repeated queries.
  166. """
  167. global _hardware_info_cache
  168. if _hardware_info_cache is None:
  169. _hardware_info_cache = cutlass.utils.HardwareInfo()
  170. return _hardware_info_cache
  171. @functools.cache
  172. def get_max_active_clusters(cluster_size: int) -> int:
  173. """Get max active clusters for a given cluster size (cached).
  174. Args:
  175. cluster_size: Product of cluster_shape_mn dimensions.
  176. Returns:
  177. Maximum number of active clusters supported by hardware.
  178. """
  179. return get_hardware_info().get_max_active_clusters(cluster_size)
  180. # Compatibility wrapper around CuTe DSL runtime pointers.
  181. class _Pointer(Pointer):
  182. """Runtime representation of a pointer that can inter-operate with
  183. various data structures, including numpy arrays and device memory.
  184. :param pointer: The pointer to the data
  185. :type pointer: int or pointer-like object
  186. :param dtype: Data type of the elements pointed to
  187. :type dtype: Type
  188. :param mem_space: Memory space where the pointer resides, defaults generic
  189. :type mem_space: _cute_ir.AddressSpace, optional
  190. :param assumed_align: Alignment of input pointer in bytes, defaults None
  191. :type assumed_align: int, optional
  192. :ivar _pointer: The underlying pointer
  193. :ivar _dtype: Data type of the elements
  194. :ivar _addr_space: Memory space of the pointer
  195. :ivar _assumed_align: Alignment of the pointer in bytes
  196. :ivar _desc: C-type descriptor for the pointer
  197. :ivar _c_pointer: C-compatible pointer representation
  198. """
  199. def __init__(
  200. self,
  201. pointer,
  202. dtype,
  203. mem_space: _cute_ir.AddressSpace = _cute_ir.AddressSpace.generic,
  204. assumed_align=None,
  205. ):
  206. self._pointer = pointer
  207. self._dtype = dtype
  208. self._addr_space = mem_space
  209. if assumed_align is None:
  210. self._assumed_align = dtype.width // 8
  211. else:
  212. self._assumed_align = assumed_align
  213. self._desc = None
  214. self._c_pointer = None
  215. assert int(self._pointer) % self._assumed_align == 0, (
  216. f"pointer must be {self._assumed_align} bytes aligned"
  217. )
  218. def size_in_bytes(self) -> int:
  219. return ctypes.sizeof(ctypes.c_void_p(int(self._pointer)))
  220. def __get_mlir_types__(self):
  221. return [self.mlir_type]
  222. def __c_pointers__(self):
  223. if self._c_pointer is None:
  224. self._desc = ctypes.c_void_p(int(self._pointer))
  225. self._c_pointer = ctypes.addressof(self._desc)
  226. return [self._c_pointer]
  227. def __new_from_mlir_values__(self, values):
  228. assert len(values) == 1
  229. return values[0]
  230. # Move mlir Type out of __init__ to decouple with mlir Context
  231. @property
  232. def mlir_type(self) -> ir.Type:
  233. return _cute_ir.PtrType.get(
  234. self._dtype.mlir_type, self._addr_space, self._assumed_align
  235. )
  236. @property
  237. def dtype(self) -> Type[Numeric]:
  238. return self._dtype
  239. @property
  240. def memspace(self):
  241. return self._addr_space
  242. def align(self, min_align: int, *, loc=None, ip=None) -> Pointer:
  243. raise NotImplementedError("align is not supported in runtime")
  244. def verify(self, expected_py_type):
  245. if expected_py_type is Pointer or (
  246. isinstance(expected_py_type, ir.Value) and expected_py_type.ty is Pointer
  247. ):
  248. return True
  249. return False
  250. @property
  251. def __cache_key__(self) -> tuple[object, ...]:
  252. return (self._dtype, self._addr_space, self._assumed_align)
  253. def __str__(self) -> str:
  254. return f"Ptr<0x{int(self._pointer):016x}@{self._addr_space}>"
  255. def __repr__(self):
  256. return self.__str__()
  257. def make_ptr(
  258. dtype: Type[Numeric],
  259. value: Union[int, ctypes._Pointer],
  260. mem_space: AddressSpace = AddressSpace.generic,
  261. assumed_align=None,
  262. ) -> Pointer:
  263. """Create a pointer from a memory address
  264. :param dtype: Data type of the pointer elements
  265. :type dtype: Type[Numeric]
  266. :param value: Memory address as integer or ctypes pointer
  267. :type value: Union[int, ctypes._Pointer]
  268. :param mem_space: Memory address space, defaults to AddressSpace.generic
  269. :type mem_space: AddressSpace, optional
  270. :param assumed_align: Alignment in bytes, defaults to None
  271. :type assumed_align: int, optional
  272. :return: A pointer object
  273. :rtype: Pointer
  274. .. code-block:: python
  275. import numpy as np
  276. import ctypes
  277. from cutlass import Float32
  278. from cutlass.cute.runtime import make_ptr
  279. # Create a numpy array
  280. a = np.random.randn(16, 32).astype(np.float32)
  281. # Get pointer address as integer
  282. ptr_address = a.ctypes.data_as(ctypes.POINTER(ctypes.c_float))
  283. # Create pointer from address
  284. y = make_ptr(cutlass.Float32, ptr_address)
  285. """
  286. # check if value is int or ctypes.POINTER
  287. if isinstance(value, int):
  288. address_value = value
  289. elif isinstance(value, ctypes._Pointer):
  290. # get address value
  291. address_value = ctypes.cast(value, ctypes.c_void_p).value
  292. assert address_value is not None, "Pointer address is None"
  293. else:
  294. raise TypeError(
  295. f"Expect int or ctypes.POINTER for value but got {type(value)=}"
  296. )
  297. return _Pointer(address_value, dtype, mem_space, assumed_align=assumed_align)
  298. def convert_sf_to_mma_layout(
  299. sf: torch.Tensor,
  300. m: int,
  301. k: int,
  302. num_groups: int = 1,
  303. sf_vec_size: int = 16,
  304. ) -> torch.Tensor:
  305. """Convert scale factors from swizzled 2D layout to 6D MMA-compatible layout.
  306. This function converts scale factors produced by `fp4_quantize(..., is_sf_swizzled_layout=True)`
  307. to the 6D layout expected by CuteDSL grouped GEMM kernels.
  308. The swizzled scale factors from `fp4_quantize` have shape `(M, K/sf_vec_size)` but are
  309. stored in a swizzled pattern internally. This function reshapes them to the explicit
  310. 6D MMA-compatible layout: `(32, 4, m_tiles, 4, k_tiles, num_groups)` with the
  311. physical storage order `(num_groups, m_tiles, k_tiles, 32, 4, 4)`.
  312. Layout mapping (from linear (m, k) position):
  313. - m_tile = m // 128
  314. - outer_m = m % 32
  315. - inner_m = (m % 128) // 32
  316. - k_tile = k // 4
  317. - inner_k = k % 4
  318. - 6D position: (outer_m, inner_m, m_tile, inner_k, k_tile, group)
  319. Args:
  320. sf: Scale factor tensor from `fp4_quantize(..., is_sf_swizzled_layout=True)`.
  321. Shape: `(M, K/sf_vec_size)` or `(num_groups * M, K/sf_vec_size)`.
  322. m: The M dimension (rows) of the original matrix before quantization.
  323. k: The K dimension (columns) of the original matrix before quantization.
  324. num_groups: Number of groups (e.g., experts). Default: 1.
  325. sf_vec_size: Scale factor vector size. Default: 16.
  326. Returns:
  327. Scale factors in 6D MMA layout: `(32, 4, m_tiles, 4, k_tiles, num_groups)`.
  328. This is a strided view (not contiguous) with physical storage order
  329. `(num_groups, m_tiles, k_tiles, 32, 4, 4)`.
  330. Example:
  331. >>> # Quantize weight tensor
  332. >>> w_q, w_sf = fp4_quantize(weight, global_scale=gs, is_sf_swizzled_layout=True)
  333. >>> # Convert scale factors to MMA layout
  334. >>> w_sf_mma = convert_sf_to_mma_layout(w_sf, m=weight.shape[0], k=weight.shape[1])
  335. Note:
  336. - The input `sf` must be produced with `is_sf_swizzled_layout=True`.
  337. - M and K dimensions must be multiples of 128 and 64 respectively for proper alignment.
  338. - For grouped tensors (e.g., expert weights), reshape to `(num_groups * M, K)`
  339. before quantization, then use this function with the appropriate `num_groups`.
  340. - The returned tensor is a strided view, NOT contiguous. This is intentional as
  341. the CuteDSL kernel expects the specific physical memory layout.
  342. """
  343. sf_k = ceil_div(k, sf_vec_size)
  344. m_tiles = ceil_div(m, 128)
  345. k_tiles = ceil_div(sf_k, 4)
  346. # Verify input shape
  347. expected_elements = num_groups * m_tiles * k_tiles * 32 * 4 * 4
  348. actual_elements = sf.numel()
  349. if actual_elements != expected_elements:
  350. raise ValueError(
  351. f"Scale factor tensor has {actual_elements} elements, "
  352. f"expected {expected_elements} for m={m}, k={k}, num_groups={num_groups}"
  353. )
  354. # Reshape from flat 2D to 6D physical storage order
  355. # Physical storage: (num_groups, m_tiles, k_tiles, 32, 4, 4)
  356. sf_6d = sf.view(num_groups, m_tiles, k_tiles, 32, 4, 4)
  357. # Permute to MMA logical order: (32, 4, m_tiles, 4, k_tiles, num_groups)
  358. # This creates a strided view (non-contiguous), which is what the kernel expects
  359. sf_6d = sf_6d.permute(3, 4, 1, 5, 2, 0)
  360. return sf_6d # Return strided view, NOT contiguous
  361. def convert_sf_from_mma_layout(
  362. sf_6d: torch.Tensor,
  363. m: int,
  364. k: int,
  365. num_groups: int = 1,
  366. sf_vec_size: int = 16,
  367. ) -> torch.Tensor:
  368. """Convert scale factors from 6D MMA layout back to 2D swizzled layout.
  369. This is the inverse of `convert_sf_to_mma_layout`.
  370. Args:
  371. sf_6d: Scale factors in 6D MMA layout: `(32, 4, m_tiles, 4, k_tiles, num_groups)`.
  372. Can be either a strided view or contiguous.
  373. m: The M dimension (rows) of the original matrix.
  374. k: The K dimension (columns) of the original matrix.
  375. num_groups: Number of groups. Default: 1.
  376. sf_vec_size: Scale factor vector size. Default: 16.
  377. Returns:
  378. Scale factors in 2D swizzled layout: `(num_groups * M_padded, K_padded/sf_vec_size)`.
  379. """
  380. sf_k = ceil_div(k, sf_vec_size)
  381. m_tiles = ceil_div(m, 128)
  382. k_tiles = ceil_div(sf_k, 4)
  383. # Permute from MMA logical order back to storage order
  384. # From: (32, 4, m_tiles, 4, k_tiles, num_groups)
  385. # To: (num_groups, m_tiles, k_tiles, 32, 4, 4)
  386. sf_storage = sf_6d.permute(5, 2, 4, 0, 1, 3).contiguous()
  387. # Reshape to 2D
  388. padded_m = m_tiles * 128
  389. padded_sf_k = k_tiles * 4
  390. sf_2d = sf_storage.reshape(num_groups * padded_m, padded_sf_k)
  391. return sf_2d
  392. def get_mma_sf_shape(
  393. m: int,
  394. k: int,
  395. num_groups: int = 1,
  396. sf_vec_size: int = 16,
  397. ) -> Tuple[int, int, int, int, int, int]:
  398. """Get the 6D MMA-compatible scale factor shape.
  399. Args:
  400. m: The M dimension (rows) of the matrix.
  401. k: The K dimension (columns) of the matrix.
  402. num_groups: Number of groups. Default: 1.
  403. sf_vec_size: Scale factor vector size. Default: 16.
  404. Returns:
  405. Shape tuple: (32, 4, m_tiles, 4, k_tiles, num_groups)
  406. """
  407. sf_k = ceil_div(k, sf_vec_size)
  408. m_tiles = ceil_div(m, 128)
  409. k_tiles = ceil_div(sf_k, 4)
  410. return (32, 4, m_tiles, 4, k_tiles, num_groups)
  411. @dsl_user_op
  412. def sm120_make_smem_layout_sfa(
  413. tiled_mma: cute.TiledMma,
  414. tile_shape_mnk: cute.Tile,
  415. sf_vec_size: int,
  416. num_stages: int,
  417. *,
  418. loc=None,
  419. ip=None,
  420. ) -> cute.Layout:
  421. """
  422. Make smem layout for SFA based on:
  423. 1. BlockScaledBasicChunk
  424. 2. MMA tiler shape
  425. 3. Scale factor vector size
  426. 4. Number of stages
  427. :param tiled_mma: The tiled MMA
  428. :type tiled_mma: cute.TiledMma
  429. :param mma_tiler_mnk: The mma tiler shape
  430. :type mma_tiler_mnk: cute.Tile
  431. :param sf_vec_size: The scale factor vector size
  432. :type sf_vec_size: int
  433. :param num_stages: The number of stages
  434. :type num_stages: int
  435. :return: Smem layout for SFA
  436. :rtype: cute.Layout
  437. """
  438. assert sf_vec_size == 16 or sf_vec_size == 32, "sf_vec_size must be 16 or 32"
  439. blk_mn = 128
  440. blk_sf = min(4, tile_shape_mnk[2] // sf_vec_size)
  441. # The global scale atom always has four K groups. A BK64 stage consumes
  442. # two groups but keeps the same row pitch inside shared memory.
  443. blk_elems = blk_mn * 4
  444. mma_nsf = tiled_mma.shape_mnk[2] // sf_vec_size
  445. mn_basic_block_shape = (32, 4)
  446. mn_basic_block_stride = (16, 4)
  447. k_basic_block_shape = (sf_vec_size, mma_nsf)
  448. k_basic_block_stride = (0, 1)
  449. assert tile_shape_mnk[0] % (blk_mn // 8) == 0, (
  450. "tile_shape_mnk[0] must be divisible by 16"
  451. )
  452. # Scale-factor tiles are quantized in 128-row blocks, so narrower MMA
  453. # tiles still allocate one full SF block and consume only the live subset.
  454. sfa_tile_m = max(blk_mn, ceil_div(tile_shape_mnk[0], blk_mn) * blk_mn)
  455. sSFA_shapeM = (mn_basic_block_shape, sfa_tile_m // blk_mn)
  456. sSF_strideM = (mn_basic_block_stride, blk_elems)
  457. assert tile_shape_mnk[2] % (blk_sf * mma_nsf) == 0, (
  458. "tile_shape_mnk[2] must be divisible by blk_sf * mma_nsf"
  459. )
  460. sSFA_shapeK = (
  461. k_basic_block_shape,
  462. blk_sf // mma_nsf,
  463. tile_shape_mnk[2] // sf_vec_size // blk_sf,
  464. )
  465. sSF_strideK = (
  466. k_basic_block_stride,
  467. mma_nsf,
  468. sfa_tile_m // blk_mn * blk_elems,
  469. )
  470. sSFA_shape = (sSFA_shapeM, sSFA_shapeK)
  471. sSFA_stride = (sSF_strideM, sSF_strideK)
  472. smem_layout = cute.make_layout(sSFA_shape, stride=sSFA_stride)
  473. sfa_smem_layout_staged = cute.append(
  474. smem_layout,
  475. cute.make_layout(
  476. num_stages, stride=cute.cosize(cute.filter_zeros(smem_layout))
  477. ),
  478. )
  479. return sfa_smem_layout_staged
  480. @dsl_user_op
  481. def sm120_make_smem_layout_sfb(
  482. tiled_mma: cute.TiledMma,
  483. tile_shape_mnk: cute.Tile,
  484. sf_vec_size: int,
  485. num_stages: int,
  486. *,
  487. loc=None,
  488. ip=None,
  489. ) -> cute.Layout:
  490. """
  491. Make smem layout for SFB based on:
  492. 1. BlockScaledBasicChunk
  493. 2. MMA tiler shape
  494. 3. Scale factor vector size
  495. 4. Number of stages
  496. :param tiled_mma: The tiled MMA
  497. :type tiled_mma: cute.TiledMma
  498. :param mma_tiler_mnk: The mma tiler shape
  499. :type mma_tiler_mnk: cute.Tile
  500. :param sf_vec_size: The scale factor vector size
  501. :type sf_vec_size: int
  502. :param num_stages: The number of stages
  503. :type num_stages: int
  504. :return: Smem layout for SFA
  505. :rtype: cute.Layout
  506. """
  507. blk_mn = 128
  508. blk_sf = min(4, tile_shape_mnk[2] // sf_vec_size)
  509. # Keep the four-group atom pitch when a BK64 stage exposes two groups.
  510. blk_elems = blk_mn * 4
  511. assert sf_vec_size == 16 or sf_vec_size == 32, "sf_vec_size must be 16 or 32"
  512. assert tile_shape_mnk[1] % (blk_mn // 8) == 0, (
  513. "tile_shape_mnk[1] must be divisible by 16"
  514. )
  515. assert tile_shape_mnk[2] % sf_vec_size == 0, (
  516. "tile_shape_mnk[2] must be divisible by sf_vec_size"
  517. )
  518. mma_nsf = tiled_mma.shape_mnk[2] // sf_vec_size
  519. mn_basic_block_shape = (32, 4)
  520. mn_basic_block_stride = (16, 4)
  521. k_basic_block_shape = (sf_vec_size, mma_nsf)
  522. k_basic_block_stride = (0, 1)
  523. # Scale-factor tiles are quantized in 128-column blocks, so narrower MMA
  524. # tiles still allocate one full SF block and consume only the live subset.
  525. sfb_tile_n = max(blk_mn, ceil_div(tile_shape_mnk[1], blk_mn) * blk_mn)
  526. sSFA_shapeN = (mn_basic_block_shape, sfb_tile_n // blk_mn)
  527. sSF_strideN = (mn_basic_block_stride, blk_elems)
  528. assert tile_shape_mnk[2] % (blk_sf * mma_nsf) == 0, (
  529. "tile_shape_mnk[2] must be divisible by blk_sf * mma_nsf"
  530. )
  531. sSFA_shapeK = (
  532. k_basic_block_shape,
  533. blk_sf // mma_nsf,
  534. tile_shape_mnk[2] // sf_vec_size // blk_sf,
  535. )
  536. sSF_strideK = (
  537. k_basic_block_stride,
  538. mma_nsf,
  539. sfb_tile_n // blk_mn * blk_elems,
  540. )
  541. sSFA_shape = (sSFA_shapeN, sSFA_shapeK)
  542. sSFA_stride = (sSF_strideN, sSF_strideK)
  543. smem_layout = cute.make_layout(sSFA_shape, stride=sSFA_stride)
  544. sfb_smem_layout_staged = cute.append(
  545. smem_layout,
  546. cute.make_layout(
  547. num_stages, stride=cute.cosize(cute.filter_zeros(smem_layout))
  548. ),
  549. )
  550. return sfb_smem_layout_staged