1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 2# See https://llvm.org/LICENSE.txt for license information. 3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 4 5"""Experimental MLIR-PyTACO with sparse tensor support. 6 7See http://tensor-compiler.org/ for TACO tensor compiler. 8 9This module implements the Python classes for PyTACO index notation. These 10include classes for data types, tensor dimension formats (aka mode formats), 11tensor dimension orderings (aka mode ordering), tensor storage formats, and 12tensors. 13 14The PyTACO API doesn't follow the naming conversion required by the style guide 15for this module. As such, we first implement the supporting classes and routines 16following the style guide, and then define the type aliases and constants to 17support the PyTACO API in the pytaco_api module. 18""" 19 20from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Union 21 22import abc 23import ctypes 24import dataclasses 25import enum 26import numpy as np 27import functools 28import operator 29import os 30import threading 31 32# Import MLIR related modules. 33from mlir import execution_engine 34from mlir import ir 35from mlir import runtime 36from mlir.dialects import arith 37from mlir.dialects import bufferization 38from mlir.dialects import builtin 39from mlir.dialects import func 40from mlir.dialects import linalg 41from mlir.dialects import sparse_tensor 42from mlir.dialects.linalg.opdsl import lang 43 44from . import mlir_pytaco_utils as utils 45 46# TACO naming prefixes. 47_TACO_INDEX_PREFIX = "i" 48_TACO_TENSOR_PREFIX = "A" 49 50# Bitwidths for pointers and indices. 51_POINTER_BIT_WIDTH = 0 52_INDEX_BIT_WIDTH = 0 53# The entry point to the JIT compiled program. 54_ENTRY_NAME = "main" 55 56# Type aliases for type annotation. 57_UnaryOp = Callable[[Any], Any] 58_BinaryOp = Callable[[Any, Any], Any] 59_ExprVisitor = Callable[..., None] 60_ExprInfoDict = Dict["IndexExpr", "_ExprInfo"] 61_LogicalOp = Callable[[bool, bool], bool] 62_ModeFormatOp = Callable[["ModeFormat", "ModeFormat"], "ModeFormat"] 63_SubtreeLeafChecker = Optional[Callable[..., bool]] 64 65 66class Type(enum.Enum): 67 """The data types supported by TACO. 68 69 We use numpy data types to implement the enum data types. 70 """ 71 INT8 = np.int8 72 INT16 = np.int16 73 INT32 = np.int32 74 INT64 = np.int64 75 # numpy _ctype_from_dtype_scalar can't handle np.float16 yet. 76 FLOAT32 = np.float32 77 FLOAT64 = np.float64 78 79 80# All floating point type enums. 81_FLOAT_TYPES = (Type.FLOAT32, Type.FLOAT64) 82# All integral type enums. 83_INT_TYPES = (Type.INT8, Type.INT16, Type.INT32, Type.INT64) 84# Type alias for any numpy type used to implement the runtime support for the 85# enum data types. 86_AnyRuntimeType = Union[np.int8, np.int16, np.int32, np.int64, np.float32, 87 np.float64] 88 89 90@dataclasses.dataclass(frozen=True) 91class DType: 92 """The data type class. 93 94 We support the TACO API dtype class with an alias of this class. 95 96 The following methods are defined by the TACO API: 97 is_float: Returns whether the data type represents a floating point value. 98 is_int: Returns whether the data type represents an integral value. 99 100 Attributes: 101 kind: A Type enum representing the data type. 102 value: The numpy data type for the TACO data type. 103 """ 104 kind: Type = Type.FLOAT32 105 106 def is_float(self) -> bool: 107 """Returns whether the data type represents a floating point value.""" 108 return self.kind in _FLOAT_TYPES 109 110 def is_int(self) -> bool: 111 """Returns whether the data type represents an integral value.""" 112 return self.kind in _INT_TYPES 113 114 @property 115 def value(self) -> _AnyRuntimeType: 116 """Returns the numpy dtype for the data type.""" 117 return self.kind.value 118 119 120def _dtype_to_mlir_str(dtype: DType) -> str: 121 """Returns the MLIR string for the given dtype.""" 122 dtype_to_str = { 123 Type.INT16: "i8", 124 Type.INT16: "i16", 125 Type.INT32: "i32", 126 Type.INT64: "i64", 127 Type.FLOAT32: "f32", 128 Type.FLOAT64: "f64" 129 } 130 return dtype_to_str[dtype.kind] 131 132 133def _nptype_to_taco_type(ty: np.dtype) -> DType: 134 """Returns the TACO type for the given numpy type.""" 135 nptype_to_dtype = { 136 np.int8: Type.INT8, 137 np.int16: Type.INT16, 138 np.int32: Type.INT32, 139 np.int64: Type.INT64, 140 np.float32: Type.FLOAT32, 141 np.float64: Type.FLOAT64 142 } 143 return DType(nptype_to_dtype[ty]) 144 145 146def _mlir_type_from_taco_type(dtype: DType) -> ir.Type: 147 """Returns the MLIR type corresponding to the given TACO type.""" 148 dtype_to_irtype = { 149 Type.INT8: ir.IntegerType.get_signless(8), 150 Type.INT16: ir.IntegerType.get_signless(16), 151 Type.INT32: ir.IntegerType.get_signless(32), 152 Type.INT64: ir.IntegerType.get_signless(64), 153 Type.FLOAT32: ir.F32Type.get(), 154 Type.FLOAT64: ir.F64Type.get() 155 } 156 return dtype_to_irtype[dtype.kind] 157 158def _ctype_pointer_from_array(array: np.ndarray) -> ctypes.pointer: 159 """Returns the ctype pointer for the given numpy array.""" 160 return ctypes.pointer( 161 ctypes.pointer(runtime.get_ranked_memref_descriptor(array))) 162 163 164class ModeFormat(enum.Enum): 165 """The tensor dimension storage format class. 166 167 We support the TACO API mode_format class with an alias of this class. 168 169 In TACO, a tensor dimension is called a mode and the storage format for a 170 tensor dimension is called a mode format. 171 """ 172 DENSE = sparse_tensor.DimLevelType.dense 173 COMPRESSED = sparse_tensor.DimLevelType.compressed 174 175 176def _mode_format_operation(a: ModeFormat, b: ModeFormat, 177 op: _LogicalOp) -> ModeFormat: 178 """Implements the given operator on ModeFormat.""" 179 return (ModeFormat.COMPRESSED 180 if op(a == ModeFormat.COMPRESSED, b == ModeFormat.COMPRESSED) else 181 ModeFormat.DENSE) 182 183 184def _mode_format_estimator(op: _BinaryOp) -> _ModeFormatOp: 185 """Produces a ModeFormat operator for the given binary operator. 186 187 The ModeFormat operator is used as a heuristic to derive the destination 188 dimension sparsity from the source dimension sparsity. In particular, if the 189 binary operator produces a disjunction of the zero values from its source 190 operands, such as the MUL operator, we return a ModeFormat operator that 191 uses operator.or_. That is, we estimate that a dimension for the MUL 192 operation result to be sparse if either of its source operands is sparse. 193 194 On the other hand, if the binary operator produces a conjunction of the 195 zero values from its source operands, such as the ADD operator, we return 196 a ModeFormat operator that uses operator.and_. In this case, we estimate 197 that a dimension for the ADD operation result to be sparse if both of its 198 source operands are sparse. 199 200 Args: 201 op: A _BinaryOp object representing a supporting operator on tensors. 202 203 Returns: 204 A ModeFormatOp for estimating the destination dimension sparsity from 205 the source dimension sparsity. 206 """ 207 conjunction = functools.partial(_mode_format_operation, op=operator.and_) 208 disjunction = functools.partial(_mode_format_operation, op=operator.or_) 209 return conjunction if op(0, 1) != 0 else disjunction 210 211 212def _all_instance_of(collection: Iterable, cls: Any) -> bool: 213 """Returns true if all elements of the iterable is an instance of cls.""" 214 return all(isinstance(e, cls) for e in collection) 215 216 217def _identity_ordering(rank: int) -> List[int]: 218 """Returns the identity ordering for tensor of given rank.""" 219 return list(range(rank)) 220 221 222@dataclasses.dataclass(frozen=True) 223class ModeOrdering: 224 """The tensor dimension ordering class. 225 226 We support the TACO API mode_ordering class with an alias of this class. 227 228 Attributes: 229 ordering: A list of integers representing the ordering of the tensor 230 dimensions. 231 """ 232 ordering: List[int] 233 234 def __post_init__(self) -> None: 235 """Verifies the value in ordering. 236 237 Raises: 238 ValueError: If ordering is not a list of integers. 239 """ 240 if (not isinstance(self.ordering, list) or 241 not _all_instance_of(self.ordering, int)): 242 raise ValueError("Ordering must be a list of integers: " 243 f"{self.ordering}") 244 # Check that ordering is a permutation of the dimension numbers. 245 if sorted(self.ordering) != _identity_ordering(self.rank()): 246 raise ValueError(f"Invalid ordering: {self.ordering} != " 247 f"permutation{_identity_ordering(self.rank())}.") 248 249 def rank(self) -> int: 250 """Returns the number of dimensions represented by the ordering.""" 251 return len(self.ordering) 252 253 254@dataclasses.dataclass(frozen=True) 255class ModeFormatPack: 256 """The tensor dimension format class. 257 258 We support the TACO API mode_format_pack class with an alias of this class. 259 260 The storage format of a tensor contains one mode_format for each tensor 261 dimension. 262 263 Attributes: 264 formats: A list of ModeFormat representing the storage format for each of 265 the tensor dimension. 266 """ 267 formats: List[ModeFormat] 268 269 def __post_init__(self) -> None: 270 """Verifies the value in formats. 271 272 Raises: 273 ValueError: If formats is not a list of ModeFormats. 274 """ 275 if (not isinstance(self.formats, list) or 276 not _all_instance_of(self.formats, ModeFormat)): 277 raise ValueError("Formats must be a list of ModeFormat: " 278 f"{self.formats}") 279 280 def rank(self) -> int: 281 """Returns the number of dimensions represented by the format pack.""" 282 return len(self.formats) 283 284 285@dataclasses.dataclass 286class Format: 287 """The tensor format class defined by the TACO API. 288 289 Attributes: 290 format_pack: A ModeFormatPack representing the storage format for the tensor 291 dimensions. 292 ordering: A ModeOrdering representing the tensor dimension ordering in the 293 storage. 294 """ 295 format_pack: ModeFormatPack 296 ordering: Optional[ModeOrdering] = None 297 298 def __post_init__(self) -> None: 299 """Verifies and fixes up the values in format_pack and ordering. 300 301 Verifies and fixes up the values in format_pack and ordering to supports the 302 initializer syntax defined by the TACO API. If format_pack is a list of 303 ModeFormat, replaces it with ModeFormatPack constructed from the list. If 304 ordering is not provided, set ordering to the natural ordering for the rank 305 corresponding to format_pack. 306 307 Raises: 308 ValueError: If format_pack is not an instance of ModeFormatPack or if 309 ordering is not an instance of ModeOrdering. 310 """ 311 if isinstance(self.format_pack, list): 312 if not _all_instance_of(self.format_pack, ModeFormat): 313 raise ValueError(f"Expected a list of ModeFormat: {self.format_pack}") 314 self.format_pack = ModeFormatPack(self.format_pack) 315 if not isinstance(self.format_pack, ModeFormatPack): 316 raise ValueError(f"Expected ModeFormatpack: {self.format_pack}") 317 318 if self.ordering is None: 319 self.ordering = ModeOrdering(list(range(self.rank()))) 320 if isinstance(self.ordering, list): 321 if not _all_instance_of(self.ordering, int): 322 raise ValueError(f"Expected a list of integer: {self.ordering}") 323 self.ordering = ModeOrdering(self.ordering) 324 if not isinstance(self.ordering, ModeOrdering): 325 raise ValueError(f"Expected ModeOrdering: {self.ordering}") 326 327 if self.format_pack.rank() != self.ordering.rank(): 328 raise ValueError("Inconsistent ModeFormatPack and ModeOrdering: " 329 f"len({self.format_pack}) != " 330 f"len({self.ordering})") 331 332 def rank(self) -> int: 333 """Returns the number of dimensions represented by the format.""" 334 return self.format_pack.rank() 335 336 def get_permutation_and_sparsity(self) -> Tuple[np.ndarray, np.ndarray]: 337 """Constructs the numpy arrays for the permutation and sparsity.""" 338 perm = np.array(self.ordering.ordering, dtype=np.ulonglong) 339 a = [0 if s == ModeFormat.DENSE else 1 for s in self.format_pack.formats] 340 sparse = np.array(a, dtype=np.uint8) 341 return (perm, sparse) 342 343 def mlir_tensor_attr(self) -> Optional[sparse_tensor.EncodingAttr]: 344 """Constructs the MLIR attributes for the tensor format.""" 345 order = ( 346 range(self.rank()) if 347 (self.ordering is None) else self.ordering.ordering) 348 mlir_storage_format = [f.value for f in self.format_pack.formats] 349 return sparse_tensor.EncodingAttr.get(mlir_storage_format, 350 ir.AffineMap.get_permutation(order), 351 _POINTER_BIT_WIDTH, _INDEX_BIT_WIDTH) 352 353 354def _make_format(formats: List[ModeFormat], 355 ordering: Optional[List[int]] = None) -> Format: 356 """Constructs a format from a list of ModeFormat and an optional ordering. 357 358 Args: 359 formats: A list of ModeFormat, one for each dimension of a tensor. 360 ordering: An optional list of integer, for the ordering of the tensor 361 dimensions. When an ordering is not given, the identity ordering is used. 362 363 Returns: 364 A tensor format object. 365 366 Raises: 367 ValueError: If formats is not a list of ModeFormat or the length of formats 368 is not consistent with the len of ordering. 369 """ 370 ordering = ordering or _identity_ordering(len(formats)) 371 return Format(ModeFormatPack(formats), ModeOrdering(ordering)) 372 373 374class IndexExpr(abc.ABC): 375 """The index notation base class. 376 377 We support the TACO API index_expression class with an alias of this class. 378 """ 379 380 def _verify_operand_and_build_expr(self, rhs, op: _BinaryOp) -> "_BinaryExpr": 381 """Verifies the RHS operand and returns a binary expression. 382 383 Args: 384 rhs: The RHS of the binary operation, which could be any Python object 385 from user inputs. 386 op: A _BinaryOp object representing the binary operator. 387 388 Raises: 389 ValueError: If rhs is not an IndexExpr. 390 """ 391 if not isinstance(rhs, IndexExpr): 392 raise ValueError(f"Expected IndexExpr: {rhs}") 393 return _BinaryExpr(op, self, rhs) 394 395 def _build_unary_expr(self, op: _UnaryOp) -> "_UnaryExpr": 396 """Build a unary expression. 397 398 Args: 399 op: A _UnaryOp object representing the unary operation. 400 """ 401 return _UnaryExpr(op, self) 402 403 def __add__(self, rhs) -> "_BinaryExpr": 404 """Defines the operator +. 405 406 Args: 407 rhs: The value being added, which could be any Python object from user 408 inputs. 409 410 Returns: 411 A _BinaryExpr object representing the operation. 412 413 Raises: 414 ValueError: If rhs is not an IndexExpr. 415 """ 416 return self._verify_operand_and_build_expr(rhs, operator.add) 417 418 def __mul__(self, rhs) -> "_BinaryExpr": 419 """Defines the operator *. 420 421 Args: 422 rhs: The value being multiplied, which could be any Python object from 423 user inputs. 424 425 Returns: 426 A _BinaryExpr object representing the operation. 427 428 Raises: 429 ValueError: If rhs is not an IndexExpr. 430 """ 431 return self._verify_operand_and_build_expr(rhs, operator.mul) 432 433 def __abs__(self) -> "_UnaryExpr": 434 """Defines the operator abs. 435 436 Returns: 437 A _UnaryExpr object representing the operation. 438 """ 439 return self._build_unary_expr(operator.abs) 440 441 def __neg__(self) -> "_UnaryExpr": 442 """Defines the operator neg. 443 444 Returns: 445 A _UnaryExpr object representing the operation. 446 """ 447 return self._build_unary_expr(operator.neg) 448 449 def __sub__(self, rhs) -> "_BinaryExpr": 450 """Defines the operator -. 451 452 Args: 453 rhs: The value being subtracted, which could be any Python object from 454 user inputs. 455 456 Returns: 457 A _BinaryExpr object representing the operation. 458 459 Raises: 460 ValueError: If rhs is not an IndexExpr. 461 """ 462 return self._verify_operand_and_build_expr(rhs, operator.sub) 463 464 @abc.abstractmethod 465 def _visit(self, 466 func: _ExprVisitor, 467 args, 468 *, 469 leaf_checker: _SubtreeLeafChecker = None) -> None: 470 """A post-order visitor. 471 472 Args: 473 func: A callable applied to each node in the expression tree. 474 args: The variable-length arguments passed to the callable. These 475 arguments are grouped as an iterable and will be unpacked before passing 476 to the callable. This is to enable the keyword argument only syntax 477 after this argument. 478 leaf_checker: A callable object to identify nodes that should be treated 479 as leaf nodes to support partial tree visiting. 480 """ 481 pass 482 483 @abc.abstractmethod 484 def _emit_expression( 485 self, 486 expr_to_opnd: Dict["IndexExpr", lang.OperandDef], 487 expr_to_info: _ExprInfoDict, 488 ) -> lang.ScalarExpression: 489 """Emits MLIR for the expression tree. 490 491 Args: 492 expr_to_opnd: A dictionary for looking up structured op input operands for 493 the input nodes of the structured op. 494 expr_to_info: A dictionary for looking up code generation information for 495 expressions. 496 497 Returns: 498 A linalg dialect ScalarExpression for the expression. 499 """ 500 pass 501 502 @abc.abstractmethod 503 def dtype(self) -> DType: 504 """Returns the data type for the result of the expression.""" 505 pass 506 507 def _emit_structured_op(self, expr_to_info: _ExprInfoDict) -> None: 508 """Emits a structured op in the linalg dialect for the expression tree. 509 510 We define a DefineOpcallable in the domain specific language for the linalg 511 dialect and execute the callable to generate the structured op. Self is the 512 root of the expression tree for the structured op. 513 514 Args: 515 expr_to_info: A dictionary for looking up code generation information for 516 expressions. 517 """ 518 op_info = expr_to_info[self].structop_info 519 op_name = op_info.dst_name 520 op_def = lang.LinalgOpDef(name=op_name) 521 op_callable = lang.DefinedOpCallable(op_name, op_def) 522 523 # Collect the input expression nodes for the structured op. 524 expr_inputs = [] 525 self._visit( 526 _gather_structured_op_input, 527 (self, expr_to_info, expr_inputs), 528 leaf_checker=_is_structured_op_leaf, 529 ) 530 531 # Create a linalg structured op operand for each input expression node and 532 # build a dictionary for looking up the information. 533 expr_to_input_opnd = { 534 e: _emit_structured_op_input(e, expr_to_info, op_def) 535 for e in expr_inputs 536 } 537 538 # Emit the expression tree, which produces the value assigned to the 539 # destination tensor. 540 value = self._emit_expression(expr_to_input_opnd, expr_to_info) 541 # Emit the structured op representation for the destination tensor. 542 dst_opnd = _emit_operand(op_def, op_info.dst_indices, op_info.dst_name, 543 lang.OperandKind.OUTPUT_TENSOR) 544 dst_dim_syms = _mlir_dimensions_from_index_vars(op_info.dst_indices) 545 dst_use = lang.TensorUse(dst_opnd, dst_dim_syms) 546 547 expr_info = expr_to_info[self] 548 # If the structured op reduces some indices, explicitly represent the 549 # reduction. This is done by generating a ReduceFn for the dimensions being 550 # reduced in the linalg dialect and calling the function with the value 551 # being reduced. We only support add reduction currently. 552 if expr_info.reduce_indices: 553 reduce_dims = _mlir_dimensions_from_index_vars(expr_info.reduce_indices) 554 value = lang.ReduceFn.add[reduce_dims](value) 555 556 # Emit the assignment as a comprehension in the linalg dialect. 557 comp = lang.Comprehension((dst_use, value)) 558 op_def.comprehensions.append(comp) 559 560 # The structured op in the linalg dialect requires an explicit 561 # initialization for the destination tensor. Emit MLIR to initialize the 562 # destination tensor. 563 init = op_info.emit_tensor_init() 564 565 # Collect MLIR values for the linalg input operands, with the assumption 566 # that dictionary preserves the insertion order. 567 args = [ 568 expr_to_info[expr].mlir_value 569 for expr, opnd in expr_to_input_opnd.items() 570 ] 571 # Execute the DefineOpcallable object for the linalg dialect operation to 572 # emit MLIR for the linalg structured op. 573 expr_info.mlir_value = op_callable(*args, outs=[init]) 574 575 def _identify_structured_ops( 576 self, 577 expr_to_info: _ExprInfoDict, 578 dst: "Tensor", 579 dst_indices: Tuple["IndexVar", ...], 580 ) -> List["IndexExpr"]: 581 """Returns expression nodes for the roots of the identified structured ops. 582 583 A structured op in the linalg dialect only supports reduction performed on 584 the whole expression. If the expression tree contains reduction that are 585 performed on part of the expression tree, the expression tree needs to be 586 implemented with multiple structured ops. This routine identifies all the 587 expression nodes that contain reduction as the root of structured ops in the 588 linalg dialect. 589 590 Args: 591 expr_to_info: A dictionary for looking up code generation information for 592 expressions. 593 dst: A destination Tensor that accepts the value of the expression tree. 594 dst_indices: The indices used by the destination index expression. 595 596 Returns: 597 An ordered list of IndexExpr for the root expressions of the structured 598 ops, where child expressions go before parent expressions that use their 599 results. 600 """ 601 reduce_indices = tuple( 602 set(expr_to_info[self].src_indices) - set(dst_indices)) 603 for reduce_index in reduce_indices: 604 _mark_structured_op_root(self, reduce_index, expr_to_info) 605 606 self._visit(_accumulate_reduce_indices, (expr_to_info,)) 607 structop_roots = [] 608 self._visit(_gather_structured_op, (expr_to_info, structop_roots)) 609 610 # Handle the root of the top level expression. 611 if not structop_roots or structop_roots[-1] != self: 612 # The top level expression is not a reduction. Add the top level 613 # expression as a structured op root. 614 structop_roots.append(self) 615 616 # Use user specified information for the destination tensor to build an 617 # _StructOpInfo for the top level expression. 618 expr_to_info[self].structop_info = _StructOpInfo(dst_indices, 619 tuple(dst.shape), 620 dst.dtype, dst.name, 621 dst.format) 622 623 return structop_roots 624 625 def _validate_and_collect_expr_info( 626 self, 627 dst: "Tensor", 628 dst_indices: Tuple["IndexVar", ...], 629 ) -> _ExprInfoDict: 630 """Propagates expression information for validation. 631 632 Propagates the indices used by child expression nodes to parent expression 633 nodes. Also collects and validates the sizes for the dimensions 634 corresponding to the indices. 635 636 Args: 637 dst: A destination Tensor that accepts the value of the expression tree. 638 dst_indices: The indices used by the destination index expression. 639 640 Raises: 641 ValueError if there is any inconsistency in indices or dimensional 642 values. 643 644 Returns: 645 A dictionary of (IndexExpr, _ExprInfo). 646 """ 647 expr_to_info = {} 648 # Validate the expression tree and construct expression information. 649 self._visit(_validate_and_collect_expr_info, (expr_to_info,)) 650 651 # Validate the destination dimension information. 652 info = expr_to_info[self] 653 index_to_dim_info = {i: d for i, d in zip(info.src_indices, info.dim_infos)} 654 for i, d, in zip(dst_indices, dst.shape): 655 if i not in index_to_dim_info: 656 raise ValueError("Destination IndexVar not used in the " 657 f"source expression: {i}") 658 else: 659 if d != index_to_dim_info[i].dim and index_to_dim_info[i].dim != -1: 660 raise ValueError(f"Inconsistent destination dimension for {i}: " 661 f"{d} vs {index_to_dim_info[i].dim}") 662 663 return expr_to_info 664 665 def _emit_assignment( 666 self, 667 module: ir.Module, 668 dst: "Tensor", 669 dst_indices: Tuple["IndexVar", ...], 670 expr_to_info: _ExprInfoDict, 671 input_accesses: List["Access"], 672 ) -> None: 673 """Emits an MLIR function for assigning the expression to a tensor.""" 674 input_types = [a.tensor.mlir_tensor_type() for a in input_accesses] 675 676 # Build the kernel for the operations. 677 with ir.InsertionPoint(module.body): 678 679 @func.FuncOp.from_py_func(*input_types, name=_ENTRY_NAME) 680 def linalg_funcop(*args): 681 # Set up the mapping from the Access nodes to their MLIR values. 682 for e, mlir in zip(input_accesses, args): 683 expr_to_info[e].mlir_value = mlir 684 685 # Emit structured ops in the linalg dialect to implement the assignment. 686 for structop_root in self._identify_structured_ops( 687 expr_to_info, dst, dst_indices): 688 structop_root._emit_structured_op(expr_to_info) 689 dst._record_stats(expr_to_info[structop_root].structop_info) 690 691 # The function returns the MLIR value of the root expression. 692 return expr_to_info[self].mlir_value 693 694 linalg_funcop.func_op.attributes[ 695 "llvm.emit_c_interface"] = ir.UnitAttr.get() 696 697 def get_input_accesses(self) -> List["Access"]: 698 """Compute the list of input accesses for the expression.""" 699 input_accesses = [] 700 self._visit(_gather_input_accesses_index_vars, (input_accesses,)) 701 return input_accesses 702 703 def compile( 704 self, 705 dst: "Tensor", 706 dst_indices: Tuple["IndexVar", ...], 707 ) -> execution_engine.ExecutionEngine: 708 """Compiles the tensor assignment dst[dst_indices] = expression. 709 710 Args: 711 dst: The destination tensor. 712 dst_indices: The tuple of IndexVar used to access the destination tensor. 713 714 Returns: 715 The execution engine for the tensor assignment. 716 717 Raises: 718 ValueError: If the expression is not proper or not supported. 719 """ 720 expr_to_info = self._validate_and_collect_expr_info(dst, dst_indices) 721 input_accesses = self.get_input_accesses() 722 723 # Build and compile the module to produce the execution engine. 724 with ir.Context(), ir.Location.unknown(): 725 module = ir.Module.create() 726 self._emit_assignment(module, dst, dst_indices, expr_to_info, 727 input_accesses) 728 engine = utils.compile_and_build_engine(module) 729 730 return engine 731 732 733class _AtomicCounter: 734 """An atomic counter.""" 735 736 def __init__(self): 737 self._counter = 0 738 self._counter_lock = threading.Lock() 739 740 def increment(self) -> int: 741 """Increments the counter by one and returns the old value.""" 742 old_value = self._counter 743 with self._counter_lock: 744 self._counter = self._counter + 1 745 return old_value 746 747 748class IndexVar(IndexExpr): 749 """The tensor index class. 750 751 We support the TACO API index_var class with an alias of this class. 752 753 An IndexVar object represents an index variable in tensor index notation. 754 755 Attributes: 756 name: A unique string name of the IndexVar. 757 """ 758 _counter = _AtomicCounter() 759 760 def __init__(self): 761 id = self._counter.increment() 762 self._name = f"{_TACO_INDEX_PREFIX}{id}" 763 764 def __repr__(self) -> str: 765 return f"IndexVar(name={repr(self._name)})" 766 767 @property 768 def name(self) -> str: 769 """Returns the name of the IndexVar.""" 770 return self._name 771 772 def _visit(self, 773 func: _ExprVisitor, 774 args, 775 *, 776 leaf_checker: _SubtreeLeafChecker = None) -> None: 777 """A post-order visitor.""" 778 if leaf_checker: 779 assert leaf_checker(self, *args) 780 func(self, *args) 781 782 def _emit_expression( 783 self, 784 expr_to_opnd: Dict[IndexExpr, lang.OperandDef], 785 expr_to_info: _ExprInfoDict, 786 ) -> lang.ScalarExpression: 787 """Emits a index value casted to the data type of the tensor expression.""" 788 dim = getattr(lang.D, self.name) 789 index = lang.index(dim) 790 int_value = lang.TypeFn.cast_unsigned(lang.TV.I64, index) 791 return lang.TypeFn.cast_unsigned(lang.T, int_value) 792 793 def dtype(self) -> DType: 794 """Returns the data type for the index value. 795 796 This is unreachable for IndexVar. 797 """ 798 assert 0 799 800 801def get_index_vars(n: int) -> List[IndexVar]: 802 """Returns a list of n IndexVar. 803 804 This routine is defined by the TACO API. 805 806 Args: 807 n: An integer representing the number of IndexVar to get. 808 809 Returns: 810 A list of IndexVar. 811 812 Raises: 813 ValueError: if n is not a positive integer. 814 """ 815 if not isinstance(n, int) or n <= 0: 816 raise ValueError(f"Expected an integer: {n}.") 817 # If lock contention ever becomes an issue, we could implement a bulk getter 818 # that returns a range by only claiming the lock once. 819 return [IndexVar() for i in range(n)] 820 821 822def _mlir_symbols_from_index_vars( 823 index_vars: Tuple[IndexVar, ...]) -> Tuple[lang.SymbolDef, ...]: 824 """Returns a tuple of MLIR symbols for the given tuple of index_var.""" 825 return tuple(getattr(lang.S, i.name) for i in index_vars) 826 827 828def _mlir_dimensions_from_index_vars( 829 index_vars: Tuple[IndexVar, ...]) -> Tuple[lang.DimDef, ...]: 830 """Returns a tuple of MLIR dimensions for the given tuple of index_var.""" 831 return tuple(getattr(lang.D, i.name) for i in index_vars) 832 833 834def _mlir_tensor_type( 835 dtype: DType, shape: Tuple[int, ...], 836 attr: Optional[sparse_tensor.EncodingAttr]) -> ir.RankedTensorType: 837 """Returns an MLIR tensor type. 838 839 Args: 840 dtype: An DType object for the element data type of the tensor. 841 shape: A tuple of integer for the shape of the tensor. 842 attr: An optional MLIR sparse tensor attribute, only provided if the tensor 843 is a sparse tensor. 844 845 Returns: 846 An MLIR ranked tensor type. 847 """ 848 ir_type = _mlir_type_from_taco_type(dtype) 849 return ir.RankedTensorType.get(shape, ir_type, attr) 850 851 852@dataclasses.dataclass(frozen=True) 853class _StructOpInfo: 854 """Information for generating a structured op in the linalg dialect. 855 856 This information is associated with an expression node that serves as the 857 root for an expression subtree implemented with a structured op. 858 859 Attributes: 860 dst_indices: A tuple of IndexVar, representing the result dimensions of the 861 structured op. This is used to construct the temporary variable for the 862 tensor to hold the structured op result. 863 dst_dims: A tuple of int, representing the result shape of the structured 864 op. 865 dst_dtype: A DType representing the data type of the structured op result. 866 dst_name: A string representing the name of the structured op result. 867 dst_format: An optional Format object representing the destination tensor 868 format. None represents a true dense tensor. 869 """ 870 dst_indices: Tuple[IndexVar, ...] 871 dst_dims: Tuple[int, ...] 872 dst_dtype: DType 873 dst_name: str 874 dst_format: Optional[Format] 875 876 def __post_init__(self) -> None: 877 """Verifies the integrity of the attribute values.""" 878 assert len(self.dst_indices) == len(self.dst_dims) 879 880 def emit_tensor_init(self) -> ir.RankedTensorType: 881 """Returns an initialization for the destination tensor.""" 882 if self.dst_format is None or self.dst_format.rank() == 0: 883 # Initialize the dense tensor. 884 ir_type = _mlir_type_from_taco_type(self.dst_dtype) 885 tensor = linalg.InitTensorOp(self.dst_dims, ir_type).result 886 zero = arith.ConstantOp(ir_type, 0.0) 887 return linalg.fill(zero, outs=[tensor]) 888 889 # Initialize the sparse tensor. 890 mlir_type = _mlir_tensor_type(self.dst_dtype, self.dst_dims, 891 self.dst_format.mlir_tensor_attr()) 892 index_type = ir.IndexType.get() 893 return bufferization.AllocTensorOp(mlir_type, [], None, None) 894 895 896class _Stats: 897 """Information to describe how a tensor expression is implemented. 898 899 Currently, we only record the temporary tensors introduced for splitting the 900 original expression. 901 """ 902 903 def __init__(self): 904 self._temps = [] 905 906 def __repr__(self) -> str: 907 return f"_Stats({repr(self._temps)})" 908 909 def add_element(self, structop: _StructOpInfo): 910 """Adds a temporary tensor.""" 911 self._temps.append(structop) 912 913 def get_total(self) -> int: 914 """Gets the total number of temporary tensors.""" 915 return len(self._temps) 916 917 def _get_element(self, idx: int) -> _StructOpInfo: 918 """Gets the ith temporary tensor.""" 919 assert idx < self.get_total() 920 return self._temps[idx] 921 922 def get_dimensions(self, idx: int) -> Tuple[int]: 923 """Gets the dimensions for the ith temporary tensor.""" 924 return self._get_element(idx).dst_dims 925 926 def get_formats(self, idx: int) -> Tuple[ModeFormat]: 927 """Gets the ModeFormats for the ith temporary tensor.""" 928 return tuple(self._get_element(idx).dst_format.format_pack.formats) 929 930 931class _SparseValueInfo(enum.Enum): 932 """Describes how a sparse tensor value is stored. 933 _UNPACKED: The sparse tensor value is stored as (coordnates, values) in 934 Python. 935 _PACKED: The sparse tensor value is stored as a C pointer to a packed MLIR 936 sparse tensor. 937 """ 938 _UNPACKED = 0 939 _PACKED = 1 940 941 942@dataclasses.dataclass(frozen=True) 943class _Assignment: 944 """Records an assignment to a tensor T as T[indices] = expression.""" 945 indices: Tuple["IndexVar", ...] 946 expression: "IndexExpr" 947 948 949class Tensor: 950 """The tensor class. 951 952 We support the TACO API tensor class with an alias of this class. 953 954 This class is part of the TACO API with the following methods: 955 insert: Inserts a value to the given coordinate in the tensor. 956 to_array: Returns a numpy ndarray for the tensor. 957 958 TACO API also defines the following arrtibutes for the class: 959 dtype: A dtype object representing the data type of the tensor. 960 format: A format object representing the storage format of the tensor. 961 name: A string object representing the name of the tensor. 962 order: An integral rank of the tensor. 963 shape: A list of integers representing the shape of the tensor. 964 965 We currently ignore the tensor dimension ordering for dense tensor. 966 """ 967 _counter = _AtomicCounter() 968 969 def _get_unique_name(self) -> str: 970 """Returns a unique name for creating a new Tensor.""" 971 return f"{_TACO_TENSOR_PREFIX}{self._counter.increment()}" 972 973 def _init_format(self, fmt: Union[ModeFormat, List[ModeFormat], 974 Format]) -> None: 975 """Process the fmt argument for the Tensor constructor. 976 977 Args: 978 fmt: This argument can be a ModeFormat, List[ModeFormat], or format. If 979 this argument is a ModeFormat, uses this ModeFormat for all the tensor 980 dimensions. If this argument is a list of ModeFormat, the len of the 981 list should equal to the rank of the tensor. If this argument is a 982 format, uses it for the format of the tensor. 983 984 Raises: 985 ValueError: If fmt is not one of the expected type or is inconsistent 986 with the rank of the tensor. This is because fmt could be an users 987 input. 988 """ 989 if isinstance(fmt, ModeFormat): 990 self._format = _make_format([fmt] * self.order) 991 elif isinstance(fmt, list): 992 if len(fmt) == self.order and isinstance(fmt[0], ModeFormat): 993 self._format = _make_format(fmt) 994 else: 995 raise ValueError("Inconsistent shape and format: " 996 f"{self._shape}, {fmt}.") 997 elif isinstance(fmt, Format): 998 if fmt.rank() != self.order: 999 raise ValueError("Inconsistent shape and format: " 1000 f"{self._shape}, {fmt}.") 1001 else: 1002 self._format = fmt 1003 else: 1004 raise ValueError(f"Invalid format argument: {fmt}.") 1005 1006 def __init__(self, 1007 value_or_shape: Optional[Union[List[int], Tuple[int, ...], float, 1008 int]] = None, 1009 fmt: Optional[Union[ModeFormat, List[ModeFormat], 1010 Format]] = None, 1011 dtype: Optional[DType] = None, 1012 name: Optional[str] = None, 1013 is_dense: bool = False): 1014 """The tensor constructor interface defined by TACO API. 1015 1016 Args: 1017 value_or_shape: This argument is optional and can be int, float, 1018 List[int], or Tuple[int, ...]. If this argument is an int or float, 1019 creates a scalar tensor and initializes it with the value. If this 1020 argument is a list or tuple of int, uses it as the shape to create a 1021 tensor. 1022 fmt: This argument can be a ModeFormat, List[ModeFormat], or format. If 1023 this argument is a ModeFormat, uses this ModeFormat for all the tensor 1024 dimensions. If this argument is a list of ModeFormat, the len of the 1025 list should equal to the rank of the tensor. If this argument is a 1026 format, uses it for the format of the tensor. 1027 dtype: An object of dtype, representing the data type of the tensor. 1028 name: A string name of the tensor. If a name is not given, creates a 1029 unique name for the tensor. 1030 is_dense: A boolean variable to indicate whether the tensor is a dense 1031 tensor without any sparsity annotation. 1032 1033 Raises: 1034 ValueError: If there is any inconsistency among the input arguments. 1035 """ 1036 # Take care of the argument default values common to both sparse tensors 1037 # and dense tensors. 1038 dtype = dtype or DType(Type.FLOAT32) 1039 self._name = name or self._get_unique_name() 1040 self._assignment = None 1041 self._engine = None 1042 self._sparse_value_location = _SparseValueInfo._UNPACKED 1043 self._dense_storage = None 1044 self._dtype = dtype 1045 1046 if is_dense: 1047 assert (fmt is None) 1048 assert (isinstance(value_or_shape, tuple) or isinstance( 1049 value_or_shape, list)) and _all_instance_of(value_or_shape, int) 1050 self._shape = value_or_shape 1051 self._format = None 1052 return 1053 1054 fmt = fmt or ModeFormat.COMPRESSED 1055 # We currently use _coords and _values to host the sparse tensor value with 1056 # COO format, and _dense_storage to host the dense tensor value. We don't 1057 # support the conversion between the two storages. 1058 self._coords = [] 1059 self._values = [] 1060 self._stats = _Stats() 1061 if value_or_shape is None or isinstance(value_or_shape, int) or isinstance( 1062 value_or_shape, float): 1063 # Create a scalar tensor and ignore the fmt parameter. 1064 self._shape = [] 1065 self._format = _make_format([], []) 1066 if value_or_shape is not None: 1067 self._dense_storage = np.array(value_or_shape, dtype=self._dtype.value) 1068 elif (isinstance(value_or_shape, tuple) or isinstance( 1069 value_or_shape, list)) and _all_instance_of(value_or_shape, int): 1070 # Create a tensor with the specified shape and format. 1071 self._shape = list(value_or_shape) 1072 self._init_format(fmt) 1073 else: 1074 raise ValueError("Invalid first argument. " 1075 "Must be a tuple or list for a shape or a single value" 1076 f"if initializing a scalar tensor: {value_or_shape}.") 1077 1078 def _set_packed_sparse_tensor(self, pointer: ctypes.c_void_p) -> None: 1079 """Records the MLIR sparse tensor pointer.""" 1080 self._sparse_value_location = _SparseValueInfo._PACKED 1081 self._packed_sparse_value = pointer 1082 1083 def is_unpacked(self) -> bool: 1084 """Returns true if the tensor value is not packed as MLIR sparse tensor.""" 1085 return (self._sparse_value_location == _SparseValueInfo._UNPACKED) 1086 1087 def unpack(self) -> None: 1088 """Unpacks the MLIR sparse tensor representation.""" 1089 if self.is_dense() or self.is_unpacked(): 1090 return 1091 1092 # Use the output MLIR sparse tensor pointer to retrieve the COO-flavored 1093 # values and verify the values. 1094 rank, nse, shape, values, indices = utils.sparse_tensor_to_coo_tensor( 1095 self._packed_sparse_value, self._dtype.value) 1096 assert rank == self.order 1097 assert np.array_equal(self.shape, shape) 1098 assert nse == len(values) 1099 self._coords = indices 1100 self._values = values 1101 self._sparse_value_location = _SparseValueInfo._UNPACKED 1102 1103 def __repr__(self) -> str: 1104 self._sync_value() 1105 self.unpack() 1106 value_str = (f"{repr(self._dense_storage)})" if self.is_dense() else 1107 f"{repr(self._coords)} {repr(self._values)})") 1108 return (f"Tensor(_name={repr(self._name)} " 1109 f"_dtype={repr(self._dtype)} : ") + value_str 1110 1111 def insert(self, coords: List[int], val: Union[float, int]) -> None: 1112 """Inserts a value to the given coordinate. 1113 1114 Args: 1115 coords: A list of integer coordinates. The length of the list must be the 1116 same as the rank of the tensor. 1117 val: A value being inserted. It is either an integral or a floating point 1118 value. This value will be converted to the data type of the tensor. 1119 1120 Raises: 1121 ValueError: When there is any problem in the parameters. 1122 """ 1123 if self.is_dense(): 1124 raise ValueError("Insert method is not supported for dense tensors.") 1125 if self._assignment != None or not self.is_unpacked(): 1126 raise ValueError( 1127 "Can't use Insert method for a tensor constructed from a file.") 1128 if not isinstance(coords, list): 1129 raise ValueError(f"Non list coordinate detected: {coords}.") 1130 if not _all_instance_of(coords, int): 1131 raise ValueError(f"Non integer coordinate detected: {coords}.") 1132 if (len(coords) != self.order or 1133 any([c < 0 or c >= self._shape[i] for i, c in enumerate(coords)])): 1134 raise ValueError("Invalid coordinate for rank: " 1135 f"{self.order}, {coords}.") 1136 1137 if not isinstance(val, int) and not isinstance(val, float): 1138 raise ValueError(f"Value is neither int nor float: {val}.") 1139 1140 self._coords.append(tuple(coords)) 1141 self._values.append(self._dtype.value(val)) 1142 1143 def is_dense(self) -> bool: 1144 """Returns true if the tensor doesn't have sparsity annotation.""" 1145 return self.order == 0 or self._format is None 1146 1147 def to_array(self) -> np.ndarray: 1148 """Returns the numpy array for the Tensor. 1149 1150 This is currenly only implemented for dense Tensor. 1151 """ 1152 if not self.is_dense(): 1153 raise ValueError("Conversion from non-dense Tensor " 1154 "to numpy array not supported yet.") 1155 1156 self._sync_value() 1157 1158 return self._dense_storage 1159 1160 @staticmethod 1161 def from_array(array: np.ndarray) -> "Tensor": 1162 """Returns a dense tensor with the value copied from the input array. 1163 1164 We currently only support the conversion of float32 and float64 numpy arrays 1165 to Tensor. 1166 1167 Args: 1168 array: The numpy array that provides the data type, shape and value for 1169 the tensor. 1170 1171 Returns: 1172 A Tensor object. 1173 1174 Raises: 1175 ValueError if the data type of the numpy array is not supported. 1176 """ 1177 if array.dtype != np.float32 and array.dtype != np.float64: 1178 raise ValueError(f"Expected floating point value type: {array.dtype}.") 1179 tensor = Tensor( 1180 array.shape, 1181 dtype=_nptype_to_taco_type(array.dtype.type), 1182 is_dense=True) 1183 tensor._dense_storage = np.copy(array) 1184 return tensor 1185 1186 @staticmethod 1187 def from_coo( 1188 coordinates: List[Tuple[int, ...]], 1189 values: List[_AnyRuntimeType], 1190 fmt: Format, 1191 dtype: DType, 1192 ) -> "Tensor": 1193 """Converts coordinates and values to a sparse tensor representation. 1194 1195 Args: 1196 coordinates: A list of coordinates with non-zero values. 1197 values: The non-zero values. 1198 fmt: The tensor storage format. 1199 dtype: The tensor element data type. 1200 1201 Returns: 1202 A tensor with the given non-zero values and storage format. The shape of 1203 the tensor has the minimum size for each dimension to make the given 1204 coordinates valid. 1205 """ 1206 assert (isinstance(coordinates, List) and 1207 _all_instance_of(coordinates, Tuple)) 1208 assert (isinstance(values, List) and _all_instance_of(values, dtype.value)) 1209 assert isinstance(fmt, Format) 1210 1211 rank = fmt.rank() 1212 assert all(len(c) == rank and _all_instance_of(c, int) for c in coordinates) 1213 1214 # Find the maximum coordinate value for each dimension. 1215 max_coordinate = list(map(max, zip(*coordinates))) 1216 # The size of each dimension is one more that such a maximum coordinate 1217 # value. 1218 shape = [c + 1 for c in max_coordinate] 1219 tensor = Tensor(shape, fmt, dtype=dtype) 1220 tensor._coords = coordinates 1221 tensor._values = values 1222 1223 return tensor 1224 1225 @staticmethod 1226 def from_file( 1227 filename: str, 1228 fmt: Format, 1229 dtype: DType, 1230 ) -> "Tensor": 1231 """Constructs a sparse tensor using the COO-flavored values from a file. 1232 1233 Args: 1234 filename: A string for the name of the file that contains the sparse 1235 tensor data. 1236 fmt: The tensor storage format. 1237 dtype: The tensor element data type. 1238 1239 Returns: 1240 A tensor with the given non-zero values and storage format. The tensor 1241 value is stored as an MLIR sparse tensor. 1242 """ 1243 sparse_tensor, shape = utils.create_sparse_tensor(filename, 1244 fmt.format_pack.formats, 1245 _dtype_to_mlir_str(dtype)) 1246 tensor = Tensor(shape.tolist(), fmt, dtype=dtype) 1247 tensor._set_packed_sparse_tensor(sparse_tensor) 1248 1249 return tensor 1250 1251 def to_file(self, filename: str) -> None: 1252 """Output the tensor value to a file. 1253 1254 This method evaluates any pending assignment to the tensor and outputs the 1255 tensor value. 1256 1257 Args: 1258 filename: A string file name. 1259 1260 Raises: 1261 ValueError: If the tensor is dense, or an unpacked sparse tensor. 1262 """ 1263 self._sync_value() 1264 1265 if self.is_dense(): 1266 raise ValueError("Writing dense tensors without sparsity annotation to " 1267 "file is not supported.") 1268 1269 if self.is_unpacked(): 1270 raise ValueError("Writing unpacked sparse tensors to file is not " 1271 "supported.") 1272 1273 utils.output_sparse_tensor(self._packed_sparse_value, filename, 1274 self._format.format_pack.formats, 1275 _dtype_to_mlir_str(self._dtype)) 1276 1277 @property 1278 def dtype(self) -> DType: 1279 """Returns the data type for the Tensor.""" 1280 return self._dtype 1281 1282 @property 1283 def format(self) -> Format: 1284 """Returns the storage format for the Tensor.""" 1285 return self._format 1286 1287 @property 1288 def name(self) -> str: 1289 """Returns the name for the Tensor.""" 1290 return self._name 1291 1292 @property 1293 def order(self) -> int: 1294 """Returns the rank of the Tensor.""" 1295 return len(self._shape) 1296 1297 @property 1298 def shape(self) -> List[int]: 1299 """Returns the shape of the Tensor.""" 1300 return self._shape 1301 1302 def _verify_and_normalize_indices(self, indices) -> Tuple[IndexVar, ...]: 1303 """Verifies and normalizes the indices to access the tensor. 1304 1305 Args: 1306 indices: The index expression used to access a tensor, which could be any 1307 Python object from user inputs. 1308 1309 Returns: 1310 A tuple of IndexVar. 1311 1312 Raises: 1313 ValueError: If indices is not 0 for scalar tensors, or not an IndexVar or 1314 a tuple of IndexVar for other tensors. 1315 """ 1316 if self.order == 0: 1317 if not isinstance(indices, int) or indices != 0: 1318 raise ValueError(f"Expected 0 to index scalar tensors: {indices}") 1319 return () 1320 1321 if isinstance(indices, IndexVar): 1322 return (indices,) 1323 elif isinstance(indices, tuple) and _all_instance_of(indices, IndexVar): 1324 return indices 1325 1326 raise ValueError(f"Expected IndexVars: {indices}") 1327 1328 def __getitem__(self, key) -> "Access": 1329 """Verifies and processes a tensor access. 1330 1331 In the tensor index notation, a tensor access T[i, j] is represented as 1332 retrieving a value with key (i, j) from the tensor object T in Python. This 1333 routine verifies the key for the tensor access and returns a tensor access 1334 object. 1335 1336 Args: 1337 key: The key used to access the tensor, which could be any Python object 1338 from user inputs. 1339 1340 Returns: 1341 The corresponding tensor access object. 1342 1343 Raises: 1344 ValueError: If key is not an IndexVar or a tuple of IndexVar. 1345 """ 1346 indices = self._verify_and_normalize_indices(key) 1347 return Access(self, indices) 1348 1349 def __setitem__(self, key, value) -> None: 1350 """Verifies and processes a tensor assignment. 1351 1352 In the tensor index notation, a tensor assignment "T[i, j] = ..." is 1353 represented as setting a value for a tensor object T via key (i, j) in 1354 Python. This routine verifies the key, evaluates the value, and assigns the 1355 value to the tensor. 1356 1357 We only support assignment of dense tensor currently. 1358 1359 Args: 1360 key: The key used to access the tensor, which could be any Python object 1361 from user inputs. 1362 value: The value assigned to the tensor, which could be any Python object 1363 from user inputs. 1364 1365 Raises: 1366 ValueError: If tensor is not a dense tensor, or the key is not an IndexVar 1367 or a tuple of IndexVar, or the length of the indices is not the same as 1368 the rank of the tensor. 1369 """ 1370 indices = self._verify_and_normalize_indices(key) 1371 if len(indices) != self.order: 1372 raise ValueError("Mismatch between indices and tensor rank: " 1373 f"len({indices}) != {self.order}.") 1374 1375 self._assignment = _Assignment(indices, value) 1376 self._engine = None 1377 1378 def compile(self, force_recompile: bool = False) -> None: 1379 """Compiles the tensor assignment to an execution engine. 1380 1381 Calling compile the second time does not do anything unless 1382 force_recompile is True. 1383 1384 Args: 1385 force_recompile: A boolean value to enable recompilation, such as for the 1386 purpose of timing. 1387 1388 Raises: 1389 ValueError: If the assignment is not proper or not supported. 1390 """ 1391 if self._assignment is None or (self._engine is not None and 1392 not force_recompile): 1393 return 1394 1395 self._engine = self._assignment.expression.compile(self, 1396 self._assignment.indices) 1397 1398 def compute(self) -> None: 1399 """Executes the engine for the tensor assignment. 1400 1401 Raises: 1402 ValueError: If the assignment hasn't been compiled yet. 1403 """ 1404 if self._assignment is None: 1405 return 1406 1407 if self._engine is None: 1408 raise ValueError("Need to invoke compile() before invoking compute().") 1409 1410 input_accesses = self._assignment.expression.get_input_accesses() 1411 # Gather the pointers for the input buffers. 1412 input_pointers = [a.tensor.ctype_pointer() for a in input_accesses] 1413 if self.is_dense(): 1414 # The pointer to receive dense output is the first argument to the 1415 # execution engine. 1416 arg_pointers = [self.dense_dst_ctype_pointer()] + input_pointers 1417 else: 1418 # The pointer to receive the sparse tensor output is the last argument 1419 # to the execution engine and is a pointer to pointer of char. 1420 arg_pointers = input_pointers + [ 1421 ctypes.pointer(ctypes.pointer(ctypes.c_char(0))) 1422 ] 1423 1424 # Invoke the execution engine to run the module. 1425 self._engine.invoke(_ENTRY_NAME, *arg_pointers) 1426 1427 # Retrieve the result. 1428 if self.is_dense(): 1429 result = runtime.ranked_memref_to_numpy(arg_pointers[0][0]) 1430 assert isinstance(result, np.ndarray) 1431 self._dense_storage = result 1432 else: 1433 self._set_packed_sparse_tensor(arg_pointers[-1][0]) 1434 1435 self._assignment = None 1436 self._engine = None 1437 1438 def evaluate(self) -> None: 1439 """Evaluates the tensor assignment.""" 1440 self.compile() 1441 self.compute() 1442 1443 def _sync_value(self) -> None: 1444 """Updates the tensor value by evaluating the pending assignment.""" 1445 if self._assignment is not None: 1446 self.evaluate() 1447 1448 def mlir_tensor_type(self) -> ir.RankedTensorType: 1449 """Returns the MLIR type for the tensor.""" 1450 mlir_attr = (None if (self._format is None or self.order == 0) else 1451 self._format.mlir_tensor_attr()) 1452 return _mlir_tensor_type(self._dtype, tuple(self._shape), mlir_attr) 1453 1454 def dense_dst_ctype_pointer(self) -> ctypes.pointer: 1455 """Returns the ctypes pointer for the pointer to an MemRefDescriptor. 1456 1457 For a dense tensor output, the MLIR compiler allocates the storage for 1458 the tensor. This routine returns the pointer to an MLIR MemRefDescriptor for 1459 receiving the tensor. 1460 """ 1461 assert self.is_dense() 1462 mem_ref_desc = runtime.make_nd_memref_descriptor( 1463 self.order, np.ctypeslib.as_ctypes_type(self.dtype.value))() 1464 return ctypes.pointer(ctypes.pointer(mem_ref_desc)) 1465 1466 def ctype_pointer(self) -> ctypes.pointer: 1467 """Returns the ctypes pointer for the pointer to the input tensor.""" 1468 if self.is_dense(): 1469 if self._dense_storage is None: 1470 self._dense_storage = np.zeros(self._shape, self._dtype.value) 1471 return _ctype_pointer_from_array(self._dense_storage) 1472 1473 if self.is_unpacked(): 1474 shape = np.array(self._shape, np.int64) 1475 indices = np.array(self._coords, np.int64) 1476 values = np.array(self._values, self._dtype.value) 1477 perm, sparse = self.format.get_permutation_and_sparsity() 1478 ptr = utils.coo_tensor_to_sparse_tensor(shape, values, indices, perm, 1479 sparse) 1480 else: 1481 ptr = self._packed_sparse_value 1482 1483 return ctypes.pointer(ctypes.cast(ptr, ctypes.c_void_p)) 1484 1485 def get_scalar_value(self) -> _AnyRuntimeType: 1486 """Returns the value for the scalar tensor. 1487 1488 This method also evaluates the assignment to the tensor. 1489 1490 Raises: 1491 ValueError: If the tensor is not a scalar. 1492 """ 1493 if self.order != 0: 1494 raise ValueError(f"Expected a scalar tensor, got: rank={self.order}") 1495 1496 self._sync_value() 1497 return self._dense_storage 1498 1499 1500 def get_coordinates_and_values( 1501 self) -> Tuple[List[Tuple[int, ...]], List[_AnyRuntimeType]]: 1502 """Returns the coordinates and values for the non-zero elements. 1503 1504 This method also evaluates the assignment to the tensor and unpack the 1505 sparse tensor. 1506 """ 1507 self._sync_value() 1508 1509 if not self.is_dense(): 1510 self.unpack() 1511 return (self._coords, self._values) 1512 1513 if self.order == 0: 1514 return ([], self._dense_storage) 1515 1516 # Coordinates for non-zero elements, grouped by dimensions. 1517 coords_by_dims = self._dense_storage.nonzero() 1518 # Coordinates for non-zero elements, grouped by elements. 1519 coords = np.transpose(coords_by_dims) 1520 values = self._dense_storage[coords_by_dims] 1521 return (coords, values) 1522 1523 def _record_stats(self, structop: "_StructOpInfo"): 1524 """Collects information for temporary tensors.""" 1525 # Exclude user specified destination tensors. 1526 if structop.dst_name == self.name: 1527 return 1528 1529 self._stats.add_element(structop) 1530 1531 1532def _emit_operand(op_def: lang.LinalgOpDef, indices: Tuple[IndexVar, ...], 1533 name: str, kind: lang.OperandKind) -> lang.OperandDef: 1534 """Emits an operand for a tensor access in the current linalg operation. 1535 1536 Args: 1537 op_def: A LinalgOpDef representing the current linalg dialect operation. 1538 indices: A tuple of IndexVar used to access the tensor. 1539 name: A unique string name of the tensor. 1540 kind: An OperandKind for the operand. 1541 1542 Returns: 1543 An OperandDef representing the operand. 1544 """ 1545 dim_sym = _mlir_symbols_from_index_vars(indices) 1546 opnd = lang.OperandDef(kind, lang.T, dim_sym) 1547 op_def.add_operand(name, opnd) 1548 return opnd 1549 1550 1551@dataclasses.dataclass(frozen=True) 1552class _DimInfo: 1553 """Information for an operand dimension. 1554 1555 Attributes: 1556 dim: An integer for the size of the dimension. 1557 mode_format: A ModeFormat for the dimension sparsity. 1558 """ 1559 dim: int 1560 mode_format: ModeFormat 1561 1562 1563def _get_dummy_dim_info() -> _DimInfo: 1564 """Constructs the _DimInfo for an index used in tensor expressions.""" 1565 return _DimInfo(-1, ModeFormat.DENSE) 1566 1567 1568@dataclasses.dataclass() 1569class _ExprInfo: 1570 """Expression information for validation and code generation. 1571 1572 Attributes: 1573 src_indices: A tuple of IndexVar for the indices used by the tensors in the 1574 expression tree. 1575 dim_infos: A tuple of _DimInfo, representing the dimension information 1576 corresponding to the src_indices. 1577 reduce_indices: A set of IndexVar for the indices reduced by the expression. 1578 acc_reduce_indices: An accumulated set of IndexVar for the indices reduced 1579 by the expression and its children. 1580 structop_info: Information to support the code generation for a structured 1581 op in the linalg dialect, if the corresponding expression node is the root 1582 of a subtree for a structured op. 1583 mlir_value: The MLIR value generated for the structured op. 1584 """ 1585 src_indices: Tuple[IndexVar, ...] 1586 dim_infos: Tuple[_DimInfo, ...] 1587 reduce_indices: Optional[Set[IndexVar]] = None 1588 acc_reduce_indices: Optional[Set[IndexVar]] = None 1589 structop_info: Optional[_StructOpInfo] = None 1590 mlir_value: Optional[ir.Value] = None 1591 1592 def __post_init__(self) -> None: 1593 """Verifies and fix up attribute values. 1594 1595 Verifies the consistency of the attributes and modifies the default values 1596 to support convenient initializer syntax. 1597 """ 1598 assert len(self.src_indices) == len(self.dim_infos) 1599 self.reduce_indices = self.reduce_indices or set() 1600 self.acc_reduce_indices = self.acc_reduce_indices or set() 1601 1602 1603@dataclasses.dataclass(frozen=True) 1604class Access(IndexExpr): 1605 """The tensor access class. 1606 1607 We support the TACO API access class with an alias of this class. 1608 1609 Attributes: 1610 tensor: A Tensor being accessed. 1611 indices: A tuple of IndexVar, representing the indices used to access the 1612 Tensor. 1613 """ 1614 tensor: Tensor 1615 indices: Tuple[IndexVar, ...] 1616 1617 def __post_init__(self) -> None: 1618 """Verifies the tensor and indices for a tensor access. 1619 1620 Raises: 1621 ValueError: If indices is not a list of IndexVar or the len of indices 1622 doesn't equal to the rank of the tensor. 1623 """ 1624 if (not isinstance(self.indices, tuple) or 1625 not _all_instance_of(self.indices, IndexVar)): 1626 raise ValueError(f"Indices contain non IndexVar: {str(self.indices)}.") 1627 if self.tensor.order != len(self.indices): 1628 raise ValueError("Invalid indices for rank: " 1629 f"str{self.tensor.order} != len({str(self.indices)}).") 1630 1631 def __repr__(self) -> str: 1632 # The Tensor __repr__ method evaluates the pending assignment to the tensor. 1633 # We want to define the __repr__ method here to avoid such evaluation of the 1634 # tensor assignment. 1635 indices_str = ", ".join(map(lambda i: i.name, self.indices)) 1636 return (f"Tensor({self.tensor.name}) " f"Indices({indices_str})") 1637 1638 def _emit_expression( 1639 self, 1640 expr_to_opnd: Dict[IndexExpr, lang.OperandDef], 1641 expr_to_info: _ExprInfoDict, 1642 ) -> lang.ScalarExpression: 1643 """Emits a linalg dialect TensorUse expression for the tensor access.""" 1644 assert self in expr_to_opnd 1645 dims = _mlir_dimensions_from_index_vars(self.indices) 1646 return lang.TensorUse(expr_to_opnd[self], dims) 1647 1648 def _visit(self, 1649 func: _ExprVisitor, 1650 args, 1651 *, 1652 leaf_checker: _SubtreeLeafChecker = None) -> None: 1653 if leaf_checker: 1654 assert leaf_checker(self, *args) 1655 func(self, *args) 1656 1657 def dtype(self) -> DType: 1658 return self.tensor.dtype 1659 1660 1661def _gather_input_accesses_index_vars( 1662 expr: IndexExpr, 1663 input_accesses: List[Access], 1664) -> None: 1665 """Collects Access nodes.""" 1666 if isinstance(expr, Access) and expr not in input_accesses: 1667 input_accesses.append(expr) 1668 1669 1670def _op_ceil(__a: Any) -> Any: 1671 """A _UnaryOp object for operation ceil.""" 1672 pass 1673 1674 1675def _op_floor(__a: Any) -> Any: 1676 """A _UnaryOp object for operation floor.""" 1677 pass 1678 1679 1680def _op_unary_to_callable(op: _UnaryOp) -> lang.UnaryFnType: 1681 """Returns the linalg dialect function object for the given operation.""" 1682 op_to_callable = { 1683 operator.abs: lang.UnaryFn.abs, 1684 operator.neg: lang.UnaryFn.negf, 1685 _op_ceil: lang.UnaryFn.ceil, 1686 _op_floor: lang.UnaryFn.floor, 1687 } 1688 return op_to_callable[op] 1689 1690 1691@dataclasses.dataclass(frozen=True) 1692class _UnaryExpr(IndexExpr): 1693 """The representation for a Unary operation. 1694 1695 Attributes: 1696 op: A _UnaryOp representing the operation. 1697 a: An IndexExpr representing the operand for the operation. 1698 """ 1699 op: _BinaryOp 1700 a: IndexExpr 1701 1702 def __post_init__(self) -> None: 1703 """Verifies that the operand being added is an IndexExpr.""" 1704 assert isinstance(self.a, IndexExpr) 1705 1706 def _emit_expression( 1707 self, 1708 expr_to_opnd: Dict[IndexExpr, lang.OperandDef], 1709 expr_to_info: _ExprInfoDict, 1710 ) -> lang.ScalarExpression: 1711 """Emits the expression tree and returns the expression.""" 1712 # The current expression node is an internal node of the structured op. 1713 if self not in expr_to_opnd: 1714 a = self.a._emit_expression(expr_to_opnd, expr_to_info) 1715 return _op_unary_to_callable(self.op)(a) 1716 1717 # The current expression is a leaf node of the structured op. That is, it is 1718 # a temporary tensor generated by its child structured op. 1719 op_info = expr_to_info[self].structop_info 1720 assert op_info is not None 1721 dims = _mlir_dimensions_from_index_vars(op_info.dst_indices) 1722 return lang.TensorUse(expr_to_opnd[self], dims) 1723 1724 def _visit(self, 1725 func: _ExprVisitor, 1726 args, 1727 *, 1728 leaf_checker: _SubtreeLeafChecker = None) -> None: 1729 """A post-order visitor.""" 1730 if leaf_checker is None or not leaf_checker(self, *args): 1731 self.a._visit(func, args, leaf_checker=leaf_checker) 1732 func(self, *args) 1733 1734 def dtype(self) -> DType: 1735 """Returns the data type of the operation.""" 1736 return self.a.dtype() 1737 1738 1739def _op_to_callable(op: _BinaryOp) -> lang.BinaryFnType: 1740 """Returns the linalg dialect function object for the given operation.""" 1741 op_to_callable = { 1742 operator.add: lang.BinaryFn.add, 1743 operator.sub: lang.BinaryFn.sub, 1744 operator.mul: lang.BinaryFn.mul, 1745 } 1746 return op_to_callable[op] 1747 1748@dataclasses.dataclass(frozen=True) 1749class _BinaryExpr(IndexExpr): 1750 """The representation for a binary operation. 1751 1752 Attributes: 1753 op: A _BinaryOp representing the binary operation. 1754 a: An IndexExpr representing the first operand of the operation. 1755 b: An IndexExpr representing the second operand of the operation. 1756 """ 1757 op: _BinaryOp 1758 a: IndexExpr 1759 b: IndexExpr 1760 1761 def __post_init__(self) -> None: 1762 """Verifies that the operands being added are IndexExpr.""" 1763 assert isinstance(self.a, IndexExpr) and isinstance(self.b, IndexExpr) 1764 1765 def _emit_expression( 1766 self, 1767 expr_to_opnd: Dict[IndexExpr, lang.OperandDef], 1768 expr_to_info: _ExprInfoDict, 1769 ) -> lang.ScalarExpression: 1770 """Emits the expression tree and returns the expression.""" 1771 # The current expression node is an internal node of the structured op. 1772 if self not in expr_to_opnd: 1773 a = self.a._emit_expression(expr_to_opnd, expr_to_info) 1774 b = self.b._emit_expression(expr_to_opnd, expr_to_info) 1775 return _op_to_callable(self.op)(a, b) 1776 1777 # The current expression is a leaf node of the structured op. That is, it is 1778 # a temporary tensor generated by its child structured op. 1779 op_info = expr_to_info[self].structop_info 1780 assert op_info is not None 1781 dims = _mlir_dimensions_from_index_vars(op_info.dst_indices) 1782 return lang.TensorUse(expr_to_opnd[self], dims) 1783 1784 def _visit(self, 1785 func: _ExprVisitor, 1786 args, 1787 *, 1788 leaf_checker: _SubtreeLeafChecker = None) -> None: 1789 """A post-order visitor.""" 1790 if leaf_checker is None or not leaf_checker(self, *args): 1791 self.a._visit(func, args, leaf_checker=leaf_checker) 1792 self.b._visit(func, args, leaf_checker=leaf_checker) 1793 func(self, *args) 1794 1795 def dtype(self) -> DType: 1796 """Returns the data type of the binary operation.""" 1797 return self.a.dtype() 1798 1799 1800def _validate_and_collect_dim_info( 1801 index_to_dim_info: Dict[IndexVar, _DimInfo], 1802 indices: Tuple[IndexVar, ...], 1803 dim_infos: Tuple[_DimInfo, ...], 1804 expr: _BinaryExpr, 1805) -> None: 1806 """Validates and collects the dimension information for an index notation. 1807 1808 Validates (indices, dim_infos) against the information collected from other 1809 source operands and is represented by index_to_dim_info. In particular, we 1810 ensure that each IndexVar corresponds to only one dimension size. We also 1811 aggregate the new information represented in (indices, dim_infos) to 1812 index_to_dim_info. 1813 1814 Args: 1815 index_to_dim: A dictionary of (IndexVar, _DimInfo) collected from the 1816 previous operands. 1817 indices: The IndexVars to be validated. 1818 dim_infos: The dimension information for the IndexVars to be validated. 1819 expr: The binary expression where (indices, dim_infos) is used. 1820 1821 Raises: 1822 ValueError if there is any problem in the IndexVars or dimensional values. 1823 """ 1824 assert len(indices) == len(dim_infos) 1825 for i, d in zip(indices, dim_infos): 1826 if i not in index_to_dim_info: 1827 index_to_dim_info[i] = d 1828 else: 1829 dim = index_to_dim_info[i].dim 1830 if dim == -1 or d.dim == -1: 1831 dim = dim if dim != -1 else d.dim 1832 elif dim != d.dim: 1833 raise ValueError(f"Inconsistent source dimension for {i}: " 1834 f"{d.dim} vs {dim}") 1835 mode_format = _mode_format_estimator(expr.op)( 1836 index_to_dim_info[i].mode_format, d.mode_format) 1837 index_to_dim_info[i] = _DimInfo(d.dim, mode_format) 1838 1839 1840def _validate_and_collect_expr_info( 1841 expr: IndexExpr, 1842 expr_to_info: _ExprInfoDict, 1843) -> None: 1844 """Validates dimension information and constructs _ExprInfo. 1845 1846 Validates that dimensional values for the same IndexVar are the same. Collects 1847 a list of IndexVar used by the expression and their corresponding dimensional 1848 values. Constructs an _ExprInfo object to record the information for the 1849 IndexExpr. 1850 1851 This routine is passed to the post-order visitor as an _ExprVisitor object. 1852 1853 Args: 1854 expr: The IndexExpr being validated. 1855 expr_to_info: The dictionary of (IndexExpr, _ExprInfo) for recording the 1856 expression information. 1857 1858 Raises: 1859 ValueError if there is any problem in the IndexVars or dimensional values. 1860 """ 1861 # Objects of class Access can be shared by different expressions. Avoid 1862 # processing Access objects multiple times by skipping the processing if expr 1863 # is already in the dictionary. 1864 if expr in expr_to_info: 1865 return 1866 1867 if isinstance(expr, IndexVar): 1868 src_indices = expr, # A tuple with one element. 1869 dim_infos = _get_dummy_dim_info(), # A tuple with one element. 1870 elif isinstance(expr, Access): 1871 src_indices = expr.indices 1872 src_dims = tuple(expr.tensor.shape) 1873 if expr.tensor.format is None: 1874 # Treat each dimension of a dense tensor as DENSE for the purpose of 1875 # calculating temporary tensor storage format. 1876 mode_formats = tuple([ModeFormat.DENSE] * len(src_dims)) 1877 else: 1878 mode_formats = tuple(expr.tensor.format.format_pack.formats) 1879 assert len(src_dims) == len(mode_formats) 1880 dim_infos = tuple([_DimInfo(d, m) for d, m in zip(src_dims, mode_formats)]) 1881 elif isinstance(expr, _UnaryExpr): 1882 a_info = expr_to_info[expr.a] 1883 index_to_dim_info = { 1884 i: d for i, d in zip(a_info.src_indices, a_info.dim_infos) 1885 } 1886 # Here we rely on the fact that dictionaries keep the insertion order for 1887 # keys and values. 1888 src_indices = tuple(index_to_dim_info.keys()) 1889 dim_infos = tuple(index_to_dim_info.values()) 1890 else: 1891 assert isinstance(expr, _BinaryExpr) 1892 a_info = expr_to_info[expr.a] 1893 index_to_dim_info = { 1894 i: d for i, d in zip(a_info.src_indices, a_info.dim_infos) 1895 } 1896 b_info = expr_to_info[expr.b] 1897 _validate_and_collect_dim_info(index_to_dim_info, b_info.src_indices, 1898 b_info.dim_infos, expr) 1899 # Here we rely on the fact that dictionaries keep the insertion order for 1900 # keys and values. 1901 src_indices = tuple(index_to_dim_info.keys()) 1902 dim_infos = tuple(index_to_dim_info.values()) 1903 1904 expr_to_info[expr] = _ExprInfo(src_indices, dim_infos) 1905 1906 1907def _mark_structured_op_root( 1908 expr: IndexExpr, 1909 reduce_index: IndexVar, 1910 expr_to_info: _ExprInfoDict, 1911) -> None: 1912 """Identifies the root expression for a structured op in the linalg dialect. 1913 1914 An linalg structured op can only perform reduction on the whole expression. 1915 For a TACO tensor algebra expression, the reduction on an IndexVar is done at 1916 the smallest expression that contains all the uses of the IndexVar. If such an 1917 expression is only part of the whole expression, we need to split this 1918 sub-expression tree out from its parent and implement the sub-expression as a 1919 structured op. 1920 1921 This routine identifies the root expression node for performing a reduction on 1922 the given IndexVar. If the reduction of the given IndexVar should be performed 1923 on expression X, then the IndexVar is added to expr_to_info[X].reduce_indices 1924 1925 Args: 1926 expr: The root IndexExpr for the tensor algebra expression. 1927 reduce_index: The IndexVar which we want to find out the proper expression 1928 to perform a reduction. 1929 expr_to_info: The dictionary to look up _ExprInfo for IndexExpr. 1930 1931 Raises: 1932 ValueError: If the expression is not proper or not supported. 1933 """ 1934 expr_info = expr_to_info[expr] 1935 if isinstance(expr, Access): 1936 # Handle simple reduction expression in the format of A[i] = B[i, j]. 1937 if reduce_index in expr_info.src_indices: 1938 expr_info.reduce_indices.add(reduce_index) 1939 return 1940 elif isinstance(expr, IndexVar): 1941 # A[i] = B[i] + j is not allowed. 1942 raise ValueError(f"IndexVar is not part of the iteration domain: {expr}.") 1943 1944 assert (isinstance(expr, _BinaryExpr)) 1945 a_info = expr_to_info[expr.a] 1946 b_info = expr_to_info[expr.b] 1947 1948 if reduce_index in a_info.src_indices and reduce_index in b_info.src_indices: 1949 expr_info.reduce_indices.add(reduce_index) 1950 return 1951 1952 if reduce_index in a_info.src_indices: 1953 _mark_structured_op_root(expr.a, reduce_index, expr_to_info) 1954 elif reduce_index in b_info.src_indices: 1955 _mark_structured_op_root(expr.b, reduce_index, expr_to_info) 1956 else: 1957 assert False, "Unreachable path" 1958 1959 1960def _accumulate_reduce_indices( 1961 expr: IndexExpr, 1962 expr_to_info: _ExprInfoDict, 1963) -> None: 1964 """Propagates reduction indices from child expressions to parent expressions. 1965 1966 This routine is passed to the post-order visitor as an _ExprVisitor object. 1967 1968 Args: 1969 expr: The IndexExpr being visited. 1970 expr_to_info: The dictionary of (IndexExpr, _ExprInfo) for recording the 1971 expression information. 1972 """ 1973 assert expr in expr_to_info 1974 expr_info = expr_to_info[expr] 1975 1976 if isinstance(expr, _BinaryExpr): 1977 a_info = expr_to_info[expr.a] 1978 b_info = expr_to_info[expr.b] 1979 expr_info.acc_reduce_indices = ( 1980 a_info.acc_reduce_indices | b_info.acc_reduce_indices 1981 | expr_info.reduce_indices) 1982 elif isinstance(expr, _UnaryExpr): 1983 a_info = expr_to_info[expr.a] 1984 expr_info.acc_reduce_indices = ( 1985 a_info.acc_reduce_indices | expr_info.reduce_indices) 1986 elif isinstance(expr, IndexVar): 1987 # If an IndexVar is reducing itself, it means the IndexVar is outside the 1988 # iteration domain. This usage is now allowed and we should emit an error 1989 # before reaching here. 1990 assert not expr_info.reduce_indices 1991 else: 1992 assert isinstance(expr, Access) 1993 # Handle simple reduction expression in the format of A[i] = B[i, j]. 1994 expr_info.acc_reduce_indices = expr_info.reduce_indices 1995 1996 1997 1998def _gather_structured_op( 1999 expr: IndexExpr, 2000 expr_to_info: _ExprInfoDict, 2001 structop_roots: List[IndexExpr], 2002) -> None: 2003 """Adds structured op root expression information to structop_roots. 2004 2005 This routine is passed to the post-order visitor as an _ExprVisitor object. 2006 2007 Args: 2008 expr: The IndexExpr being visited. 2009 expr_to_info: The dictionary to look up _ExprInfo for IndexExpr. 2010 structop_roots: The resulting list of IndexExpr that are the roots for 2011 linalg structured ops. 2012 """ 2013 if not expr_to_info[expr].reduce_indices: 2014 return 2015 2016 # If the expression is the root for reducing some indices, collect the indices 2017 # and dimensions for the reduction result. 2018 dst_indices = [] 2019 dst_dims = [] 2020 mode_fmts = [] 2021 for i, d in zip(expr_to_info[expr].src_indices, expr_to_info[expr].dim_infos): 2022 if i not in expr_to_info[expr].acc_reduce_indices: 2023 dst_indices.append(i) 2024 dst_dims.append(d.dim) 2025 mode_fmts.append(d.mode_format) 2026 2027 # Add the information to the dictionary. 2028 op_info = _StructOpInfo( 2029 tuple(dst_indices), 2030 tuple(dst_dims), 2031 expr.dtype(), 2032 f"temp{len(structop_roots)}", 2033 _make_format(mode_fmts), 2034 ) 2035 expr_to_info[expr].structop_info = op_info 2036 2037 # Add the expression to the list of structured op roots. 2038 structop_roots.append(expr) 2039 2040 2041def _is_structured_op_leaf( 2042 expr: IndexExpr, 2043 root: IndexExpr, 2044 expr_to_info: _ExprInfoDict, 2045 *unused_args, 2046) -> bool: 2047 """Returns true iff the expression is a leaf node for a structured op. 2048 2049 The root of a structured op is a leaf of its parent structured op that uses 2050 its result. An expression node is a leaf node for the current structured op if 2051 it is an Access node or the root for a structured op that is not the current 2052 structured op. 2053 2054 This routine is passed to the post-order visitor as a _SubtreeLeafChecker 2055 object. Because the post-order visitor pass the same parameters to both 2056 _SubtreeLeafChecker and _ExprVisitor, this routine may received unused 2057 parameters. 2058 2059 Args: 2060 expr: The IndexExpr being visited. 2061 root: The root of the current structured op. 2062 expr_to_info: The dictionary to look up _ExprInfo for IndexExpr. 2063 2064 Returns: 2065 True if the current IndexExpr is a leaf for the current structured op. 2066 """ 2067 return (expr != root and 2068 expr_to_info[expr].structop_info is not None) or isinstance( 2069 expr, Access) or isinstance(expr, IndexVar) 2070 2071 2072def _gather_structured_op_input( 2073 expr: IndexExpr, 2074 root: IndexExpr, 2075 expr_to_info: _ExprInfoDict, 2076 structop_inputs: List[IndexExpr], 2077) -> None: 2078 """Adds the IndexExpr to structop_inputs if it is an input. 2079 2080 If the current IndexExpr is an input for the current structured op, adds it to 2081 structop_inputs. The current IndexExpr is an input if it is an Access node or 2082 if it is the root for a structured op that is not the current structured op. 2083 2084 This routine is passed to the post-order visitor as an _ExprVisitor object. 2085 2086 Args: 2087 expr: The IndexExpr being visited. 2088 root: The root of the current structured op. 2089 expr_to_info: The dictionary to look up _ExprInfo for IndexExpr. 2090 structop_inputs: The resulting list of IndexExpr that provide input to the 2091 current structured op. 2092 """ 2093 if ((expr != root or isinstance(expr, Access)) and 2094 expr not in structop_inputs) and (isinstance(expr, Access) or 2095 (expr in expr_to_info and 2096 expr_to_info[expr].structop_info)): 2097 structop_inputs.append(expr) 2098 2099 2100def _emit_structured_op_input( 2101 expr: IndexExpr, 2102 expr_to_info: _ExprInfoDict, 2103 op_def: lang.LinalgOpDef, 2104) -> lang.OperandDef: 2105 """Emits OperandDef in the linalg dialect for the input IndexExpr. 2106 2107 Args: 2108 expr: The input IndexExpr for the current structured op. 2109 expr_to_info: The dictionary to look up _ExprInfo for IndexExpr. 2110 op_def: The linalg operation for the current structured op. 2111 2112 Returns: 2113 An OperandDef in the linalg dialect for the input IndexExpr. 2114 """ 2115 op_info = expr_to_info[expr].structop_info 2116 if op_info and not isinstance(expr, Access): 2117 # The input is a temporary tensor produced by another structured op. 2118 indices = op_info.dst_indices 2119 name = op_info.dst_name 2120 else: 2121 # The input is a user provided tensor. 2122 assert isinstance(expr, Access) 2123 indices = expr.indices 2124 name = expr.tensor.name 2125 2126 dim_sym = _mlir_symbols_from_index_vars(indices) 2127 opnd = lang.OperandDef(lang.OperandKind.INPUT_TENSOR, lang.T, dim_sym) 2128 op_def.add_operand(name, opnd) 2129 return opnd 2130 2131 2132def _check_and_build_unary(a: Access, op: _UnaryOp) -> "_UnaryExpr": 2133 """Build a unary operation ceil. 2134 2135 Args: 2136 a: The operand, which could be any Python object from user inputs. 2137 op: An _UnaryOp object representing the operation. 2138 2139 Returns: 2140 A _UnaryExpr object representing the operation. 2141 2142 Raises: 2143 ValueError: If a is not an IndexExpr. 2144 """ 2145 if not isinstance(a, Access): 2146 raise ValueError(f"Expected an Access Operand: {a}") 2147 return a._build_unary_expr(op) 2148 2149 2150def ceil(a: Access) -> "_UnaryExpr": 2151 """Defines the operation ceil. 2152 2153 Args: 2154 a: The operand, which could be any Python object from user inputs. 2155 2156 Returns: 2157 A _UnaryExpr object representing the operation. 2158 2159 Raises: 2160 ValueError: If a is not an IndexExpr. 2161 """ 2162 return _check_and_build_unary(a, _op_ceil) 2163 2164 2165def floor(a: Access) -> "_UnaryExpr": 2166 """Defines the operation floor. 2167 2168 Args: 2169 a: The operand, which could be any Python object from user inputs. 2170 2171 Returns: 2172 A _UnaryExpr object representing the operation. 2173 2174 Raises: 2175 ValueError: If a is not an IndexExpr. 2176 """ 2177 return _check_and_build_unary(a, _op_floor) 2178