Rate this Page

Source code for helion.language.matmul_ops

from __future__ import annotations

import dataclasses
from itertools import zip_longest

import torch

from .. import exc
from .._compat import min_dot_size
from .._compiler.compile_environment import CompileEnvironment
from .._compiler.compile_environment import _to_sympy
from .._compiler.compile_environment import format_shape
from .._compiler.compile_environment import shape_env_var_hints
from .._compiler.cute.matmul_utils import cute_outer_accumulates_result
from .._compiler.cute.tcgen05_constants import TCGEN05_TWO_CTA_BLOCK_M
from .._compiler.cute.tcgen05_constants import TCGEN05_TWO_CTA_BLOCK_N
from .._compiler.cute.tcgen05_constants import TCGEN05_TWO_CTA_EDGE_K_TAIL_MIN_DIM
from .._compiler.cute.tcgen05_constants import TCGEN05_TWO_CTA_FP8_SMALL_GRID_BLOCK_M
from .._compiler.cute.tcgen05_constants import TCGEN05_TWO_CTA_FP8_SMALL_GRID_BLOCK_N
from .._compiler.cute.tcgen05_constants import TCGEN05_TWO_CTA_MAX_K_TILES
from .._compiler.matmul_utils import _compute_out_dtype
from ..autotuner.config_spec import MatmulFact
from . import _decorators


def _static_dim_value(env: CompileEnvironment, size: int | torch.SymInt) -> int | None:
    if isinstance(size, int):
        return size
    expr = _to_sympy(size)
    expr = env.specialize_expr(env.shape_env.replace(expr))
    if expr.free_symbols:
        if not env.settings.static_shapes:
            return None
        expr = expr.xreplace(shape_env_var_hints(env.shape_env))
    if expr.free_symbols:
        return None
    return int(expr)


def _dot_result_shape(
    mat1: torch.Tensor, mat2: torch.Tensor
) -> list[int | torch.SymInt]:
    """Return the broadcasted ``[..., M, N]`` result shape for ``hl.dot``."""
    batch_shape = torch.broadcast_shapes(mat1.shape[:-2], mat2.shape[:-2])
    return [*batch_shape, mat1.shape[-2], mat2.shape[-1]]


def _cute_dot_outer_accumulates_result(fx_node: object, *, is_acc_none: bool) -> bool:
    if not isinstance(fx_node, torch.fx.Node):
        fx_node = getattr(fx_node, "fx_node", fx_node)
    if not isinstance(fx_node, torch.fx.Node):
        fx_node = None
    return cute_outer_accumulates_result(fx_node, is_acc_none=is_acc_none)


def _cuda_num_sms_or_zero(device: torch.device) -> int:
    """Return the device SM count, or 0 on devices ``get_num_sm`` does not support.

    Used by the cluster_m=2 small-shape wave-quantization gate in
    ``enforce_dot_requirements`` (cute_plan.md §7.6.3.2). The 0 fallback
    keeps cluster_m=2 search live for configuration round-trip tests
    that bind on CPU or other unsupported device types.
    """
    if device.type != "cuda":
        return 0
    # Local import: ``helion.runtime`` is in the import chain that loads
    # this module, so a top-level import would be circular.
    from ..runtime import get_num_sm

    try:
        return get_num_sm(device)
    except (AssertionError, NotImplementedError):
        return 0


[docs] @_decorators.api(is_device_only=True) def dot( mat1: torch.Tensor, mat2: torch.Tensor, acc: torch.Tensor | None = None, out_dtype: torch.dtype | None = None, ) -> torch.Tensor: """ Performs a matrix multiplication of tensors with support for multiple dtypes. This operation performs matrix multiplication with inputs of various dtypes including float16, bfloat16, float32, int8, and FP8 formats (e4m3fn, e5m2). The computation is performed with appropriate precision based on the input dtypes. Args: mat1: First matrix (2D or 3D tensor of torch.float16, torch.bfloat16, torch.float32, torch.int8, torch.float8_e4m3fn, or torch.float8_e5m2) mat2: Second matrix (2D or 3D tensor of torch.float16, torch.bfloat16, torch.float32, torch.int8, torch.float8_e4m3fn, or torch.float8_e5m2) acc: The accumulator tensor (2D or 3D tensor of torch.float16, torch.float32, or torch.int32). If not None, the result is added to this tensor. If None, a new tensor is created with appropriate dtype based on inputs. out_dtype: Optional dtype that controls the output type of the multiplication prior to any accumulation. This maps directly to the Triton ``tl.dot`` ``out_dtype`` argument and overrides the default promotion rules when provided. Returns: Result of matrix multiplication. If acc is provided, returns acc + (mat1 @ mat2). Otherwise returns (mat1 @ mat2) with promoted dtype. Example: >>> # FP8 example >>> a = torch.randn(32, 64, device="cuda").to(torch.float8_e4m3fn) >>> b = torch.randn(64, 128, device="cuda").to(torch.float8_e4m3fn) >>> c = torch.zeros(32, 128, device="cuda", dtype=torch.float32) >>> result = hl.dot(a, b, acc=c) # result is c + (a @ b) >>> # Float16 example >>> a = torch.randn(32, 64, device="cuda", dtype=torch.float16) >>> b = torch.randn(64, 128, device="cuda", dtype=torch.float16) >>> result = hl.dot(a, b) # result dtype will be torch.float16 >>> # Int8 example >>> a = torch.randint(-128, 127, (32, 64), device="cuda", dtype=torch.int8) >>> b = torch.randint(-128, 127, (64, 128), device="cuda", dtype=torch.int8) >>> acc = torch.zeros(32, 128, device="cuda", dtype=torch.int32) >>> result = hl.dot(a, b, acc=acc) # int8 x int8 -> int32 """ raise exc.NotInsideKernel
@_decorators.prepare_args(dot) def _( mat1: torch.Tensor, mat2: torch.Tensor, acc: torch.Tensor | None = None, out_dtype: torch.dtype | None = None, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None, torch.dtype | None]: # Define supported dtypes supported_dtypes = ( torch.float16, torch.bfloat16, torch.float32, torch.int8, torch.float8_e4m3fn, torch.float8_e5m2, ) # Validate input types if mat1.dtype not in supported_dtypes: raise TypeError( f"hl.dot: mat1 must be one of {[str(d) for d in supported_dtypes]}, got {mat1.dtype}" ) if mat2.dtype not in supported_dtypes: raise TypeError( f"hl.dot: mat2 must be one of {[str(d) for d in supported_dtypes]}, got {mat2.dtype}" ) # Validate shapes for matrix multiplication if mat1.ndim not in (2, 3): raise ValueError(f"hl.dot: mat1 must be 2D or 3D tensor, got {mat1.ndim}D") if mat2.ndim not in (2, 3): raise ValueError(f"hl.dot: mat2 must be 2D or 3D tensor, got {mat2.ndim}D") # Check matrix multiplication compatibility if mat1.shape[-1] != mat2.shape[-2]: raise ValueError( f"hl.dot: incompatible matrix dimensions for multiplication: " f"{mat1.shape} @ {mat2.shape}" ) # Check batch dimension compatibility (broadcastable or matching) if any input is 3D if mat1.ndim == 3 or mat2.ndim == 3: batch_shape_1 = mat1.shape[:-2] if mat1.ndim > 2 else () batch_shape_2 = mat2.shape[:-2] if mat2.ndim > 2 else () for lhs_dim, rhs_dim in zip_longest( reversed(batch_shape_1), reversed(batch_shape_2), fillvalue=1 ): # Allow broadcasting with 1 if str(lhs_dim) == "1" or str(rhs_dim) == "1": continue # Check if dimensions match if str(lhs_dim) != str(rhs_dim): raise exc.DotBatchDimensionMismatch( lhs=format_shape(batch_shape_1), rhs=format_shape(batch_shape_2), ) if out_dtype is not None and not isinstance(out_dtype, torch.dtype): raise TypeError( f"hl.dot: out_dtype must be a torch.dtype or None, got {type(out_dtype)}" ) # Validate accumulator if provided if acc is not None: # Allow int32 accumulator for int8 inputs valid_acc_dtypes = (torch.float16, torch.float32, torch.int32) if acc.dtype not in valid_acc_dtypes: raise TypeError( f"hl.dot: acc must be one of {[str(d) for d in valid_acc_dtypes]}, got {acc.dtype}" ) # Check int8 inputs require int32 accumulator if mat1.dtype == torch.int8 or mat2.dtype == torch.int8: if acc.dtype != torch.int32: raise TypeError( f"hl.dot: int8 inputs require int32 accumulator, got {acc.dtype}" ) # Check accumulator shape compatibility expected_shape = _dot_result_shape(mat1, mat2) if acc.ndim not in (2, 3): raise ValueError(f"hl.dot: acc must be 2D or 3D tensor, got {acc.ndim}D") if list(acc.shape) != expected_shape: raise ValueError( f"hl.dot: acc shape {list(acc.shape)} incompatible with result shape {expected_shape}" ) # Apply min-dot-size constraints so autotuning cannot select an invalid tile. # Leading-passthrough tcgen05 search is intentionally not inferred from rank: # the DeviceIR post-pass opens it only after the same structural analyzer # used by codegen accepts the operands. enforce_dot_requirements(mat1, mat2) return (mat1, mat2, acc, out_dtype) def _dot_dimensions( lhs: torch.Tensor, rhs: torch.Tensor ) -> tuple[int | torch.SymInt, int | torch.SymInt, int | torch.SymInt]: """Return the M, N, and K problem extents for a dot.""" m, k = lhs.shape[-2:] k2, n = rhs.shape[-2:] assert k == k2, f"Mismatched K dimensions for dot: {k} vs {k2}" return m, n, k def _static_problem_extent( env: CompileEnvironment, size: int | torch.SymInt ) -> int | None: block_idx = env.get_block_id(size) if block_idx is not None: block_size = env.block_sizes[block_idx].size if isinstance(block_size, (int, torch.SymInt)): return _static_dim_value(env, block_size) return _static_dim_value(env, size) def enforce_dot_requirements(lhs: torch.Tensor, rhs: torch.Tensor) -> None: """Record a matmul fact and apply backend-independent dot constraints.""" m, n, k = _dot_dimensions(lhs, rhs) from ..autotuner.config_spec import SMALL_DIM_BLOCK_SIZE_OVERSHOOT env = CompileEnvironment.current() a, b, c = min_dot_size(lhs.device, lhs.dtype, rhs.dtype) # M and N are the output tile dims; K is the contraction loop. for shape, min_size, is_output_dim in ( (m, a, True), (n, b, True), (k, c, False), ): block_idx = env.get_block_id(shape) if block_idx is not None: # On Pallas, clamp min to the tensor dimension so we don't # force blocks larger than the tensor (Pallas BlockSpecs can't # handle that, unlike Triton which masks out-of-bounds accesses). # The dot-level padding in matmul_utils.py will pad the smaller # tile up to min_dot_size at codegen time. if env.backend_name == "pallas": try: bspec = env.config_spec.block_sizes.block_id_lookup(block_idx) min_size = min(min_size, bspec.size_hint) except KeyError: pass env.block_sizes[block_idx].update_min_block(min_size, allow_flattened=True) # Let the autotuner try output block sizes larger than a small matmul # dimension: masked rows/cols can map to a more efficient MMA tile. if is_output_dim and env.backend_name == "triton": try: spec = env.config_spec.block_sizes.block_id_lookup(block_idx) except KeyError: pass else: spec.allow_overshoot(SMALL_DIM_BLOCK_SIZE_OVERSHOOT) env.config_spec.matmul_facts.append( MatmulFact( lhs_ndim=lhs.ndim, rhs_ndim=rhs.ndim, m_block_id=env.get_block_id(m), n_block_id=env.get_block_id(n), k_block_id=env.get_block_id(k), static_m=_static_problem_extent(env, m), static_n=_static_problem_extent(env, n), static_k=_static_problem_extent(env, k), lhs_dtype=lhs.dtype, rhs_dtype=rhs.dtype, ) ) # Triton only supports 2D dot operations. Constrain each leading axis from # a rank-3 operand to one element so codegen can squeeze it before tl.dot. # Pallas uses jnp.dot_general and handles leading axes natively. if (lhs.ndim == 3 or rhs.ndim == 3) and env.backend_name != "pallas": leading_dims = [] if lhs.ndim == 3: leading_dims.append(lhs.shape[0]) if rhs.ndim == 3: leading_dims.append(rhs.shape[0]) for leading_dim in leading_dims: block_idx = env.get_block_id(leading_dim) if block_idx is not None: env.block_sizes[block_idx].update_max_block(1) @dataclasses.dataclass(frozen=True) class CuteTcgen05SearchPlan: """Side-effect-free tcgen05 search limits for one MMA candidate.""" m: int | torch.SymInt n: int | torch.SymInt k: int | torch.SymInt static_m: int static_n: int static_k: int leading_work_multiplier: int is_fp8: bool mma_k: int max_tcgen05_m: int max_tcgen05_n: int max_search_m: int max_search_n: int max_search_k: int min_search_m: int def plan_cute_tcgen05_search( lhs: torch.Tensor, rhs: torch.Tensor, *, has_leading_passthrough: bool, ) -> CuteTcgen05SearchPlan | None: """Return search limits without mutating the shared ``ConfigSpec``.""" m, n, k = _dot_dimensions(lhs, rhs) env = CompileEnvironment.current() static_m = _static_problem_extent(env, m) static_n = _static_problem_extent(env, n) static_k = _static_problem_extent(env, k) is_fp8 = lhs.dtype == torch.float8_e4m3fn mma_k = 32 if is_fp8 else 16 if ( env.backend_name != "cute" or lhs.dtype not in (torch.float16, torch.bfloat16, torch.float8_e4m3fn) or rhs.dtype != lhs.dtype or static_m is None or static_n is None or static_k is None or static_m < 64 or static_n < 8 or static_k < mma_k ): return None from .._compiler.cute.mma_support import get_cute_mma_support support = get_cute_mma_support() if not (support.tcgen05_f8 if is_fp8 else support.tcgen05_f16bf16): return None def pow2_floor_at_least(value: int, minimum: int) -> int: return 1 << (max(minimum, value).bit_length() - 1) max_tcgen05_n = min(256, pow2_floor_at_least(static_n, 8)) max_tcgen05_m = 256 if max_tcgen05_n >= 128 and static_m >= 256 else 128 max_search_m = min(max_tcgen05_m, pow2_floor_at_least(static_m, 64)) max_search_n = max_tcgen05_n max_search_k = min(128, pow2_floor_at_least(static_k, mma_k)) min_search_m = 128 if max_tcgen05_m >= 256 else 64 leading_work_multiplier = 1 if has_leading_passthrough: leading_operand = lhs if lhs.ndim == 3 else rhs static_leading = _static_problem_extent(env, leading_operand.shape[0]) if static_leading is not None: leading_work_multiplier = max(static_leading, 1) if static_m % min_search_m != 0: min_search_m = 64 max_search_m = min(max_search_m, static_m & -static_m) max_search_n = min(max_search_n, static_n & -static_n) max_search_k = min(max_search_k, static_k & -static_k) if max_search_m < min_search_m or max_search_n < 8 or max_search_k < mma_k: return None if static_m % max_search_m != 0 and static_n % max_search_n != 0: max_search_m = min(max_search_m, 128) return CuteTcgen05SearchPlan( m=m, n=n, k=k, static_m=static_m, static_n=static_n, static_k=static_k, leading_work_multiplier=leading_work_multiplier, is_fp8=is_fp8, mma_k=mma_k, max_tcgen05_m=max_tcgen05_m, max_tcgen05_n=max_tcgen05_n, max_search_m=max_search_m, max_search_n=max_search_n, max_search_k=max_search_k, min_search_m=min_search_m, ) def enable_cute_tcgen05_search( lhs: torch.Tensor, *, plan: CuteTcgen05SearchPlan, m_block_id: int, n_block_id: int, k_block_id: int, input_dtype: torch.dtype, has_leading_passthrough: bool, explicit_epi_tile_compatible: bool, ) -> None: """Apply one preflighted tcgen05 search plan to the shared config.""" env = CompileEnvironment.current() spec = env.config_spec spec.register_cute_tcgen05_mma_analysis( m_block_id=m_block_id, n_block_id=n_block_id, k_block_id=k_block_id, input_dtype=input_dtype, has_leading_passthrough=has_leading_passthrough, explicit_epi_tile_compatible=explicit_epi_tile_compatible, ) static_m = plan.static_m static_n = plan.static_n static_k = plan.static_k max_search_m = plan.max_search_m max_search_n = plan.max_search_n max_search_k = plan.max_search_k spec.cute_tcgen05_search_enabled = True allow_full_tile_persistent_pid_types = ( static_m % max_search_m == 0 and static_n % max_search_n == 0 and static_k % max_search_k == 0 ) max_cluster_m2_search_k = TCGEN05_TWO_CTA_MAX_K_TILES * max_search_k allow_full_tile_cluster_m2_search = ( allow_full_tile_persistent_pid_types and max_search_m >= TCGEN05_TWO_CTA_BLOCK_M and max_search_n >= TCGEN05_TWO_CTA_BLOCK_N and static_k <= max_cluster_m2_search_k ) allow_edge_cluster_m2_search = ( not has_leading_passthrough and not allow_full_tile_persistent_pid_types and plan.max_tcgen05_m >= TCGEN05_TWO_CTA_BLOCK_M and plan.max_tcgen05_n >= TCGEN05_TWO_CTA_BLOCK_N and static_m >= TCGEN05_TWO_CTA_EDGE_K_TAIL_MIN_DIM and static_n >= TCGEN05_TWO_CTA_EDGE_K_TAIL_MIN_DIM and static_k >= TCGEN05_TWO_CTA_EDGE_K_TAIL_MIN_DIM and static_k <= max_cluster_m2_search_k and static_m % TCGEN05_TWO_CTA_BLOCK_M != 0 and static_n % TCGEN05_TWO_CTA_BLOCK_N != 0 and static_k % max_search_k != 0 ) allow_fp8_small_grid_cluster_m2_search = ( not has_leading_passthrough and plan.is_fp8 and allow_full_tile_persistent_pid_types and max_search_m >= TCGEN05_TWO_CTA_FP8_SMALL_GRID_BLOCK_M and max_search_n >= TCGEN05_TWO_CTA_FP8_SMALL_GRID_BLOCK_N and static_k <= max_cluster_m2_search_k ) allow_cluster_m2_search = ( allow_full_tile_cluster_m2_search or allow_edge_cluster_m2_search or allow_fp8_small_grid_cluster_m2_search ) if allow_cluster_m2_search: num_sms = _cuda_num_sms_or_zero(lhs.device) if num_sms > 0: if allow_fp8_small_grid_cluster_m2_search: cluster_m = TCGEN05_TWO_CTA_FP8_SMALL_GRID_BLOCK_M cluster_n = TCGEN05_TWO_CTA_FP8_SMALL_GRID_BLOCK_N else: cluster_m = TCGEN05_TWO_CTA_BLOCK_M cluster_n = TCGEN05_TWO_CTA_BLOCK_N work_clusters = ( plan.leading_work_multiplier * (static_m // cluster_m) * (static_n // cluster_n) ) if work_clusters < num_sms // 4: allow_cluster_m2_search = False allow_fp8_small_grid_cluster_m2_search = False spec.narrow_tcgen05_autotune_to_validated_configs( allow_persistent_pid_types=allow_full_tile_persistent_pid_types, allow_cluster_m2_search=allow_cluster_m2_search, cluster_m2_static_k=static_k if allow_cluster_m2_search else None, allow_cluster_m2_edge_k_tail_family=allow_edge_cluster_m2_search, allow_cluster_m2_fp8_small_grid=allow_fp8_small_grid_cluster_m2_search, ab_stages_three_dtype_bytes=lhs.dtype.itemsize, ab_stages_three_device=lhs.device, ) for axis_name, shape, max_size in ( ("m", plan.m, max_search_m), ("n", plan.n, max_search_n), ("k", plan.k, max_search_k), ): block_idx = env.get_block_id(shape) if block_idx is None: continue if axis_name == "k": min_size = plan.mma_k elif axis_name == "m": min_size = plan.min_search_m else: min_size = 8 env.block_sizes[block_idx].update_min_block(min_size, allow_flattened=True) env.block_sizes[block_idx].update_max_block(max_size) @_decorators.register_fake(dot) def _( mat1: torch.Tensor, mat2: torch.Tensor, acc: torch.Tensor | None = None, out_dtype: torch.dtype | None = None, ) -> torch.Tensor: result_shape = _dot_result_shape(mat1, mat2) if acc is not None: return acc.new_empty(result_shape) # Determine output dtype using the helper function resolved_out_dtype = out_dtype or _compute_out_dtype(mat1.dtype, mat2.dtype) return torch.empty(result_shape, dtype=resolved_out_dtype, device=mat1.device) @_decorators.ref(dot) def _( mat1: torch.Tensor, mat2: torch.Tensor, acc: torch.Tensor | None = None, out_dtype: torch.dtype | None = None, ) -> torch.Tensor: resolved_out_dtype = out_dtype or _compute_out_dtype( mat1.dtype, mat2.dtype, None if acc is None else acc.dtype ) is_fp8 = mat1.dtype in (torch.float8_e4m3fn, torch.float8_e5m2) or mat2.dtype in ( torch.float8_e4m3fn, torch.float8_e5m2, ) if is_fp8: # Use torch._scaled_mm for FP8 operations # Ensure column-major for second operand as required by torch._scaled_mm mat2_t = mat2.T.contiguous().T scale_a = torch.tensor(1.0, device=mat1.device) scale_b = torch.tensor(1.0, device=mat2.device) result = torch._scaled_mm( mat1, mat2_t, scale_a, scale_b, use_fast_accum=False, out_dtype=resolved_out_dtype, ) else: # For non-FP8 tensors, use regular matmul if mat1.ndim == 3 or mat2.ndim == 3: mat1_batched = mat1 if mat1.ndim == 3 else mat1.unsqueeze(0) mat2_batched = mat2 if mat2.ndim == 3 else mat2.unsqueeze(0) batch = max(mat1_batched.shape[0], mat2_batched.shape[0]) result = torch.bmm( mat1_batched.expand(batch, -1, -1), mat2_batched.expand(batch, -1, -1), out_dtype=resolved_out_dtype, ) else: result = torch.mm(mat1, mat2, out_dtype=resolved_out_dtype) if acc is not None: return acc + result return result VALID_SCALED_FORMATS = frozenset({"e2m1", "e4m3", "e5m2", "bf16", "fp16"})
[docs] @_decorators.api(is_device_only=True) def dot_scaled( mat1: torch.Tensor, mat1_scale: torch.Tensor, mat1_format: str, mat2: torch.Tensor, mat2_scale: torch.Tensor, mat2_format: str, acc: torch.Tensor | None = None, out_dtype: torch.dtype | None = None, ) -> torch.Tensor: """ Performs a block-scaled matrix multiplication using Triton's tl.dot_scaled. This operation performs matrix multiplication with block-scaled inputs in formats such as FP4 (e2m1), FP8 (e4m3, e5m2), BF16, and FP16. Each input tensor has an associated scale factor tensor and format string. Args: mat1: First matrix (2D tensor of packed data) mat1_scale: Scale factors for mat1 (2D tensor) mat1_format: Format string for mat1 (one of "e2m1", "e4m3", "e5m2", "bf16", "fp16") mat2: Second matrix (2D tensor of packed data) mat2_scale: Scale factors for mat2 (2D tensor) mat2_format: Format string for mat2 (one of "e2m1", "e4m3", "e5m2", "bf16", "fp16") acc: Optional accumulator tensor (2D, float32 or float16) out_dtype: Optional output dtype for the multiplication Returns: Result of block-scaled matrix multiplication. """ raise exc.NotInsideKernel
@_decorators.prepare_args(dot_scaled) def _( mat1: torch.Tensor, mat1_scale: torch.Tensor, mat1_format: str, mat2: torch.Tensor, mat2_scale: torch.Tensor, mat2_format: str, acc: torch.Tensor | None = None, out_dtype: torch.dtype | None = None, ) -> tuple[ torch.Tensor, torch.Tensor, str, torch.Tensor, torch.Tensor, str, torch.Tensor | None, torch.dtype | None, ]: if mat1_format not in VALID_SCALED_FORMATS: raise ValueError( f"hl.dot_scaled: mat1_format must be one of {sorted(VALID_SCALED_FORMATS)}, " f"got '{mat1_format}'" ) if mat2_format not in VALID_SCALED_FORMATS: raise ValueError( f"hl.dot_scaled: mat2_format must be one of {sorted(VALID_SCALED_FORMATS)}, " f"got '{mat2_format}'" ) if mat1.ndim != 2: raise ValueError(f"hl.dot_scaled: mat1 must be a 2D tensor, got {mat1.ndim}D") if mat2.ndim != 2: raise ValueError(f"hl.dot_scaled: mat2 must be a 2D tensor, got {mat2.ndim}D") if mat1_scale.ndim != 2: raise ValueError( f"hl.dot_scaled: mat1_scale must be a 2D tensor, got {mat1_scale.ndim}D" ) if mat2_scale.ndim != 2: raise ValueError( f"hl.dot_scaled: mat2_scale must be a 2D tensor, got {mat2_scale.ndim}D" ) if acc is not None: expected_shape = [mat1.shape[0], mat2.shape[-1]] if acc.ndim != 2: raise ValueError(f"hl.dot_scaled: acc must be a 2D tensor, got {acc.ndim}D") if list(acc.shape) != expected_shape: raise ValueError( f"hl.dot_scaled: acc shape {list(acc.shape)} incompatible with " f"result shape {expected_shape}" ) valid_acc_dtypes = (torch.float16, torch.float32) if acc.dtype not in valid_acc_dtypes: raise TypeError( f"hl.dot_scaled: acc must be one of {[str(d) for d in valid_acc_dtypes]}, " f"got {acc.dtype}" ) if out_dtype is not None and not isinstance(out_dtype, torch.dtype): raise TypeError( f"hl.dot_scaled: out_dtype must be a torch.dtype or None, got {type(out_dtype)}" ) # Enforce minimum block sizes so autotuner picks valid configs. enforce_dot_requirements(mat1, mat2) # K must be >= 32 because scale tensors have shape [dim, K // 32]. env = CompileEnvironment.current() k_dim = mat1.shape[-1] k_block_idx = env.get_block_id(k_dim) if k_block_idx is not None: env.block_sizes[k_block_idx].update_min_block(32, allow_flattened=True) return ( mat1, mat1_scale, mat1_format, mat2, mat2_scale, mat2_format, acc, out_dtype, ) @_decorators.register_fake(dot_scaled) def _( mat1: torch.Tensor, mat1_scale: torch.Tensor, mat1_format: str, mat2: torch.Tensor, mat2_scale: torch.Tensor, mat2_format: str, acc: torch.Tensor | None = None, out_dtype: torch.dtype | None = None, ) -> torch.Tensor: result_shape = [mat1.shape[0], mat2.shape[-1]] if acc is not None: return acc.new_empty(result_shape) resolved_dtype = out_dtype or torch.float32 return torch.empty(result_shape, dtype=resolved_dtype, device=mat1.device) @_decorators.ref(dot_scaled) def _( mat1: torch.Tensor, mat1_scale: torch.Tensor, mat1_format: str, mat2: torch.Tensor, mat2_scale: torch.Tensor, mat2_format: str, acc: torch.Tensor | None = None, out_dtype: torch.dtype | None = None, ) -> torch.Tensor: def _dequant(data: torch.Tensor, scale: torch.Tensor, fmt: str) -> torch.Tensor: data_f32 = data.to(torch.float32) # Scale is in e8m0 format (uint8): value = 2^(byte - 127) # e.g. byte=127 means 2^0=1.0, byte=0 means 2^(-127), byte=254 means 2^127 scale_f32 = torch.pow(2.0, scale.to(torch.float32) - 127.0) k_data = data_f32.shape[-1] k_scale = scale_f32.shape[-1] if k_scale < k_data: repeat_factor = k_data // k_scale scale_f32 = scale_f32.repeat_interleave(repeat_factor, dim=-1) return data_f32 * scale_f32 mat1_dequant = _dequant(mat1, mat1_scale, mat1_format) mat2_dequant = _dequant(mat2, mat2_scale, mat2_format) result = torch.mm(mat1_dequant, mat2_dequant) resolved_dtype = out_dtype or torch.float32 result = result.to(resolved_dtype) if acc is not None: return acc + result return result