Rate this Page
★ ★ ★ ★ ★

Source code for helion.language.memory_ops

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

import torch
from torch.fx import has_side_effect

from .. import exc
from .._compiler.ast_extension import expr_from_string
from .._compiler.compile_environment import CompileEnvironment
from .._compiler.indexing_strategy import SubscriptIndexing
from .._compiler.utils import normalize_index_slices
from . import _decorators
from .stack_tensor import StackTensor

if TYPE_CHECKING:
    import ast
    from collections.abc import Sequence

    from .._compiler.inductor_lowering import CodegenState

# TileBeginWithOffset removed - using TileBeginWithOffsetPattern instead

__all__ = ["load", "store"]

log = logging.getLogger(__name__)


[docs] @has_side_effect @_decorators.api(tiles_as_sizes=True, allow_host_tensor=True) def store( tensor: torch.Tensor | StackTensor, index: list[object], value: torch.Tensor | torch.SymInt | float, extra_mask: torch.Tensor | None = None, ) -> None: """Store a value to a tensor using a list of indices. This function is equivalent to `tensor[index] = value` but allows setting `extra_mask=` to mask elements beyond the default masking based on the hl.tile range. Args: tensor: The tensor / stack tensor to store to index: The indices to use to index into the tensor value: The value to store extra_mask: The extra mask (beyond automatic tile bounds masking) to apply to the tensor Returns: None """ raise exc.NotInsideKernel
@_decorators.prepare_args(store) def _( tensor: torch.Tensor | StackTensor, index: list[object], value: torch.Tensor | torch.SymInt | float, extra_mask: torch.Tensor | None = None, ) -> tuple[ torch.Tensor | tuple, list[object], torch.Tensor | torch.SymInt | float | int, torch.Tensor | None, ]: from .tile_proxy import Tile if isinstance(value, torch.Tensor) and value.dtype != tensor.dtype: value = value.to(tensor.dtype) index = Tile._tiles_to_sizes_for_index(index) if isinstance(tensor, StackTensor): index = normalize_index_slices(tensor.tensor_like.shape, index) return (tuple(tensor), index, value, extra_mask) if isinstance(tensor, torch.Tensor): index = normalize_index_slices(tensor.shape, index) return (tensor, index, value, extra_mask) raise NotImplementedError(f"Cannot store to type: {type(tensor)}") @_decorators.register_fake(store) def _( tensor: torch.Tensor | tuple[object, ...], index: list[object], value: torch.Tensor | torch.SymInt | float, extra_mask: torch.Tensor | None = None, ) -> None: # Allocate the reduction dims of full slices as ``tensor[index] = value`` # and ``hl.load`` do, so the slots of the store have blocks to address # them by. target = tensor if isinstance(tensor, torch.Tensor) else tensor[0] assert isinstance(target, torch.Tensor) SubscriptIndexing.compute_shape(target, index) return None def _record_pad_info( state: CodegenState, tensor: torch.Tensor, tensor_dim: int, block_id: int, extra_pad: int = 0, ) -> None: """Record that a tensor dimension uses pl.ds() and may need host-side padding. *extra_pad* accounts for non-zero loop begins: 0 when the loop starts at offset 0, ``begin % block_size`` for a constant begin, or ``block_size - 1`` for a data-dependent begin. Every block_id that tiles the dim is kept: two inner loops can tile the same dim with different block sizes, and the pad must cover both. """ pad_info = state.device_function.pallas_pad_info blocks = pad_info.setdefault(id(tensor), {}).setdefault(tensor_dim, {}) blocks[block_id] = max(blocks.get(block_id, 0), extra_pad) def _ref_masked_slice_indices( tensor: torch.Tensor, indices: list[object] ) -> list[object]: """Include sliced axes in Cartesian indexing and mask broadcasting.""" index_dtype = next( (index.dtype for index in indices if isinstance(index, torch.Tensor)), torch.int64, ) return [ torch.arange(tensor.size(dim), device=tensor.device, dtype=index_dtype)[index] if isinstance(index, slice) else index for dim, index in enumerate(indices) ] # TODO(joydddd): Add support for stack tensor in ref mode. @_decorators.ref(store) def _( tensor: torch.Tensor, index: list[object], value: torch.Tensor | torch.SymInt | float, extra_mask: torch.Tensor | None = None, ) -> None: from ..runtime.ref_mode import DEVICE_LOAD_NAME from ..runtime.ref_mode import is_device_load from .ref_tile import RefTile if is_device_load(tensor): raise exc.DeviceTensorSubscriptAssignmentNotAllowed(DEVICE_LOAD_NAME) # Normalize indices and identify tensor indices indices = [idx.index if isinstance(idx, RefTile) else idx for idx in index] if extra_mask is not None: indices = _ref_masked_slice_indices(tensor, indices) tensor_idx_positions = [ i for i, idx in enumerate(indices) if isinstance(idx, torch.Tensor) ] # Handle broadcasting for multiple tensor indices if len(tensor_idx_positions) > 1: grids = torch.meshgrid( # pyrefly: ignore [bad-argument-type] *(indices[i] for i in tensor_idx_positions), indexing="ij", ) for i, grid in zip(tensor_idx_positions, grids, strict=False): # pyrefly: ignore [unsupported-operation] indices[i] = grid if extra_mask is not None: mask = extra_mask.to(torch.bool) # Check bounds for tensor indices for i, idx in enumerate(indices): if isinstance(idx, torch.Tensor): mask = mask & (idx >= 0) & (idx < tensor.shape[i]) mask_count = int(mask.sum().item()) if mask_count == 0: return # Use index_put_ for masked stores valid_indices = [] for idx in indices: if isinstance(idx, torch.Tensor): valid_indices.append(idx[mask].long()) else: idx_val = int(idx) if isinstance(idx, torch.SymInt) else idx valid_indices.append( # pyrefly: ignore [no-matching-overload] torch.full( (mask_count,), idx_val, dtype=torch.long, device=tensor.device ) ) if isinstance(value, torch.Tensor): values = value[mask] else: val = int(value) if isinstance(value, torch.SymInt) else value values = torch.full( (mask_count,), val, dtype=tensor.dtype, device=tensor.device ) # Check for duplicate indices - this is undefined behavior in Triton if valid_indices: stacked = torch.stack(valid_indices, dim=1) unique_count = stacked.unique(dim=0).size(0) if unique_count < stacked.size(0): raise exc.DuplicateStoreIndicesError( "hl.store with duplicate indices has undefined behavior in compiled mode. " "The order in which values are written to the same memory location is " "non-deterministic and may vary between Triton versions and backends." ) tensor.index_put_(tuple(valid_indices), values, accumulate=False) return # Simple assignment tensor[tuple(indices)] = ( # pyrefly: ignore[unsupported-operation] int(value) if isinstance(value, torch.SymInt) else value )
[docs] @_decorators.api(tiles_as_sizes=True, allow_host_tensor=True) def load( tensor: torch.Tensor | StackTensor, index: list[object], extra_mask: torch.Tensor | None = None, eviction_policy: str | None = None, ) -> torch.Tensor: """Load a value from a tensor using a list of indices. This function is equivalent to `tensor[index]` but allows setting `extra_mask=` to mask elements beyond the default masking based on the hl.tile range. It also accepts an optional `eviction_policy` which is forwarded to the underlying Triton `tl.load` call to control the cache eviction behavior (e.g., "evict_last"). Args: tensor: The tensor / stack tensor to load from index: The indices to use to index into the tensor extra_mask: The extra mask (beyond automatic tile bounds masking) to apply to the tensor eviction_policy: Optional Triton load eviction policy to hint cache behavior Returns: torch.Tensor: The loaded value """ raise exc.NotInsideKernel
@_decorators.prepare_args(load) def _( tensor: torch.Tensor | StackTensor, index: list[object], extra_mask: torch.Tensor | None = None, eviction_policy: str | None = None, ) -> tuple[torch.Tensor | tuple, list[object], torch.Tensor | None, str | None]: from .tile_proxy import Tile index = Tile._tiles_to_sizes_for_index(index) if isinstance(tensor, StackTensor): index = normalize_index_slices(tensor.tensor_like.shape, index) return (tuple(tensor), index, extra_mask, eviction_policy) assert isinstance(tensor, torch.Tensor) index = normalize_index_slices(tensor.shape, index) return (tensor, index, extra_mask, eviction_policy) @_decorators.register_fake(load) def _( tensor: torch.Tensor | tuple[object, ...], index: list[object], extra_mask: torch.Tensor | None = None, eviction_policy: str | None = None, ) -> torch.Tensor: if isinstance(tensor, torch.Tensor): target_shape = SubscriptIndexing.compute_shape(tensor, index) env = CompileEnvironment.current() env.backend.process_fake_tensor_load(tensor, index) return env.new_index_result(tensor, target_shape) if isinstance(tensor, tuple): tensor_like, dev_ptrs = tensor assert isinstance(tensor_like, torch.Tensor) assert isinstance(dev_ptrs, torch.Tensor) tensor_shape = SubscriptIndexing.compute_shape(tensor_like, index) target_shape = list(dev_ptrs.size()) + tensor_shape return tensor_like.new_empty(target_shape) raise NotImplementedError(f"Unsupported tensor type: {type(tensor)}") def _maybe_materialize_tile_index_load( state: CodegenState, tensor: torch.Tensor, subscript: list[object] | tuple[object, ...], ) -> ast.AST | None: """If this load is on a ``tile.index`` value (e.g. ``tile_m.index[:, None]``), emit the inline ``indices_<bid>[<sub>]`` expression and return it. Returns ``None`` otherwise. ``tile.index`` tensors are synthesized inside the kernel — they aren't registered in ``tensor_to_origin`` — so the regular load path's ``tensor_arg`` lookup would ``KeyError``. Supported subscript entries are ``None`` (new axis) and ``slice(None)`` (full slice). """ from ..language import tile_index tensor_node = state.fx_node.args[0] if state.fx_node is not None else None if not ( isinstance(tensor_node, torch.fx.Node) and tensor_node.op == "call_function" and tensor_node.target == tile_index ): return None env = CompileEnvironment.current() block_id = env.get_block_id(tensor.size(0)) assert block_id is not None base_var = state.codegen.index_var(block_id) parts = [] for idx in subscript: if idx is None: parts.append("None") elif idx == slice(None): parts.append(":") else: raise AssertionError(f"Unexpected index type in tile_index load: {idx}") return expr_from_string(f"{base_var}[{', '.join(parts)}]") @_decorators.get_masked_value(load) def _(node: torch.fx.Node) -> int: return 0 # loads are always masked to 0 # TODO(joydddd): Add support for stack tensor in ref mode. @_decorators.ref(load) def _( tensor: torch.Tensor, index: list[object], extra_mask: torch.Tensor | None = None, eviction_policy: str | None = None, ) -> torch.Tensor: from ..runtime.ref_mode import device_load from .ref_tile import RefTile if extra_mask is None: # Convert RefTiles to indices indices = [idx.index if isinstance(idx, RefTile) else idx for idx in index] # Use meshgrid for Cartesian product when we have multiple tensor indices tensor_idxs = [ i for i, idx in enumerate(indices) if isinstance(idx, torch.Tensor) ] if len(tensor_idxs) > 1: # pyrefly: ignore [bad-argument-type] grids = torch.meshgrid(*(indices[i] for i in tensor_idxs), indexing="ij") for i, grid in zip(tensor_idxs, grids, strict=False): indices[i] = grid # Integer and slice indices give a view. return device_load( # pyrefly: ignore [bad-argument-type, bad-index] tensor[tuple(indices)] ) # Create zero result matching mask shape result = torch.zeros(extra_mask.shape, dtype=tensor.dtype, device=tensor.device) index = _ref_masked_slice_indices( tensor, [idx.index if isinstance(idx, RefTile) else idx for idx in index] ) # Process indices: clamp tensor indices orig_indices, safe_indices, is_tensor_mask = [], [], [] for i, idx in enumerate(index): if isinstance(idx, torch.Tensor): dim_size = tensor.shape[i] if i < len(tensor.shape) else tensor.numel() orig_indices.append(idx) safe_indices.append(torch.clamp(idx, 0, dim_size - 1)) is_tensor_mask.append(True) else: orig_indices.append(idx) safe_indices.append(idx) is_tensor_mask.append(False) # Apply broadcasting if we have multiple tensor indices tensor_positions = [i for i, is_tensor in enumerate(is_tensor_mask) if is_tensor] if len(tensor_positions) > 1: # Add unsqueeze operations for broadcasting broadcast_indices = [] for i, (idx, is_tensor) in enumerate( zip(safe_indices, is_tensor_mask, strict=False) ): if is_tensor: new_idx = idx # Add dimension for each other tensor index for j, other_pos in enumerate(tensor_positions): if other_pos != i: new_idx = new_idx.unsqueeze(j if other_pos < i else -1) broadcast_indices.append(new_idx) else: broadcast_indices.append(idx) values = tensor[tuple(broadcast_indices)] else: values = tensor[tuple(safe_indices)] # Build validity mask valid_mask = extra_mask.clone() for i, (orig_idx, is_tensor) in enumerate( zip(orig_indices, is_tensor_mask, strict=False) ): if is_tensor: dim_size = tensor.shape[i] if i < len(tensor.shape) else tensor.numel() in_bounds = (orig_idx >= 0) & (orig_idx < dim_size) # Broadcast to match mask shape by adding dimensions # Count how many tensor indices come before and after this one n_before = sum(1 for j in range(i) if is_tensor_mask[j]) n_after = sum( 1 for j in range(i + 1, len(is_tensor_mask)) if is_tensor_mask[j] ) # Add dimensions: n_after dimensions at the end, n_before at the beginning for _ in range(n_after): in_bounds = in_bounds.unsqueeze(-1) for _ in range(n_before): in_bounds = in_bounds.unsqueeze(0) valid_mask = valid_mask & in_bounds return torch.where(valid_mask, values, result)
[docs] @has_side_effect @_decorators.api(tiles_as_sizes=True, allow_host_tensor=True) def prefetch( tensor: torch.Tensor, index: list[object], extra_mask: torch.Tensor | None = None, ) -> None: """Hint that ``tensor[index]`` will be read soon by warming the L2 cache. ``index`` and ``extra_mask`` follow :func:`load` (tiles, integers, slices, ``hl.arange`` / integer-tensor gathers). Nothing is returned and nothing is ordered: a prefetch neither waits for earlier stores nor makes later loads see anything, so dropping it never changes a result. Unlike :func:`load` it takes no ``indexing``, eviction-policy or cache-modifier config slot, so adding one leaves every load's tunables where they were. On NVIDIA GPUs (Triton backend), when the last index is a full slice or a tile of a stride-1 dim and there is no ``extra_mask``, each element of the leading indices issues one ``cp.async.bulk.prefetch.L2`` covering its row segment (sm_90+). Any other prefetch issues ``prefetch.global.L2`` per element (one 128-byte line each). Other targets drop it. Args: tensor: The tensor to prefetch from index: The indices to use to index into the tensor extra_mask: The extra mask (beyond automatic tile bounds masking) to apply to the tensor Returns: None """ raise exc.NotInsideKernel
@_decorators.prepare_args(prefetch) def _( tensor: torch.Tensor, index: list[object], extra_mask: torch.Tensor | None = None, ) -> tuple[torch.Tensor, list[object], torch.Tensor | None]: from .tile_proxy import Tile if not isinstance(tensor, torch.Tensor): raise NotImplementedError(f"Cannot prefetch from type: {type(tensor)}") index = Tile._tiles_to_sizes_for_index(index) return (tensor, normalize_index_slices(tensor.shape, index), extra_mask) def prefetch_is_bulk( tensor: torch.Tensor, index: Sequence[object], has_extra_mask: bool ) -> bool: """Whether ``hl.prefetch(tensor, index)`` issues one bulk L2 prefetch per element of its leading indices: the last index is a full slice or a tile of a stride-1 dim, there is no extra mask, and the target is sm_90+.""" env = CompileEnvironment.current() capability = env.ptx_capability if capability is None or capability < (9, 0) or has_extra_mask: return False if tensor.ndim == 0 or len(index) != tensor.ndim: return False if any(idx is None or idx is Ellipsis for idx in index): return False stride = tensor.stride(-1) if not (isinstance(stride, int) and stride == 1): return False if env.known_equal(tensor.size(-1), 1): return False last = index[-1] if isinstance(last, slice): return last.start is None and last.stop is None and last.step is None return isinstance(last, torch.SymInt) and env.get_block_id(last) is not None @_decorators.register_fake(prefetch) def _( tensor: torch.Tensor, index: list[object], extra_mask: torch.Tensor | None = None, ) -> None: # Allocate the reduction dims of full slices as ``hl.load`` does, except # a bulk prefetch's row segment, which its byte count covers. if prefetch_is_bulk(tensor, index, extra_mask is not None): index = [*index[:-1], 0] SubscriptIndexing.compute_shape(tensor, index) return None @_decorators.codegen(prefetch, "common") def _(state: CodegenState) -> None: return None # a hint: targets without a prefetch drop it @_decorators.ref(prefetch) def _( tensor: torch.Tensor, index: list[object], extra_mask: torch.Tensor | None = None, ) -> None: return None